mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Merge remote-tracking branch 'origin' into joriks/eslint-config
This commit is contained in:
commit
b110639c76
103 changed files with 3118 additions and 1035 deletions
197
CHANGELOG.md
197
CHANGELOG.md
|
@ -1,3 +1,200 @@
|
|||
Changes in [2.8.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.1) (2020-06-29)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0...v2.8.1)
|
||||
|
||||
* Support accounts with cross signing but no SSSS
|
||||
[\#4852](https://github.com/matrix-org/matrix-react-sdk/pull/4852)
|
||||
|
||||
Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0) (2020-06-23)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0)
|
||||
|
||||
* Upgrade to JS SDK 7.0.0
|
||||
* Update read receipt remainder for internal font size change
|
||||
[\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807)
|
||||
* Revert "Use recovery keys over passphrases"
|
||||
[\#4793](https://github.com/matrix-org/matrix-react-sdk/pull/4793)
|
||||
|
||||
Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 7.0.0-rc.1
|
||||
* Fix Styled Checkbox and Radio Button disabled state
|
||||
[\#4778](https://github.com/matrix-org/matrix-react-sdk/pull/4778)
|
||||
* clean up and fix the isMasterRuleEnabled logic
|
||||
[\#4782](https://github.com/matrix-org/matrix-react-sdk/pull/4782)
|
||||
* Fix case-sensitivity of /me to match rest of slash commands
|
||||
[\#4763](https://github.com/matrix-org/matrix-react-sdk/pull/4763)
|
||||
* Add a 'show less' button to the new room list
|
||||
[\#4765](https://github.com/matrix-org/matrix-react-sdk/pull/4765)
|
||||
* Update from Weblate
|
||||
[\#4781](https://github.com/matrix-org/matrix-react-sdk/pull/4781)
|
||||
* Sticky and collapsing headers for new room list
|
||||
[\#4758](https://github.com/matrix-org/matrix-react-sdk/pull/4758)
|
||||
* Make the room list labs setting reload on change
|
||||
[\#4780](https://github.com/matrix-org/matrix-react-sdk/pull/4780)
|
||||
* Handle/hide old rooms in the room list
|
||||
[\#4767](https://github.com/matrix-org/matrix-react-sdk/pull/4767)
|
||||
* Add some media queries to improve UI on mobile (#3991)
|
||||
[\#4656](https://github.com/matrix-org/matrix-react-sdk/pull/4656)
|
||||
* Match fuzzy filtering a bit more reliably in the new room list
|
||||
[\#4769](https://github.com/matrix-org/matrix-react-sdk/pull/4769)
|
||||
* Improve Field ts definitions some more
|
||||
[\#4777](https://github.com/matrix-org/matrix-react-sdk/pull/4777)
|
||||
* Fix alignment of checkboxes in new room list's context menu
|
||||
[\#4776](https://github.com/matrix-org/matrix-react-sdk/pull/4776)
|
||||
* Fix Field ts def, fix LocalEchoWrapper and NotificationsEnabledController
|
||||
[\#4775](https://github.com/matrix-org/matrix-react-sdk/pull/4775)
|
||||
* Add presence indicators and globes to new room list
|
||||
[\#4774](https://github.com/matrix-org/matrix-react-sdk/pull/4774)
|
||||
* Include the sticky room when filtering in the new room list
|
||||
[\#4772](https://github.com/matrix-org/matrix-react-sdk/pull/4772)
|
||||
* Add a home button to the new room list menu when available
|
||||
[\#4771](https://github.com/matrix-org/matrix-react-sdk/pull/4771)
|
||||
* use group layout for search results
|
||||
[\#4764](https://github.com/matrix-org/matrix-react-sdk/pull/4764)
|
||||
* Fix m.id.phone spec compliance
|
||||
[\#4757](https://github.com/matrix-org/matrix-react-sdk/pull/4757)
|
||||
* User Info default power levels for ban/kick/redact to 50 as per spec
|
||||
[\#4759](https://github.com/matrix-org/matrix-react-sdk/pull/4759)
|
||||
* Match new room list's text search to old room list
|
||||
[\#4768](https://github.com/matrix-org/matrix-react-sdk/pull/4768)
|
||||
* Fix ordering of recent rooms in the new room list
|
||||
[\#4766](https://github.com/matrix-org/matrix-react-sdk/pull/4766)
|
||||
* Change theme selector to use new styled radio buttons
|
||||
[\#4731](https://github.com/matrix-org/matrix-react-sdk/pull/4731)
|
||||
* Use recovery keys over passphrases
|
||||
[\#4686](https://github.com/matrix-org/matrix-react-sdk/pull/4686)
|
||||
* Update from Weblate
|
||||
[\#4760](https://github.com/matrix-org/matrix-react-sdk/pull/4760)
|
||||
* Initial dark theme support for new room list
|
||||
[\#4756](https://github.com/matrix-org/matrix-react-sdk/pull/4756)
|
||||
* Support per-list options and algorithms on the new room list
|
||||
[\#4754](https://github.com/matrix-org/matrix-react-sdk/pull/4754)
|
||||
* Send read marker updates immediately after moving visually
|
||||
[\#4755](https://github.com/matrix-org/matrix-react-sdk/pull/4755)
|
||||
* Add a minimized view to the new room list
|
||||
[\#4753](https://github.com/matrix-org/matrix-react-sdk/pull/4753)
|
||||
* Fix e2e icon alignment in irc-layout
|
||||
[\#4752](https://github.com/matrix-org/matrix-react-sdk/pull/4752)
|
||||
* Add some resource leak protection to new room list badges
|
||||
[\#4750](https://github.com/matrix-org/matrix-react-sdk/pull/4750)
|
||||
* Fix read-receipt alignment
|
||||
[\#4747](https://github.com/matrix-org/matrix-react-sdk/pull/4747)
|
||||
* Show message previews on the new room list tiles
|
||||
[\#4751](https://github.com/matrix-org/matrix-react-sdk/pull/4751)
|
||||
* Fix various layout concerns with the new room list
|
||||
[\#4749](https://github.com/matrix-org/matrix-react-sdk/pull/4749)
|
||||
* Prioritize text on the clipboard over file
|
||||
[\#4748](https://github.com/matrix-org/matrix-react-sdk/pull/4748)
|
||||
* Move Settings flag to ts
|
||||
[\#4729](https://github.com/matrix-org/matrix-react-sdk/pull/4729)
|
||||
* Add a context menu to rooms in the new room list
|
||||
[\#4743](https://github.com/matrix-org/matrix-react-sdk/pull/4743)
|
||||
* Add hover states and basic context menu to new room list
|
||||
[\#4742](https://github.com/matrix-org/matrix-react-sdk/pull/4742)
|
||||
* Update resize handle for new designs in new room list
|
||||
[\#4741](https://github.com/matrix-org/matrix-react-sdk/pull/4741)
|
||||
* Improve general stability in the new room list
|
||||
[\#4740](https://github.com/matrix-org/matrix-react-sdk/pull/4740)
|
||||
* Reimplement breadcrumbs for new room list
|
||||
[\#4735](https://github.com/matrix-org/matrix-react-sdk/pull/4735)
|
||||
* Add styled radio buttons
|
||||
[\#4744](https://github.com/matrix-org/matrix-react-sdk/pull/4744)
|
||||
* Hide checkbox tick on dark backgrounds
|
||||
[\#4730](https://github.com/matrix-org/matrix-react-sdk/pull/4730)
|
||||
* Make checkboxes a11y friendly
|
||||
[\#4746](https://github.com/matrix-org/matrix-react-sdk/pull/4746)
|
||||
* EventIndex: Store and restore the encryption info for encrypted events.
|
||||
[\#4738](https://github.com/matrix-org/matrix-react-sdk/pull/4738)
|
||||
* Use IDestroyable instead of IDisposable
|
||||
[\#4739](https://github.com/matrix-org/matrix-react-sdk/pull/4739)
|
||||
* Add/improve badge counts in new room list
|
||||
[\#4734](https://github.com/matrix-org/matrix-react-sdk/pull/4734)
|
||||
* Convert FormattingUtils to TypeScript and add badge utility function
|
||||
[\#4732](https://github.com/matrix-org/matrix-react-sdk/pull/4732)
|
||||
* Add filtering and exploring to the new room list
|
||||
[\#4736](https://github.com/matrix-org/matrix-react-sdk/pull/4736)
|
||||
* Support prioritized room list filters
|
||||
[\#4737](https://github.com/matrix-org/matrix-react-sdk/pull/4737)
|
||||
* Clean up font scaling appearance
|
||||
[\#4733](https://github.com/matrix-org/matrix-react-sdk/pull/4733)
|
||||
* Add user menu to new room list
|
||||
[\#4722](https://github.com/matrix-org/matrix-react-sdk/pull/4722)
|
||||
* New room list basic styling and layout
|
||||
[\#4711](https://github.com/matrix-org/matrix-react-sdk/pull/4711)
|
||||
* Fix read receipt overlap
|
||||
[\#4727](https://github.com/matrix-org/matrix-react-sdk/pull/4727)
|
||||
* Load correct default font size
|
||||
[\#4726](https://github.com/matrix-org/matrix-react-sdk/pull/4726)
|
||||
* send state of lowBandwidth in rageshakes
|
||||
[\#4724](https://github.com/matrix-org/matrix-react-sdk/pull/4724)
|
||||
* Change internal font size from from 15 to 10
|
||||
[\#4725](https://github.com/matrix-org/matrix-react-sdk/pull/4725)
|
||||
* Upgrade deps
|
||||
[\#4723](https://github.com/matrix-org/matrix-react-sdk/pull/4723)
|
||||
* Ensure active Jitsi conference is closed on widget pop-out
|
||||
[\#4444](https://github.com/matrix-org/matrix-react-sdk/pull/4444)
|
||||
* Introduce sticky rooms to the new room list
|
||||
[\#4720](https://github.com/matrix-org/matrix-react-sdk/pull/4720)
|
||||
* Handle remaining cases for room updates in new room list
|
||||
[\#4721](https://github.com/matrix-org/matrix-react-sdk/pull/4721)
|
||||
* Allow searching the emoji picker using other emoji
|
||||
[\#4719](https://github.com/matrix-org/matrix-react-sdk/pull/4719)
|
||||
* New room list scrolling and resizing
|
||||
[\#4697](https://github.com/matrix-org/matrix-react-sdk/pull/4697)
|
||||
* Don't show FormatBar if composer is empty
|
||||
[\#4696](https://github.com/matrix-org/matrix-react-sdk/pull/4696)
|
||||
* Split the left panel into new and old for new room list designs
|
||||
[\#4687](https://github.com/matrix-org/matrix-react-sdk/pull/4687)
|
||||
* Fix compact layout regression
|
||||
[\#4712](https://github.com/matrix-org/matrix-react-sdk/pull/4712)
|
||||
* fix emoji in safari
|
||||
[\#4710](https://github.com/matrix-org/matrix-react-sdk/pull/4710)
|
||||
* Fix not being able to dismiss new login toasts
|
||||
[\#4709](https://github.com/matrix-org/matrix-react-sdk/pull/4709)
|
||||
* Fix exceptions from Tooltip
|
||||
[\#4708](https://github.com/matrix-org/matrix-react-sdk/pull/4708)
|
||||
* Stop removing variation selector from quick reactions
|
||||
[\#4707](https://github.com/matrix-org/matrix-react-sdk/pull/4707)
|
||||
* Tidy up continuation algorithm and make it work for hidden profile changes
|
||||
[\#4704](https://github.com/matrix-org/matrix-react-sdk/pull/4704)
|
||||
* Profile settings should never show a disambiguated display name
|
||||
[\#4699](https://github.com/matrix-org/matrix-react-sdk/pull/4699)
|
||||
* Prevent (double) 4S bootstrap from RestoreKeyBackupDialog
|
||||
[\#4701](https://github.com/matrix-org/matrix-react-sdk/pull/4701)
|
||||
* Stop checkbox styling bleeding through room address selector
|
||||
[\#4691](https://github.com/matrix-org/matrix-react-sdk/pull/4691)
|
||||
* Center HeaderButtons
|
||||
[\#4695](https://github.com/matrix-org/matrix-react-sdk/pull/4695)
|
||||
* Add .well-known option to control default e2ee behaviour
|
||||
[\#4605](https://github.com/matrix-org/matrix-react-sdk/pull/4605)
|
||||
* Add max-width to right and left panels
|
||||
[\#4692](https://github.com/matrix-org/matrix-react-sdk/pull/4692)
|
||||
* Fix login loop where the sso flow returns to `#/login`
|
||||
[\#4685](https://github.com/matrix-org/matrix-react-sdk/pull/4685)
|
||||
* Don't clear MAU toasts when a successful sync comes in
|
||||
[\#4690](https://github.com/matrix-org/matrix-react-sdk/pull/4690)
|
||||
* Add initial filtering support to new room list
|
||||
[\#4681](https://github.com/matrix-org/matrix-react-sdk/pull/4681)
|
||||
* Bubble up a decline-to-render of verification events to outside wrapper
|
||||
[\#4664](https://github.com/matrix-org/matrix-react-sdk/pull/4664)
|
||||
* upgrade to twemoji 13.0.0
|
||||
[\#4672](https://github.com/matrix-org/matrix-react-sdk/pull/4672)
|
||||
* Apply FocusLock to ImageView to capture Escape handling
|
||||
[\#4666](https://github.com/matrix-org/matrix-react-sdk/pull/4666)
|
||||
* Fix the 'complete security' screen
|
||||
[\#4689](https://github.com/matrix-org/matrix-react-sdk/pull/4689)
|
||||
* add null-guard for Autocomplete containerRef
|
||||
[\#4688](https://github.com/matrix-org/matrix-react-sdk/pull/4688)
|
||||
* Remove legacy codepaths for Unknown Device Error (UDE/UDD) handling
|
||||
[\#4660](https://github.com/matrix-org/matrix-react-sdk/pull/4660)
|
||||
* Remove feature_cross_signing
|
||||
[\#4655](https://github.com/matrix-org/matrix-react-sdk/pull/4655)
|
||||
* Autocomplete: use scrollIntoView for auto-scroll to fix it
|
||||
[\#4670](https://github.com/matrix-org/matrix-react-sdk/pull/4670)
|
||||
|
||||
Changes in [2.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.2) (2020-06-16)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.1...v2.7.2)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "2.7.2",
|
||||
"version": "2.8.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -428,6 +428,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
border-radius: 8px;
|
||||
padding: 0px;
|
||||
box-shadow: none;
|
||||
|
||||
/* Don't show scroll-bars on spinner dialogs */
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
// TODO: Review mx_GeneralButton usage to see if it can use a different class
|
||||
|
@ -596,14 +600,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_optionList {
|
||||
// the notFirst class is for cases where the optionList might be under a header of sorts.
|
||||
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
|
||||
// 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.
|
||||
|
@ -634,7 +638,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 20px 0 0;
|
||||
padding: 12px 0 0;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
text-decoration: none;
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_TopLeftMenuButton.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_UserMenuButton.scss";
|
||||
@import "./structures/_UserMenu.scss";
|
||||
@import "./structures/_ViewSource.scss";
|
||||
@import "./structures/auth/_CompleteSecurity.scss";
|
||||
@import "./structures/auth/_Login.scss";
|
||||
|
|
|
@ -38,6 +38,12 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
// TagPanel handles its own CSS
|
||||
}
|
||||
|
||||
&:not(.mx_LeftPanel2_hasTagPanel) {
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
|
@ -48,13 +54,13 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
flex-direction: column;
|
||||
|
||||
.mx_LeftPanel2_userHeader {
|
||||
padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom
|
||||
padding: 12px 12px 20px; // 12px 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
|
||||
// This is basically just breadcrumbs. The row above that is handled by the UserMenu
|
||||
.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.
|
||||
|
@ -62,22 +68,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
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 {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
@ -143,21 +133,16 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
min-width: unset;
|
||||
|
||||
// We have to forcefully set the width to override the resizer's style attribute.
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
&.mx_LeftPanel2_hasTagPanel {
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
}
|
||||
&:not(.mx_LeftPanel2_hasTagPanel) {
|
||||
width: 68px !important;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
width: 68px;
|
||||
|
||||
.mx_LeftPanel2_userHeader {
|
||||
.mx_LeftPanel2_headerRow {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_userAvatarContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_filterContainer {
|
||||
// Organize the flexbox into a centered column layout
|
||||
flex-direction: column;
|
||||
|
|
192
res/css/structures/_UserMenu.scss
Normal file
192
res/css/structures/_UserMenu.scss
Normal file
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
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_UserMenu {
|
||||
.mx_UserMenu_headerButtons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/more-horizontal.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
|
||||
|
||||
.mx_UserMenu_userAvatar {
|
||||
border-radius: 32px; // should match avatar size
|
||||
}
|
||||
}
|
||||
|
||||
.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_minimized {
|
||||
.mx_UserMenu_userHeader {
|
||||
.mx_UserMenu_row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_UserMenu_userAvatarContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenu {
|
||||
width: 247px;
|
||||
|
||||
.mx_UserMenu_contextMenu_redRow {
|
||||
.mx_AccessibleButton {
|
||||
color: $warning-color !important; // !important to override styles from context menu
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_icon::before {
|
||||
background-color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenu_header {
|
||||
// Create a flexbox to organize the header a bit easier
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_UserMenu_contextMenu_name {
|
||||
// Create another flexbox of columns to handle large user IDs
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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_UserMenu_contextMenu_displayName {
|
||||
font-weight: bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenu_userId {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_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_IconizedContextMenu_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconHome::before {
|
||||
mask-image: url('$(res)/img/feather-customised/home.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconBell::before {
|
||||
mask-image: url('$(res)/img/feather-customised/notifications.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconLock::before {
|
||||
mask-image: url('$(res)/img/feather-customised/lock.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconSettings::before {
|
||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconArchive::before {
|
||||
mask-image: url('$(res)/img/feather-customised/archive.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconMessage::before {
|
||||
mask-image: url('$(res)/img/feather-customised/message-circle.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconSignOut::before {
|
||||
mask-image: url('$(res)/img/feather-customised/sign-out.svg');
|
||||
}
|
||||
}
|
|
@ -1,82 +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.
|
||||
*/
|
||||
|
||||
.mx_UserMenuButton {
|
||||
// No special styles on the button itself
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu {
|
||||
width: 247px;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.mx_InlineSpinner img {
|
||||
.mx_InlineSpinner_spin img {
|
||||
margin: 0px 6px;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,16 @@ limitations under the License.
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_Spinner_spin img {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MatrixChat_middlePanel .mx_Spinner {
|
||||
height: auto;
|
||||
}
|
||||
|
|
|
@ -77,8 +77,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
&:checked:disabled + label > .mx_Checkbox_background {
|
||||
background-color: $muted-fg-color;
|
||||
border-color: rgba($muted-fg-color, 0.5);
|
||||
background-color: $accent-color;
|
||||
border-color: $accent-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -354,6 +354,11 @@ limitations under the License.
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon_unauthenticated {
|
||||
background-image: url('$(res)/img/e2e/normal.svg');
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -121,11 +121,6 @@ $irc-line-height: $font-18px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_line .mx_MessageActionBar,
|
||||
.mx_EventTile_line .mx_ReplyThread_wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_EventTile_reply {
|
||||
order: 4;
|
||||
}
|
||||
|
|
|
@ -22,10 +22,12 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 12px; // so we're not up against the search/filter
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer {
|
||||
// Create a flexbox to make alignment easy
|
||||
display: flex;
|
||||
|
@ -83,23 +85,30 @@ limitations under the License.
|
|||
// ***************************
|
||||
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
opacity: 0.8;
|
||||
width: 16px;
|
||||
margin-right: 5px; // aligns with the room tile's badge
|
||||
|
||||
// Create another flexbox row because it's super easy to position the badge this way.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// Apply the width and margin to the badge so the container doesn't occupy dead space
|
||||
.mx_NotificationBadge {
|
||||
width: 16px;
|
||||
margin-left: 8px; // same as menu+aux buttons
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
.mx_NotificationBadge {
|
||||
margin-right: 4px; // just to push it over a bit, aligning it with the other elements
|
||||
}
|
||||
}
|
||||
|
||||
// Both of these buttons are hidden by default until the list is hovered
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
margin-left: 8px; // should be the same as the notification badge
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 32px;
|
||||
|
||||
&::before {
|
||||
|
@ -116,6 +125,13 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// Hide the menu button by default
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||
}
|
||||
|
@ -128,9 +144,9 @@ limitations under the License.
|
|||
flex: 1;
|
||||
max-width: calc(100% - 16px); // 16px is the badge width
|
||||
text-transform: uppercase;
|
||||
opacity: 0.5;
|
||||
line-height: $font-16px;
|
||||
font-size: $font-12px;
|
||||
font-weight: 600;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
|
@ -140,11 +156,9 @@ limitations under the License.
|
|||
.mx_RoomSublist2_collapseBtn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
// Default hidden
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
@ -156,7 +170,7 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
background-color: $primary-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
|
||||
|
@ -224,6 +238,16 @@ limitations under the License.
|
|||
.mx_RoomSublist2_showLessButtonChevron {
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
|
||||
}
|
||||
|
||||
&.mx_RoomSublist2_isCutting::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
box-shadow: 0px -2px 3px rgba(46, 47, 50, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
// Class name comes from the ResizableBox component
|
||||
|
@ -231,69 +255,34 @@ limitations under the License.
|
|||
// so that selector is below and one level higher.
|
||||
.react-resizable-handle {
|
||||
cursor: ns-resize;
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
|
||||
// Update RESIZE_HANDLE_HEIGHT if this changes
|
||||
height: 4px;
|
||||
|
||||
// This is positioned directly below the 'show more' button.
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
// This is to visually align the bar in the list. Should be 12px from
|
||||
// either side of the list. We define this after the positioning to
|
||||
// trick the browser.
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
// Together, these make the bar 64px wide
|
||||
left: calc(50% - 32px);
|
||||
right: calc(50% - 32px);
|
||||
}
|
||||
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.react-resizable-handle {
|
||||
opacity: 0.8;
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The aforementioned selector for the hover state.
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.react-resizable-handle {
|
||||
opacity: 0.2;
|
||||
|
||||
// Update the render() function for RoomSublist2 if this changes
|
||||
border: 2px solid $primary-fg-color;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer {
|
||||
// If the header doesn't have an aux button we still need to hide the badge for
|
||||
// the menu button.
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
// The menu button will be the rightmost button, so make it correctly aligned.
|
||||
.mx_RoomSublist2_menuButton {
|
||||
margin-right: 1px; // line it up with the badges on the room tiles
|
||||
}
|
||||
}
|
||||
|
||||
// Both of these buttons have circled backgrounds and are visible at this point,
|
||||
// so make them so.
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 16px;
|
||||
visibility: visible;
|
||||
background-color: $roomlist2-button-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer {
|
||||
.mx_RoomSublist2_headerText {
|
||||
.mx_RoomSublist2_collapseBtn {
|
||||
visibility: visible;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
&.mx_RoomSublist2_hasMenuOpen,
|
||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: visible;
|
||||
width: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,18 +293,18 @@ limitations under the License.
|
|||
position: relative;
|
||||
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
order: 1;
|
||||
order: 0;
|
||||
align-self: flex-end;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerText {
|
||||
order: 2;
|
||||
.mx_RoomSublist2_stickable {
|
||||
order: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton {
|
||||
order: 4;
|
||||
order: 2;
|
||||
visibility: visible;
|
||||
width: 32px !important; // !important to override hover styles
|
||||
height: 32px !important; // !important to override hover styles
|
||||
|
@ -342,7 +331,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.mx_RoomSublist2_hasMenuOpen,
|
||||
& > .mx_RoomSublist2_headerContainer:hover {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
|
@ -363,7 +357,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
&.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
|
||||
}
|
||||
|
@ -372,27 +366,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// We have a hover style on the room list with no specific list hovered, so account for that
|
||||
.mx_RoomList2:hover .mx_RoomSublist2:not(.mx_RoomSublist2_minimized),
|
||||
.mx_RoomSublist2_hasMenuOpen:not(.mx_RoomSublist2_minimized) {
|
||||
.mx_RoomSublist2_headerContainer_withAux {
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton {
|
||||
// Show the aux button, but not the list button
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 1px; // line it up with the badges on the room tiles
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu {
|
||||
padding: 20px 16px;
|
||||
width: 250px;
|
||||
|
@ -402,6 +375,7 @@ limitations under the License.
|
|||
margin-bottom: 16px;
|
||||
margin-right: 16px; // additional 16px
|
||||
border: 1px solid $roomsublist2-divider-color;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu_title {
|
||||
|
|
|
@ -67,7 +67,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mx_RoomTile2_messagePreview {
|
||||
|
@ -193,10 +193,6 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/arrow-down.svg');
|
||||
}
|
||||
|
||||
.mx_RoomTile2_iconUser::before {
|
||||
mask-image: url('$(res)/img/feather-customised/user.svg');
|
||||
}
|
||||
|
||||
.mx_RoomTile2_iconSettings::before {
|
||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||
}
|
||||
|
|
|
@ -16,11 +16,14 @@ limitations under the License.
|
|||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider,
|
||||
.mx_AppearanceUserSettingsTab_fontSlider_preview,
|
||||
.mx_AppearanceUserSettingsTab_Layout,
|
||||
.mx_AppearanceUserSettingsTab .mx_Field {
|
||||
.mx_AppearanceUserSettingsTab_Layout {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab .mx_Field {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontScaling {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
@ -204,3 +207,20 @@ limitations under the License.
|
|||
background-color: rgba($accent-color, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_Advanced {
|
||||
color: $primary-fg-color;
|
||||
|
||||
> * {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_AdvancedToggle {
|
||||
color: $accent-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_systemFont {
|
||||
margin-left: calc($font-16px + 10px);
|
||||
}
|
||||
}
|
||||
|
|
3
res/img/spinner.svg
Normal file
3
res/img/spinner.svg
Normal 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="M5.60236 3.67346C3.10764 5.59313 1.5 8.60882 1.5 12C1.5 17.799 6.20101 22.5 12 22.5C17.799 22.5 22.5 17.799 22.5 12C22.5 8.6452 20.9267 5.65787 18.4776 3.73562L17.7648 4.44842C20.0354 6.18437 21.5 8.92114 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C6.75329 21.5 2.5 17.2467 2.5 12C2.5 8.88471 3.9995 6.11966 6.31612 4.38722L5.60236 3.67346Z" fill="#03b381"/>
|
||||
</svg>
|
After Width: | Height: | Size: 508 B |
|
@ -113,7 +113,7 @@ $theme-button-bg-color: #e3e8f0;
|
|||
$roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
||||
$roomlist2-bg-color: $header-panel-bg-color;
|
||||
|
||||
$roomsublist2-divider-color: #e9eaeb;
|
||||
$roomsublist2-divider-color: $primary-fg-color;
|
||||
|
||||
$roomtile2-preview-color: #9e9e9e;
|
||||
$roomtile2-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$font-family: var(--font-family, $font-family);
|
||||
$monospace-font-family: var(--font-family-monospace, $monospace-font-family);
|
||||
//
|
||||
// --accent-color
|
||||
$accent-color: var(--accent-color);
|
||||
|
|
|
@ -180,7 +180,7 @@ $theme-button-bg-color: #e3e8f0;
|
|||
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
||||
$roomlist2-bg-color: $header-panel-bg-color;
|
||||
|
||||
$roomsublist2-divider-color: #e9eaeb;
|
||||
$roomsublist2-divider-color: $primary-fg-color;
|
||||
|
||||
$roomtile2-preview-color: #9e9e9e;
|
||||
$roomtile2-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -25,8 +25,8 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
|||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
|
||||
export const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
export const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
|
@ -221,7 +221,7 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
return url;
|
||||
|
@ -235,9 +235,9 @@ export default abstract class BasePlatform {
|
|||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
|
||||
// persist hs url and is url for when the user is returned to the app with the login token
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
|
|
|
@ -25,6 +25,7 @@ import RoomViewStore from "./stores/RoomViewStore";
|
|||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {Capability} from "./widgets/WidgetApi";
|
||||
import {objectClone} from "./utils/objects";
|
||||
|
||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
|
@ -247,7 +248,7 @@ export default class FromWidgetPostMessageApi {
|
|||
* @param {Object} res Response data
|
||||
*/
|
||||
sendResponse(event, res) {
|
||||
const data = JSON.parse(JSON.stringify(event.data));
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
@ -260,7 +261,7 @@ export default class FromWidgetPostMessageApi {
|
|||
*/
|
||||
sendError(event, msg, nestedError) {
|
||||
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
||||
const data = JSON.parse(JSON.stringify(event.data));
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
error: {
|
||||
message: msg,
|
||||
|
|
|
@ -41,7 +41,10 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -164,8 +167,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const homeserver = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
|
||||
if (!homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
return Promise.resolve(false);
|
||||
|
|
|
@ -122,7 +122,7 @@ const Notifier = {
|
|||
}
|
||||
},
|
||||
|
||||
getSoundForRoom: async function(roomId) {
|
||||
getSoundForRoom: function(roomId) {
|
||||
// We do no caching here because the SDK caches setting
|
||||
// and the browser will cache the sound.
|
||||
const content = SettingsStore.getValue("notificationSound", roomId);
|
||||
|
@ -151,7 +151,7 @@ const Notifier = {
|
|||
},
|
||||
|
||||
_playAudioNotification: async function(ev, room) {
|
||||
const sound = await this.getSoundForRoom(room.roomId);
|
||||
const sound = this.getSoundForRoom(room.roomId);
|
||||
console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`);
|
||||
|
||||
try {
|
||||
|
|
|
@ -56,10 +56,11 @@ export function countRoomsWithNotif(rooms) {
|
|||
}
|
||||
|
||||
export function aggregateNotificationCount(rooms) {
|
||||
return rooms.reduce((result, room, index) => {
|
||||
return rooms.reduce((result, room) => {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
// use helper method to include highlights in the previous version of the room
|
||||
const notificationCount = getUnreadNotificationCount(room);
|
||||
|
||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||
|
|
|
@ -244,16 +244,17 @@ import RoomViewStore from './stores/RoomViewStore';
|
|||
import { _t } from './languageHandler';
|
||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import {WidgetType} from "./widgets/WidgetType";
|
||||
import {objectClone} from "./utils/objects";
|
||||
|
||||
function sendResponse(event, res) {
|
||||
const data = JSON.parse(JSON.stringify(event.data));
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
function sendError(event, msg, nestedError) {
|
||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||
const data = JSON.parse(JSON.stringify(event.data));
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
error: {
|
||||
message: msg,
|
||||
|
|
|
@ -265,22 +265,13 @@ function textForServerACLEvent(ev) {
|
|||
return text + changes.join(" ");
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev, skipUserPrefix) {
|
||||
function textForMessageEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (skipUserPrefix) {
|
||||
message = ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('sent an image.');
|
||||
}
|
||||
} else {
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
@ -621,8 +612,8 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateHandlers[evType] = textForMjolnirEvent;
|
||||
}
|
||||
|
||||
export function textForEvent(ev, skipUserPrefix) {
|
||||
export function textForEvent(ev) {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
if (handler) return handler(ev, skipUserPrefix);
|
||||
if (handler) return handler(ev);
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -15,21 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import TagPanel from "./TagPanel";
|
||||
import classNames from "classnames";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import SearchBox from "./SearchBox";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import UserMenuButton from "./UserMenuButton";
|
||||
import UserMenu from "./UserMenu";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -41,14 +41,19 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
|||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchFilter: string; // TODO: Move search into room list?
|
||||
showBreadcrumbs: boolean;
|
||||
showTagPanel: boolean;
|
||||
}
|
||||
|
||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private tagPanelWatcherRef: string;
|
||||
|
||||
// TODO: Properly support TagPanel
|
||||
// TODO: Properly support searching/filtering
|
||||
// TODO: Properly support breadcrumbs
|
||||
|
@ -62,13 +67,23 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
this.state = {
|
||||
searchFilter: "",
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
||||
};
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||
});
|
||||
|
||||
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
||||
// We listen to the noisy channel to avoid choppy reaction times.
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
private onSearch = (term: string): void => {
|
||||
|
@ -86,9 +101,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
// TODO: Apply this on resize, init, etc for reliability
|
||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
const list = ev.target as HTMLDivElement;
|
||||
private handleStickyHeaders(list: HTMLDivElement) {
|
||||
const rlRect = list.getBoundingClientRect();
|
||||
const bottom = rlRect.bottom;
|
||||
const top = rlRect.top;
|
||||
|
@ -123,6 +136,17 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
header.style.top = `unset`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Apply this on resize, init, etc for reliability
|
||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
const list = ev.target as HTMLDivElement;
|
||||
this.handleStickyHeaders(list);
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||
this.handleStickyHeaders(this.listContainerRef.current);
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
|
@ -130,16 +154,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
// 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;
|
||||
}
|
||||
|
||||
let breadcrumbs;
|
||||
if (this.state.showBreadcrumbs) {
|
||||
|
@ -150,34 +164,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let name = <span className="mx_LeftPanel2_userName">{displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_LeftPanel2_headerButtons">
|
||||
<UserMenuButton />
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
|
||||
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>
|
||||
{name}
|
||||
{buttons}
|
||||
</div>
|
||||
<UserMenu isMinimized={this.props.isMinimized} />
|
||||
{breadcrumbs}
|
||||
</div>
|
||||
);
|
||||
|
@ -200,7 +189,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const tagPanel = (
|
||||
const tagPanel = !this.state.showTagPanel ? null : (
|
||||
<div className="mx_LeftPanel2_tagPanelContainer">
|
||||
<TagPanel/>
|
||||
</div>
|
||||
|
@ -221,6 +210,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
|
||||
const containerClasses = classNames({
|
||||
"mx_LeftPanel2": true,
|
||||
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
|
||||
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
||||
});
|
||||
|
||||
|
@ -230,9 +220,11 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
<aside className="mx_LeftPanel2_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}>
|
||||
{roomList}
|
||||
</div>
|
||||
<div
|
||||
className="mx_LeftPanel2_actualRoomListContainer"
|
||||
onScroll={this.onScroll}
|
||||
ref={this.listContainerRef}
|
||||
>{roomList}</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -123,7 +123,7 @@ interface IState {
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
class LoggedInView extends React.PureComponent<IProps, IState> {
|
||||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
static displayName = 'LoggedInView';
|
||||
|
||||
static propTypes = {
|
||||
|
@ -677,7 +677,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||
// TODO: Supply props like collapsed and disabled to LeftPanel2
|
||||
leftPanel = (
|
||||
<LeftPanel2 isMinimized={this.props.collapseLhs || false} />
|
||||
<LeftPanel2
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
// @ts-ignore - XXX: no idea why this import fails
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
@ -1612,6 +1614,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
} else if (screen === 'directory') {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
} else if (screen === "start_sso" || screen === "start_cas") {
|
||||
// TODO if logged in, skip SSO
|
||||
let cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
cli = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const type = screen === "start_sso" ? "sso" : "cas";
|
||||
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
|
||||
} else if (screen === 'groups') {
|
||||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
|
@ -1828,7 +1843,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
updateStatusIndicator(state: string, prevState: string) {
|
||||
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count;
|
||||
// only count visible rooms to not torment the user with notification counts in rooms they can't see
|
||||
// it will include highlights from the previous version of the room internally
|
||||
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getVisibleRooms()).count;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
||||
|
@ -1913,9 +1930,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.onLoggedIn();
|
||||
};
|
||||
|
||||
render() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
getFragmentAfterLogin() {
|
||||
let fragmentAfterLogin = "";
|
||||
if (this.props.initialScreenAfterLogin &&
|
||||
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
|
||||
|
@ -1923,7 +1938,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
) {
|
||||
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
|
||||
}
|
||||
return fragmentAfterLogin;
|
||||
}
|
||||
|
||||
render() {
|
||||
const fragmentAfterLogin = this.getFragmentAfterLogin();
|
||||
let view;
|
||||
|
||||
if (this.state.view === Views.LOADING) {
|
||||
|
@ -2002,7 +2021,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
} else if (this.state.view === Views.WELCOME) {
|
||||
const Welcome = sdk.getComponent('auth.Welcome');
|
||||
view = <Welcome {...this.getServerProperties()} fragmentAfterLogin={fragmentAfterLogin} />;
|
||||
view = <Welcome />;
|
||||
} else if (this.state.view === Views.REGISTER) {
|
||||
const Registration = sdk.getComponent('structures.auth.Registration');
|
||||
view = (
|
||||
|
|
332
src/components/structures/UserMenu.tsx
Normal file
332
src/components/structures/UserMenu.tsx
Normal file
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
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 { 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, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
menuDisplayed: boolean;
|
||||
isDarkTheme: boolean;
|
||||
}
|
||||
|
||||
export default class UserMenu 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,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
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);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme.startsWith("custom-")) {
|
||||
return getCustomTheme(theme.substring("custom-".length)).is_dark;
|
||||
}
|
||||
return theme === "dark";
|
||||
}
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// the store triggered an update, so force a layout update. We don't
|
||||
// have any state to store here for that to magically happen.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
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 = (ev: InputEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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.DEVICE, newTheme); // set at same level as Appearance tab
|
||||
};
|
||||
|
||||
private onSettingsOpen = (ev: ButtonEvent, 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: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view (deferred)
|
||||
console.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.menuDisplayed) return null;
|
||||
|
||||
let hostingLink;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
<div className="mx_UserMenu_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>
|
||||
);
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
if (this.hasHomePage) {
|
||||
homeButton = (
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onHomeClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
|
||||
<span>{_t("Home")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const elementRect = this.buttonRef.current.getBoundingClientRect();
|
||||
return (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.width + elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
|
||||
<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
|
||||
className="mx_UserMenu_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_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||
<ul>
|
||||
{homeButton}
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
|
||||
<span>{_t("Notification settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
|
||||
<span>{_t("Security & privacy")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
|
||||
<span>{_t("All settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onShowArchived}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
|
||||
<span>{_t("Archived rooms")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onProvideFeedback}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
|
||||
<span>{_t("Feedback")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mx_IconizedContextMenu_optionList">
|
||||
<ul>
|
||||
<li className="mx_UserMenu_contextMenu_redRow">
|
||||
<AccessibleButton onClick={this.onSignOutClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
|
||||
<span>{_t("Sign out")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const avatarSize = 32; // should match border-radius of the avatar
|
||||
|
||||
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("Account settings")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
||||
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{name}
|
||||
{buttons}
|
||||
</div>
|
||||
{this.renderContextMenu()}
|
||||
</ContextMenuButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,296 +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 * 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, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
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("custom-".length)).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: ButtonEvent, 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: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view (deferred)
|
||||
console.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
if (this.hasHomePage) {
|
||||
homeButton = (
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onHomeClick}>
|
||||
<img src={require("../../../res/img/feather-customised/home.svg")} width={16} />
|
||||
<span>{_t("Home")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const elementRect = this.buttonRef.current.getBoundingClientRect();
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_IconizedContextMenu 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_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||
<ul>
|
||||
{homeButton}
|
||||
<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_IconizedContextMenu_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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -158,8 +158,8 @@ export default class SoftLogout extends React.Component {
|
|||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
|
|
|
@ -18,9 +18,7 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import {_td} from "../../../languageHandler";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
@ -39,15 +37,6 @@ export default class Welcome extends React.PureComponent {
|
|||
pageUrl = 'welcome.html';
|
||||
}
|
||||
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
const tmpClient = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
const plaf = PlatformPeg.get();
|
||||
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
|
||||
this.props.fragmentAfterLogin);
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
|
@ -55,8 +44,8 @@ export default class Welcome extends React.PureComponent {
|
|||
className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
replaceMap={{
|
||||
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
|
||||
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
|
||||
"$riot:ssoUrl": "#/start_sso",
|
||||
"$riot:casUrl": "#/start_cas",
|
||||
}}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
|
|
|
@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import Spinner from './Spinner';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
@ -741,7 +741,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
<MessageSpinner msg='Loading...' />
|
||||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (!this.state.hasPermissionToLoad) {
|
||||
|
|
|
@ -54,6 +54,8 @@ interface IProps {
|
|||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode;
|
||||
// If specified the tooltip will be shown regardless of feedback
|
||||
forceTooltipVisible?: boolean;
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string;
|
||||
|
@ -242,10 +244,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let fieldTooltip;
|
||||
if (tooltipContent || this.state.feedback) {
|
||||
const addlClassName = tooltipClassName ? tooltipClassName : '';
|
||||
fieldTooltip = <Tooltip
|
||||
tooltipClassName={`mx_Field_tooltip ${addlClassName}`}
|
||||
visible={this.state.feedbackVisible}
|
||||
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
|
||||
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
|
||||
label={tooltipContent || this.state.feedback}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InlineSpinner',
|
||||
|
@ -25,9 +27,25 @@ export default createReactClass({
|
|||
const h = this.props.h || 16;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
|
||||
let divClass;
|
||||
let imageSource;
|
||||
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
|
||||
divClass = "mx_InlineSpinner mx_Spinner_spin";
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
divClass = "mx_InlineSpinner";
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
<div className={divClass}>
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClass}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations 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 from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MessageSpinner',
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -30,6 +30,7 @@ interface IProps {
|
|||
isExplicit?: boolean;
|
||||
// XXX: once design replaces all toggles make this the default
|
||||
useCheckbox?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -78,14 +79,23 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
else label = _t(label);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={!canChange} >
|
||||
return <StyledCheckbox
|
||||
checked={this.state.value}
|
||||
onChange={this.checkBoxOnChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
>
|
||||
{label}
|
||||
</StyledCheckbox>;
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<span className="mx_SettingsFlag_label">{label}</span>
|
||||
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
|
||||
<ToggleSwitch
|
||||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,19 +16,39 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from "prop-types";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Spinner',
|
||||
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
|
||||
let divClass;
|
||||
let imageSource;
|
||||
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
|
||||
divClass = "mx_Spinner mx_Spinner_spin";
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
divClass = "mx_Spinner";
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className={divClass}>
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div> </React.Fragment> }
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClassName}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Spinner.propTypes = {
|
||||
w: PropTypes.number,
|
||||
h: PropTypes.number,
|
||||
imgClassName: PropTypes.string,
|
||||
message: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
61
src/components/views/elements/StyledRadioGroup.tsx
Normal file
61
src/components/views/elements/StyledRadioGroup.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
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 from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import StyledRadioButton from "./StyledRadioButton";
|
||||
|
||||
interface IDefinition<T extends string> {
|
||||
value: T;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label: React.ReactChild;
|
||||
description?: React.ReactChild;
|
||||
}
|
||||
|
||||
interface IProps<T extends string> {
|
||||
name: string;
|
||||
className?: string;
|
||||
definitions: IDefinition<T>[];
|
||||
value?: T; // if not provided no options will be selected
|
||||
onChange(newValue: T);
|
||||
}
|
||||
|
||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, onChange}: IProps<T>) {
|
||||
const _onChange = e => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
return <React.Fragment>
|
||||
{definitions.map(d => <React.Fragment>
|
||||
<StyledRadioButton
|
||||
key={d.value}
|
||||
className={classNames(className, d.className)}
|
||||
onChange={_onChange}
|
||||
checked={d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={d.disabled}
|
||||
>
|
||||
{d.label}
|
||||
</StyledRadioButton>
|
||||
{d.description}
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
export default StyledRadioGroup;
|
|
@ -22,6 +22,7 @@ import MFileBody from './MFileBody';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -94,7 +95,7 @@ export default class MAudioBody extends React.Component {
|
|||
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -365,12 +366,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <img
|
||||
src={require("../../../../res/img/spinner.gif")}
|
||||
alt={content.body}
|
||||
width="32"
|
||||
height="32"
|
||||
/>;
|
||||
placeholder = <InlineSpinner w={32} h={32} />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
|
|
|
@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -147,7 +148,7 @@ export default createReactClass({
|
|||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
|
||||
<InlineSpinner />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {formatFullDate} from "../../../DateUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -36,8 +38,12 @@ const RedactedBody = React.forwardRef<any, IProps>(({mxEvent}, ref) => {
|
|||
text = _t("Message deleted by %(name)s", { name: sender ? sender.name : redactedBecauseUserId });
|
||||
}
|
||||
|
||||
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||
const fullDate = formatFullDate(new Date(unsigned.redacted_because.origin_server_ts), showTwelveHour);
|
||||
const titleText = _t("Message deleted on %(date)s", { date: fullDate });
|
||||
|
||||
return (
|
||||
<span className="mx_RedactedBody" ref={ref}>
|
||||
<span className="mx_RedactedBody" ref={ref} title={titleText}>
|
||||
{ text }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -28,6 +28,7 @@ export const E2E_STATE = {
|
|||
WARNING: "warning",
|
||||
UNKNOWN: "unknown",
|
||||
NORMAL: "normal",
|
||||
UNAUTHENTICATED: "unauthenticated",
|
||||
};
|
||||
|
||||
const crossSigningUserTitles = {
|
||||
|
|
|
@ -313,35 +313,52 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// If we directly trust the device, short-circuit here
|
||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||
if (verified) {
|
||||
const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent);
|
||||
const senderId = mxEvent.getSender();
|
||||
const userTrust = this.context.checkUserTrust(senderId);
|
||||
|
||||
if (encryptionInfo.mismatchedSender) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
verified: E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) {
|
||||
if (!userTrust.isCrossSigningVerified()) {
|
||||
// user is not verified, so default to everything is normal
|
||||
this.setState({
|
||||
verified: E2E_STATE.NORMAL,
|
||||
}, this.props.onHeightChanged);
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent);
|
||||
const eventSenderTrust = this.context.checkDeviceTrust(
|
||||
senderId, encryptionInfo.sender.deviceId,
|
||||
);
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNKNOWN,
|
||||
}, this.props.onHeightChanged); // Decryption may have cause a change in size
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventSenderTrust.isVerified()) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionInfo.authenticated) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNAUTHENTICATED,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
},
|
||||
|
||||
|
@ -526,6 +543,8 @@ export default createReactClass({
|
|||
return; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||
return; // no icon for verified
|
||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
||||
return (<E2ePadlockUnauthenticated />);
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
|
@ -976,6 +995,12 @@ function E2ePadlockUnknown(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnauthenticated(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")} icon="unauthenticated" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
class E2ePadlock extends React.Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
|
|
|
@ -18,27 +18,24 @@ import React from "react";
|
|||
import classNames from "classnames";
|
||||
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
||||
import * as Unread from '../../../Unread';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { EventEmitter } from "events";
|
||||
import { arrayDiff } from "../../../utils/arrays";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
||||
|
||||
export const NOTIFICATION_STATE_UPDATE = "update";
|
||||
|
||||
export enum NotificationColor {
|
||||
// Inverted (None -> Red) because we do integer comparisons on this
|
||||
None, // nothing special
|
||||
Bold, // no badge, show as unread
|
||||
Bold, // no badge, show as unread // TODO: This goes away with new notification structures
|
||||
Grey, // unread notified messages
|
||||
Red, // unread pings
|
||||
}
|
||||
|
@ -53,18 +50,45 @@ interface IProps {
|
|||
notification: INotificationState;
|
||||
|
||||
/**
|
||||
* If true, the badge will conditionally display a badge without count for the user.
|
||||
* If true, the badge will show a count if at all possible. This is typically
|
||||
* used to override the user's preference for things like room sublists.
|
||||
*/
|
||||
allowNoCount: boolean;
|
||||
forceCount: boolean;
|
||||
|
||||
/**
|
||||
* The room ID, if any, the badge represents.
|
||||
*/
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||
}
|
||||
|
||||
export default class NotificationBadge extends React.PureComponent<IProps, IState> {
|
||||
private countWatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
||||
this.state = {
|
||||
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
||||
};
|
||||
|
||||
this.countWatcherRef = SettingsStore.watchSetting(
|
||||
"Notifications.alwaysShowBadgeCounts", this.roomId,
|
||||
this.countPreferenceChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private get roomId(): string {
|
||||
// We should convert this to null for safety with the SettingsStore
|
||||
return this.props.roomId || null;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
SettingsStore.unwatchSetting(this.countWatcherRef);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
|
@ -75,24 +99,34 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
|||
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
}
|
||||
|
||||
private countPreferenceChanged = () => {
|
||||
this.setState({showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId)});
|
||||
};
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// Don't show a badge if we don't need to
|
||||
if (this.props.notification.color <= NotificationColor.Bold) return null;
|
||||
if (this.props.notification.color <= NotificationColor.None) return null;
|
||||
|
||||
const hasNotif = this.props.notification.color >= NotificationColor.Red;
|
||||
const hasCount = this.props.notification.color >= NotificationColor.Grey;
|
||||
const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount");
|
||||
const hasUnread = this.props.notification.color >= NotificationColor.Bold;
|
||||
const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif;
|
||||
let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount);
|
||||
if (this.props.forceCount) {
|
||||
isEmptyBadge = false;
|
||||
if (!hasCount) return null; // Can't render a badge
|
||||
}
|
||||
|
||||
let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
|
||||
if (isEmptyBadge) symbol = "";
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': hasCount,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount,
|
||||
'mx_NotificationBadge_highlighted': hasNotif,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
||||
|
@ -107,14 +141,28 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
}
|
||||
|
||||
export class RoomNotificationState extends EventEmitter implements IDestroyable {
|
||||
export class StaticNotificationState extends EventEmitter implements INotificationState {
|
||||
constructor(public symbol: string, public count: number, public color: NotificationColor) {
|
||||
super();
|
||||
}
|
||||
|
||||
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
|
||||
return new StaticNotificationState(null, count, color);
|
||||
}
|
||||
|
||||
public static forSymbol(symbol: string, color: NotificationColor): StaticNotificationState {
|
||||
return new StaticNotificationState(symbol, 0, color);
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||
private _symbol: string;
|
||||
private _count: number;
|
||||
private _color: NotificationColor;
|
||||
|
||||
constructor(private room: Room) {
|
||||
super();
|
||||
this.room.on("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.receipt", this.handleReadReceipt);
|
||||
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||
|
@ -138,7 +186,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
|
@ -146,6 +194,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
}
|
||||
|
||||
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
||||
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
||||
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
|
@ -205,13 +259,38 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable
|
|||
}
|
||||
}
|
||||
|
||||
export class ListNotificationState extends EventEmitter implements IDestroyable {
|
||||
export class TagSpecificNotificationState extends RoomNotificationState {
|
||||
private static TAG_TO_COLOR: {
|
||||
// @ts-ignore - TS wants this to be a string key, but we know better
|
||||
[tagId: TagID]: NotificationColor,
|
||||
} = {
|
||||
[DefaultTagID.DM]: NotificationColor.Red,
|
||||
};
|
||||
|
||||
private readonly colorWhenNotIdle?: NotificationColor;
|
||||
|
||||
constructor(room: Room, tagId: TagID) {
|
||||
super(room);
|
||||
|
||||
const specificColor = TagSpecificNotificationState.TAG_TO_COLOR[tagId];
|
||||
if (specificColor) this.colorWhenNotIdle = specificColor;
|
||||
}
|
||||
|
||||
public get color(): NotificationColor {
|
||||
if (!this.colorWhenNotIdle) return super.color;
|
||||
|
||||
if (super.color !== NotificationColor.None) return this.colorWhenNotIdle;
|
||||
return super.color;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||
private _count: number;
|
||||
private _color: NotificationColor;
|
||||
private rooms: Room[] = [];
|
||||
private states: { [roomId: string]: RoomNotificationState } = {};
|
||||
|
||||
constructor(private byTileCount = false) {
|
||||
constructor(private byTileCount = false, private tagId: TagID) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
@ -246,7 +325,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
|
|||
state.destroy();
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = new RoomNotificationState(newRoom);
|
||||
const state = new TagSpecificNotificationState(newRoom, this.tagId);
|
||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
if (this.states[newRoom.roomId]) {
|
||||
// "Should never happen" disclaimer.
|
||||
|
@ -259,6 +338,12 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
|
|||
this.calculateTotalState();
|
||||
}
|
||||
|
||||
public getForRoom(room: Room) {
|
||||
const state = this.states[room.roomId];
|
||||
if (!state) throw new Error("Unknown room for notification state");
|
||||
return state;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const state of Object.values(this.states)) {
|
||||
state.destroy();
|
||||
|
|
|
@ -193,6 +193,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
components.push(
|
||||
<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
tagId={orderedTagId}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
|
|
|
@ -32,6 +32,7 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
|||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||
import { TagID } from "../../../stores/room-list/models";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -56,6 +57,7 @@ interface IProps {
|
|||
isInvite: boolean;
|
||||
layout: ListLayout;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
|
||||
// TODO: Collapsed state
|
||||
// TODO: Group invites
|
||||
|
@ -68,6 +70,7 @@ interface IProps {
|
|||
interface IState {
|
||||
notificationState: ListNotificationState;
|
||||
menuDisplayed: boolean;
|
||||
isResizing: boolean;
|
||||
}
|
||||
|
||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
|
@ -78,8 +81,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
notificationState: new ListNotificationState(this.props.isInvite),
|
||||
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
|
||||
menuDisplayed: false,
|
||||
isResizing: false,
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
}
|
||||
|
@ -109,13 +113,21 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onResizeStart = () => {
|
||||
this.setState({isResizing: true});
|
||||
};
|
||||
|
||||
private onResizeStop = () => {
|
||||
this.setState({isResizing: false});
|
||||
};
|
||||
|
||||
private onShowAllClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onShowLessClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.minVisibleTiles;
|
||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
|
@ -130,13 +142,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onUnreadFirstChanged = async () => {
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||
await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm);
|
||||
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
||||
};
|
||||
|
||||
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
||||
await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort);
|
||||
await RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
||||
};
|
||||
|
||||
private onMessagePreviewChanged = () => {
|
||||
|
@ -176,7 +188,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
key={`room-${room.roomId}`}
|
||||
showMessagePreview={this.props.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.layout.tagId}
|
||||
tag={this.props.tagId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -189,8 +201,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
let contextMenu = null;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
|
@ -204,14 +216,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<StyledRadioButton
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||
checked={!isAlphabetical}
|
||||
name={`mx_${this.props.layout.tagId}_sortBy`}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("Activity")}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
||||
checked={isAlphabetical}
|
||||
name={`mx_${this.props.layout.tagId}_sortBy`}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{_t("A-Z")}
|
||||
</StyledRadioButton>
|
||||
|
@ -267,7 +279,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
// TODO: Collapsed state
|
||||
|
||||
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
||||
const badge = <NotificationBadge forceCount={true} notification={this.state.notificationState}/>;
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
|
@ -291,7 +303,18 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
||||
});
|
||||
|
||||
const badgeContainer = (
|
||||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO: a11y (see old component)
|
||||
// Note: the addRoomButton conditionally gets moved around
|
||||
// the DOM depending on whether or not the list is minimized.
|
||||
// If we're minimized, we want it below the header so it
|
||||
// doesn't become sticky.
|
||||
// The same applies to the notification badge.
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className='mx_RoomSublist2_stickable'>
|
||||
|
@ -307,11 +330,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{this.renderMenu()}
|
||||
{addRoomButton}
|
||||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
</div>
|
||||
{this.props.isMinimized ? null : badgeContainer}
|
||||
{this.props.isMinimized ? null : addRoomButton}
|
||||
</div>
|
||||
{this.props.isMinimized ? badgeContainer : null}
|
||||
{this.props.isMinimized ? addRoomButton : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -343,6 +366,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
const nVisible = Math.floor(layout.visibleTiles);
|
||||
const visibleTiles = tiles.slice(0, nVisible);
|
||||
|
||||
const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length);
|
||||
const showMoreBtnClasses = classNames({
|
||||
'mx_RoomSublist2_showNButton': true,
|
||||
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
||||
});
|
||||
|
||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
|
@ -357,14 +386,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (this.props.isMinimized) showMoreText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showNButton'>
|
||||
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{showMoreText}
|
||||
</div>
|
||||
);
|
||||
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) {
|
||||
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.defaultVisibleTiles) {
|
||||
// we have all tiles visible - add a button to show less
|
||||
let showLessText = (
|
||||
<span className='mx_RoomSublist2_showNButtonText'>
|
||||
|
@ -373,7 +402,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (this.props.isMinimized) showLessText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowLessClick} className='mx_RoomSublist2_showNButton'>
|
||||
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
|
@ -419,6 +448,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
resizeHandles={handles}
|
||||
onResize={this.onResize}
|
||||
className="mx_RoomSublist2_resizeBox"
|
||||
onResizeStart={this.onResizeStart}
|
||||
onResizeStop={this.onResizeStop}
|
||||
>
|
||||
{visibleTiles}
|
||||
{showNButton}
|
||||
|
|
|
@ -26,11 +26,15 @@ import RoomAvatar from "../../views/avatars/RoomAvatar";
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge";
|
||||
import NotificationBadge, {
|
||||
INotificationState,
|
||||
NotificationColor,
|
||||
TagSpecificNotificationState
|
||||
} from "./NotificationBadge";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import RoomTileIcon from "./RoomTileIcon";
|
||||
|
||||
/*******************************************************************
|
||||
|
@ -79,7 +83,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: new RoomNotificationState(this.props.room),
|
||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
generalMenuDisplayed: false,
|
||||
};
|
||||
|
@ -131,11 +135,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (tagId === DefaultTagID.DM) {
|
||||
// TODO: DM Flagging
|
||||
} else {
|
||||
// TODO: XOR favourites and low priority
|
||||
}
|
||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
||||
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
||||
};
|
||||
|
||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
||||
|
@ -192,12 +193,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
<span>{_t("Low Priority")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser" />
|
||||
<span>{_t("Direct Chat")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||
|
@ -248,7 +243,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
'mx_RoomTile2_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
|
||||
const badge = (
|
||||
<NotificationBadge
|
||||
notification={this.state.notificationState}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
);
|
||||
|
||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||
let name = this.props.room.name;
|
||||
|
@ -261,7 +262,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
let messagePreview = null;
|
||||
if (this.props.showMessagePreview && !this.props.isMinimized) {
|
||||
// The preview store heavily caches this info, so should be safe to hammer.
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
|
||||
// Only show the preview if there is one to show.
|
||||
if (text) {
|
||||
|
|
|
@ -154,13 +154,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
}
|
||||
|
||||
// Whether the various keys exist on your account (but not necessarily
|
||||
// on this device).
|
||||
const enabledForAccount = (
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
);
|
||||
|
||||
let summarisedStatus;
|
||||
if (homeserverSupportsCrossSigning === undefined) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
|
@ -184,8 +177,19 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
)}</p>;
|
||||
}
|
||||
|
||||
const keysExistAnywhere = (
|
||||
secretStorageKeyInAccount ||
|
||||
crossSigningPrivateKeysInStorage ||
|
||||
crossSigningPublicKeysOnDevice
|
||||
);
|
||||
const keysExistEverywhere = (
|
||||
secretStorageKeyInAccount &&
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
crossSigningPublicKeysOnDevice
|
||||
);
|
||||
|
||||
let resetButton;
|
||||
if (enabledForAccount) {
|
||||
if (keysExistAnywhere) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
|
@ -197,10 +201,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
// TODO: determine how better to expose this to users in addition to prompts at login/toast
|
||||
let bootstrapButton;
|
||||
if (
|
||||
(!enabledForAccount || !crossSigningPublicKeysOnDevice) &&
|
||||
homeserverSupportsCrossSigning
|
||||
) {
|
||||
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
|
||||
bootstrapButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._onBootstrapClick}>
|
||||
|
|
|
@ -39,12 +39,11 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
Notifier.getSoundForRoom(this.props.roomId).then((soundData) => {
|
||||
if (!soundData) {
|
||||
return;
|
||||
}
|
||||
this.setState({currentSound: soundData.name || soundData.url});
|
||||
});
|
||||
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
||||
if (!soundData) {
|
||||
return;
|
||||
}
|
||||
this.setState({currentSound: soundData.name || soundData.url});
|
||||
|
||||
this._soundUpload = createRef();
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import {_t} from "../../../../../languageHandler";
|
|||
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import { enumerateThemes } from "../../../../../theme";
|
||||
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
|
||||
import Field from "../../../elements/Field";
|
||||
import Slider from "../../../elements/Slider";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
|
@ -32,7 +31,9 @@ import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
|||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import StyledCheckbox from '../../../elements/StyledCheckbox';
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import Field from '../../../elements/Field';
|
||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -55,6 +56,9 @@ interface IState extends IThemeState {
|
|||
customThemeUrl: string;
|
||||
customThemeMessage: CustomThemeMessage;
|
||||
useCustomFontSize: boolean;
|
||||
useSystemFont: boolean;
|
||||
systemFont: string;
|
||||
showAdvanced: boolean;
|
||||
useIRCLayout: boolean;
|
||||
}
|
||||
|
||||
|
@ -73,6 +77,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
customThemeUrl: "",
|
||||
customThemeMessage: {isError: false, text: ""},
|
||||
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
|
||||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
systemFont: SettingsStore.getValue("systemFont"),
|
||||
showAdvanced: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
};
|
||||
}
|
||||
|
@ -110,8 +117,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
};
|
||||
}
|
||||
|
||||
private onThemeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const newTheme = e.target.value;
|
||||
private onThemeChange = (newTheme: string): void => {
|
||||
if (this.state.theme === newTheme) return;
|
||||
|
||||
// doing getValue in the .catch will still return the value we failed to set,
|
||||
|
@ -271,19 +277,18 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||
{systemThemeSection}
|
||||
<div className="mx_ThemeSelectors" onChange={this.onThemeChange}>
|
||||
{orderedThemes.map(theme => {
|
||||
return <StyledRadioButton
|
||||
key={theme.id}
|
||||
value={theme.id}
|
||||
name="theme"
|
||||
disabled={this.state.useSystemTheme}
|
||||
checked={!this.state.useSystemTheme && theme.id === this.state.theme}
|
||||
className={"mx_ThemeSelector_" + theme.id}
|
||||
>
|
||||
{theme.name}
|
||||
</StyledRadioButton>;
|
||||
})}
|
||||
<div className="mx_ThemeSelectors">
|
||||
<StyledRadioGroup
|
||||
name="theme"
|
||||
definitions={orderedThemes.map(t => ({
|
||||
value: t.id,
|
||||
label: t.name,
|
||||
disabled: this.state.useSystemTheme,
|
||||
className: "mx_ThemeSelector_" + t.id,
|
||||
}))}
|
||||
onChange={this.onThemeChange}
|
||||
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
||||
/>
|
||||
</div>
|
||||
{customThemeForm}
|
||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} useCheckbox={true} />
|
||||
|
@ -374,6 +379,53 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
const toggle = <div
|
||||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||
>
|
||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
||||
if (this.state.showAdvanced) {
|
||||
advanced = <>
|
||||
<SettingsFlag
|
||||
name="useCompactLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
disabled={this.state.useIRCLayout}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useSystemFont: checked})}
|
||||
/>
|
||||
<Field
|
||||
className="mx_AppearanceUserSettingsTab_systemFont"
|
||||
label={SettingsStore.getDisplayName("systemFont")}
|
||||
onChange={(value) => {
|
||||
this.setState({
|
||||
systemFont: value.target.value,
|
||||
});
|
||||
|
||||
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
|
||||
}}
|
||||
tooltipContent="Set the name of a font installed on your system & Riot will attempt to use it."
|
||||
forceTooltipVisible={true}
|
||||
disabled={!this.state.useSystemFont}
|
||||
value={this.state.systemFont}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
|
||||
{toggle}
|
||||
{advanced}
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
|
@ -384,6 +436,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
{this.renderThemeSection()}
|
||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
||||
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
|
||||
{this.renderAdvancedSection()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import {_t, pickBestLanguage} from "../../../languageHandler";
|
||||
import * as sdk from "../../..";
|
||||
import {objectClone} from "../../../utils/objects";
|
||||
|
||||
export default class InlineTermsAgreement extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -56,7 +57,7 @@ export default class InlineTermsAgreement extends React.Component {
|
|||
}
|
||||
|
||||
_togglePolicy = (index) => {
|
||||
const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone
|
||||
const policies = objectClone(this.state.policies);
|
||||
policies[index].checked = !policies[index].checked;
|
||||
this.setState({policies});
|
||||
};
|
||||
|
|
|
@ -69,4 +69,14 @@ export enum Action {
|
|||
* Opens the user menu (previously known as the top left menu). No additional payload information required.
|
||||
*/
|
||||
ToggleUserMenu = "toggle_user_menu",
|
||||
|
||||
/**
|
||||
* Sets the apps root font size. Should be used with UpdateFontSizePayload
|
||||
*/
|
||||
UpdateFontSize = "update_font_size",
|
||||
|
||||
/**
|
||||
* Sets a system font. Should be used with UpdateSystemFontPayload
|
||||
*/
|
||||
UpdateSystemFont = "update_system_font",
|
||||
}
|
||||
|
|
27
src/dispatcher/payloads/UpdateFontSizePayload.ts
Normal file
27
src/dispatcher/payloads/UpdateFontSizePayload.ts
Normal file
|
@ -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 UpdateFontSizePayload extends ActionPayload {
|
||||
action: Action.UpdateFontSize;
|
||||
|
||||
/**
|
||||
* The font size to set the root to
|
||||
*/
|
||||
size: number;
|
||||
}
|
32
src/dispatcher/payloads/UpdateSystemFontPayload.ts
Normal file
32
src/dispatcher/payloads/UpdateSystemFontPayload.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
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 UpdateSystemFontPayload extends ActionPayload {
|
||||
action: Action.UpdateSystemFont;
|
||||
|
||||
/**
|
||||
* Specify whether to use a system font or the stylesheet font
|
||||
*/
|
||||
useSystemFont: boolean;
|
||||
|
||||
/**
|
||||
* The system font to use
|
||||
*/
|
||||
font: string;
|
||||
}
|
50
src/hooks/useAccountData.ts
Normal file
50
src/hooks/useAccountData.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 {useCallback, useState} from "react";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {useEventEmitter} from "./useEventEmitter";
|
||||
|
||||
const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined;
|
||||
|
||||
// Hook to simplify listening to Matrix account data
|
||||
export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: string) => {
|
||||
const [value, setValue] = useState<T>(() => tryGetContent(cli.getAccountData(eventType)));
|
||||
|
||||
const handler = useCallback((event) => {
|
||||
if (event.getType() !== eventType) return;
|
||||
setValue(event.getContent());
|
||||
}, [cli, eventType]);
|
||||
useEventEmitter(cli, "accountData", handler);
|
||||
|
||||
return value || {} as T;
|
||||
};
|
||||
|
||||
// Hook to simplify listening to Matrix room account data
|
||||
export const useRoomAccountData = <T extends {}>(room: Room, eventType: string) => {
|
||||
const [value, setValue] = useState<T>(() => tryGetContent(room.getAccountData(eventType)));
|
||||
|
||||
const handler = useCallback((event) => {
|
||||
if (event.getType() !== eventType) return;
|
||||
setValue(event.getContent());
|
||||
}, [room, eventType]);
|
||||
useEventEmitter(room, "Room.accountData", handler);
|
||||
|
||||
return value || {} as T;
|
||||
};
|
|
@ -247,7 +247,6 @@
|
|||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
||||
"sent an image.": "sent an image.",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
||||
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||
|
@ -421,11 +420,66 @@
|
|||
"Restart": "Restart",
|
||||
"Upgrade your Riot": "Upgrade your Riot",
|
||||
"A new version of Riot is available!": "A new version of Riot is available!",
|
||||
"You: %(message)s": "You: %(message)s",
|
||||
"Guest": "Guest",
|
||||
"There was an error joining the room": "There was an error joining the room",
|
||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"You joined the call": "You joined the call",
|
||||
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
||||
"Call in progress": "Call in progress",
|
||||
"You left the call": "You left the call",
|
||||
"%(senderName)s left the call": "%(senderName)s left the call",
|
||||
"Call ended": "Call ended",
|
||||
"You started a call": "You started a call",
|
||||
"%(senderName)s started a call": "%(senderName)s started a call",
|
||||
"Waiting for answer": "Waiting for answer",
|
||||
"%(senderName)s is calling": "%(senderName)s is calling",
|
||||
"You created the room": "You created the room",
|
||||
"%(senderName)s created the room": "%(senderName)s created the room",
|
||||
"You made the chat encrypted": "You made the chat encrypted",
|
||||
"%(senderName)s made the chat encrypted": "%(senderName)s made the chat encrypted",
|
||||
"You made history visible to new members": "You made history visible to new members",
|
||||
"%(senderName)s made history visible to new members": "%(senderName)s made history visible to new members",
|
||||
"You made history visible to anyone": "You made history visible to anyone",
|
||||
"%(senderName)s made history visible to anyone": "%(senderName)s made history visible to anyone",
|
||||
"You made history visible to future members": "You made history visible to future members",
|
||||
"%(senderName)s made history visible to future members": "%(senderName)s made history visible to future members",
|
||||
"You were invited": "You were invited",
|
||||
"%(targetName)s was invited": "%(targetName)s was invited",
|
||||
"You left": "You left",
|
||||
"%(targetName)s left": "%(targetName)s left",
|
||||
"You were kicked (%(reason)s)": "You were kicked (%(reason)s)",
|
||||
"%(targetName)s was kicked (%(reason)s)": "%(targetName)s was kicked (%(reason)s)",
|
||||
"You were kicked": "You were kicked",
|
||||
"%(targetName)s was kicked": "%(targetName)s was kicked",
|
||||
"You rejected the invite": "You rejected the invite",
|
||||
"%(targetName)s rejected the invite": "%(targetName)s rejected the invite",
|
||||
"You were uninvited": "You were uninvited",
|
||||
"%(targetName)s was uninvited": "%(targetName)s was uninvited",
|
||||
"You were banned (%(reason)s)": "You were banned (%(reason)s)",
|
||||
"%(targetName)s was banned (%(reason)s)": "%(targetName)s was banned (%(reason)s)",
|
||||
"You were banned": "You were banned",
|
||||
"%(targetName)s was banned": "%(targetName)s was banned",
|
||||
"You joined": "You joined",
|
||||
"%(targetName)s joined": "%(targetName)s joined",
|
||||
"You changed your name": "You changed your name",
|
||||
"%(targetName)s changed their name": "%(targetName)s changed their name",
|
||||
"You changed your avatar": "You changed your avatar",
|
||||
"%(targetName)s changed their avatar": "%(targetName)s changed their avatar",
|
||||
"%(senderName)s %(emote)s": "%(senderName)s %(emote)s",
|
||||
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
||||
"You changed the room name": "You changed the room name",
|
||||
"%(senderName)s changed the room name": "%(senderName)s changed the room name",
|
||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"You uninvited %(targetName)s": "You uninvited %(targetName)s",
|
||||
"%(senderName)s uninvited %(targetName)s": "%(senderName)s uninvited %(targetName)s",
|
||||
"You invited %(targetName)s": "You invited %(targetName)s",
|
||||
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
|
||||
"You changed the room topic": "You changed the room topic",
|
||||
"%(senderName)s changed the room topic": "%(senderName)s changed the room topic",
|
||||
"New spinner design": "New spinner design",
|
||||
"Font scaling": "Font scaling",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Custom user status messages": "Custom user status messages",
|
||||
|
@ -440,7 +494,7 @@
|
|||
"Font size": "Font size",
|
||||
"Use custom size": "Use custom size",
|
||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||
"Use compact timeline layout": "Use compact timeline layout",
|
||||
"Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout",
|
||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||
"Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)",
|
||||
"Show avatar changes": "Show avatar changes",
|
||||
|
@ -460,6 +514,8 @@
|
|||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||
"Match system theme": "Match system theme",
|
||||
"Use a system font": "Use a system font",
|
||||
"System font name": "System font name",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
|
||||
"Send analytics data": "Send analytics data",
|
||||
"Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session",
|
||||
|
@ -1028,6 +1084,7 @@
|
|||
"Encrypted by an unverified session": "Encrypted by an unverified session",
|
||||
"Unencrypted": "Unencrypted",
|
||||
"Encrypted by a deleted session": "Encrypted by a deleted session",
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
||||
"Invite only": "Invite only",
|
||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||
|
@ -1163,7 +1220,6 @@
|
|||
"Unread messages.": "Unread messages.",
|
||||
"Favourite": "Favourite",
|
||||
"Low Priority": "Low Priority",
|
||||
"Direct Chat": "Direct Chat",
|
||||
"Leave Room": "Leave Room",
|
||||
"Room options": "Room options",
|
||||
"Add a topic": "Add a topic",
|
||||
|
@ -1371,6 +1427,7 @@
|
|||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||
"Message deleted": "Message deleted",
|
||||
"Message deleted by %(name)s": "Message deleted by %(name)s",
|
||||
"Message deleted on %(date)s": "Message deleted on %(date)s",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||
|
@ -1841,6 +1898,7 @@
|
|||
"Mentions only": "Mentions only",
|
||||
"Leave": "Leave",
|
||||
"Forget": "Forget",
|
||||
"Direct Chat": "Direct Chat",
|
||||
"Clear status": "Clear status",
|
||||
"Update status": "Update status",
|
||||
"Set status": "Set status",
|
||||
|
@ -2056,7 +2114,6 @@
|
|||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
"Guest": "Guest",
|
||||
"Your profile": "Your profile",
|
||||
"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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -15,9 +15,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import {PushRuleVectorState, State} from "./PushRuleVectorState";
|
||||
import {IExtendedPushRule, IPushRuleSet, IRuleSets} from "./types";
|
||||
|
||||
import {PushRuleVectorState} from "./PushRuleVectorState";
|
||||
export interface IContentRules {
|
||||
vectorState: State;
|
||||
rules: IExtendedPushRule[];
|
||||
externalRules: IExtendedPushRule[];
|
||||
}
|
||||
|
||||
export const SCOPE = "global";
|
||||
export const KIND = "content";
|
||||
|
||||
export class ContentRules {
|
||||
/**
|
||||
|
@ -31,7 +39,7 @@ export class ContentRules {
|
|||
* externalRules: a list of other keyword rules, with states other than
|
||||
* vectorState
|
||||
*/
|
||||
static parseContentRules(rulesets) {
|
||||
static parseContentRules(rulesets: IRuleSets): IContentRules {
|
||||
// first categorise the keyword rules in terms of their actions
|
||||
const contentRules = this._categoriseContentRules(rulesets);
|
||||
|
||||
|
@ -51,59 +59,72 @@ export class ContentRules {
|
|||
|
||||
if (contentRules.loud.length) {
|
||||
return {
|
||||
vectorState: PushRuleVectorState.LOUD,
|
||||
vectorState: State.Loud,
|
||||
rules: contentRules.loud,
|
||||
externalRules: [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other),
|
||||
externalRules: [
|
||||
...contentRules.loud_but_disabled,
|
||||
...contentRules.on,
|
||||
...contentRules.on_but_disabled,
|
||||
...contentRules.other,
|
||||
],
|
||||
};
|
||||
} else if (contentRules.loud_but_disabled.length) {
|
||||
return {
|
||||
vectorState: PushRuleVectorState.OFF,
|
||||
vectorState: State.Off,
|
||||
rules: contentRules.loud_but_disabled,
|
||||
externalRules: [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other),
|
||||
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
||||
};
|
||||
} else if (contentRules.on.length) {
|
||||
return {
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
vectorState: State.On,
|
||||
rules: contentRules.on,
|
||||
externalRules: [].concat(contentRules.on_but_disabled, contentRules.other),
|
||||
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
||||
};
|
||||
} else if (contentRules.on_but_disabled.length) {
|
||||
return {
|
||||
vectorState: PushRuleVectorState.OFF,
|
||||
vectorState: State.Off,
|
||||
rules: contentRules.on_but_disabled,
|
||||
externalRules: contentRules.other,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
vectorState: State.On,
|
||||
rules: [],
|
||||
externalRules: contentRules.other,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static _categoriseContentRules(rulesets) {
|
||||
const contentRules = {on: [], on_but_disabled: [], loud: [], loud_but_disabled: [], other: []};
|
||||
static _categoriseContentRules(rulesets: IRuleSets) {
|
||||
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = {
|
||||
on: [],
|
||||
on_but_disabled: [],
|
||||
loud: [],
|
||||
loud_but_disabled: [],
|
||||
other: [],
|
||||
};
|
||||
|
||||
for (const kind in rulesets.global) {
|
||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
const r = rulesets.global[kind][i];
|
||||
|
||||
// check it's not a default rule
|
||||
if (r.rule_id[0] === '.' || kind !== 'content') {
|
||||
if (r.rule_id[0] === '.' || kind !== "content") {
|
||||
continue;
|
||||
}
|
||||
|
||||
r.kind = kind; // is this needed? not sure
|
||||
// this is needed as we are flattening an object of arrays into a single array
|
||||
r.kind = kind;
|
||||
|
||||
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
||||
case PushRuleVectorState.ON:
|
||||
case State.On:
|
||||
if (r.enabled) {
|
||||
contentRules.on.push(r);
|
||||
} else {
|
||||
contentRules.on_but_disabled.push(r);
|
||||
}
|
||||
break;
|
||||
case PushRuleVectorState.LOUD:
|
||||
case State.Loud:
|
||||
if (r.enabled) {
|
||||
contentRules.loud.push(r);
|
||||
} else {
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import {Action, Actions} from "./types";
|
||||
|
||||
interface IEncodedActions {
|
||||
notify: boolean;
|
||||
sound?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export class NotificationUtils {
|
||||
// Encodes a dictionary of {
|
||||
|
@ -24,12 +30,12 @@ export class NotificationUtils {
|
|||
// "highlight: true/false,
|
||||
// }
|
||||
// to a list of push actions.
|
||||
static encodeActions(action) {
|
||||
static encodeActions(action: IEncodedActions) {
|
||||
const notify = action.notify;
|
||||
const sound = action.sound;
|
||||
const highlight = action.highlight;
|
||||
if (notify) {
|
||||
const actions = ["notify"];
|
||||
const actions: Action[] = [Actions.Notify];
|
||||
if (sound) {
|
||||
actions.push({"set_tweak": "sound", "value": sound});
|
||||
}
|
||||
|
@ -40,7 +46,7 @@ export class NotificationUtils {
|
|||
}
|
||||
return actions;
|
||||
} else {
|
||||
return ["dont_notify"];
|
||||
return [Actions.DontNotify];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,18 +56,18 @@ export class NotificationUtils {
|
|||
// "highlight: true/false,
|
||||
// }
|
||||
// If the actions couldn't be decoded then returns null.
|
||||
static decodeActions(actions) {
|
||||
static decodeActions(actions: Action[]): IEncodedActions {
|
||||
let notify = false;
|
||||
let sound = null;
|
||||
let highlight = false;
|
||||
|
||||
for (let i = 0; i < actions.length; ++i) {
|
||||
const action = actions[i];
|
||||
if (action === "notify") {
|
||||
if (action === Actions.Notify) {
|
||||
notify = true;
|
||||
} else if (action === "dont_notify") {
|
||||
} else if (action === Actions.DontNotify) {
|
||||
notify = false;
|
||||
} else if (typeof action === 'object') {
|
||||
} else if (typeof action === "object") {
|
||||
if (action.set_tweak === "sound") {
|
||||
sound = action.value;
|
||||
} else if (action.set_tweak === "highlight") {
|
||||
|
@ -81,7 +87,7 @@ export class NotificationUtils {
|
|||
highlight = true;
|
||||
}
|
||||
|
||||
const result = {notify: notify, highlight: highlight};
|
||||
const result: IEncodedActions = { notify, highlight };
|
||||
if (sound !== null) {
|
||||
result.sound = sound;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -15,43 +15,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {StandardActions} from "./StandardActions";
|
||||
import {NotificationUtils} from "./NotificationUtils";
|
||||
import {IPushRule} from "./types";
|
||||
|
||||
export enum State {
|
||||
/** The push rule is disabled */
|
||||
Off = "off",
|
||||
/** The user will receive push notification for this rule */
|
||||
On = "on",
|
||||
/** The user will receive push notification for this rule with sound and
|
||||
highlight if this is legitimate */
|
||||
Loud = "loud",
|
||||
}
|
||||
|
||||
export class PushRuleVectorState {
|
||||
// Backwards compatibility (things should probably be using .states instead)
|
||||
static OFF = "off";
|
||||
static ON = "on";
|
||||
static LOUD = "loud";
|
||||
// Backwards compatibility (things should probably be using the enum above instead)
|
||||
static OFF = State.Off;
|
||||
static ON = State.On;
|
||||
static LOUD = State.Loud;
|
||||
|
||||
/**
|
||||
* Enum for state of a push rule as defined by the Vector UI.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
static states = {
|
||||
/** The push rule is disabled */
|
||||
OFF: PushRuleVectorState.OFF,
|
||||
|
||||
/** The user will receive push notification for this rule */
|
||||
ON: PushRuleVectorState.ON,
|
||||
|
||||
/** The user will receive push notification for this rule with sound and
|
||||
highlight if this is legitimate */
|
||||
LOUD: PushRuleVectorState.LOUD,
|
||||
};
|
||||
static states = State;
|
||||
|
||||
/**
|
||||
* Convert a PushRuleVectorState to a list of actions
|
||||
*
|
||||
* @return [object] list of push-rule actions
|
||||
*/
|
||||
static actionsFor(pushRuleVectorState) {
|
||||
if (pushRuleVectorState === PushRuleVectorState.ON) {
|
||||
static actionsFor(pushRuleVectorState: State) {
|
||||
if (pushRuleVectorState === State.On) {
|
||||
return StandardActions.ACTION_NOTIFY;
|
||||
} else if (pushRuleVectorState === PushRuleVectorState.LOUD) {
|
||||
} else if (pushRuleVectorState === State.Loud) {
|
||||
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +62,7 @@ export class PushRuleVectorState {
|
|||
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
||||
* state. Returns null if it does not match these categories.
|
||||
*/
|
||||
static contentRuleVectorStateKind(rule) {
|
||||
static contentRuleVectorStateKind(rule: IPushRule): State {
|
||||
const decoded = NotificationUtils.decodeActions(rule.actions);
|
||||
|
||||
if (!decoded) {
|
||||
|
@ -81,10 +80,10 @@ export class PushRuleVectorState {
|
|||
let stateKind = null;
|
||||
switch (tweaks) {
|
||||
case 0:
|
||||
stateKind = PushRuleVectorState.ON;
|
||||
stateKind = State.On;
|
||||
break;
|
||||
case 2:
|
||||
stateKind = PushRuleVectorState.LOUD;
|
||||
stateKind = State.Loud;
|
||||
break;
|
||||
}
|
||||
return stateKind;
|
|
@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {NotificationUtils} from "./NotificationUtils";
|
||||
|
||||
const encodeActions = NotificationUtils.encodeActions;
|
111
src/notifications/types.ts
Normal file
111
src/notifications/types.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export enum NotificationSetting {
|
||||
AllMessages = "all_messages", // .m.rule.message = notify
|
||||
DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default.
|
||||
MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread
|
||||
Never = "never", // .m.rule.master = enabled (dont_notify)
|
||||
}
|
||||
|
||||
export interface ISoundTweak {
|
||||
set_tweak: "sound";
|
||||
value: string;
|
||||
}
|
||||
export interface IHighlightTweak {
|
||||
set_tweak: "highlight";
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
export type Tweak = ISoundTweak | IHighlightTweak;
|
||||
|
||||
export enum Actions {
|
||||
Notify = "notify",
|
||||
DontNotify = "dont_notify", // no-op
|
||||
Coalesce = "coalesce", // unused
|
||||
MarkUnread = "mark_unread", // new
|
||||
}
|
||||
|
||||
export type Action = Actions | Tweak;
|
||||
|
||||
// Push rule kinds in descending priority order
|
||||
export enum Kind {
|
||||
Override = "override",
|
||||
ContentSpecific = "content",
|
||||
RoomSpecific = "room",
|
||||
SenderSpecific = "sender",
|
||||
Underride = "underride",
|
||||
}
|
||||
|
||||
export interface IEventMatchCondition {
|
||||
kind: "event_match";
|
||||
key: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition {
|
||||
kind: "contains_display_name";
|
||||
}
|
||||
|
||||
export interface IRoomMemberCountCondition {
|
||||
kind: "room_member_count";
|
||||
is: string;
|
||||
}
|
||||
|
||||
export interface ISenderNotificationPermissionCondition {
|
||||
kind: "sender_notification_permission";
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
IEventMatchCondition |
|
||||
IContainsDisplayNameCondition |
|
||||
IRoomMemberCountCondition |
|
||||
ISenderNotificationPermissionCondition;
|
||||
|
||||
export enum RuleIds {
|
||||
MasterRule = ".m.rule.master", // The master rule (all notifications disabling)
|
||||
MessageRule = ".m.rule.message",
|
||||
EncryptedMessageRule = ".m.rule.encrypted",
|
||||
RoomOneToOneRule = ".m.rule.room_one_to_one",
|
||||
EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one",
|
||||
}
|
||||
|
||||
export interface IPushRule {
|
||||
enabled: boolean;
|
||||
rule_id: RuleIds | string;
|
||||
actions: Action[];
|
||||
default: boolean;
|
||||
conditions?: Condition[]; // only applicable to `underride` and `override` rules
|
||||
pattern?: string; // only applicable to `content` rules
|
||||
}
|
||||
|
||||
// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor
|
||||
export interface IExtendedPushRule extends IPushRule {
|
||||
kind: Kind;
|
||||
}
|
||||
|
||||
export interface IPushRuleSet {
|
||||
override: IPushRule[];
|
||||
content: IPushRule[];
|
||||
room: IPushRule[];
|
||||
sender: IPushRule[];
|
||||
underride: IPushRule[];
|
||||
}
|
||||
|
||||
export interface IRuleSets {
|
||||
global: IPushRuleSet;
|
||||
}
|
|
@ -30,6 +30,8 @@ import PushToMatrixClientController from './controllers/PushToMatrixClientContro
|
|||
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
|
||||
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
|
||||
import FontSizeController from './controllers/FontSizeController';
|
||||
import SystemFontController from './controllers/SystemFontController';
|
||||
import UseSystemFontController from './controllers/UseSystemFontController';
|
||||
|
||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||
const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
|
||||
|
@ -95,6 +97,12 @@ export const SETTINGS = {
|
|||
// // not use this for new settings.
|
||||
// invertedSettingName: "my-negative-setting",
|
||||
// },
|
||||
"feature_new_spinner": {
|
||||
isFeature: true,
|
||||
displayName: _td("New spinner design"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_font_scaling": {
|
||||
isFeature: true,
|
||||
displayName: _td("Font scaling"),
|
||||
|
@ -188,9 +196,14 @@ export const SETTINGS = {
|
|||
default: true,
|
||||
invertedSettingName: 'MessageComposerInput.dontSuggestEmoji',
|
||||
},
|
||||
// TODO: Wire up appropriately to UI (FTUE notifications)
|
||||
"Notifications.alwaysShowBadgeCounts": {
|
||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||
default: false,
|
||||
},
|
||||
"useCompactLayout": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Use compact timeline layout'),
|
||||
displayName: _td('Use a more compact ‘Modern’ layout'),
|
||||
default: false,
|
||||
},
|
||||
"showRedactions": {
|
||||
|
@ -314,6 +327,18 @@ export const SETTINGS = {
|
|||
default: true,
|
||||
displayName: _td("Match system theme"),
|
||||
},
|
||||
"useSystemFont": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
displayName: _td("Use a system font"),
|
||||
controller: new UseSystemFontController(),
|
||||
},
|
||||
"systemFont": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: "",
|
||||
displayName: _td("System font name"),
|
||||
controller: new SystemFontController(),
|
||||
},
|
||||
"webRtcAllowPeerToPeer": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
|
||||
|
|
|
@ -51,8 +51,17 @@ export class WatchManager {
|
|||
const roomWatchers = this._watchers[settingName];
|
||||
const callbacks = [];
|
||||
|
||||
if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]);
|
||||
if (roomWatchers[null]) callbacks.push(...roomWatchers[null]);
|
||||
if (inRoomId !== null && roomWatchers[inRoomId]) {
|
||||
callbacks.push(...roomWatchers[inRoomId]);
|
||||
}
|
||||
|
||||
if (!inRoomId) {
|
||||
// Fire updates to all the individual room watchers too, as they probably
|
||||
// care about the change higher up.
|
||||
callbacks.push(...Object.values(roomWatchers).reduce((r, a) => [...r, ...a], []));
|
||||
} else if (roomWatchers[null]) {
|
||||
callbacks.push(...roomWatchers[null]);
|
||||
}
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback(inRoomId, atLevel, newValueAtLevel);
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
import SettingController from "./SettingController";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { UpdateFontSizePayload } from "../../dispatcher/payloads/UpdateFontSizePayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
export default class FontSizeController extends SettingController {
|
||||
constructor() {
|
||||
|
@ -24,8 +26,8 @@ export default class FontSizeController extends SettingController {
|
|||
|
||||
onChange(level, roomId, newValue) {
|
||||
// Dispatch font size change so that everything open responds to the change.
|
||||
dis.dispatch({
|
||||
action: "update-font-size",
|
||||
dis.dispatch<UpdateFontSizePayload>({
|
||||
action: Action.UpdateFontSize,
|
||||
size: newValue,
|
||||
});
|
||||
}
|
36
src/settings/controllers/SystemFontController.ts
Normal file
36
src/settings/controllers/SystemFontController.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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 SettingController from "./SettingController";
|
||||
import SettingsStore from "../SettingsStore";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
export default class SystemFontController extends SettingController {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onChange(level, roomId, newValue) {
|
||||
// Dispatch font size change so that everything open responds to the change.
|
||||
dis.dispatch<UpdateSystemFontPayload>({
|
||||
action: Action.UpdateSystemFont,
|
||||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
font: newValue,
|
||||
});
|
||||
}
|
||||
}
|
36
src/settings/controllers/UseSystemFontController.ts
Normal file
36
src/settings/controllers/UseSystemFontController.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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 SettingController from "./SettingController";
|
||||
import SettingsStore from "../SettingsStore";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
export default class UseSystemFontController extends SettingController {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onChange(level, roomId, newValue) {
|
||||
// Dispatch font size change so that everything open responds to the change.
|
||||
dis.dispatch<UpdateSystemFontPayload>({
|
||||
action: Action.UpdateSystemFont,
|
||||
useSystemFont: newValue,
|
||||
font: SettingsStore.getValue("systemFont"),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import {SettingLevel} from "../SettingsStore";
|
||||
import {objectClone, objectKeyChanges} from "../../utils/objects";
|
||||
|
||||
const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms";
|
||||
const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs";
|
||||
|
@ -45,7 +46,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||
newClient.on("accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
_onAccountData(event) {
|
||||
_onAccountData(event, prevEvent) {
|
||||
if (event.getType() === "org.matrix.preview_urls") {
|
||||
let val = event.getContent()['disable'];
|
||||
if (typeof(val) !== "boolean") {
|
||||
|
@ -56,8 +57,10 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||
|
||||
this._watchers.notifyUpdate("urlPreviewsEnabled", null, SettingLevel.ACCOUNT, val);
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
// Figure out what changed and fire those updates
|
||||
const prevContent = prevEvent ? prevEvent.getContent() : {};
|
||||
const changedSettings = objectKeyChanges(prevContent, event.getContent());
|
||||
for (const settingName of changedSettings) {
|
||||
const val = event.getContent()[settingName];
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val);
|
||||
}
|
||||
|
@ -159,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||
|
||||
const event = cli.getAccountData(eventType);
|
||||
if (!event || !event.getContent()) return null;
|
||||
return event.getContent();
|
||||
return objectClone(event.getContent()); // clone to prevent mutation
|
||||
}
|
||||
|
||||
_notifyBreadcrumbsUpdate(event) {
|
||||
|
|
|
@ -42,6 +42,10 @@ export default class MatrixClientBackedSettingsHandler extends SettingsHandler {
|
|||
MatrixClientBackedSettingsHandler._instances.push(this);
|
||||
}
|
||||
|
||||
get client() {
|
||||
return MatrixClientBackedSettingsHandler._matrixClient;
|
||||
}
|
||||
|
||||
initMatrixClient() {
|
||||
console.warn("initMatrixClient not overridden");
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import {SettingLevel} from "../SettingsStore";
|
||||
import {objectClone, objectKeyChanges} from "../../utils/objects";
|
||||
|
||||
const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets";
|
||||
|
||||
|
@ -40,7 +41,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||
newClient.on("Room.accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
_onAccountData(event, room) {
|
||||
_onAccountData(event, room, prevEvent) {
|
||||
const roomId = room.roomId;
|
||||
|
||||
if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
|
@ -55,8 +56,10 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||
} else if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
this._watchers.notifyUpdate("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent());
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
// Figure out what changed and fire those updates
|
||||
const prevContent = prevEvent ? prevEvent.getContent() : {};
|
||||
const changedSettings = objectKeyChanges(prevContent, event.getContent());
|
||||
for (const settingName of changedSettings) {
|
||||
const val = event.getContent()[settingName];
|
||||
this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_ACCOUNT, val);
|
||||
}
|
||||
|
@ -134,6 +137,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||
|
||||
const event = room.getAccountData(eventType);
|
||||
if (!event || !event.getContent()) return null;
|
||||
return event.getContent();
|
||||
return objectClone(event.getContent()); // clone to prevent mutation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import {SettingLevel} from "../SettingsStore";
|
||||
import {objectClone, objectKeyChanges} from "../../utils/objects";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room" level.
|
||||
|
@ -38,8 +39,15 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl
|
|||
newClient.on("RoomState.events", this._onEvent);
|
||||
}
|
||||
|
||||
_onEvent(event) {
|
||||
_onEvent(event, state, prevEvent) {
|
||||
const roomId = event.getRoomId();
|
||||
const room = this.client.getRoom(roomId);
|
||||
|
||||
// Note: the tests often fire setting updates that don't have rooms in the store, so
|
||||
// we fail softly here. We shouldn't assume that the state being fired is current
|
||||
// state, but we also don't need to explode just because we didn't find a room.
|
||||
if (!room) console.warn(`Unknown room caused setting update: ${roomId}`);
|
||||
if (room && state !== room.currentState) return; // ignore state updates which are not current
|
||||
|
||||
if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
let val = event.getContent()['disable'];
|
||||
|
@ -51,8 +59,10 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl
|
|||
|
||||
this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, SettingLevel.ROOM, val);
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
// Figure out what changed and fire those updates
|
||||
const prevContent = prevEvent ? prevEvent.getContent() : {};
|
||||
const changedSettings = objectKeyChanges(prevContent, event.getContent());
|
||||
for (const settingName of changedSettings) {
|
||||
this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM, event.getContent()[settingName]);
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +117,6 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl
|
|||
|
||||
const event = room.currentState.getStateEvents(eventType, "");
|
||||
if (!event || !event.getContent()) return null;
|
||||
return event.getContent();
|
||||
return objectClone(event.getContent()); // clone to prevent mutation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import SettingsStore, {SettingLevel} from '../SettingsStore';
|
||||
import IWatcher from "./Watcher";
|
||||
import { toPx } from '../../utils/units';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { UpdateSystemFontPayload } from '../../dispatcher/payloads/UpdateSystemFontPayload';
|
||||
|
||||
export class FontWatcher implements IWatcher {
|
||||
public static readonly MIN_SIZE = 8;
|
||||
|
@ -33,6 +35,10 @@ export class FontWatcher implements IWatcher {
|
|||
|
||||
public start() {
|
||||
this.setRootFontSize(SettingsStore.getValue("baseFontSize"));
|
||||
this.setSystemFont({
|
||||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
font: SettingsStore.getValue("systemFont"),
|
||||
});
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
|
@ -41,8 +47,10 @@ export class FontWatcher implements IWatcher {
|
|||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
if (payload.action === 'update-font-size') {
|
||||
if (payload.action === Action.UpdateFontSize) {
|
||||
this.setRootFontSize(payload.size);
|
||||
} else if (payload.action === Action.UpdateSystemFont) {
|
||||
this.setSystemFont(payload);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -54,4 +62,8 @@ export class FontWatcher implements IWatcher {
|
|||
}
|
||||
(<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize);
|
||||
};
|
||||
|
||||
private setSystemFont = ({useSystemFont, font}) => {
|
||||
document.body.style.fontFamily = useSystemFont ? font : "";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,135 +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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
|
||||
import { textForEvent } from "../TextForEvent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
const PREVIEWABLE_EVENTS = [
|
||||
// This is the same list from RiotX
|
||||
{type: "m.room.message", isState: false},
|
||||
{type: "m.room.name", isState: true},
|
||||
{type: "m.room.topic", isState: true},
|
||||
{type: "m.room.member", isState: true},
|
||||
{type: "m.room.history_visibility", isState: true},
|
||||
{type: "m.call.invite", isState: false},
|
||||
{type: "m.call.hangup", isState: false},
|
||||
{type: "m.call.answer", isState: false},
|
||||
{type: "m.room.encrypted", isState: false},
|
||||
{type: "m.room.encryption", isState: true},
|
||||
{type: "m.room.third_party_invite", isState: true},
|
||||
{type: "m.sticker", isState: false},
|
||||
{type: "m.room.create", isState: true},
|
||||
];
|
||||
|
||||
// The maximum number of events we're willing to look back on to get a preview.
|
||||
const MAX_EVENTS_BACKWARDS = 50;
|
||||
|
||||
interface IState {
|
||||
[roomId: string]: string | null; // null indicates the preview is empty
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
public static get instance(): MessagePreviewStore {
|
||||
return MessagePreviewStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pre-translated preview for a given room
|
||||
* @param room The room to get the preview for.
|
||||
* @returns {string} The preview, or null if none present.
|
||||
*/
|
||||
public getPreviewForRoom(room: Room): string {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
// It's faster to do a lookup this way than it is to use Object.keys().includes()
|
||||
// We only want to generate a preview if there's one actually missing and not explicitly
|
||||
// set as 'none'.
|
||||
const val = this.state[room.roomId];
|
||||
if (val !== null && typeof(val) !== "string") {
|
||||
this.generatePreview(room);
|
||||
}
|
||||
|
||||
return this.state[room.roomId];
|
||||
}
|
||||
|
||||
private generatePreview(room: Room) {
|
||||
const timeline = room.getLiveTimeline();
|
||||
if (!timeline) return; // usually only happens in tests
|
||||
const events = timeline.getEvents();
|
||||
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
|
||||
|
||||
const event = events[i];
|
||||
const preview = this.generatePreviewForEvent(event);
|
||||
if (preview.isPreviewable) {
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: preview.preview});
|
||||
return; // break - we found some text
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find anything, subscribe ourselves to an update
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: null});
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
// TODO: Remove when new room list is made the default
|
||||
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
|
||||
|
||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const event = payload.event; // TODO: Type out the dispatcher
|
||||
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
||||
|
||||
const preview = this.generatePreviewForEvent(event);
|
||||
if (preview.isPreviewable) {
|
||||
await this.updateState({[event.getRoomId()]: preview.preview});
|
||||
return; // break - we found some text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
|
||||
if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
|
||||
const isSelf = event.getSender() === this.matrixClient.getUserId();
|
||||
let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
|
||||
if (!text || text.trim().length === 0) text = null; // force null if useless to us
|
||||
if (text && isSelf) {
|
||||
// XXX: i18n doesn't really work here if the language doesn't support prefixing.
|
||||
// We'd ideally somehow route the `You:` bit to the textForEvent call, however
|
||||
// threading that through is non-trivial.
|
||||
text = _t("You: %(message)s", {message: text});
|
||||
}
|
||||
return {isPreviewable: true, preview: text};
|
||||
}
|
||||
return {isPreviewable: false, preview: null};
|
||||
}
|
||||
}
|
122
src/stores/OwnProfileStore.ts
Normal file
122
src/stores/OwnProfileStore.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
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 "../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { throttle } from "lodash";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
interface IState {
|
||||
displayName?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new OwnProfileStore();
|
||||
|
||||
private monitoredUser: User;
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
public static get instance(): OwnProfileStore {
|
||||
return OwnProfileStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for the user, or null if not present.
|
||||
*/
|
||||
public get displayName(): string {
|
||||
if (!this.matrixClient) return this.state.displayName || null;
|
||||
|
||||
if (this.matrixClient.isGuest()) {
|
||||
return _t("Guest");
|
||||
} else if (this.state.displayName) {
|
||||
return this.state.displayName;
|
||||
} else {
|
||||
return this.matrixClient.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MXC URI of the user's avatar, or null if not present.
|
||||
*/
|
||||
public get avatarMxc(): string {
|
||||
return this.state.avatarUrl || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's avatar as an HTTP URL of the given size. If the user's
|
||||
* avatar is not present, this returns null.
|
||||
* @param size The size of the avatar
|
||||
* @returns The HTTP URL of the user's avatar
|
||||
*/
|
||||
public getHttpAvatarUrl(size: number): string {
|
||||
if (!this.avatarMxc) return null;
|
||||
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
|
||||
}
|
||||
|
||||
protected async onNotReady() {
|
||||
if (this.monitoredUser) {
|
||||
this.monitoredUser.removeListener("User.displayName", this.onProfileUpdate);
|
||||
this.monitoredUser.removeListener("User.avatarUrl", this.onProfileUpdate);
|
||||
}
|
||||
if (this.matrixClient) {
|
||||
this.matrixClient.removeListener("RoomState.events", this.onStateEvents);
|
||||
}
|
||||
await this.reset({});
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
const myUserId = this.matrixClient.getUserId();
|
||||
this.monitoredUser = this.matrixClient.getUser(myUserId);
|
||||
if (this.monitoredUser) {
|
||||
this.monitoredUser.on("User.displayName", this.onProfileUpdate);
|
||||
this.monitoredUser.on("User.avatarUrl", this.onProfileUpdate);
|
||||
}
|
||||
|
||||
// We also have to listen for membership events for ourselves as the above User events
|
||||
// are fired only with presence, which matrix.org (and many others) has disabled.
|
||||
this.matrixClient.on("RoomState.events", this.onStateEvents);
|
||||
|
||||
await this.onProfileUpdate(); // trigger an initial update
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
// we don't actually do anything here
|
||||
}
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// We specifically do not use the User object we stored for profile info as it
|
||||
// could easily be wrong (such as per-room instead of global profile).
|
||||
const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId());
|
||||
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
|
||||
};
|
||||
|
||||
// TSLint wants this to be a member, but we don't want that.
|
||||
// tslint:disable-next-line
|
||||
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||
await this.onProfileUpdate();
|
||||
}
|
||||
}, 200, {trailing: true, leading: true});
|
||||
}
|
|
@ -18,6 +18,10 @@ import { TagID } from "./models";
|
|||
|
||||
const TILE_HEIGHT_PX = 44;
|
||||
|
||||
// the .65 comes from the CSS where the show more button is
|
||||
// mathematically 65% of a tile when floating.
|
||||
const RESIZER_BOX_FACTOR = 0.65;
|
||||
|
||||
interface ISerializedListLayout {
|
||||
numTiles: number;
|
||||
showPreviews: boolean;
|
||||
|
@ -67,6 +71,7 @@ export class ListLayout {
|
|||
}
|
||||
|
||||
public get visibleTiles(): number {
|
||||
if (this._n === 0) return this.defaultVisibleTiles;
|
||||
return Math.max(this._n, this.minVisibleTiles);
|
||||
}
|
||||
|
||||
|
@ -76,9 +81,13 @@ export class ListLayout {
|
|||
}
|
||||
|
||||
public get minVisibleTiles(): number {
|
||||
// the .65 comes from the CSS where the show more button is
|
||||
// mathematically 65% of a tile when floating.
|
||||
return 4.65;
|
||||
return 1 + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public get defaultVisibleTiles(): number {
|
||||
// TODO: Remove dogfood flag
|
||||
const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4);
|
||||
return val + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
|
||||
|
@ -92,6 +101,10 @@ export class ListLayout {
|
|||
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
|
||||
}
|
||||
|
||||
public tilesWithResizerBoxFactor(n: number): number {
|
||||
return n + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public tilesWithPadding(n: number, paddingPx: number): number {
|
||||
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
|
||||
}
|
||||
|
|
204
src/stores/room-list/MessagePreviewStore.ts
Normal file
204
src/stores/room-list/MessagePreviewStore.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy";
|
||||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||
import { NameEventPreview } from "./previews/NameEventPreview";
|
||||
import { TagID } from "./models";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { TopicEventPreview } from "./previews/TopicEventPreview";
|
||||
import { MembershipEventPreview } from "./previews/MembershipEventPreview";
|
||||
import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview";
|
||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
|
||||
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
|
||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { CreationEventPreview } from "./previews/CreationEventPreview";
|
||||
|
||||
const PREVIEWS = {
|
||||
'm.room.message': {
|
||||
isState: false,
|
||||
previewer: new MessageEventPreview(),
|
||||
},
|
||||
'm.room.name': {
|
||||
isState: true,
|
||||
previewer: new NameEventPreview(),
|
||||
},
|
||||
'm.room.topic': {
|
||||
isState: true,
|
||||
previewer: new TopicEventPreview(),
|
||||
},
|
||||
'm.room.member': {
|
||||
isState: true,
|
||||
previewer: new MembershipEventPreview(),
|
||||
},
|
||||
'm.room.history_visibility': {
|
||||
isState: true,
|
||||
previewer: new HistoryVisibilityEventPreview(),
|
||||
},
|
||||
'm.call.invite': {
|
||||
isState: false,
|
||||
previewer: new CallInviteEventPreview(),
|
||||
},
|
||||
'm.call.answer': {
|
||||
isState: false,
|
||||
previewer: new CallAnswerEventPreview(),
|
||||
},
|
||||
'm.call.hangup': {
|
||||
isState: false,
|
||||
previewer: new CallHangupEvent(),
|
||||
},
|
||||
'm.room.encryption': {
|
||||
isState: true,
|
||||
previewer: new EncryptionEventPreview(),
|
||||
},
|
||||
'm.room.third_party_invite': {
|
||||
isState: true,
|
||||
previewer: new ThirdPartyInviteEventPreview(),
|
||||
},
|
||||
'm.sticker': {
|
||||
isState: false,
|
||||
previewer: new StickerEventPreview(),
|
||||
},
|
||||
'm.reaction': {
|
||||
isState: false,
|
||||
previewer: new ReactionEventPreview(),
|
||||
},
|
||||
'm.room.create': {
|
||||
isState: true,
|
||||
previewer: new CreationEventPreview(),
|
||||
},
|
||||
};
|
||||
|
||||
// The maximum number of events we're willing to look back on to get a preview.
|
||||
const MAX_EVENTS_BACKWARDS = 50;
|
||||
|
||||
// type merging ftw
|
||||
type TAG_ANY = "im.vector.any";
|
||||
const TAG_ANY: TAG_ANY = "im.vector.any";
|
||||
|
||||
interface IState {
|
||||
[roomId: string]: Map<TagID | TAG_ANY, string | null>; // null indicates the preview is empty / irrelevant
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
public static get instance(): MessagePreviewStore {
|
||||
return MessagePreviewStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pre-translated preview for a given room
|
||||
* @param room The room to get the preview for.
|
||||
* @param inTagId The tag ID in which the room resides
|
||||
* @returns The preview, or null if none present.
|
||||
*/
|
||||
public getPreviewForRoom(room: Room, inTagId: TagID): string {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
const val = this.state[room.roomId];
|
||||
if (!val) this.generatePreview(room, inTagId);
|
||||
|
||||
const previews = this.state[room.roomId];
|
||||
if (!previews) return null;
|
||||
|
||||
if (!previews.has(inTagId)) {
|
||||
return previews.get(TAG_ANY);
|
||||
}
|
||||
return previews.get(inTagId);
|
||||
}
|
||||
|
||||
private generatePreview(room: Room, tagId?: TagID) {
|
||||
const events = room.timeline;
|
||||
if (!events) return; // should only happen in tests
|
||||
|
||||
let map = this.state[room.roomId];
|
||||
if (!map) {
|
||||
map = new Map<TagID | TAG_ANY, string | null>();
|
||||
|
||||
// We set the state later with the map, so no need to send an update now
|
||||
}
|
||||
|
||||
// Set the tags so we know what to generate
|
||||
if (!map.has(TAG_ANY)) map.set(TAG_ANY, null);
|
||||
if (tagId && !map.has(tagId)) map.set(tagId, null);
|
||||
|
||||
let changed = false;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
|
||||
|
||||
const event = events[i];
|
||||
const previewDef = PREVIEWS[event.getType()];
|
||||
if (!previewDef) continue;
|
||||
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
|
||||
|
||||
const anyPreview = previewDef.previewer.getTextFor(event, null);
|
||||
if (!anyPreview) continue; // not previewable for some reason
|
||||
|
||||
changed = changed || anyPreview !== map.get(TAG_ANY);
|
||||
map.set(TAG_ANY, anyPreview);
|
||||
|
||||
const tagsToGenerate = Array.from(map.keys()).filter(t => t !== TAG_ANY); // we did the any tag above
|
||||
for (const genTagId of tagsToGenerate) {
|
||||
const realTagId: TagID = genTagId === TAG_ANY ? null : genTagId;
|
||||
const preview = previewDef.previewer.getTextFor(event, realTagId);
|
||||
if (preview === anyPreview) {
|
||||
changed = changed || anyPreview !== map.get(genTagId);
|
||||
map.delete(genTagId);
|
||||
} else {
|
||||
changed = changed || preview !== map.get(genTagId);
|
||||
map.set(genTagId, preview);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Update state for good measure - causes emit for update
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: map});
|
||||
}
|
||||
return; // we're done
|
||||
}
|
||||
|
||||
// At this point, we didn't generate a preview so clear it
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: null});
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
// TODO: Remove when new room list is made the default
|
||||
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
|
||||
|
||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const event = payload.event; // TODO: Type out the dispatcher
|
||||
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
||||
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -158,12 +158,12 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|||
// First see if the receipt event is for our own user. If it was, trigger
|
||||
// a room update (we probably read the room on a different device).
|
||||
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
|
||||
console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`);
|
||||
const room = this.matrixClient.getRoom(payload.event.roomId);
|
||||
const room = payload.room;
|
||||
if (!room) {
|
||||
console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`);
|
||||
console.warn(`Own read receipt was in unknown room ${room.roomId}`);
|
||||
return;
|
||||
}
|
||||
console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
|
||||
await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -170,12 +170,16 @@ export class Algorithm extends EventEmitter {
|
|||
// When we do have a room though, we expect to be able to find it
|
||||
const tag = this.roomIdsToTags[val.roomId][0];
|
||||
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
|
||||
let position = this.cachedRooms[tag].indexOf(val);
|
||||
|
||||
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
|
||||
// means we'll be off by 1 when the user is switching rooms. This leads to visual jumping
|
||||
// when the user is moving south in the list (not north, because of math).
|
||||
let position = this.getOrderedRoomsWithoutSticky()[tag].indexOf(val);
|
||||
if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);
|
||||
|
||||
// 🐉 Here be dragons.
|
||||
// Before we can go through with lying to the underlying algorithm about a room
|
||||
// we need to ensure that when we do we're ready for the innevitable sticky room
|
||||
// we need to ensure that when we do we're ready for the inevitable sticky room
|
||||
// update we'll receive. To prepare for that, we first remove the sticky room and
|
||||
// recalculate the state ourselves so that when the underlying algorithm calls for
|
||||
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
|
||||
|
@ -208,6 +212,12 @@ export class Algorithm extends EventEmitter {
|
|||
position: position,
|
||||
tag: tag,
|
||||
};
|
||||
|
||||
// We update the filtered rooms just in case, as otherwise users will end up visiting
|
||||
// a room while filtering and it'll disappear. We don't update the filter earlier in
|
||||
// this function simply because we don't have to.
|
||||
this.recalculateFilteredRoomsForTag(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag);
|
||||
this.recalculateStickyRoom();
|
||||
|
||||
// Finally, trigger an update
|
||||
|
@ -231,9 +241,7 @@ export class Algorithm extends EventEmitter {
|
|||
// We optimize our lookups by trying to reduce sample size as much as possible
|
||||
// to the rooms we know will be deduped by the Set.
|
||||
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
|
||||
if (this._stickyRoom && this._stickyRoom.tag === tagId && this._stickyRoom.room) {
|
||||
rooms.push(this._stickyRoom.room);
|
||||
}
|
||||
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
|
||||
let remainingRooms = rooms.map(r => r);
|
||||
let allowedRoomsInThisTag = [];
|
||||
let lastFilterPriority = orderedFilters[0].relativePriority;
|
||||
|
@ -263,6 +271,7 @@ export class Algorithm extends EventEmitter {
|
|||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
// TODO: Remove or use.
|
||||
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
|
||||
const filters = this.allowedByFilter.keys();
|
||||
for (const room of added) {
|
||||
|
@ -281,7 +290,8 @@ export class Algorithm extends EventEmitter {
|
|||
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
|
||||
console.log(`Recalculating filtered rooms for ${tagId}`);
|
||||
delete this.filteredRooms[tagId];
|
||||
const rooms = this.cachedRooms[tagId];
|
||||
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
|
||||
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
|
||||
const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
|
||||
if (filteredRooms.length > 0) {
|
||||
this.filteredRooms[tagId] = filteredRooms;
|
||||
|
@ -289,6 +299,17 @@ export class Algorithm extends EventEmitter {
|
|||
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||
}
|
||||
|
||||
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
|
||||
if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return;
|
||||
|
||||
const position = this._stickyRoom.position;
|
||||
if (position >= rooms.length) {
|
||||
rooms.push(this._stickyRoom.room);
|
||||
} else {
|
||||
rooms.splice(position, 0, this._stickyRoom.room);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the sticky room position. If this is being called in relation to
|
||||
* a specific tag being updated, it should be given to this function to optimize
|
||||
|
@ -377,6 +398,20 @@ export class Algorithm extends EventEmitter {
|
|||
return this.filteredRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the same as getOrderedRooms(), but without the sticky room
|
||||
* map as it causes issues for sticky room handling (see sticky room handling
|
||||
* for more information).
|
||||
* @returns {ITagMap} The cached list of rooms, ordered,
|
||||
* for each tag. May be empty, but never null/undefined.
|
||||
*/
|
||||
private getOrderedRoomsWithoutSticky(): ITagMap {
|
||||
if (!this.hasFilters) {
|
||||
return this.cachedRooms;
|
||||
}
|
||||
return this.filteredRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
|
||||
* previously known information and instead use these rooms instead.
|
||||
|
|
35
src/stores/room-list/previews/CallAnswerEventPreview.ts
Normal file
35
src/stores/room-list/previews/CallAnswerEventPreview.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallAnswerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You joined the call");
|
||||
} else {
|
||||
return _t("%(senderName)s joined the call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
return _t("Call in progress");
|
||||
}
|
||||
}
|
||||
}
|
35
src/stores/room-list/previews/CallHangupEvent.ts
Normal file
35
src/stores/room-list/previews/CallHangupEvent.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallHangupEvent implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You left the call");
|
||||
} else {
|
||||
return _t("%(senderName)s left the call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
return _t("Call ended");
|
||||
}
|
||||
}
|
||||
}
|
39
src/stores/room-list/previews/CallInviteEventPreview.ts
Normal file
39
src/stores/room-list/previews/CallInviteEventPreview.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You started a call");
|
||||
} else {
|
||||
return _t("%(senderName)s started a call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
if (isSelf(event)) {
|
||||
return _t("Waiting for answer");
|
||||
} else {
|
||||
return _t("%(senderName)s is calling", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
src/stores/room-list/previews/CreationEventPreview.ts
Normal file
31
src/stores/room-list/previews/CreationEventPreview.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CreationEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You created the room");
|
||||
} else {
|
||||
return _t("%(senderName)s created the room", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
31
src/stores/room-list/previews/EncryptionEventPreview.ts
Normal file
31
src/stores/room-list/previews/EncryptionEventPreview.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class EncryptionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You made the chat encrypted");
|
||||
} else {
|
||||
return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class HistoryVisibilityEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const visibility = event.getContent()['history_visibility'];
|
||||
const isUs = isSelf(event);
|
||||
|
||||
if (visibility === 'invited' || visibility === 'joined') {
|
||||
return isUs
|
||||
? _t("You made history visible to new members")
|
||||
: _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)});
|
||||
} else if (visibility === 'world_readable') {
|
||||
return isUs
|
||||
? _t("You made history visible to anyone")
|
||||
: _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)});
|
||||
} else { // shared, default
|
||||
return isUs
|
||||
? _t("You made history visible to future members")
|
||||
: _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
31
src/stores/room-list/previews/IPreview.ts
Normal file
31
src/stores/room-list/previews/IPreview.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { TagID } from "../models";
|
||||
|
||||
/**
|
||||
* Represents an event preview.
|
||||
*/
|
||||
export interface IPreview {
|
||||
/**
|
||||
* Gets the text which represents the event as a preview.
|
||||
* @param event The event to preview.
|
||||
* @param tagId Optional. The tag where the room the event was sent in resides.
|
||||
* @returns The preview.
|
||||
*/
|
||||
getTextFor(event: MatrixEvent, tagId?: TagID): string;
|
||||
}
|
90
src/stores/room-list/previews/MembershipEventPreview.ts
Normal file
90
src/stores/room-list/previews/MembershipEventPreview.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getTargetName, isSelfTarget } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class MembershipEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const newMembership = event.getContent()['membership'];
|
||||
const oldMembership = event.getPrevContent()['membership'];
|
||||
const reason = event.getContent()['reason'];
|
||||
const isUs = isSelfTarget(event);
|
||||
|
||||
if (newMembership === 'invite') {
|
||||
return isUs
|
||||
? _t("You were invited")
|
||||
: _t("%(targetName)s was invited", {targetName: getTargetName(event)});
|
||||
} else if (newMembership === 'leave' && oldMembership !== 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You left")
|
||||
: _t("%(targetName)s left", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were kicked (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were kicked")
|
||||
: _t("%(targetName)s was kicked", {targetName: getTargetName(event)});
|
||||
}
|
||||
}
|
||||
} else if (newMembership === 'leave' && oldMembership === 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You rejected the invite")
|
||||
: _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were uninvited")
|
||||
: _t("%(targetName)s was uninvited", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'ban') {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were banned (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were banned")
|
||||
: _t("%(targetName)s was banned", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'join' && oldMembership !== 'join') {
|
||||
return isUs
|
||||
? _t("You joined")
|
||||
: _t("%(targetName)s joined", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname'];
|
||||
const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url'];
|
||||
if (isDisplayNameChange) {
|
||||
return isUs
|
||||
? _t("You changed your name")
|
||||
: _t("%(targetName)s changed their name", {targetName: getTargetName(event)});
|
||||
} else if (isAvatarChange) {
|
||||
return isUs
|
||||
? _t("You changed your avatar")
|
||||
: _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return null; // no change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
55
src/stores/room-list/previews/MessageEventPreview.ts
Normal file
55
src/stores/room-list/previews/MessageEventPreview.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import ReplyThread from "../../../components/views/elements/ReplyThread";
|
||||
|
||||
export class MessageEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
let eventContent = event.getContent();
|
||||
|
||||
if (event.isRelation("m.replace")) {
|
||||
// It's an edit, generate the preview on the new text
|
||||
eventContent = event.getContent()['m.new_content'];
|
||||
}
|
||||
|
||||
let body = (eventContent['body'] || '').trim();
|
||||
const msgtype = eventContent['msgtype'];
|
||||
if (!body || !msgtype) return null; // invalid event, no preview
|
||||
|
||||
// XXX: Newer relations have a getRelation() function which is not compatible with replies.
|
||||
const mRelatesTo = event.getWireContent()['m.relates_to'];
|
||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||
// If this is a reply, get the real reply and use that
|
||||
body = (ReplyThread.stripPlainReply(body) || '').trim();
|
||||
if (!body) return null; // invalid event, no preview
|
||||
}
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||
}
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return body;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(message)s", {senderName: getSenderName(event), message: body});
|
||||
}
|
||||
}
|
||||
}
|
31
src/stores/room-list/previews/NameEventPreview.ts
Normal file
31
src/stores/room-list/previews/NameEventPreview.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class NameEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room name");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
34
src/stores/room-list/previews/ReactionEventPreview.ts
Normal file
34
src/stores/room-list/previews/ReactionEventPreview.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class ReactionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const reaction = event.getRelation().key;
|
||||
if (!reaction) return;
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return reaction;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(reaction)s", {senderName: getSenderName(event), reaction});
|
||||
}
|
||||
}
|
||||
}
|
34
src/stores/room-list/previews/StickerEventPreview.ts
Normal file
34
src/stores/room-list/previews/StickerEventPreview.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class StickerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const stickerName = event.getContent()['body'];
|
||||
if (!stickerName) return null;
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return stickerName;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(stickerName)s", {senderName: getSenderName(event), stickerName});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
|
||||
export class ThirdPartyInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (!isValid3pidInvite(event)) {
|
||||
const targetName = event.getPrevContent().display_name || _t("Someone");
|
||||
if (isSelf(event)) {
|
||||
return _t("You uninvited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
} else {
|
||||
const targetName = event.getContent().display_name;
|
||||
if (isSelf(event)) {
|
||||
return _t("You invited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
src/stores/room-list/previews/TopicEventPreview.ts
Normal file
31
src/stores/room-list/previews/TopicEventPreview.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class TopicEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room topic");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
49
src/stores/room-list/previews/utils.ts
Normal file
49
src/stores/room-list/previews/utils.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { DefaultTagID, TagID } from "../models";
|
||||
|
||||
export function isSelf(event: MatrixEvent): boolean {
|
||||
const selfUserId = MatrixClientPeg.get().getUserId();
|
||||
if (event.getType() === 'm.room.member') {
|
||||
return event.getStateKey() === selfUserId;
|
||||
}
|
||||
return event.getSender() === selfUserId;
|
||||
}
|
||||
|
||||
export function isSelfTarget(event: MatrixEvent): boolean {
|
||||
const selfUserId = MatrixClientPeg.get().getUserId();
|
||||
return event.getStateKey() === selfUserId;
|
||||
}
|
||||
|
||||
export function shouldPrefixMessagesIn(roomId: string, tagId: TagID): boolean {
|
||||
if (tagId !== DefaultTagID.DM) return true;
|
||||
|
||||
// We don't prefix anything in 1:1s
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return true;
|
||||
return room.currentState.getJoinedMemberCount() !== 2;
|
||||
}
|
||||
|
||||
export function getSenderName(event: MatrixEvent): string {
|
||||
return event.sender ? event.sender.name : event.getSender();
|
||||
}
|
||||
|
||||
export function getTargetName(event: MatrixEvent): string {
|
||||
return event.target ? event.target.name : event.getStateKey();
|
||||
}
|
82
src/theme.js
82
src/theme.js
|
@ -35,11 +35,67 @@ export function enumerateThemes() {
|
|||
return Object.assign({}, customThemeNames, BUILTIN_THEMES);
|
||||
}
|
||||
|
||||
function clearCustomTheme() {
|
||||
// remove all css variables, we assume these are there because of the custom theme
|
||||
const inlineStyleProps = Object.values(document.body.style);
|
||||
for (const prop of inlineStyleProps) {
|
||||
if (prop.startsWith("--")) {
|
||||
document.body.style.removeProperty(prop);
|
||||
}
|
||||
}
|
||||
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
|
||||
if (customFontFaceStyle) {
|
||||
customFontFaceStyle.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const allowedFontFaceProps = [
|
||||
"font-display",
|
||||
"font-family",
|
||||
"font-stretch",
|
||||
"font-style",
|
||||
"font-weight",
|
||||
"font-variant",
|
||||
"font-feature-settings",
|
||||
"font-variation-settings",
|
||||
"src",
|
||||
"unicode-range",
|
||||
];
|
||||
|
||||
function generateCustomFontFaceCSS(faces) {
|
||||
return faces.map(face => {
|
||||
const src = face.src && face.src.map(srcElement => {
|
||||
let format;
|
||||
if (srcElement.format) {
|
||||
format = `format("${srcElement.format}")`;
|
||||
}
|
||||
if (srcElement.url) {
|
||||
return `url("${srcElement.url}") ${format}`;
|
||||
} else if (srcElement.local) {
|
||||
return `local("${srcElement.local}") ${format}`;
|
||||
}
|
||||
return "";
|
||||
}).join(", ");
|
||||
const props = Object.keys(face).filter(prop => allowedFontFaceProps.includes(prop));
|
||||
const body = props.map(prop => {
|
||||
let value;
|
||||
if (prop === "src") {
|
||||
value = src;
|
||||
} else if (prop === "font-family") {
|
||||
value = `"${face[prop]}"`;
|
||||
} else {
|
||||
value = face[prop];
|
||||
}
|
||||
return `${prop}: ${value}`;
|
||||
}).join(";");
|
||||
return `@font-face {${body}}`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function setCustomThemeVars(customTheme) {
|
||||
const {style} = document.body;
|
||||
|
||||
function setCSSVariable(name, hexColor, doPct = true) {
|
||||
function setCSSColorVariable(name, hexColor, doPct = true) {
|
||||
style.setProperty(`--${name}`, hexColor);
|
||||
if (doPct) {
|
||||
// uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50%
|
||||
|
@ -53,13 +109,30 @@ function setCustomThemeVars(customTheme) {
|
|||
for (const [name, value] of Object.entries(customTheme.colors)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
setCSSVariable(`${name}_${i}`, value[i], false);
|
||||
setCSSColorVariable(`${name}_${i}`, value[i], false);
|
||||
}
|
||||
} else {
|
||||
setCSSVariable(name, value);
|
||||
setCSSColorVariable(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customTheme.fonts) {
|
||||
const {fonts} = customTheme;
|
||||
if (fonts.faces) {
|
||||
const css = generateCustomFontFaceCSS(fonts.faces);
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("title", "custom-theme-font-faces");
|
||||
style.setAttribute("type", "text/css");
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (fonts.general) {
|
||||
style.setProperty("--font-family", fonts.general);
|
||||
}
|
||||
if (fonts.monospace) {
|
||||
style.setProperty("--font-family-monospace", fonts.monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomTheme(themeName) {
|
||||
|
@ -88,6 +161,7 @@ export async function setTheme(theme) {
|
|||
const themeWatcher = new ThemeWatcher();
|
||||
theme = themeWatcher.getEffectiveTheme();
|
||||
}
|
||||
clearCustomTheme();
|
||||
let stylesheetName = theme;
|
||||
if (theme.startsWith("custom-")) {
|
||||
const customTheme = getCustomTheme(theme.substr(7));
|
||||
|
@ -136,7 +210,7 @@ export async function setTheme(theme) {
|
|||
if (a == styleElements[stylesheetName]) return;
|
||||
a.disabled = true;
|
||||
});
|
||||
const bodyStyles = global.getComputedStyle(document.getElementsByTagName("body")[0]);
|
||||
const bodyStyles = global.getComputedStyle(document.body);
|
||||
if (bodyStyles.backgroundColor) {
|
||||
document.querySelector('meta[name="theme-color"]').content = bodyStyles.backgroundColor;
|
||||
}
|
||||
|
|
|
@ -24,14 +24,12 @@ import GenericToast from "../components/views/toasts/GenericToast";
|
|||
import ToastStore from "../stores/ToastStore";
|
||||
|
||||
const onAccept = () => {
|
||||
console.log("DEBUG onAccept AnalyticsToast");
|
||||
dis.dispatch({
|
||||
action: 'accept_cookies',
|
||||
});
|
||||
};
|
||||
|
||||
const onReject = () => {
|
||||
console.log("DEBUG onReject AnalyticsToast");
|
||||
dis.dispatch({
|
||||
action: "reject_cookies",
|
||||
});
|
||||
|
|
|
@ -15,9 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/**
|
||||
* Fires when the middle panel has been resized.
|
||||
* Fires when the middle panel has been resized (throttled).
|
||||
* @event module:utils~ResizeNotifier#"middlePanelResized"
|
||||
*/
|
||||
/**
|
||||
* Fires when the middle panel has been resized by a pixel.
|
||||
* @event module:utils~ResizeNotifier#"middlePanelResizedNoisy"
|
||||
*/
|
||||
import { EventEmitter } from "events";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
|
@ -29,15 +33,24 @@ export default class ResizeNotifier extends EventEmitter {
|
|||
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
|
||||
}
|
||||
|
||||
_noisyMiddlePanel() {
|
||||
this.emit("middlePanelResizedNoisy");
|
||||
}
|
||||
|
||||
_updateMiddlePanel() {
|
||||
this._throttledMiddlePanel();
|
||||
this._noisyMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
notifyLeftHandleResized() {
|
||||
// don't emit event for own region
|
||||
this._throttledMiddlePanel();
|
||||
this._updateMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
notifyRightHandleResized() {
|
||||
this._throttledMiddlePanel();
|
||||
this._updateMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
|
@ -48,7 +61,7 @@ export default class ResizeNotifier extends EventEmitter {
|
|||
// taller than the available space
|
||||
this.emit("leftPanelResized");
|
||||
|
||||
this._throttledMiddlePanel();
|
||||
this._updateMiddlePanel();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
|||
import {Capability} from "../widgets/WidgetApi";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {WidgetType} from "../widgets/WidgetType";
|
||||
import {objectClone} from "./objects";
|
||||
|
||||
export default class WidgetUtils {
|
||||
/* Returns true if user is able to send state events to modify widgets in this room
|
||||
|
@ -222,7 +223,7 @@ export default class WidgetUtils {
|
|||
const client = MatrixClientPeg.get();
|
||||
// Get the current widgets and clone them before we modify them, otherwise
|
||||
// we'll modify the content of the old event.
|
||||
const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets()));
|
||||
const userWidgets = objectClone(WidgetUtils.getUserWidgets());
|
||||
|
||||
// Delete existing widget with ID
|
||||
try {
|
||||
|
|
|
@ -46,6 +46,28 @@ export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of two arrays.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns The union of the arrays.
|
||||
*/
|
||||
export function arrayUnion<T>(a: T[], b: T[]): T[] {
|
||||
return a.filter(i => b.includes(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays, deduping contents using a Set.
|
||||
* @param a The arrays to merge.
|
||||
* @returns The merged array.
|
||||
*/
|
||||
export function arrayMerge<T>(...a: T[][]): T[] {
|
||||
return Array.from(a.reduce((c, v) => {
|
||||
v.forEach(i => c.add(i));
|
||||
return c;
|
||||
}, new Set<T>()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to perform LINQ-like queries on arrays.
|
||||
*/
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue