Merge remote-tracking branch 'origin/develop' into dbkr/weird_cancel_dialog

This commit is contained in:
David Baker 2020-07-06 15:07:31 +01:00
commit 77377821ae
174 changed files with 7454 additions and 2258 deletions

View file

@ -1,3 +1,356 @@
Changes in [2.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.9.0) (2020-07-03)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.9.0-rc.1...v2.9.0)
* Upgrade to JS SDK 7.1.0
* Remove duplicate compact settings, handle device level updates
[\#4889](https://github.com/matrix-org/matrix-react-sdk/pull/4889)
Changes in [2.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.9.0-rc.1) (2020-07-01)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.1...v2.9.0-rc.1)
* Upgrade to JS SDK 7.1.0-rc.1
* Update from Weblate
[\#4869](https://github.com/matrix-org/matrix-react-sdk/pull/4869)
* Fix a number of proliferation issues in the new room list
[\#4828](https://github.com/matrix-org/matrix-react-sdk/pull/4828)
* Fix jumping to read marker for events without tiles
[\#4860](https://github.com/matrix-org/matrix-react-sdk/pull/4860)
* De-duplicate rooms from the room autocomplete provider
[\#4859](https://github.com/matrix-org/matrix-react-sdk/pull/4859)
* Add file upload button to recovery key input
[\#4847](https://github.com/matrix-org/matrix-react-sdk/pull/4847)
* Implement new design on security setup & login
[\#4831](https://github.com/matrix-org/matrix-react-sdk/pull/4831)
* Fix /join slash command via servers including room id as a via
[\#4856](https://github.com/matrix-org/matrix-react-sdk/pull/4856)
* Add Generic Expiring Toast and timing hooks
[\#4855](https://github.com/matrix-org/matrix-react-sdk/pull/4855)
* Fix Room Custom Sounds regression and make ProgressBar relevant again
[\#4846](https://github.com/matrix-org/matrix-react-sdk/pull/4846)
* Including start_sso and start_cas in redirect loop prevention
[\#4854](https://github.com/matrix-org/matrix-react-sdk/pull/4854)
* Clean up TODO comments for new room list
[\#4850](https://github.com/matrix-org/matrix-react-sdk/pull/4850)
* Show timestamp of redaction on hover
[\#4622](https://github.com/matrix-org/matrix-react-sdk/pull/4622)
* Remove the DM button from new room tiles
[\#4849](https://github.com/matrix-org/matrix-react-sdk/pull/4849)
* Hide room list show less button if it would do nothing
[\#4848](https://github.com/matrix-org/matrix-react-sdk/pull/4848)
* Improve message preview copy in new room list
[\#4823](https://github.com/matrix-org/matrix-react-sdk/pull/4823)
* Allow the tag panel to be disabled in the new room list
[\#4844](https://github.com/matrix-org/matrix-react-sdk/pull/4844)
* Make the whole user row clickable in the new room list
[\#4843](https://github.com/matrix-org/matrix-react-sdk/pull/4843)
* Add a new spinner design behind a labs flag
[\#4842](https://github.com/matrix-org/matrix-react-sdk/pull/4842)
* ts-ignore because something is made of fail
[\#4845](https://github.com/matrix-org/matrix-react-sdk/pull/4845)
* Fix Welcome.html CAS and SSO URLs not working
[\#4838](https://github.com/matrix-org/matrix-react-sdk/pull/4838)
* More small tweaks in preparation for Notifications rework
[\#4835](https://github.com/matrix-org/matrix-react-sdk/pull/4835)
* Iterate on the new room list resize handle
[\#4840](https://github.com/matrix-org/matrix-react-sdk/pull/4840)
* Update sublists for new hover states
[\#4837](https://github.com/matrix-org/matrix-react-sdk/pull/4837)
* Tweak parts of the new room list design
[\#4839](https://github.com/matrix-org/matrix-react-sdk/pull/4839)
* Implement new resize handle for dogfooding
[\#4836](https://github.com/matrix-org/matrix-react-sdk/pull/4836)
* Hide app badge count for hidden upgraded rooms (non-highlight)
[\#4834](https://github.com/matrix-org/matrix-react-sdk/pull/4834)
* Move compact modern layout checkbox to 'advanced'
[\#4822](https://github.com/matrix-org/matrix-react-sdk/pull/4822)
* Allow the user to resize the new sublists to 1 tile
[\#4825](https://github.com/matrix-org/matrix-react-sdk/pull/4825)
* Make LoggedInView a real component because it uses shouldComponentUpdate
[\#4832](https://github.com/matrix-org/matrix-react-sdk/pull/4832)
* Small tweaks in preparation for Notifications rework
[\#4829](https://github.com/matrix-org/matrix-react-sdk/pull/4829)
* Remove extraneous debug from the new left panel
[\#4826](https://github.com/matrix-org/matrix-react-sdk/pull/4826)
* Fix icons in the new user menu not showing up
[\#4824](https://github.com/matrix-org/matrix-react-sdk/pull/4824)
* Fix sticky room disappearing/jumping in search results
[\#4817](https://github.com/matrix-org/matrix-react-sdk/pull/4817)
* Show cross-signing / secret storage reset button in more cases
[\#4821](https://github.com/matrix-org/matrix-react-sdk/pull/4821)
* Use theme-capable icons in the user menu
[\#4819](https://github.com/matrix-org/matrix-react-sdk/pull/4819)
* Font support in custom themes
[\#4814](https://github.com/matrix-org/matrix-react-sdk/pull/4814)
* Decrease margin between new sublists
[\#4816](https://github.com/matrix-org/matrix-react-sdk/pull/4816)
* Update profile information in User Menu and truncate where needed
[\#4818](https://github.com/matrix-org/matrix-react-sdk/pull/4818)
* Fix MessageActionBar in irc layout
[\#4802](https://github.com/matrix-org/matrix-react-sdk/pull/4802)
* Mark messages with a black shield if the megolm session isn't trusted
[\#4797](https://github.com/matrix-org/matrix-react-sdk/pull/4797)
* Custom font selection
[\#4761](https://github.com/matrix-org/matrix-react-sdk/pull/4761)
* Use the correct timeline reference for message previews
[\#4812](https://github.com/matrix-org/matrix-react-sdk/pull/4812)
* Fix read receipt handling in the new room list
[\#4811](https://github.com/matrix-org/matrix-react-sdk/pull/4811)
* Improve unread/badge states in new room list (mk II)
[\#4805](https://github.com/matrix-org/matrix-react-sdk/pull/4805)
* Only fire setting changes for changed settings
[\#4803](https://github.com/matrix-org/matrix-react-sdk/pull/4803)
* Trigger room-specific watchers whenever a higher level change happens
[\#4804](https://github.com/matrix-org/matrix-react-sdk/pull/4804)
* Have the theme switcher set the device-level theme to match settings
[\#4810](https://github.com/matrix-org/matrix-react-sdk/pull/4810)
* Fix layout of minimized view for new room list
[\#4808](https://github.com/matrix-org/matrix-react-sdk/pull/4808)
* Fix sticky headers over/under extending themselves in the new room list
[\#4809](https://github.com/matrix-org/matrix-react-sdk/pull/4809)
* Update read receipt remainder for internal font size change
[\#4806](https://github.com/matrix-org/matrix-react-sdk/pull/4806)
* Fix some appearance tab crash and implement style nits
[\#4801](https://github.com/matrix-org/matrix-react-sdk/pull/4801)
* Add message preview for font slider
[\#4770](https://github.com/matrix-org/matrix-react-sdk/pull/4770)
* Add layout options to the appearance tab
[\#4773](https://github.com/matrix-org/matrix-react-sdk/pull/4773)
* Update from Weblate
[\#4800](https://github.com/matrix-org/matrix-react-sdk/pull/4800)
* Support accounts with cross signing but no SSSS
[\#4717](https://github.com/matrix-org/matrix-react-sdk/pull/4717)
* Look for existing verification requests after login
[\#4762](https://github.com/matrix-org/matrix-react-sdk/pull/4762)
* Add a checkpoint to index newly encrypted rooms.
[\#4611](https://github.com/matrix-org/matrix-react-sdk/pull/4611)
* Add support to paginate search results when using Seshat.
[\#4705](https://github.com/matrix-org/matrix-react-sdk/pull/4705)
* User versions in the event index.
[\#4788](https://github.com/matrix-org/matrix-react-sdk/pull/4788)
* Fix crash when filtering new room list too fast
[\#4796](https://github.com/matrix-org/matrix-react-sdk/pull/4796)
* hide search results from unknown rooms
[\#4795](https://github.com/matrix-org/matrix-react-sdk/pull/4795)
* Mark the new room list as ready for general testing
[\#4794](https://github.com/matrix-org/matrix-react-sdk/pull/4794)
* Extend QueryMatcher's sorting heuristic
[\#4784](https://github.com/matrix-org/matrix-react-sdk/pull/4784)
* Lint ts semicolons (aka. The great semicolon migration)
[\#4791](https://github.com/matrix-org/matrix-react-sdk/pull/4791)
* Revert "Use recovery keys over passphrases"
[\#4790](https://github.com/matrix-org/matrix-react-sdk/pull/4790)
* Clear `top` when not sticking headers to the top
[\#4783](https://github.com/matrix-org/matrix-react-sdk/pull/4783)
* Don't show a 'show less' button when it's impossible to collapse
[\#4785](https://github.com/matrix-org/matrix-react-sdk/pull/4785)
* Fix show less/more button occluding the list automatically
[\#4786](https://github.com/matrix-org/matrix-react-sdk/pull/4786)
* Improve room switching in the new room list
[\#4787](https://github.com/matrix-org/matrix-react-sdk/pull/4787)
* Remove labs option to cache 'passphrase'
[\#4789](https://github.com/matrix-org/matrix-react-sdk/pull/4789)
* Remove escape backslashes in non-Markdown messages
[\#4694](https://github.com/matrix-org/matrix-react-sdk/pull/4694)
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)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "2.7.2",
"version": "2.9.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {

View file

@ -319,7 +319,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
}
.mx_Dialog_titleImage {
vertical-align: middle;
vertical-align: sub;
width: 25px;
height: 25px;
margin-left: -2px;
@ -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
@ -584,27 +588,16 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
// A context menu that largely fits the | [icon] [label] | format.
.mx_IconizedContextMenu {
// Put 20px of padding around the whole menu. We do this instead of a
// simple `padding: 20px` rule so the horizontal rules added by the
// optionLists is rendered correctly (full width).
> * {
padding-left: 20px;
padding-right: 20px;
&:first-child {
padding-top: 20px;
}
&:last-child {
padding-bottom: 20px;
}
}
min-width: 146px;
.mx_IconizedContextMenu_optionList {
& > * {
padding-left: 20px;
padding-right: 20px;
}
// 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;
// This is a bit of a hack when we could just use a simple border-top property,
// however we have a (kinda) good reason for doing it this way: we need opacity.
// To get the right color, we need an opacity modifier which means we have to work
@ -627,72 +620,76 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
}
}
ul {
list-style: none;
margin: 0;
padding: 0;
// round the top corners of the top button for the hover effect to be bounded
&:first-child .mx_AccessibleButton:first-child {
border-radius: 4px 4px 0 0; // radius matches .mx_ContextualMenu
}
li {
margin: 0;
padding: 20px 0 0;
// round the bottom corners of the bottom button for the hover effect to be bounded
&:last-child .mx_AccessibleButton:last-child {
border-radius: 0 0 4px 4px; // radius matches .mx_ContextualMenu
}
.mx_AccessibleButton {
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
.mx_AccessibleButton {
// pad the inside of the button so that the hover background is padded too
padding-top: 12px;
padding-bottom: 12px;
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
&:hover {
background-color: $menu-selected-color;
}
span:last-child { // labels
padding-left: 14px;
width: 100%;
flex: 1;
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
span.mx_IconizedContextMenu_label { // labels
padding-left: 14px;
width: 100%;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
&.mx_IconizedContextMenu_compact {
> * {
padding-left: 11px;
padding-right: 16px;
&:first-child {
padding-top: 13px;
}
&:last-child {
padding-bottom: 13px;
}
}
.mx_IconizedContextMenu_optionList {
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
margin-top: 10px;
li:first-child {
padding-top: 10px;
}
}
li:first-child {
padding-top: 0;
}
.mx_IconizedContextMenu_optionList > * {
padding: 8px 16px 8px 11px;
}
}
}
@define-mixin ProgressBarColour $colour {
color: $colour;
&::-moz-progress-bar {
background-color: $colour;
}
&::-webkit-progress-value {
background-color: $colour;
}
}
@define-mixin ProgressBarBorderRadius $radius {
border-radius: $radius;
&::-moz-progress-bar {
border-radius: $radius;
}
&::-webkit-progress-bar,
&::-webkit-progress-value {
border-radius: $radius;
}
}

View file

@ -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";
@ -49,6 +49,7 @@
@import "./views/auth/_ServerTypeSelector.scss";
@import "./views/auth/_Welcome.scss";
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_DecoratedRoomAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_LeftPanel during replacement of old component
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
$tagPanelWidth: 70px; // only applies in this file, used for calculations
@ -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,25 +68,10 @@ $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;
overflow-y: hidden;
overflow-x: scroll;
margin-top: 8px;
}
}
@ -143,21 +134,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;

View file

@ -0,0 +1,196 @@
/*
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 {
padding-top: 16px;
padding-bottom: 16px;
color: $warning-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $warning-color;
}
}
.mx_UserMenu_contextMenu_header {
padding: 20px;
// 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');
}
}

View file

@ -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;
}
}
}

View file

@ -18,16 +18,6 @@ $PassphraseStrengthHigh: $accent-color;
$PassphraseStrengthMedium: $username-variant5-color;
$PassphraseStrengthLow: $notice-primary-color;
@define-mixin ProgressBarColour $colour {
color: $colour;
&::-moz-progress-bar {
background-color: $colour;
}
&::-webkit-progress-value {
background-color: $colour;
}
}
progress.mx_PassphraseField_progress {
appearance: none;
width: 100%;
@ -36,15 +26,7 @@ progress.mx_PassphraseField_progress {
position: absolute;
top: -12px;
border-radius: 2px;
&::-moz-progress-bar {
border-radius: 2px;
}
&::-webkit-progress-bar,
&::-webkit-progress-value {
border-radius: 2px;
}
@mixin ProgressBarBorderRadius "2px";
@mixin ProgressBarColour $PassphraseStrengthLow;
&[value="2"], &[value="3"] {
@mixin ProgressBarColour $PassphraseStrengthMedium;

View 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.
*/
// XXX: We shouldn't be using TemporaryTile anywhere - delete it.
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
position: relative;
.mx_RoomTileIcon {
position: absolute;
bottom: 0;
right: 0;
}
.mx_NotificationBadge {
position: absolute;
top: 0;
right: 0;
height: 18px;
width: 18px;
}
}

View file

@ -15,20 +15,79 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AccessSecretStorageDialog_titleWithIcon::before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
top: 5px;
background-color: $primary-fg-color;
}
.mx_AccessSecretStorageDialog_secureBackupTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
}
.mx_AccessSecretStorageDialog_securePhraseTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-phrase.svg');
}
.mx_AccessSecretStorageDialog_keyStatus {
height: 30px;
}
.mx_AccessSecretStorageDialog_primaryContainer {
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
padding: 20px;
}
.mx_AccessSecretStorageDialog_passPhraseInput,
.mx_AccessSecretStorageDialog_recoveryKeyInput {
.mx_AccessSecretStorageDialog_passPhraseInput {
width: 300px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry {
display: flex;
align-items: center;
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput {
flex-grow: 1;
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText {
margin: 16px;
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback {
&::before {
content: "";
display: inline-block;
vertical-align: bottom;
width: 20px;
height: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 20px;
margin-right: 5px;
}
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid {
color: $input-valid-border-color;
&::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
background-color: $input-valid-border-color;
}
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid {
color: $input-invalid-border-color;
&::before {
mask-image: url('$(res)/img/feather-customised/x.svg');
background-color: $input-invalid-border-color;
}
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput {
display: none;
}

View file

@ -48,6 +48,29 @@ limitations under the License.
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_titleWithIcon::before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
top: 5px;
background-color: $primary-fg-color;
}
.mx_CreateSecretStorageDialog_secureBackupTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
}
.mx_CreateSecretStorageDialog_securePhraseTitle::before {
mask-image: url('$(res)/img/feather-customised/secure-phrase.svg');
}
.mx_CreateSecretStorageDialog_centeredTitle, .mx_CreateSecretStorageDialog_centeredBody {
text-align: center;
}
.mx_CreateSecretStorageDialog_primaryContainer {
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
padding-top: 20px;
@ -59,6 +82,36 @@ limitations under the License.
display: block;
}
.mx_CreateSecretStorageDialog_primaryContainer .mx_RadioButton {
margin-bottom: 16px;
padding: 11px;
}
.mx_CreateSecretStorageDialog_optionTitle {
color: $dialog-title-fg-color;
font-weight: 600;
font-size: $font-18px;
padding-bottom: 10px;
}
.mx_CreateSecretStorageDialog_optionIcon {
display: inline-block;
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
top: 5px;
background-color: $primary-fg-color;
}
.mx_CreateSecretStorageDialog_optionIcon_securePhrase {
mask-image: url('$(res)/img/feather-customised/secure-phrase.svg');
}
.mx_CreateSecretStorageDialog_optionIcon_secureBackup {
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
}
.mx_CreateSecretStorageDialog_passPhraseContainer {
display: flex;
align-items: flex-start;
@ -73,33 +126,42 @@ limitations under the License.
margin-left: 20px;
}
.mx_CreateSecretStorageDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_recoveryKeyContainer {
display: flex;
width: 380px;
margin-left: auto;
margin-right: auto;
}
.mx_CreateSecretStorageDialog_recoveryKey {
width: 262px;
font-weight: bold;
text-align: center;
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
margin-right: 12px;
border-radius: 6px;
word-spacing: 1em;
margin-bottom: 20px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
margin-right: 10px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
flex: 1;
width: 160px;
padding-left: 0px;
padding-right: 0px;
white-space: nowrap;
}
.mx_CreateSecretStorageDialog_continueSpinner {
margin-top: 33px;
text-align: right;
}
.mx_CreateSecretStorageDialog_continueSpinner img {
width: 20px;
height: 20px;
}

View file

@ -18,7 +18,7 @@ limitations under the License.
display: inline;
}
.mx_InlineSpinner img {
.mx_InlineSpinner_spin img {
margin: 0px 6px;
vertical-align: -3px;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -14,12 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ProgressBar {
height: 5px;
border: 1px solid $progressbar-color;
}
progress.mx_ProgressBar {
height: 4px;
width: 60px;
border-radius: 10px;
overflow: hidden;
appearance: none;
border: 0;
.mx_ProgressBar_fill {
height: 100%;
background-color: $progressbar-color;
@mixin ProgressBarBorderRadius "10px";
@mixin ProgressBarColour $accent-color;
::-webkit-progress-value {
transition: width 1s;
}
::-moz-progress-bar {
transition: padding-bottom 1s;
padding-bottom: var(--value);
transform-origin: 0 0;
transform: rotate(-90deg) translateX(-15px);
padding-left: 15px;
height: 0;
}
}

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -25,13 +25,14 @@ limitations under the License.
position: relative;
display: flex;
align-items: center;
align-items: baseline;
flex-grow: 1;
> span {
> .mx_RadioButton_content {
flex-grow: 1;
display: flex;
flex-direction: column;
margin-left: 8px;
margin-right: 8px;
@ -105,3 +106,12 @@ limitations under the License.
}
}
}
.mx_RadioButton_outlined {
border: 1px solid $input-darker-bg-color;
border-radius: 8px;
}
.mx_RadioButton_checked {
border-color: $accent-color;
}

View file

@ -55,7 +55,7 @@ limitations under the License.
border-radius: 4px;
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
background-color: $menu-bg-color;
z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs
z-index: 6000; // Higher than context menu so tooltips can be used everywhere
padding: 10px;
pointer-events: none;
line-height: $font-14px;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
.mx_RoomBreadcrumbs2 {
width: 100%;
@ -49,3 +51,18 @@ limitations under the License.
height: 32px;
}
}
.mx_RoomBreadcrumbs2_Tooltip {
margin-left: -42px;
margin-top: -42px;
&.mx_Tooltip {
background-color: $tagpanel-bg-color;
color: $accent-fg-color;
border: 0;
.mx_Tooltip_chevron {
display: none;
}
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_RoomSublist during replacement of old component
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
.mx_RoomSublist2 {
// The sublist is a column of rows, essentially
@ -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 {
// Do not set a width so the badges get properly sized
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,35 @@ 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:focus-within,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
.mx_RoomSublist2_menuButton {
visibility: visible;
width: 24px;
margin-left: 8px;
}
}
@ -304,18 +294,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 +332,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 +358,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 +367,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 +376,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 {
@ -415,3 +390,7 @@ limitations under the License.
margin-top: 8px;
}
}
.mx_RoomSublist2_addRoomTooltip {
margin-top: -3px;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_RoomTile during replacement of old component
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 {
@ -23,27 +23,23 @@ limitations under the License.
// The tile is also a flexbox row itself
display: flex;
flex-wrap: wrap;
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
&.mx_RoomTile2_selected,
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
background-color: $roomtile2-selected-bg-color;
border-radius: 32px;
}
.mx_RoomTile2_avatarContainer {
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 8px;
position: relative;
.mx_RoomTileIcon {
position: absolute;
bottom: 0;
right: 0;
}
}
.mx_RoomTile2_nameContainer {
flex-grow: 1;
max-width: calc(100% - 58px); // 32px avatar, 18px badge area, 8px margin on avatar
min-width: 0; // allow flex to shrink it
margin-right: 8px; // spacing to buttons/badges
// Create a new column layout flexbox for the name parts
display: flex;
@ -67,7 +63,7 @@ limitations under the License.
}
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
font-weight: 600;
font-weight: 700;
}
.mx_RoomTile2_messagePreview {
@ -81,30 +77,44 @@ limitations under the License.
}
}
.mx_RoomTile2_badgeContainer {
width: 18px;
height: 32px;
// Create another flexbox row because it's super easy to position the badge at
// the end this way.
display: flex;
align-items: center;
justify-content: center;
.mx_RoomTile2_menuButton {
margin-left: 4px; // spacing between buttons
}
// The menu button is hidden by default
// TODO: [Notifications] Use mx_RoomTile2_notificationsButton, similar to the following approach:
// https://github.com/matrix-org/matrix-react-sdk/blob/2180a56074f3698fc0241c309a72ba6cad802d1c/res/css/views/rooms/_RoomSublist2.scss#L48-L76
// You'll need to do the same down below on the &:hover selector for the tile.
// ... also remove this 4 line TODO comment.
.mx_RoomTile2_badgeContainer {
height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
position: relative; // fixes badge alignment in some scenarios
// Create a flexbox to make aligning dot badges easier
display: flex;
align-items: center;
.mx_NotificationBadge {
margin-right: 2px; // centering
}
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin-left: 5px;
margin-right: 7px;
}
}
// The context menu buttons are hidden by default
.mx_RoomTile2_menuButton,
.mx_RoomTile2_notificationsButton {
width: 0;
height: 0;
visibility: hidden;
width: 20px;
min-width: 20px; // yay flex
height: 20px;
margin: auto 0;
position: relative;
display: none;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
@ -116,25 +126,29 @@ limitations under the License.
}
}
// If the room has an overriden notification setting then we always show the notifications menu button
.mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show {
display: block;
}
.mx_RoomTile2_menuButton::before {
top: 8px;
left: -1px; // this is off-center to align it with the badges
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
}
&:not(.mx_RoomTile2_minimized) {
&:hover, &.mx_RoomTile2_hasMenuOpen {
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_RoomTile2_badgeContainer {
width: 0;
height: 0;
visibility: hidden;
display: none;
}
.mx_RoomTile2_notificationsButton,
.mx_RoomTile2_menuButton {
width: 18px;
height: 32px;
visibility: visible;
display: block;
}
}
}
@ -144,19 +158,29 @@ limitations under the License.
align-items: center;
position: relative;
.mx_RoomTile2_avatarContainer {
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 0;
}
.mx_RoomTile2_badgeContainer {
position: absolute;
top: 0;
right: 0;
height: 18px;
}
}
}
// We use these both in context menus and the room tiles
.mx_RoomTile2_iconBell::before {
mask-image: url('$(res)/img/feather-customised/bell.svg');
}
.mx_RoomTile2_iconBellDot::before {
mask-image: url('$(res)/img/feather-customised/bell-notification.custom.svg');
}
.mx_RoomTile2_iconBellCrossed::before {
mask-image: url('$(res)/img/feather-customised/bell-crossed.svg');
}
.mx_RoomTile2_iconBellMentions::before {
mask-image: url('$(res)/img/feather-customised/bell-mentions.custom.svg');
}
.mx_RoomTile2_iconCheck::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
}
.mx_RoomTile2_contextMenu {
.mx_RoomTile2_contextMenu_redRow {
.mx_AccessibleButton {
@ -168,6 +192,16 @@ limitations under the License.
}
}
.mx_RoomTile2_contextMenu_activeRow {
&.mx_AccessibleButton, .mx_AccessibleButton {
color: $accent-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $accent-color;
}
}
.mx_IconizedContextMenu_icon {
position: relative;
width: 16px;
@ -193,10 +227,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');
}

View file

@ -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;
}
@ -30,7 +33,7 @@ limitations under the License.
flex-direction: row;
align-items: center;
padding: 15px;
background: $font-slider-bg-color;
background: rgba($appearance-tab-border-color, 0.2);
border-radius: 10px;
font-size: 10px;
margin-top: 24px;
@ -38,7 +41,7 @@ limitations under the License.
}
.mx_AppearanceUserSettingsTab_fontSlider_preview {
border: 1px solid $input-darker-bg-color;
border: 1px solid $appearance-tab-border-color;
border-radius: 10px;
padding: 0 16px 9px 16px;
pointer-events: none;
@ -56,12 +59,14 @@ limitations under the License.
font-size: 15px;
padding-right: 20px;
padding-left: 5px;
font-weight: 500;
}
.mx_AppearanceUserSettingsTab_fontSlider_largeText {
font-size: 18px;
padding-left: 20px;
padding-right: 5px;
font-weight: 500;
}
.mx_AppearanceUserSettingsTab {
@ -115,7 +120,8 @@ limitations under the License.
}
&.mx_ThemeSelector_dark {
background-color: #181b21;
// 5% lightened version of 181b21
background-color: #25282e;
color: #f3f8fd;
> input > div {
@ -163,10 +169,11 @@ limitations under the License.
width: 300px;
border: 1px solid $input-darker-bg-color;
border: 1px solid $appearance-tab-border-color;
border-radius: 10px;
.mx_EventTile_msgOption {
.mx_EventTile_msgOption,
.mx_MessageActionBar {
display: none;
}
@ -175,6 +182,7 @@ limitations under the License.
display: flex;
align-items: center;
padding: 10px;
pointer-events: none;
}
.mx_RadioButton {
@ -185,10 +193,14 @@ limitations under the License.
.mx_EventTile_content {
margin-right: 0;
}
&.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
border-color: $accent-color;
}
}
.mx_RadioButton {
border-top: 1px solid $input-darker-bg-color;
border-top: 1px solid $appearance-tab-border-color;
> input + div {
border-color: rgba($muted-fg-color, 0.2);
@ -199,3 +211,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);
}
}

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.31422 2.4647C8.07372 2.6004 7.98877 2.90537 8.12448 3.14587C8.26018 3.38637 8.56515 3.47132 8.80565 3.33562L8.31422 2.4647ZM18.9999 9.00016L18.4999 8.9999V9.00016H18.9999ZM18.4999 13.0002C18.4999 13.2763 18.7238 13.5002 18.9999 13.5002C19.2761 13.5002 19.4999 13.2763 19.4999 13.0002H18.4999ZM17 17.5004C17.2761 17.5004 17.5 17.2765 17.5 17.0004C17.5 16.7242 17.2761 16.5004 17 16.5004V17.5004ZM2 16.5004C1.72386 16.5004 1.5 16.7242 1.5 17.0004C1.5 17.2765 1.72386 17.5004 2 17.5004V16.5004ZM5 9.00036H5.5L5.5 8.99973L5 9.00036ZM6.22429 6.00974C6.35096 5.76436 6.25474 5.46276 6.00937 5.33608C5.764 5.2094 5.46239 5.30562 5.33571 5.551L6.22429 6.00974ZM14.1625 21.2509C14.301 21.012 14.2197 20.7061 13.9808 20.5675C13.742 20.4289 13.436 20.5103 13.2975 20.7491L14.1625 21.2509ZM10.7025 20.7491C10.5639 20.5103 10.2579 20.4289 10.0191 20.5675C9.78021 20.7061 9.6989 21.012 9.83746 21.2509L10.7025 20.7491ZM8.80565 3.33562C10.8187 2.19975 13.2834 2.21831 15.2791 3.38436L15.7836 2.52094C13.4809 1.17549 10.6369 1.15408 8.31422 2.4647L8.80565 3.33562ZM15.2791 3.38436C17.2748 4.55042 18.5011 6.68854 18.4999 8.9999L19.4999 9.00041C19.5013 6.33346 18.0863 3.86639 15.7836 2.52094L15.2791 3.38436ZM18.4999 9.00016V13.0002H19.4999V9.00016H18.4999ZM17 16.5004H2V17.5004H17V16.5004ZM2 17.5004C3.933 17.5004 5.5 15.9334 5.5 14.0004H4.5C4.5 15.3811 3.38071 16.5004 2 16.5004V17.5004ZM5.5 14.0004V9.00036H4.5V14.0004H5.5ZM5.5 8.99973C5.49869 7.95947 5.74707 6.93408 6.22429 6.00974L5.33571 5.551C4.78509 6.61755 4.49849 7.80069 4.5 9.00099L5.5 8.99973ZM13.2975 20.7491C13.0291 21.2117 12.5348 21.4965 12 21.4965V22.4965C12.8913 22.4965 13.7152 22.0219 14.1625 21.2509L13.2975 20.7491ZM12 21.4965C11.4652 21.4965 10.9708 21.2117 10.7025 20.7491L9.83746 21.2509C10.2847 22.0219 11.1086 22.4965 12 22.4965V21.4965Z" fill="#2E2F32"/>
<path d="M1 1L23 23" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.62998 3.57476C7.55241 1.6636 9.6476 0.644608 11.692 1.13263C13.7342 1.62012 15.1666 3.47754 15.1667 5.60305V5.60308V6.01222C15.1667 6.95553 14.4163 7.73965 13.4668 7.73965C12.9349 7.73965 12.4655 7.49363 12.1555 7.11141C11.7768 7.49925 11.2519 7.74098 10.6668 7.74098C9.49647 7.74098 8.56689 6.77368 8.56689 5.60441C8.56689 4.43514 9.49647 3.46784 10.6668 3.46784C11.8348 3.46784 12.7629 4.43111 12.7668 5.59709L12.7668 5.60308V6.01222C12.7668 6.4247 13.0908 6.73965 13.4668 6.73965C13.8428 6.73965 14.1667 6.4247 14.1667 6.01222V5.60311V5.60308C14.1666 3.92595 13.0379 2.48201 11.4598 2.1053C9.8839 1.72911 8.25387 2.51086 7.53057 4.00944C6.80579 5.5111 7.19017 7.3233 8.44894 8.38151C9.70415 9.43672 11.5011 9.46808 12.7905 8.45807C13.0079 8.28778 13.3221 8.32596 13.4924 8.54335C13.6627 8.76074 13.6245 9.07501 13.4071 9.24529C11.745 10.5473 9.42229 10.5062 7.80545 9.14696C6.19216 7.79072 5.70903 5.48285 6.62998 3.57476ZM10.6668 4.46784C10.07 4.46784 9.56689 4.96597 9.56689 5.60441C9.56689 6.24285 10.07 6.74098 10.6668 6.74098C11.2637 6.74098 11.7668 6.24285 11.7668 5.60441C11.7668 4.96597 11.2637 4.46784 10.6668 4.46784ZM5.48951 2.14C5.61741 2.38474 5.5227 2.68682 5.27796 2.81472C3.92878 3.51981 3 4.95881 3 6.62506V10.0347C3 10.6137 2.8091 11.1505 2.48631 11.5805H13.8333C14.1095 11.5805 14.3333 11.8043 14.3333 12.0805C14.3333 12.3566 14.1095 12.5805 13.8333 12.5805H0.5C0.223858 12.5805 0 12.3566 0 12.0805C0 11.8043 0.223858 11.5805 0.5 11.5805C1.31782 11.5805 2 10.8991 2 10.0347V6.62506C2 4.58053 3.14094 2.80322 4.81479 1.92845C5.05953 1.80055 5.36161 1.89527 5.48951 2.14ZM5.76678 14.3741C6.00698 14.2379 6.31214 14.3222 6.44836 14.5624C6.59999 14.8298 6.8752 14.9886 7.16676 14.9886C7.45832 14.9886 7.73354 14.8298 7.88516 14.5624C8.02139 14.3222 8.32654 14.2379 8.56674 14.3741C8.80695 14.5104 8.89124 14.8155 8.75502 15.0557C8.42959 15.6296 7.82596 15.9886 7.16676 15.9886C6.50756 15.9886 5.90393 15.6296 5.5785 15.0557C5.44228 14.8155 5.52657 14.5104 5.76678 14.3741Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.73 21C13.3722 21.6168 12.7131 21.9965 12 21.9965C11.287 21.9965 10.6278 21.6168 10.27 21" stroke="#2E2F32" stroke-linecap="round"/>
<path d="M11.9999 2.00024C8.13388 2.00024 4.99988 5.13425 4.99988 9.00024V14.0002C4.99988 15.6571 3.65673 17.0002 1.99988 17.0002H21.9999C20.343 17.0002 18.9999 15.6571 18.9999 14.0002V12.75" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="18.75" cy="5.25" r="4.75" stroke="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 17.5C22.2761 17.5 22.5 17.2761 22.5 17C22.5 16.7239 22.2761 16.5 22 16.5V17.5ZM2 16.5C1.72386 16.5 1.5 16.7239 1.5 17C1.5 17.2761 1.72386 17.5 2 17.5V16.5ZM5 9H4.5H5ZM19 9H19.5H19ZM14.1625 21.2509C14.3011 21.012 14.2197 20.7061 13.9809 20.5675C13.742 20.4289 13.4361 20.5103 13.2975 20.7491L14.1625 21.2509ZM10.7025 20.7491C10.5639 20.5103 10.258 20.4289 10.0191 20.5675C9.78025 20.7061 9.69894 21.012 9.8375 21.2509L10.7025 20.7491ZM22 16.5H2V17.5H22V16.5ZM2 17.5C3.933 17.5 5.5 15.933 5.5 14H4.5C4.5 15.3807 3.38071 16.5 2 16.5V17.5ZM5.5 14V9H4.5V14H5.5ZM5.5 9C5.5 5.41015 8.41015 2.5 12 2.5V1.5C7.85786 1.5 4.5 4.85786 4.5 9H5.5ZM12 2.5C15.5899 2.5 18.5 5.41015 18.5 9H19.5C19.5 4.85786 16.1421 1.5 12 1.5V2.5ZM18.5 9V14H19.5V9H18.5ZM18.5 14C18.5 15.933 20.067 17.5 22 17.5V16.5C20.6193 16.5 19.5 15.3807 19.5 14H18.5ZM13.2975 20.7491C13.0292 21.2117 12.5348 21.4965 12 21.4965V22.4965C12.8913 22.4965 13.7153 22.0219 14.1625 21.2509L13.2975 20.7491ZM12 21.4965C11.4652 21.4965 10.9708 21.2117 10.7025 20.7491L9.8375 21.2509C10.2847 22.0219 11.1087 22.4965 12 22.4965V21.4965Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="1" y="2" width="22" height="21">
<rect x="1" y="2" width="21.5" height="5" fill="white"/>
<rect x="1" y="17.7" width="21.5" height="5" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M3.16626 12.4014C3.16626 7.64675 6.99526 3.81775 11.75 3.81775C13.0964 3.81775 14.4429 4.15437 15.6631 4.74345H14.9899C14.5691 4.74345 14.2325 5.08006 14.2325 5.50083C14.2325 5.9216 14.5691 6.25822 14.9899 6.25822H17.3041C17.809 6.25822 18.1877 5.83745 18.1877 5.3746V3.06037C18.1877 2.6396 17.8511 2.30298 17.4303 2.30298C17.0096 2.30298 16.673 2.6396 16.673 3.06037V3.60737C16.6309 3.56529 16.5888 3.5653 16.5467 3.52322C15.074 2.72376 13.433 2.30298 11.75 2.30298C6.1958 2.30298 1.65149 6.84729 1.65149 12.4014C1.65149 14.0845 2.07226 15.7676 2.87172 17.2403C2.99795 17.4928 3.25041 17.619 3.54495 17.619C3.67118 17.619 3.79741 17.5769 3.92364 17.5348C4.30233 17.3245 4.42857 16.8616 4.21819 16.525C3.50288 15.2627 3.16626 13.8321 3.16626 12.4014Z" fill="#2E2F32"/>
<path d="M20.6281 7.56263C20.4177 7.18394 19.9548 7.05771 19.6182 7.2681C19.2395 7.47848 19.1133 7.94133 19.3237 8.27794C19.9969 9.54025 20.3756 10.9288 20.3756 12.4015C20.3756 17.1562 16.5045 20.9852 11.7919 20.9852C10.4454 20.9852 9.09897 20.6486 7.87874 20.0595H8.55198C8.97275 20.0595 9.30937 19.7229 9.30937 19.3021C9.30937 18.8813 8.97275 18.5447 8.55198 18.5447H6.23774C5.73282 18.5447 5.35413 18.9655 5.35413 19.4283V21.7426C5.35413 22.1633 5.69075 22.4999 6.11152 22.4999C6.53229 22.4999 6.8689 22.1633 6.8689 21.7426V21.1956C6.91098 21.2376 6.95306 21.2376 6.99514 21.2797C8.42575 22.0792 10.0667 22.4999 11.7498 22.4999C17.304 22.4999 21.8483 17.9556 21.8483 12.4015C21.8483 10.7184 21.4275 9.03532 20.6281 7.56263Z" fill="#2E2F32"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 9C1.89543 9 1 9.89543 1 11V14C1 15.1046 1.89543 16 3 16H21C22.1046 16 23 15.1046 23 14V11C23 9.89543 22.1046 9 21 9H3ZM5.25 10.5C4.83579 10.5 4.5 10.8358 4.5 11.25C4.5 11.6642 4.83579 12 5.25 12H7.75C8.16421 12 8.5 11.6642 8.5 11.25C8.5 10.8358 8.16421 10.5 7.75 10.5H5.25ZM9.5 11.25C9.5 10.8358 9.83579 10.5 10.25 10.5H10.75C11.1642 10.5 11.5 10.8358 11.5 11.25C11.5 11.6642 11.1642 12 10.75 12H10.25C9.83579 12 9.5 11.6642 9.5 11.25ZM13.25 10.5C12.8358 10.5 12.5 10.8358 12.5 11.25C12.5 11.6642 12.8358 12 13.25 12H15.75C16.1642 12 16.5 11.6642 16.5 11.25C16.5 10.8358 16.1642 10.5 15.75 10.5H13.25ZM17.5 11.25C17.5 10.8358 17.8358 10.5 18.25 10.5H18.75C19.1642 10.5 19.5 10.8358 19.5 11.25C19.5 11.6642 19.1642 12 18.75 12H18.25C17.8358 12 17.5 11.6642 17.5 11.25ZM5.25 13C4.83579 13 4.5 13.3358 4.5 13.75C4.5 14.1642 4.83579 14.5 5.25 14.5H5.75C6.16421 14.5 6.5 14.1642 6.5 13.75C6.5 13.3358 6.16421 13 5.75 13H5.25ZM7.5 13.75C7.5 13.3358 7.83579 13 8.25 13H10.75C11.1642 13 11.5 13.3358 11.5 13.75C11.5 14.1642 11.1642 14.5 10.75 14.5H8.25C7.83579 14.5 7.5 14.1642 7.5 13.75ZM13.25 13C12.8358 13 12.5 13.3358 12.5 13.75C12.5 14.1642 12.8358 14.5 13.25 14.5H13.75C14.1642 14.5 14.5 14.1642 14.5 13.75C14.5 13.3358 14.1642 13 13.75 13H13.25Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="1" y="2" width="22" height="21">
<rect x="1" y="2" width="21.5" height="5" fill="white"/>
<rect x="1" y="17.7" width="21.5" height="5" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M3.16626 12.4014C3.16626 7.64675 6.99526 3.81775 11.75 3.81775C13.0964 3.81775 14.4429 4.15437 15.6631 4.74345H14.9899C14.5691 4.74345 14.2325 5.08006 14.2325 5.50083C14.2325 5.9216 14.5691 6.25822 14.9899 6.25822H17.3041C17.809 6.25822 18.1877 5.83745 18.1877 5.3746V3.06037C18.1877 2.6396 17.8511 2.30298 17.4303 2.30298C17.0096 2.30298 16.673 2.6396 16.673 3.06037V3.60737C16.6309 3.56529 16.5888 3.5653 16.5467 3.52322C15.074 2.72376 13.433 2.30298 11.75 2.30298C6.1958 2.30298 1.65149 6.84729 1.65149 12.4014C1.65149 14.0845 2.07226 15.7676 2.87172 17.2403C2.99795 17.4928 3.25041 17.619 3.54495 17.619C3.67118 17.619 3.79741 17.5769 3.92364 17.5348C4.30233 17.3245 4.42857 16.8616 4.21819 16.525C3.50288 15.2627 3.16626 13.8321 3.16626 12.4014Z" fill="#2E2F32"/>
<path d="M20.6281 7.56263C20.4177 7.18394 19.9548 7.05771 19.6182 7.2681C19.2395 7.47848 19.1133 7.94133 19.3237 8.27794C19.9969 9.54025 20.3756 10.9288 20.3756 12.4015C20.3756 17.1562 16.5045 20.9852 11.7919 20.9852C10.4454 20.9852 9.09897 20.6486 7.87874 20.0595H8.55198C8.97275 20.0595 9.30937 19.7229 9.30937 19.3021C9.30937 18.8813 8.97275 18.5447 8.55198 18.5447H6.23774C5.73282 18.5447 5.35413 18.9655 5.35413 19.4283V21.7426C5.35413 22.1633 5.69075 22.4999 6.11152 22.4999C6.53229 22.4999 6.8689 22.1633 6.8689 21.7426V21.1956C6.91098 21.2376 6.95306 21.2376 6.99514 21.2797C8.42575 22.0792 10.0667 22.4999 11.7498 22.4999C17.304 22.4999 21.8483 17.9556 21.8483 12.4015C21.8483 10.7184 21.4275 9.03532 20.6281 7.56263Z" fill="#2E2F32"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 11C1 9.89543 1.89543 9 3 9H21C22.1046 9 23 9.89543 23 11V14C23 15.1046 22.1046 16 21 16H3C1.89543 16 1 15.1046 1 14V11ZM6 12.5C6 13.3284 5.32843 14 4.5 14C3.67157 14 3 13.3284 3 12.5C3 11.6716 3.67157 11 4.5 11C5.32843 11 6 11.6716 6 12.5ZM9.5 14C10.3284 14 11 13.3284 11 12.5C11 11.6716 10.3284 11 9.5 11C8.67157 11 8 11.6716 8 12.5C8 13.3284 8.67157 14 9.5 14ZM16 12.5C16 13.3284 15.3284 14 14.5 14C13.6716 14 13 13.3284 13 12.5C13 11.6716 13.6716 11 14.5 11C15.3284 11 16 11.6716 16 12.5ZM19.5 14C20.3284 14 21 13.3284 21 12.5C21 11.6716 20.3284 11 19.5 11C18.6716 11 18 11.6716 18 12.5C18 13.3284 18.6716 14 19.5 14Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

3
res/img/spinner.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="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

View file

@ -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;
@ -198,8 +198,8 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color;
// FontSlider colors
$font-slider-bg-color: $room-highlight-color;
// Appearance tab colors
$appearance-tab-border-color: $room-highlight-color;
// ***** Mixins! *****

View file

@ -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);

View file

@ -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;
@ -327,7 +327,7 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color;
// FontSlider colors
$font-slider-bg-color: rgba($input-darker-bg-color, 0.2);
$appearance-tab-border-color: $input-darker-bg-color;
// ***** Mixins! *****

View file

@ -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

View file

@ -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,

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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,

View file

@ -495,8 +495,7 @@ export const Commands = [
});
return success();
} else if (params[0][0] === '!') {
const roomId = params[0];
const viaServers = params.splice(0);
const [roomId, ...viaServers] = params;
dis.dispatch({
action: 'view_room',

View file

@ -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 '';
}

View file

@ -26,20 +26,27 @@ import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
const PHASE_MIGRATE = 2;
const PHASE_PASSPHRASE = 3;
const PHASE_PASSPHRASE_CONFIRM = 4;
const PHASE_SHOWKEY = 5;
const PHASE_KEEPITSAFE = 6;
const PHASE_STORING = 7;
const PHASE_DONE = 8;
const PHASE_CONFIRM_SKIP = 9;
const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
const PHASE_MIGRATE = 3;
const PHASE_PASSPHRASE = 4;
const PHASE_PASSPHRASE_CONFIRM = 5;
const PHASE_SHOWKEY = 6;
const PHASE_STORING = 8;
const PHASE_CONFIRM_SKIP = 10;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
// these end up as strings from being values in the radio buttons, so just use strings
const CREATE_STORAGE_OPTION_KEY = 'key';
const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase';
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
@ -70,6 +77,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
passPhraseConfirm: '',
copied: false,
downloaded: false,
setPassphrase: false,
backupInfo: null,
backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password
@ -77,8 +85,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
// status of the key backup toggle switch
useKeyBackup: true,
passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
};
this._passphraseField = createRef();
@ -110,7 +118,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
const { force } = this.props;
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE;
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
this.setState({
phase,
@ -152,14 +160,33 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
}
_onKeyPassphraseChange = e => {
this.setState({
passPhraseKeySelected: e.target.value,
});
}
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
_onUseKeyBackupChange = (enabled) => {
this.setState({
useKeyBackup: enabled,
});
_onChooseKeyPassphraseFormSubmit = async () => {
if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
setPassphrase: false,
phase: PHASE_SHOWKEY,
});
} else {
this.setState({
copied: false,
downloaded: false,
phase: PHASE_PASSPHRASE,
});
}
}
_onMigrateFormSubmit = (e) => {
@ -176,7 +203,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (successful) {
this.setState({
copied: true,
phase: PHASE_KEEPITSAFE,
});
}
}
@ -189,7 +215,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
}
@ -259,22 +284,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: this.state.useKeyBackup,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
if (!this.state.useKeyBackup && this.state.backupInfo) {
// If the user is resetting their cross-signing keys and doesn't want
// key backup (but had it enabled before), delete the key backup as it's
// no longer valid.
console.log("Deleting invalid key backup (secrets have been reset; key backup not requested)");
await cli.deleteKeyBackupVersion(this.state.backupInfo.version);
}
} else {
await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
setupNewKeyBackup: !this.state.backupInfo,
getKeyBackupPassphrase: () => {
// We may already have the backup key if we earlier went
// through the restore backup path, so pass it along
@ -286,9 +304,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
},
});
}
this.setState({
phase: PHASE_DONE,
});
this.props.onFinished(true);
} catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({
@ -342,22 +358,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._fetchBackupInfo();
}
_onSkipSetupClick = () => {
_onShowKeyContinueClick = () => {
this._bootstrapSecretStorage();
}
_onCancelClick = () => {
this.setState({phase: PHASE_CONFIRM_SKIP});
}
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
}
_onSkipPassPhraseClick = async () => {
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
_onGoBackClick = () => {
this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE});
}
_onPassPhraseNextClick = async (e) => {
@ -384,6 +394,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({
copied: false,
downloaded: false,
setPassphrase: true,
phase: PHASE_SHOWKEY,
});
}
@ -397,12 +408,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
});
}
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseValidate = (result) => {
this.setState({
passPhraseValid: result.valid,
@ -427,13 +432,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
});
}
_renderPhaseChooseKeyPassphrase() {
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
)}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}>
<StyledRadioButton
key={CREATE_STORAGE_OPTION_KEY}
value={CREATE_STORAGE_OPTION_KEY}
name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
{_t("Generate a Security Key")}
</div>
<div>{_t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
</StyledRadioButton>
<StyledRadioButton
key={CREATE_STORAGE_OPTION_PASSPHRASE}
value={CREATE_STORAGE_OPTION_PASSPHRASE}
name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
{_t("Enter a Security Phrase")}
</div>
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
</StyledRadioButton>
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit}
onCancel={this._onCancelClick}
hasCancel={true}
/>
</form>;
}
_renderPhaseMigrate() {
// TODO: This is a temporary screen so people who have the labs flag turned on and
// click the button are aware they're making a change to their account.
// Once we're confident enough in this (and it's supported enough) we can do
// it automatically.
// https://github.com/vector-im/riot-web/issues/11696
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
let authPrompt;
@ -446,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
label={_t("Password")}
value={this.state.accountPassword}
onChange={this._onAccountPasswordChange}
flagInvalid={this.state.accountPasswordCorrect === false}
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true}
/></div>
</div>;
@ -474,7 +521,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this._onSkipSetupClick}>
<button type="button" className="danger" onClick={this._onCancelClick}>
{_t('Skip')}
</button>
</DialogButtons>
@ -482,14 +529,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
"This should be different to your account password:",
"Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
@ -508,11 +551,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
/>
</div>
<LabelledToggleSwitch
label={ _t("Back up encrypted message keys")}
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
/>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
@ -520,22 +558,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
disabled={!this.state.passPhraseValid}
>
<button type="button"
onClick={this._onSkipSetupClick}
onClick={this._onCancelClick}
className="danger"
>{_t("Skip")}</button>
>{_t("Cancel")}</button>
</DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</AccessibleButton>
</details>
</form>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Field = sdk.getComponent('views.elements.Field');
let matchText;
@ -566,7 +596,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Enter your recovery passphrase a second time to confirm it.",
@ -592,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button"
onClick={this._onSkipSetupClick}
onClick={this._onCancelClick}
className="danger"
>{_t("Skip")}</button>
</DialogButtons>
@ -600,66 +629,48 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_renderPhaseShowKey() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let continueButton;
if (this.state.phase === PHASE_SHOWKEY) {
continueButton = <DialogButtons primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
onPrimaryButtonClick={this._onShowKeyContinueClick}
hasCancel={false}
/>;
} else {
continueButton = <div className="mx_CreateSecretStorageDialog_continueSpinner">
<InlineSpinner />
</div>;
}
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your recovery passphrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
"Store your Security Key somewhere safe, like a password manager or a safe, " +
"as its used to safeguard your encrypted data.",
)}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
{_t("Your recovery key")}
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary"
onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING}
>
{_t("Download")}
</AccessibleButton>
<span>{_t("or")}</span>
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING}
>
{_t("Copy")}
</AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
{this.state.copied ? _t("Copied!") : _t("Copy")}
</AccessibleButton>
</div>
</div>
</div>
</div>;
}
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
{continueButton}
</div>;
}
@ -671,7 +682,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_renderPhaseLoadError() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Unable to query secret storage status")}</p>
<div className="mx_Dialog_buttons">
@ -684,53 +694,39 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>;
}
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
_renderPhaseSkipConfirm() {
return <div>
<p>{_t(
"You can now verify your other devices, " +
"and other users to keep your chats safe.",
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
)}</p>
<p>{_t(
"You can also set up Secure Backup & manage your keys in Settings.",
)}</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
/>
</div>;
}
_renderPhaseSkipConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}
<DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onSetUpClick}
onPrimaryButtonClick={this._onGoBackClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this._onCancel}>{_t('Skip')}</button>
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button>
</DialogButtons>
</div>;
}
_titleForPhase(phase) {
switch (phase) {
case PHASE_CHOOSE_KEY_PASSPHRASE:
return _t('Set up Secure backup');
case PHASE_MIGRATE:
return _t('Upgrade your encryption');
case PHASE_PASSPHRASE:
return _t('Set up encryption');
return _t('Set a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm recovery passphrase');
return _t('Confirm Security Phrase');
case PHASE_CONFIRM_SKIP:
return _t('Are you sure?');
case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE:
return _t('Make a copy of your recovery key');
return _t('Save your Security Key');
case PHASE_STORING:
return _t('Setting up keys');
case PHASE_DONE:
return _t("You're done!");
default:
return '';
}
@ -741,7 +737,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content;
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons">
@ -760,6 +755,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_LOADERROR:
content = this._renderPhaseLoadError();
break;
case PHASE_CHOOSE_KEY_PASSPHRASE:
content = this._renderPhaseChooseKeyPassphrase();
break;
case PHASE_MIGRATE:
content = this._renderPhaseMigrate();
break;
@ -772,31 +770,40 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_SHOWKEY:
content = this._renderPhaseShowKey();
break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
break;
case PHASE_CONFIRM_SKIP:
content = this._renderPhaseSkipConfirm();
break;
}
}
let headerImage;
if (this._titleForPhase(this.state.phase)) {
headerImage = require("../../../../../res/img/e2e/normal.svg");
let titleClass = null;
switch (this.state.phase) {
case PHASE_PASSPHRASE:
case PHASE_PASSPHRASE_CONFIRM:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_securePhraseTitle',
];
break;
case PHASE_SHOWKEY:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_secureBackupTitle',
];
break;
case PHASE_CHOOSE_KEY_PASSPHRASE:
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
break;
}
return (
<BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
headerImage={headerImage}
titleClass={titleClass}
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
fixedWidth={false}
>

View file

@ -25,9 +25,9 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components';
import * as sdk from '../index';
import _sortBy from 'lodash/sortBy';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter";
import { uniqBy, sortBy } from 'lodash';
const ROOM_REGEX = /\B#\S*/g;
@ -91,10 +91,11 @@ export default class RoomProvider extends AutocompleteProvider {
this.matcher.setObjects(matcherObjects);
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = _sortBy(completions, [
completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length,
]);
completions = uniqBy(completions, (match) => match.room);
completions = completions.map((room) => {
return {
completion: room.displayedAlias,

View file

@ -116,6 +116,7 @@ export class ContextMenu extends React.Component {
this.props.onFinished();
e.preventDefault();
e.stopPropagation();
const x = e.clientX;
const y = e.clientY;
@ -133,6 +134,19 @@ export class ContextMenu extends React.Component {
}
};
onContextMenuPreventBubbling = (e) => {
// stop propagation so that any context menu handlers don't leak out of this context menu
// but do not inhibit the default browser menu
e.stopPropagation();
};
// Prevent clicks on the background from going through to the component which opened the menu.
_onFinished = (ev: InputEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (this.props.onFinished) this.props.onFinished();
};
_onMoveFocus = (element, up) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
@ -319,12 +333,12 @@ export class ContextMenu extends React.Component {
let background;
if (hasBackground) {
background = (
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={this._onFinished} onContextMenu={this.onContextMenu} />
);
}
return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown} onContextMenu={this.onContextMenuPreventBubbling}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
{ chevron }
{ props.children }
@ -340,10 +354,18 @@ export class ContextMenu extends React.Component {
}
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => {
export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} title={label} aria-label={label} aria-haspopup={true} aria-expanded={isExpanded}>
<AccessibleButton
{...props}
onClick={onClick}
onContextMenu={onContextMenu || onClick}
title={label}
aria-label={label}
aria-haspopup={true}
aria-expanded={isExpanded}
>
{ children }
</AccessibleButton>
);

View file

@ -15,21 +15,26 @@ 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";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
import {Key} from "../../Keyboard";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
@ -41,20 +46,21 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
interface IProps {
isMinimized: boolean;
resizeNotifier: ResizeNotifier;
}
interface IState {
searchFilter: string; // TODO: Move search into room list?
searchFilter: string;
showBreadcrumbs: boolean;
showTagPanel: boolean;
}
export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel
// TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs
// TODO: a11y
// TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?)
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
private focusedElement = null;
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
constructor(props: IProps) {
super(props);
@ -62,13 +68,25 @@ 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);
RoomListStore.instance.on(LISTS_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);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
}
private onSearch = (term: string): void => {
@ -86,9 +104,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,75 +139,111 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.top = `unset`;
}
}
}
// TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
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 onFocus = (ev: React.FocusEvent) => {
this.focusedElement = ev.target;
};
private onBlur = () => {
this.focusedElement = null;
};
private onKeyDown = (ev: React.KeyboardEvent) => {
if (!this.focusedElement) return;
switch (ev.key) {
case Key.ARROW_UP:
case Key.ARROW_DOWN:
ev.stopPropagation();
ev.preventDefault();
this.onMoveFocus(ev.key === Key.ARROW_UP);
break;
}
};
private onMoveFocus = (up: boolean) => {
let element = this.focusedElement;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && !(
classes.contains("mx_RoomTile2") ||
classes.contains("mx_RoomSublist2_headerText") ||
classes.contains("mx_RoomSearch_input")));
if (element) {
element.focus();
this.focusedElement = element;
}
};
private renderHeader(): React.ReactNode {
// TODO: Update when profile info changes
// TODO: Presence
// TODO: Breadcrumbs toggle
// TODO: Menu button
const avatarSize = 32;
// TODO: Don't do this profile lookup in render()
const client = MatrixClientPeg.get();
let displayName = client.getUserId();
let avatarUrl: string = null;
const myUser = client.getUser(client.getUserId());
if (myUser) {
displayName = myUser.rawDisplayName;
avatarUrl = myUser.avatarUrl;
}
let breadcrumbs;
if (this.state.showBreadcrumbs) {
breadcrumbs = (
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar">
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
</div>
);
}
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>
);
}
private renderSearchExplore(): React.ReactNode {
// TODO: Collapsed support
return (
<div className="mx_LeftPanel2_filterContainer">
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
<div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
<RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown}
/>
<AccessibleButton
tabIndex={-1}
className='mx_LeftPanel2_exploreButton'
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
className="mx_LeftPanel2_exploreButton"
onClick={this.onExplore}
alt={_t("Explore rooms")}
/>
@ -200,37 +252,49 @@ 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>
);
// TODO: Improve props for RoomList2
const roomList = <RoomList2
onKeyDown={() => {/*TODO*/}}
onKeyDown={this.onKeyDown}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
onFocus={this.onFocus}
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}
/>;
// TODO: Conference handling / calls
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
const containerClasses = classNames({
"mx_LeftPanel2": true,
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
"mx_LeftPanel2_minimized": this.props.isMinimized,
});
const roomListClasses = classNames(
"mx_LeftPanel2_actualRoomListContainer",
"mx_AutoHideScrollbar",
);
return (
<div className={containerClasses}>
{tagPanel}
<aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}>
<div
className={roomListClasses}
onScroll={this.onScroll}
ref={this.listContainerRef}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
{roomList}
</div>
</aside>

View file

@ -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 = {
@ -146,6 +146,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected readonly _compactLayoutWatcherRef: string;
protected resizer: Resizer;
constructor(props, context) {
@ -177,6 +178,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this._matrixClient.on("sync", this.onSync);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
"useCompactLayout", null, this.onCompactLayoutChanged,
);
fixupColorFonts();
this._roomView = React.createRef();
@ -194,6 +199,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
@ -263,16 +269,17 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}
onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
});
}
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
};
onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => {
this.setState({
useCompactLayout: valueAtLevel,
});
};
onSync = (syncState, oldSyncState, data) => {
const oldErrCode = (
this.state.syncErrorData &&
@ -677,7 +684,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}
/>
);
}

View file

@ -18,10 +18,11 @@ 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";
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible';
// what-input helps improve keyboard accessibility
@ -1612,6 +1613,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 +1842,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,17 +1929,20 @@ 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 &&
const initialScreenAfterLogin = this.props.initialScreenAfterLogin;
if (initialScreenAfterLogin &&
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
!["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen)
!["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen)
) {
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
fragmentAfterLogin = `/${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 = (

View file

@ -388,8 +388,11 @@ export default class MessagePanel extends React.Component {
}
return (
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
className="mx_RoomView_myReadMarker_container">
<li key={"readMarker_"+eventId}
ref={this._readMarkerNode}
className="mx_RoomView_myReadMarker_container"
data-scroll-tokens={eventId}
>
{ hr }
</li>
);

View file

@ -25,6 +25,8 @@ import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
*******************************************************************
@ -36,6 +38,7 @@ import { Action } from "../../dispatcher/actions";
interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent);
}
interface IState {
@ -109,6 +112,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (ev.key === Key.ESCAPE) {
this.clearInput();
defaultDispatcher.fire(Action.FocusComposer);
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
this.props.onVerticalArrow(ev);
}
};

View file

@ -1819,6 +1819,7 @@ export default createReactClass({
);
const showRoomRecoveryReminder = (
this.context.isCryptoEnabled() &&
SettingsStore.getValue("showRoomRecoveryReminder") &&
this.context.isRoomEncrypted(this.state.room.roomId) &&
this.context.getKeyBackupEnabled() === false

View file

@ -0,0 +1,356 @@
/*
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, MenuItem} 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 {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";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
interface IProps {
isMinimized: boolean;
}
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
}
interface IMenuButtonProps {
iconClassName: string;
label: string;
onClick(ev: ButtonEvent);
}
const MenuButton: React.FC<IMenuButtonProps> = ({iconClassName, label, onClick}) => {
return <MenuItem label={label} onClick={onClick}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};
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 = {
contextMenuPosition: null,
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
if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
};
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({contextMenuPosition: target.getBoundingClientRect()});
};
private onContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
contextMenuPosition: {
left: ev.clientX,
top: ev.clientY,
width: 20,
height: 0,
},
});
};
private onCloseMenu = () => {
this.setState({contextMenuPosition: null});
};
private onSwitchThemeClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
// 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({contextMenuPosition: null}); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038
console.log("TODO: Show archived rooms");
};
private onProvideFeedback = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
this.setState({contextMenuPosition: null}); // also close the menu
};
private onSignOutClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
this.setState({contextMenuPosition: null}); // 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.contextMenuPosition) 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 = (
<MenuButton
iconClassName="mx_UserMenu_iconHome"
label={_t("Home")}
onClick={this.onHomeClick}
/>
);
}
return (
<ContextMenu
chevronFace="none"
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.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>
<AccessibleTooltipButton
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}
/>
</AccessibleTooltipButton>
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
{homeButton}
<MenuButton
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
<MenuButton
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/>
<MenuButton
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</div>
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
<MenuButton
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</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.contextMenuPosition}
onContextMenu={this.onContextMenu}
>
<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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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'],

View file

@ -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 />

View file

@ -0,0 +1,65 @@
/*
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 { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import RoomTileIcon from "../rooms/RoomTileIcon";
import NotificationBadge from '../rooms/NotificationBadge';
import { INotificationState } from "../../../stores/notifications/INotificationState";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
interface IProps {
room: Room;
avatarSize: number;
tag: TagID;
displayBadge?: boolean;
forceCount?: boolean;
}
interface IState {
notificationState?: INotificationState;
}
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
};
}
public render(): React.ReactNode {
let badge: React.ReactNode;
if (this.props.displayBadge) {
badge = <NotificationBadge
notification={this.state.notificationState}
forceCount={this.props.forceCount}
roomId={this.props.room.roomId}
/>;
}
return <div className="mx_DecoratedRoomAvatar">
<RoomAvatar room={this.props.room} width={this.props.avatarSize} height={this.props.avatarSize} />
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
{badge}
</div>;
}
}

View file

@ -75,8 +75,12 @@ export default createReactClass({
// If provided, this is used to add a aria-describedby attribute
contentId: PropTypes.string,
// optional additional class for the title element
titleClass: PropTypes.string,
// optional additional class for the title element (basically anything that can be passed to classnames)
titleClass: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
]),
},
getDefaultProps: function() {

View file

@ -15,13 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { debounce } from 'lodash';
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
// maybe that's a thing people would do?
const KEY_FILE_MAX_SIZE = 128;
// Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200;
/*
* Access Secure Secret Storage by requesting the user's passphrase.
@ -36,9 +47,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
this._fileUpload = React.createRef();
this.state = {
recoveryKey: "",
recoveryKeyValid: false,
recoveryKeyValid: null,
recoveryKeyCorrect: null,
recoveryKeyFileError: null,
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
@ -55,18 +71,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onResetRecoveryClick = () => {
// Re-enter the access flow, but resetting storage this time around.
this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true);
_validateRecoveryKeyOnChange = debounce(() => {
this._validateRecoveryKey();
}, VALIDATION_THROTTLE_MS);
async _validateRecoveryKey() {
if (this.state.recoveryKey === '') {
this.setState({
recoveryKeyValid: null,
recoveryKeyCorrect: null,
});
return;
}
try {
const cli = MatrixClientPeg.get();
const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
const correct = await cli.checkSecretStorageKey(
decodedKey, this.props.keyInfo,
);
this.setState({
recoveryKeyValid: true,
recoveryKeyCorrect: correct,
});
} catch (e) {
this.setState({
recoveryKeyValid: false,
recoveryKeyCorrect: false,
});
}
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
keyMatches: null,
recoveryKeyFileError: null,
});
// also clear the file upload control so that the user can upload the same file
// the did before (otherwise the onchange wouldn't fire)
if (this._fileUpload.current) this._fileUpload.current.value = null;
// We don't use Field's validation here because a) we want it in a separate place rather
// than in a tooltip and b) we want it to display feedback based on the uploaded file
// as well as the text box. Ideally we would refactor Field's validation logic so we could
// re-use some of it.
this._validateRecoveryKeyOnChange();
}
_onRecoveryKeyFileChange = async e => {
if (e.target.files.length === 0) return;
const f = e.target.files[0];
if (f.size > KEY_FILE_MAX_SIZE) {
this.setState({
recoveryKeyFileError: true,
recoveryKeyCorrect: false,
recoveryKeyValid: false,
});
} else {
const contents = await f.text();
// test it's within the base58 alphabet. We could be more strict here, eg. require the
// right number of characters, but it's really just to make sure that what we're reading is
// text because we'll put it in the text field.
if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
this.setState({
recoveryKeyFileError: null,
recoveryKey: contents.trim(),
});
this._validateRecoveryKey();
} else {
this.setState({
recoveryKeyFileError: true,
recoveryKeyCorrect: false,
recoveryKeyValid: false,
recoveryKey: '',
});
}
}
}
_onRecoveryKeyFileUploadClick = () => {
this._fileUpload.current.click();
}
_onPassPhraseNext = async (e) => {
@ -106,6 +193,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
getKeyValidationText() {
if (this.state.recoveryKeyFileError) {
return _t("Wrong file type");
} else if (this.state.recoveryKeyCorrect) {
return _t("Looks good!");
} else if (this.state.recoveryKeyValid) {
return _t("Wrong Recovery Key");
} else if (this.state.recoveryKeyValid === null) {
return '';
} else {
return _t("Invalid Recovery Key");
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -118,10 +219,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
let content;
let title;
let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter recovery passphrase");
title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
let keyStatus;
if (this.state.keyMatches === false) {
@ -137,12 +240,15 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your recovery passphrase.",
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
{
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
},
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
@ -153,10 +259,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
value={this.state.passPhrase}
autoFocus={true}
autoComplete="new-password"
placeholder={_t("Security Phrase")}
/>
{keyStatus}
<DialogButtons
primaryButton={_t('Next')}
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
@ -164,87 +271,61 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.passPhrase.length === 0}
/>
</form>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter recovery key");
title = _t("Security Key");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery key.",
)}
</div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
</div>;
}
const feedbackClasses = classNames({
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
});
const recoveryKeyFeedback = <div className={feedbackClasses}>
{this.getKeyValidationText()}
</div>;
content = <div>
<p>{_t(
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your recovery key.",
)}</p>
<p>{_t("Use your Security Key to continue.")}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
<Field
type="text"
label={_t('Security Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect}
/>
</div>
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
{_t("or")}
</span>
<div>
<input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this._fileUpload}
onChange={this._onRecoveryKeyFileChange}
/>
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
{_t("Upload")}
</AccessibleButton>
</div>
</div>
{recoveryKeyFeedback}
<DialogButtons
primaryButton={_t('Next')}
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
cancelButton={_t("Go Back")}
cancelButtonClass='danger'
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</form>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
@ -252,6 +333,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<BaseDialog className='mx_AccessSecretStorageDialog'
onFinished={this.props.onFinished}
title={title}
titleClass={titleClass}
>
<div>
{content}

View file

@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
interface IProps extends React.InputHTMLAttributes<Element> {
export interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>;
element?: string;
// The kind of button, similar to how Bootstrap works.

View file

@ -16,21 +16,28 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import * as sdk from "../../../index";
import {IProps} from "./AccessibleButton";
import Tooltip from './Tooltip';
export default class AccessibleTooltipButton extends React.PureComponent {
static propTypes = {
...AccessibleButton.propTypes,
// The tooltip to render on hover
title: PropTypes.string.isRequired,
};
interface ITooltipProps extends IProps {
title: string;
tooltipClassName?: string;
}
state = {
hover: false,
};
interface IState {
hover: boolean;
}
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({
@ -45,14 +52,15 @@ export default class AccessibleTooltipButton extends React.PureComponent {
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {title, children, ...props} = this.props;
const tooltipClassName = classnames(
"mx_AccessibleTooltipButton_tooltip",
this.props.tooltipClassName,
);
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
tooltipClassName={tooltipClassName}
label={title}
/> : <div />;
return (

View file

@ -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';
@ -740,7 +740,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) {

View file

@ -108,7 +108,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
},
};
return event;
}
public render() {

View file

@ -50,10 +50,12 @@ interface IProps {
// to the user.
onValidate?: (input: IFieldState) => Promise<IValidationResult>;
// If specified, overrides the value returned by onValidate.
flagInvalid?: boolean;
forceValidity?: boolean;
// 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;
@ -201,7 +203,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
public render() {
const {
element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
// Set some defaults for the <input> element
const ref = input => this.input = input;
@ -226,15 +228,15 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? flagInvalid
? !forceValidity
: onValidate && this.state.valid === false,
});
@ -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}
/>;
}

View file

@ -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>
);
},

View file

@ -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>&nbsp;
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
</div>
);
},
});

View file

@ -1,39 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'ProgressBar',
propTypes: {
value: PropTypes.number,
max: PropTypes.number,
},
render: function() {
// Would use an HTML5 progress tag but if that doesn't animate if you
// use the HTML attributes rather than styles
const progressStyle = {
width: ((this.props.value / this.props.max) * 100)+"%",
};
return (
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
);
},
});

View file

@ -0,0 +1,28 @@
/*
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";
interface IProps {
value: number;
max: number;
}
const ProgressBar: React.FC<IProps> = ({value, max}) => {
return <progress className="mx_ProgressBar" max={max} value={value} />;
};
export default ProgressBar;

View file

@ -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>
);
}

View file

@ -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>&nbsp;</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;

View file

@ -18,6 +18,7 @@ import React from 'react';
import classnames from 'classnames';
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
outlined?: boolean;
}
interface IState {
@ -29,7 +30,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
};
public render() {
const { children, className, disabled, ...otherProps } = this.props;
const { children, className, disabled, outlined, ...otherProps } = this.props;
const _className = classnames(
'mx_RadioButton',
className,
@ -37,12 +38,13 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
"mx_RadioButton_disabled": disabled,
"mx_RadioButton_enabled": !disabled,
"mx_RadioButton_checked": this.props.checked,
"mx_RadioButton_outlined": outlined,
});
return <label className={_className}>
<input type='radio' disabled={disabled} {...otherProps} />
{/* Used to render the radio button circle */}
<div><div></div></div>
<span>{children}</span>
<div><div /></div>
<div className="mx_RadioButton_content">{children}</div>
<div className="mx_RadioButton_spacer" />
</label>;
}

View file

@ -0,0 +1,62 @@
/*
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
outlined?: boolean;
onChange(newValue: T);
}
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
const _onChange = e => {
onChange(e.target.value);
};
return <React.Fragment>
{definitions.map(d => <React.Fragment key={d.value}>
<StyledRadioButton
className={classNames(className, d.className)}
onChange={_onChange}
checked={d.value === value}
name={name}
value={d.value}
disabled={d.disabled}
outlined={outlined}
>
{d.label}
</StyledRadioButton>
{d.description}
</React.Fragment>)}
</React.Fragment>;
}
export default StyledRadioGroup;

View file

@ -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>
);
}

View file

@ -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();

View file

@ -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>
);

View file

@ -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>
);

View file

@ -28,6 +28,7 @@ export const E2E_STATE = {
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
UNAUTHENTICATED: "unauthenticated",
};
const crossSigningUserTitles = {

View file

@ -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,

View file

@ -17,54 +17,64 @@ limitations under the License.
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";
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
Grey, // unread notified messages
Red, // unread pings
}
export interface INotificationState extends EventEmitter {
symbol?: string;
count: number;
color: NotificationColor;
}
import SettingsStore from "../../../settings/SettingsStore";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
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 IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
/**
* If specified will return an AccessibleButton instead of a div.
*/
onClick?(ev: React.MouseEvent);
}
interface IState {
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
}
export default class NotificationBadge extends React.PureComponent<IProps, IState> {
export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, 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,30 +85,53 @@ 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 {
const {notification, forceCount, roomId, onClick, ...props} = this.props;
// Don't show a badge if we don't need to
if (this.props.notification.color <= NotificationColor.Bold) return null;
if (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");
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
// See git diff for what that boolean state looks like.
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
const hasNotif = notification.color >= NotificationColor.Red;
const hasCount = notification.color >= NotificationColor.Grey;
const hasAnySymbol = notification.symbol || notification.count > 0;
let isEmptyBadge = !hasAnySymbol || !hasCount;
if (forceCount) {
isEmptyBadge = false;
if (!hasCount) return null; // Can't render a badge
}
let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
let symbol = notification.symbol || formatMinimalBadgeCount(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,
'mx_NotificationBadge_3char': symbol.length > 2,
});
if (onClick) {
return (
<AccessibleButton {...props} className={classes} onClick={onClick}>
<span className="mx_NotificationBadge_count">{symbol}</span>
</AccessibleButton>
);
}
return (
<div className={classes}>
<span className="mx_NotificationBadge_count">{symbol}</span>
@ -106,189 +139,3 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
);
}
}
export class RoomNotificationState extends EventEmitter implements IDestroyable {
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.timeline", this.handleRoomEventUpdate);
this.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
this.updateNotificationState();
}
public get symbol(): string {
return this._symbol;
}
public get count(): number {
return this._count;
}
public get color(): NotificationColor {
return this._color;
}
private get roomIsInvite(): boolean {
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
}
public destroy(): void {
this.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
}
}
private handleRoomEventUpdate = (event: MatrixEvent) => {
const roomId = event.getRoomId();
if (roomId !== this.room.roomId) return; // ignore - not for us
this.updateNotificationState();
};
private updateNotificationState() {
const before = {count: this.count, symbol: this.symbol, color: this.color};
if (this.roomIsInvite) {
this._color = NotificationColor.Red;
this._symbol = "!";
this._count = 1; // not used, technically
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red
// count. If that count is broken for some reason, assume zero. This avoids us showing
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
// Note: we only set the symbol if we have an actual count. We don't want to show
// zero on badges.
if (redNotifs > 0) {
this._color = NotificationColor.Red;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else if (greyNotifs > 0) {
this._color = NotificationColor.Grey;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);
if (hasUnread) {
this._color = NotificationColor.Bold;
} else {
this._color = NotificationColor.None;
}
// no symbol or count for this state
this._count = 0;
this._symbol = null;
}
}
// finally, publish an update if needed
const after = {count: this.count, symbol: this.symbol, color: this.color};
if (JSON.stringify(before) !== JSON.stringify(after)) {
this.emit(NOTIFICATION_STATE_UPDATE);
}
}
}
export class ListNotificationState extends EventEmitter implements IDestroyable {
private _count: number;
private _color: NotificationColor;
private rooms: Room[] = [];
private states: { [roomId: string]: RoomNotificationState } = {};
constructor(private byTileCount = false) {
super();
}
public get symbol(): string {
return null; // This notification state doesn't support symbols
}
public get count(): number {
return this._count;
}
public get color(): NotificationColor {
return this._color;
}
public setRooms(rooms: Room[]) {
// If we're only concerned about the tile count, don't bother setting up listeners.
if (this.byTileCount) {
this.rooms = rooms;
this.calculateTotalState();
return;
}
const oldRooms = this.rooms;
const diff = arrayDiff(oldRooms, rooms);
this.rooms = rooms;
for (const oldRoom of diff.removed) {
const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.destroy();
}
for (const newRoom of diff.added) {
const state = new RoomNotificationState(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
if (this.states[newRoom.roomId]) {
// "Should never happen" disclaimer.
console.warn("Overwriting notification state for room:", newRoom.roomId);
this.states[newRoom.roomId].destroy();
}
this.states[newRoom.roomId] = state;
}
this.calculateTotalState();
}
public destroy() {
for (const state of Object.values(this.states)) {
state.destroy();
}
this.states = {};
}
private onRoomNotificationStateUpdate = () => {
this.calculateTotalState();
};
private calculateTotalState() {
const before = {count: this.count, symbol: this.symbol, color: this.color};
if (this.byTileCount) {
this._color = NotificationColor.Red;
this._count = this.rooms.length;
} else {
this._count = 0;
this._color = NotificationColor.None;
for (const state of Object.values(this.states)) {
this._count += state.count;
this._color = Math.max(this.color, state.color);
}
}
// finally, publish an update if needed
const after = {count: this.count, symbol: this.symbol, color: this.color};
if (JSON.stringify(before) !== JSON.stringify(after)) {
this.emit(NOTIFICATION_STATE_UPDATE);
}
}
}

View file

@ -17,13 +17,19 @@ limitations under the License.
import React from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { DefaultTagID } from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
@ -86,17 +92,29 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
};
public render(): React.ReactElement {
// TODO: Decorate crumbs with icons
// TODO: Decorate crumbs with icons: https://github.com/vector-im/riot-web/issues/14040
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return (
<AccessibleButton
<AccessibleTooltipButton
className="mx_RoomBreadcrumbs2_crumb"
key={r.roomId}
onClick={() => this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})}
title={r.name}
tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"}
>
<RoomAvatar room={r} width={32} height={32}/>
</AccessibleButton>
<DecoratedRoomAvatar
room={r}
avatarSize={32}
tag={roomTag}
displayBadge={true}
forceCount={true}
/>
</AccessibleTooltipButton>
);
});

View file

@ -25,10 +25,19 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { Dispatcher } from "flux";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
import TemporaryTile from "./TemporaryTile";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
@ -120,6 +129,8 @@ const TAG_AESTHETICS: {
isInvite: false,
defaultHidden: false,
},
// TODO: Replace with archived view: https://github.com/vector-im/riot-web/issues/14038
[DefaultTagID.Archived]: {
sectionLabel: _td("Historical"),
isInvite: false,
@ -155,16 +166,57 @@ export default class RoomList2 extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
const newLists = store.orderedLists;
console.log("new lists", newLists);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.updateLists(); // trigger the first update
}
const layoutMap = new Map<TagID, ListLayout>();
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
public componentWillUnmount() {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
}
this.setState({sublists: newLists, layouts: layoutMap});
private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists;
console.log("new lists", newLists);
const layoutMap = new Map<TagID, ListLayout>();
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
this.setState({sublists: newLists, layouts: layoutMap});
};
private renderCommunityInvites(): React.ReactElement[] {
// TODO: Put community invites in a more sensible place (not in the room list)
return MatrixClientPeg.get().getGroups().filter(g => {
if (g.myMembership !== 'invite') return false;
return !this.searchFilter || this.searchFilter.matches(g.name);
}).map(g => {
const avatar = (
<GroupAvatar
groupId={g.groupId}
groupName={g.name}
groupAvatarUrl={g.avatarUrl}
width={32} height={32} resizeMethod='crop'
/>
);
const openGroup = () => {
defaultDispatcher.dispatch({
action: 'view_group',
group_id: g.groupId,
});
};
return (
<TemporaryTile
isMinimized={this.props.isMinimized}
isSelected={false}
displayName={g.name}
avatar={avatar}
notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
onClick={openGroup}
key={`temporaryGroupTile_${g.groupId}`}
/>
);
});
}
@ -174,11 +226,11 @@ export default class RoomList2 extends React.Component<IProps, IState> {
for (const orderedTagId of TAG_ORDER) {
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
// Populate community invites if we have the chance
// TODO
// TODO: Community invites: https://github.com/vector-im/riot-web/issues/14179
}
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
// Populate custom tags if needed
// TODO
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
}
const orderedRooms = this.state.sublists[orderedTagId] || [];
@ -190,9 +242,11 @@ export default class RoomList2 extends React.Component<IProps, IState> {
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
components.push(
<RoomSublist2
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
@ -202,6 +256,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized}
extraBadTilesThatShouldntExist={extraTiles}
/>
);
}

View file

@ -26,12 +26,21 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
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 { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import NotificationBadge from "./NotificationBadge";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import Tooltip from "../elements/Tooltip";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
@ -56,37 +65,46 @@ interface IProps {
isInvite: boolean;
layout: ListLayout;
isMinimized: boolean;
tagId: TagID;
// TODO: Collapsed state
// TODO: Group invites
// TODO: Calls
// TODO: forceExpand?
// TODO: Header clicking
// TODO: Spinner support for historical
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
// You should feel bad if you use this.
extraBadTilesThatShouldntExist?: React.ReactElement[];
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
}
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState {
notificationState: ListNotificationState;
menuDisplayed: boolean;
contextMenuPosition: PartialDOMRect;
isResizing: boolean;
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef();
private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
this.state = {
notificationState: new ListNotificationState(this.props.isInvite),
menuDisplayed: false,
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
contextMenuPosition: null,
isResizing: false,
};
this.state.notificationState.setRooms(this.props.rooms);
}
private get numTiles(): number {
// TODO: Account for group invites
return (this.props.rooms || []).length;
return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length;
}
private get numVisibleTiles(): number {
if (!this.props.layout) return 0;
const nVisible = Math.floor(this.props.layout.visibleTiles);
return Math.min(nVisible, this.numTiles);
}
public componentDidUpdate() {
@ -105,38 +123,59 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
const direction = e.movementY < 0 ? -1 : +1;
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
this.props.layout.visibleTiles += tileDiff;
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles);
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
};
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({menuDisplayed: true});
const target = ev.target as HTMLButtonElement;
this.setState({contextMenuPosition: target.getBoundingClientRect()});
};
private onContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
contextMenuPosition: {
left: ev.clientX,
top: ev.clientY,
height: 0,
},
});
};
private onCloseMenu = () => {
this.setState({menuDisplayed: false});
this.setState({contextMenuPosition: null});
};
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 = () => {
@ -144,6 +183,30 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onBadgeClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
let room;
if (this.props.tagId === DefaultTagID.Invite) {
// switch to first room as that'll be the top of the list for the user
room = this.props.rooms && this.props.rooms[0];
} else {
// find the first room with a count of the same colour as the badge count
room = this.props.rooms.find((r: Room) => {
const notifState = this.state.notificationState.getForRoom(r);
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
});
}
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
}
};
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
let target = ev.target as HTMLDivElement;
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
@ -158,44 +221,108 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
sublist.scrollIntoView({behavior: 'smooth'});
} else {
// on screen - toggle collapse
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
this.toggleCollapsed();
}
};
private renderTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering
private toggleCollapsed = () => {
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
if (!isCollapsed) {
// On ARROW_LEFT collapse the room sublist if it isn't already
this.toggleCollapsed();
}
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (isCollapsed) {
// On ARROW_RIGHT expand the room sublist if it isn't already
this.toggleCollapsed();
} else if (this.sublistRef.current) {
// otherwise focus the first room
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement;
if (element) {
element.focus();
}
}
break;
}
}
};
private onKeyDown = (ev: React.KeyboardEvent) => {
switch (ev.key) {
// On ARROW_LEFT go to the sublist header
case Key.ARROW_LEFT:
ev.stopPropagation();
this.headerButton.current.focus();
break;
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
case Key.ARROW_RIGHT:
ev.stopPropagation();
}
};
private renderVisibleTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) {
// don't waste time on rendering
return [];
}
const tiles: React.ReactElement[] = [];
if (this.props.extraBadTilesThatShouldntExist) {
tiles.push(...this.props.extraBadTilesThatShouldntExist);
}
if (this.props.rooms) {
for (const room of this.props.rooms) {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
tiles.push(
<RoomTile2
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.props.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.layout.tagId}
tag={this.props.tagId}
/>
);
}
}
// We only have to do this because of the extra tiles. We do it conditionally
// to avoid spending cycles on slicing. It's generally fine to do this though
// as users are unlikely to have more than a handful of tiles when the extra
// tiles are used.
if (tiles.length > this.numVisibleTiles) {
return tiles.slice(0, this.numVisibleTiles);
}
return tiles;
}
private renderMenu(): React.ReactElement {
// TODO: Get a proper invite context menu, or take invites out of the room list.
if (this.props.tagId === DefaultTagID.Invite) {
return null;
}
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;
if (this.state.contextMenuPosition) {
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
left={this.state.contextMenuPosition.left}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
onFinished={this.onCloseMenu}
>
<div className="mx_RoomSublist2_contextMenu">
@ -204,14 +331,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>
@ -246,9 +373,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<ContextMenuButton
className="mx_RoomSublist2_menuButton"
onClick={this.onOpenMenuClick}
inputRef={this.menuButtonRef}
label={_t("List options")}
isExpanded={this.state.menuDisplayed}
isExpanded={!!this.state.contextMenuPosition}
/>
{contextMenu}
</React.Fragment>
@ -256,27 +382,30 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
private renderHeader(): React.ReactElement {
// TODO: Title on collapsed
// TODO: Incoming call box
return (
<RovingTabIndexWrapper inputRef={this.headerButton}>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) => {
// TODO: Use onFocus
const tabIndex = isActive ? 0 : -1;
// TODO: Collapsed state
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
const badge = (
<NotificationBadge
forceCount={true}
notification={this.state.notificationState}
onClick={this.onBadgeClick}
tabIndex={tabIndex}
/>
);
let addRoomButton = null;
if (!!this.props.onAddRoom) {
addRoomButton = (
<AccessibleButton
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist2_auxButton"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"}
/>
);
}
@ -291,27 +420,40 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
});
// TODO: a11y (see old component)
const badgeContainer = (
<div className="mx_RoomSublist2_badgeContainer">
{badge}
</div>
);
// TODO: a11y (see old component): https://github.com/vector-im/riot-web/issues/14180
// 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'>
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
<div className="mx_RoomSublist2_stickable">
<AccessibleButton
onFocus={onFocus}
inputRef={ref}
tabIndex={tabIndex}
className={"mx_RoomSublist2_headerText"}
className="mx_RoomSublist2_headerText"
role="treeitem"
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
>
<span className={collapseClasses} />
<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>
);
}}
@ -320,36 +462,33 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}
public render(): React.ReactElement {
// TODO: Proper rendering
// TODO: Error boundary
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
const tiles = this.renderTiles();
const visibleTiles = this.renderVisibleTiles();
const classes = classNames({
// TODO: Proper collapse support
'mx_RoomSublist2': true,
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist2_minimized': this.props.isMinimized,
});
let content = null;
if (tiles.length > 0) {
if (visibleTiles.length > 0) {
const layout = this.props.layout; // to shorten calls
// TODO: Lazy list rendering
// TODO: Whatever scrolling magic needs to happen here
const nVisible = Math.floor(layout.visibleTiles);
const visibleTiles = tiles.slice(0, nVisible);
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
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'.
let showNButton = null;
if (tiles.length > nVisible) {
if (this.numTiles > visibleTiles.length) {
// we have a cutoff condition - add the button to show all
const numMissing = tiles.length - visibleTiles.length;
const numMissing = this.numTiles - visibleTiles.length;
let showMoreText = (
<span className='mx_RoomSublist2_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})}
@ -357,14 +496,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 (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
@ -373,7 +512,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>
@ -384,7 +523,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// Figure out if we need a handle
let handles = ['s'];
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
handles = []; // no handles, we're at a minimum
}
@ -403,9 +542,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
const relativeTiles = layout.tilesWithPadding(tiles.length, padding);
const relativeTiles = layout.tilesWithPadding(this.numTiles, padding);
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding);
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
@ -419,6 +558,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}
@ -426,12 +567,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
}
// TODO: onKeyDown support
return (
<div
ref={this.sublistRef}
className={classes}
role="group"
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{this.renderHeader()}
{content}

View file

@ -17,21 +17,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from "react";
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
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 { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
import RoomTileIcon from "./RoomTileIcon";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { setRoomNotifsState } from "../../../RoomNotifs";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
/*******************************************************************
* CAUTION *
@ -47,46 +55,74 @@ interface IProps {
isMinimized: boolean;
tag: TagID;
// TODO: Allow falsifying counts (for invites and stuff)
// TODO: Transparency? Was this ever used?
// TODO: Incoming call boxes?
// TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177
}
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
interface IState {
hover: boolean;
notificationState: INotificationState;
selected: boolean;
generalMenuDisplayed: boolean;
notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect;
}
export default class RoomTile2 extends React.Component<IProps, IState> {
private roomTileRef: React.RefObject<HTMLDivElement> = createRef();
private generalMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset - 9;
const top = elementRect.bottom + window.pageYOffset + 17;
const chevronFace = "none";
return {left, top, chevronFace};
};
// TODO: Custom status
// TODO: Lock icon
// TODO: Presence indicator
// TODO: e2e shields
// TODO: Handle changes to room aesthetics (name, join rules, etc)
// TODO: scrollIntoView?
// TODO: hover, badge, etc
// TODO: isSelected for hover effects
// TODO: Context menu
// TODO: a11y
interface INotifOptionProps {
active: boolean;
iconClassName: string;
label: string;
onClick(ev: ButtonEvent);
}
const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassName, label}) => {
const classes = classNames({
mx_RoomTile2_contextMenu_activeRow: active,
});
let activeIcon;
if (active) {
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconCheck" />;
}
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ activeIcon }
</MenuItemRadio>
);
};
export default class RoomTile2 extends React.Component<IProps, IState> {
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
constructor(props: IProps) {
super(props);
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,
notificationsMenuPosition: null,
generalMenuPosition: null,
};
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
private get showContextMenu(): boolean {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@ -102,9 +138,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
};
private onTileClick = (ev: React.KeyboardEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'view_room',
// TODO: Support show_room_tile in new room list
// TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
show_room_tile: true, // make sure the room is visible in the list
room_id: this.props.room.roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
@ -115,27 +153,48 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({selected: isActive});
};
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({notificationsMenuPosition: target.getBoundingClientRect()});
};
private onCloseNotificationsMenu = () => {
this.setState({notificationsMenuPosition: null});
};
private onGeneralMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: true});
const target = ev.target as HTMLButtonElement;
this.setState({generalMenuPosition: target.getBoundingClientRect()});
};
private onCloseGeneralMenu = (ev: InputEvent) => {
private onContextMenu = (ev: React.MouseEvent) => {
// If we don't have a context menu to show, ignore the action.
if (!this.showContextMenu) return;
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: false});
this.setState({
generalMenuPosition: {
left: ev.clientX,
bottom: ev.clientY,
},
});
};
private onCloseGeneralMenu = () => {
this.setState({generalMenuPosition: null});
};
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
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) => {
@ -146,7 +205,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
action: 'leave_room',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
this.setState({generalMenuPosition: null}); // hide the menu
};
private onOpenRoomSettings = (ev: ButtonEvent) => {
@ -157,64 +216,126 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
this.setState({generalMenuPosition: null}); // hide the menu
};
private renderGeneralMenu(): React.ReactElement {
if (this.props.isMinimized) return null; // no menu when minimized
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
ev.preventDefault();
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
try {
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
await setRoomNotifsState(this.props.room.roomId, newState);
} catch (error) {
// TODO: some form of error notification to the user to inform them that their state change failed.
// https://github.com/vector-im/riot-web/issues/14281
console.error(error);
}
this.setState({notificationsMenuPosition: null}); // Close the context menu
}
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD);
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
private onClickMute = ev => this.saveNotifState(ev, MUTE);
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
// the menu makes no sense in these cases so do not show one
return null;
}
const state = getRoomNotifsState(this.props.room.roomId);
let contextMenu = null;
if (this.state.generalMenuDisplayed) {
// The context menu appears within the list, so use the room tile as a reference point
const elementRect = this.roomTileRef.current.getBoundingClientRect();
if (this.state.notificationsMenuPosition) {
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height + 8}
onFinished={this.onCloseGeneralMenu}
>
<div
className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"
style={{width: elementRect.width}}
>
<ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span>{_t("Favourite")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown" />
<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" />
<span>{_t("Settings")}</span>
</AccessibleButton>
</li>
</ul>
<NotifOption
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile2_iconBell"
onClick={this.onClickAllNotifs}
/>
<NotifOption
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile2_iconBellDot"
onClick={this.onClickAlertMe}
/>
<NotifOption
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile2_iconBellMentions"
onClick={this.onClickMentions}
/>
<NotifOption
label={_t("None")}
active={state === MUTE}
iconClassName="mx_RoomTile2_iconBellCrossed"
onClick={this.onClickMute}
/>
</div>
</div>
</ContextMenu>
);
}
const classes = classNames("mx_RoomTile2_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile2_iconBell: state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile2_iconBellCrossed: state === MUTE,
// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile2_notificationsButton_show: state === MUTE,
});
return (
<React.Fragment>
<ContextMenuButton
className={classes}
onClick={this.onNotificationsMenuOpenClick}
label={_t("Notification options")}
isExpanded={!!this.state.notificationsMenuPosition}
tabIndex={isActive ? 0 : -1}
/>
{contextMenu}
</React.Fragment>
);
}
private renderGeneralMenu(): React.ReactElement {
if (!this.showContextMenu) return null; // no menu to show
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
let contextMenu = null;
if (this.state.generalMenuPosition) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span>{_t("Leave Room")}</span>
</AccessibleButton>
</li>
</ul>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</AccessibleButton>
</div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</AccessibleButton>
</div>
</div>
</ContextMenu>
@ -226,9 +347,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<ContextMenuButton
className="mx_RoomTile2_menuButton"
onClick={this.onGeneralMenuOpenClick}
inputRef={this.generalMenuButtonRef}
label={_t("Room options")}
isExpanded={this.state.generalMenuDisplayed}
isExpanded={!!this.state.generalMenuPosition}
/>
{contextMenu}
</React.Fragment>
@ -236,32 +356,45 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
}
public render(): React.ReactElement {
// TODO: Collapsed state
// TODO: Invites
// TODO: a11y proper
// TODO: Render more than bare minimum
// TODO: Invites: https://github.com/vector-im/riot-web/issues/14198
// TODO: a11y proper: https://github.com/vector-im/riot-web/issues/14180
const classes = classNames({
'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected,
'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed,
'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile2_minimized': this.props.isMinimized,
});
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
const roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized}
/>;
let badge: React.ReactNode;
if (!this.props.isMinimized) {
badge = (
<div className="mx_RoomTile2_badgeContainer">
<NotificationBadge
notification={this.state.notificationState}
forceCount={false}
roomId={this.props.room.roomId}
/>
</div>
);
}
// TODO: the original RoomTile uses state for the room name. Do we need to?
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
// TODO: Support collapsed state properly
// TODO: Tooltip?
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) {
@ -289,10 +422,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) nameContainer = null;
const avatarSize = 32;
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
@ -303,15 +435,12 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onMouseLeave={this.onTileMouseLeave}
onClick={this.onTileClick}
role="treeitem"
onContextMenu={this.onContextMenu}
>
<div className="mx_RoomTile2_avatarContainer">
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} />
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
</div>
{roomAvatar}
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
{badge}
{this.renderNotificationsMenu(isActive)}
{this.renderGeneralMenu()}
</AccessibleButton>
}

View file

@ -0,0 +1,116 @@
/*
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 { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
interface IProps {
isMinimized: boolean;
isSelected: boolean;
displayName: string;
avatar: React.ReactElement;
notificationState: INotificationState;
onClick: () => void;
}
interface IState {
hover: boolean;
}
export default class TemporaryTile extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
hover: false,
};
}
private onTileMouseEnter = () => {
this.setState({hover: true});
};
private onTileMouseLeave = () => {
this.setState({hover: false});
};
public render(): React.ReactElement {
// XXX: We copy classes because it's easier
const classes = classNames({
'mx_RoomTile2': true,
'mx_TemporaryTile': true,
'mx_RoomTile2_selected': this.props.isSelected,
'mx_RoomTile2_minimized': this.props.isMinimized,
});
const badge = (
<NotificationBadge
notification={this.props.notificationState}
forceCount={false}
/>
);
let name = this.props.displayName;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
});
let nameContainer = (
<div className="mx_RoomTile2_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
</div>
);
if (this.props.isMinimized) nameContainer = null;
return (
<React.Fragment>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.props.onClick}
role="treeitem"
>
<div className="mx_RoomTile2_avatarContainer">
{this.props.avatar}
</div>
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
</AccessibleButton>
}
</RovingTabIndexWrapper>
</React.Fragment>
);
}
}

View file

@ -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}>

View file

@ -413,7 +413,7 @@ export default class SetIdServer extends React.Component {
tooltipContent={this._getTooltip()}
tooltipClassName="mx_SetIdServer_tooltip"
disabled={this.state.busy}
flagInvalid={!!this.state.error}
forceValidity={this.state.error ? false : null}
/>
<AccessibleButton type="submit" kind="primary_sm"
onClick={this._checkIdServer}

View file

@ -28,6 +28,8 @@ export default class NotificationsSettingsTab extends React.Component {
roomId: PropTypes.string.isRequired,
};
_soundUpload = createRef();
constructor() {
super();
@ -39,14 +41,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});
});
this._soundUpload = createRef();
const soundData = Notifier.getSoundForRoom(this.props.roomId);
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
}
async _triggerUploader(e) {

View file

@ -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,10 @@ 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";
import classNames from 'classnames';
interface IProps {
}
@ -55,6 +57,9 @@ interface IState extends IThemeState {
customThemeUrl: string;
customThemeMessage: CustomThemeMessage;
useCustomFontSize: boolean;
useSystemFont: boolean;
systemFont: string;
showAdvanced: boolean;
useIRCLayout: boolean;
}
@ -73,6 +78,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 +118,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,22 +278,21 @@ 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}
outlined
/>
</div>
{customThemeForm}
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} useCheckbox={true} />
</div>
);
}
@ -338,8 +344,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons" >
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.useIRCLayout,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
@ -355,7 +363,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: !this.state.useIRCLayout,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
@ -374,6 +384,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 +441,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>
);
}

View file

@ -23,6 +23,7 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {RoomListStoreTempProxy} from "../../../../../stores/room-list/RoomListStoreTempProxy";
export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [
@ -31,6 +32,19 @@ export default class PreferencesUserSettingsTab extends React.Component {
'breadcrumbs',
];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14231
static ROOM_LIST_2_SETTINGS = [
'breadcrumbs',
];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14231
static eligibleRoomListSettings = () => {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS;
}
return PreferencesUserSettingsTab.ROOM_LIST_SETTINGS;
};
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
@ -175,7 +189,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
{this._renderGroup(PreferencesUserSettingsTab.eligibleRoomListSettings())}
</div>
<div className="mx_SettingsTab_section">

View file

@ -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});
};

View file

@ -0,0 +1,53 @@
/*
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 ToastStore from "../../../stores/ToastStore";
import GenericToast, { IProps as IGenericToastProps } from "./GenericToast";
import {useExpiringCounter} from "../../../hooks/useTimeout";
interface IProps extends IGenericToastProps {
toastKey: string;
numSeconds: number;
dismissLabel: string;
onDismiss?();
}
const SECOND = 1000;
const GenericExpiringToast: React.FC<IProps> = ({description, acceptLabel, dismissLabel, onAccept, onDismiss, toastKey, numSeconds}) => {
const onReject = () => {
if (onDismiss) onDismiss();
ToastStore.sharedInstance().dismissToast(toastKey);
};
const counter = useExpiringCounter(onReject, SECOND, numSeconds);
let rejectLabel = dismissLabel;
if (counter > 0) {
rejectLabel += ` (${counter})`;
}
return <GenericToast
description={description}
acceptLabel={acceptLabel}
onAccept={onAccept}
rejectLabel={rejectLabel}
onReject={onReject}
/>;
};
export default GenericExpiringToast;

View file

@ -19,7 +19,7 @@ import React, {ReactChild} from "react";
import FormButton from "../elements/FormButton";
import {XOR} from "../../../@types/common";
interface IProps {
export interface IProps {
description: ReactChild;
acceptLabel: string;

View file

@ -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",
}

View 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;
}

View 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;
}

View 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;
};

67
src/hooks/useTimeout.ts Normal file
View file

@ -0,0 +1,67 @@
/*
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 {useEffect, useRef, useState} from "react";
type Handler = () => void;
// Hook to simplify timeouts in functional components
export const useTimeout = (handler: Handler, timeoutMs: number) => {
// Create a ref that stores handler
const savedHandler = useRef<Handler>();
// Update ref.current value if handler changes.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
// Set up timer
useEffect(() => {
const timeoutID = setTimeout(() => {
savedHandler.current();
}, timeoutMs);
return () => clearTimeout(timeoutID);
}, [timeoutMs]);
};
// Hook to simplify intervals in functional components
export const useInterval = (handler: Handler, intervalMs: number) => {
// Create a ref that stores handler
const savedHandler = useRef<Handler>();
// Update ref.current value if handler changes.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
// Set up timer
useEffect(() => {
const intervalID = setInterval(() => {
savedHandler.current();
}, intervalMs);
return () => clearInterval(intervalID);
}, [intervalMs]);
};
// Hook to simplify a variable counting down to 0, handler called when it reached 0
export const useExpiringCounter = (handler: Handler, intervalMs: number, initialCount: number) => {
const [count, setCount] = useState(initialCount);
useInterval(() => setCount(c => c - 1), intervalMs);
if (count === 0) {
handler();
}
return count;
};

View file

@ -2325,7 +2325,7 @@
"Verify this login": "Потвърди тази сесия",
"Session verified": "Сесията беше потвърдена",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Промяната на паролата ще нулира всички ключове за шифроване от-край-до-край по всички ваши сесии, правейки шифрованата история на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете на стаите от друга сесия преди да промените паролата си.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Администраторът на сървъра е изключиш шифроване от край-до-край по подразбиране за лични стаи и за директни съобщения.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Администраторът на сървъра е изключил шифроване от край-до-край по подразбиране за лични стаи и за директни съобщения.",
"Emoji picker": "Избор на емоджи",
"People": "Хора",
"Show %(n)s more": "Покажи още %(n)s",
@ -2446,5 +2446,27 @@
"A-Z": "Азбучен ред",
"Unread rooms": "Непрочетени стаи",
"Show %(count)s more|other": "Покажи още %(count)s",
"Show %(count)s more|one": "Покажи още %(count)s"
"Show %(count)s more|one": "Покажи още %(count)s",
"Light": "Светла",
"Dark": "Тъмна",
"Use the improved room list (will refresh to apply changes)": "Използвай подобрения списък със стаи (ще презареди за да се приложи промяната)",
"Enable IRC layout option in the appearance tab": "Включи опцията за IRC изглед в раздел Изглед",
"Use custom size": "Използвай собствен размер",
"Use a system font": "Използвай системния шрифт",
"System font name": "Име на системния шрифт",
"Hey you. You're the best!": "Хей, ти. Върхът си!",
"Message layout": "Изглед на съобщенията",
"Compact": "Компактен",
"Modern": "Модерен",
"Customise your appearance": "Настройте изгледа",
"Appearance Settings only affect this Riot session.": "Настройките на изгледа влияят само на тази Riot сесия.",
"The authenticity of this encrypted message can't be guaranteed on this device.": "Автентичността на това шифровано съобщение не може да бъде гарантирана на това устройство.",
"Always show first": "Винаги показвай първо",
"Show": "Покажи",
"Message preview": "Преглед на съобщението",
"List options": "Опции на списъка",
"Leave Room": "Напусни стаята",
"Room options": "Настройки на стаята",
"Use Recovery Key or Passphrase": "Използвай ключ за възстановяване или парола",
"Use Recovery Key": "Използвай ключ за възстановяване"
}

View file

@ -2428,8 +2428,8 @@
"Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.",
"You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Du hast für diese Sitzung zuvor eine neuere Version von Riot verwendet. Um diese Version mit Ende-zu-Ende-Verschlüsselung wieder zu benutzen, musst du dich erst ab- und dann wieder anmelden.",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Soll die Raum-Adresse %(alias)s gelöscht und %(name)s aus dem Raum-Verzeichnis entfernt werden?",
"Switch to light mode": "Zum Light Mode wechseln",
"Switch to dark mode": "Zum Dark Mode wechseln",
"Switch to light mode": "Zum hellen Thema wechseln",
"Switch to dark mode": "Zum dunklen Thema wechseln",
"Switch theme": "Design ändern",
"Security & privacy": "Sicherheit & Datenschutz",
"All settings": "Alle Einstellungen",
@ -2462,5 +2462,79 @@
"Enter your Recovery Key to continue.": "Gib deinen Wiederherstellungsschlüssel ein um fortzufahren.",
"Create a Recovery Key": "Erzeuge einen Wiederherstellungsschlüssel",
"Upgrade your Recovery Key": "Aktualisiere deinen Wiederherstellungsschlüssel",
"Store your Recovery Key": "Speichere deinen Wiederherstellungsschlüssel"
"Store your Recovery Key": "Speichere deinen Wiederherstellungsschlüssel",
"Light": "Hell",
"Dark": "Dunkel",
"Use the improved room list (will refresh to apply changes)": "Verwende die verbesserte Raumliste (lädt die Anwendung neu)",
"Use custom size": "Verwende individuelle Größe",
"Hey you. You're the best!": "Hey du. Du bist der Beste!",
"Message layout": "Nachrichtenlayout",
"Compact": "Kompakt",
"Modern": "Modern",
"Enable IRC layout option in the appearance tab": "Option für IRC Layout in den Erscheinungsbild-Einstellungen aktivieren",
"Use a system font": "Verwende die System-Schriftart",
"System font name": "System-Schriftart",
"Customise your appearance": "Verändere das Erscheinungsbild",
"Appearance Settings only affect this Riot session.": "Einstellungen zum Erscheinungsbild wirken sich nur auf diese Riot Sitzung aus.",
"The authenticity of this encrypted message can't be guaranteed on this device.": "Die Echtheit dieser verschlüsselten Nachricht kann auf diesem Gerät nicht garantiert werden.",
"You joined the call": "Du bist dem Anruf beigetreten",
"%(senderName)s joined the call": "%(senderName)s ist dem Anruf beigetreten",
"Call in progress": "Laufendes Gespräch",
"You left the call": "Du hast den Anruf verlassen",
"%(senderName)s left the call": "%(senderName)s hat den Anruf verlassen",
"Call ended": "Anruf beendet",
"You started a call": "Du hast einen Anruf gestartet",
"%(senderName)s started a call": "%(senderName)s hat einen Anruf gestartet",
"Waiting for answer": "Warte auf Antwort",
"%(senderName)s is calling": "%(senderName)s ruft an",
"You created the room": "Du hast den Raum erstellt",
"%(senderName)s created the room": "%(senderName)s hat den Raum erstellt",
"You made the chat encrypted": "Du hast den Raum verschlüsselt",
"%(senderName)s made the chat encrypted": "%(senderName)s hat den Raum verschlüsselt",
"You made history visible to new members": "Du hast die bisherige Kommunikation für neue Teilnehmern sichtbar gemacht",
"%(senderName)s made history visible to new members": "%(senderName)s hat die bisherige Kommunikation für neue Teilnehmern sichtbar gemacht",
"You made history visible to anyone": "Du hast die bisherige Kommunikation für alle sichtbar gemacht",
"%(senderName)s made history visible to anyone": "%(senderName)s hat die bisherige Kommunikation für alle sichtbar gemacht",
"You made history visible to future members": "Du hast die bisherige Kommunikation für zukünftige Teilnehmer sichtbar gemacht",
"%(senderName)s made history visible to future members": "%(senderName)s hat die bisherige Kommunikation für zukünftige Teilnehmer sichtbar gemacht",
"You were invited": "Du wurdest eingeladen",
"%(targetName)s was invited": "%(targetName)s wurde eingeladen",
"You left": "Du hast den Raum verlassen",
"%(targetName)s left": "%(targetName)s hat den Raum verlassen",
"You were kicked (%(reason)s)": "Du wurdest herausgeworfen (%(reason)s)",
"%(targetName)s was kicked (%(reason)s)": "%(targetName)s wurde herausgeworfen (%(reason)s)",
"You were kicked": "Du wurdest herausgeworfen",
"%(targetName)s was kicked": "%(targetName)s wurde herausgeworfen",
"You rejected the invite": "Du hast die Einladung abgelehnt",
"%(targetName)s rejected the invite": "%(targetName)s hat die Einladung abgelehnt",
"You were uninvited": "Deine Einladung wurde zurückgezogen",
"%(targetName)s was uninvited": "Die Einladung für %(targetName)s wurde zurückgezogen",
"You were banned (%(reason)s)": "Du wurdest verbannt (%(reason)s)",
"%(targetName)s was banned (%(reason)s)": "%(targetName)s wurde verbannt (%(reason)s)",
"You were banned": "Du wurdest verbannt",
"%(targetName)s was banned": "%(targetName)s wurde verbannt",
"You joined": "Du bist beigetreten",
"%(targetName)s joined": "%(targetName)s ist beigetreten",
"You changed your name": "Du hast deinen Namen geändert",
"%(targetName)s changed their name": "%(targetName)s hat den Namen geändert",
"You changed your avatar": "Du hast deinen Avatar geändert",
"%(targetName)s changed their avatar": "%(targetName)s hat den Avatar geändert",
"%(senderName)s %(emote)s": "%(senderName)s %(emote)s",
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
"You changed the room name": "Du hast den Raumnamen geändert",
"%(senderName)s changed the room name": "%(senderName)s hat den Raumnamen geändert",
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"You uninvited %(targetName)s": "Du hast die Einladung für %(targetName)s zurückgezogen",
"%(senderName)s uninvited %(targetName)s": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen",
"You invited %(targetName)s": "Du hast %(targetName)s eingeladen",
"%(senderName)s invited %(targetName)s": "%(senderName)s hat %(targetName)s eingeladen",
"You changed the room topic": "Du hast das Raumthema geändert",
"%(senderName)s changed the room topic": "%(senderName)s hat das Raumthema geändert",
"New spinner design": "Neue Warteanimation",
"Use a more compact Modern layout": "Verwende ein kompakteres 'modernes' Layout",
"Message deleted on %(date)s": "Nachricht am %(date)s gelöscht",
"Wrong file type": "Falscher Dateityp",
"Wrong Recovery Key": "Falscher Wiederherstellungsschlüssel",
"Invalid Recovery Key": "Ungültiger Wiederherstellungsschlüssel"
}

View file

@ -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",
@ -1161,9 +1218,11 @@
"%(count)s unread messages.|one": "1 unread message.",
"Unread mentions.": "Unread mentions.",
"Unread messages.": "Unread messages.",
"Use default": "Use default",
"All messages": "All messages",
"Mentions & Keywords": "Mentions & Keywords",
"Notification options": "Notification options",
"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 +1430,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/>",
@ -1782,17 +1842,16 @@
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Enter recovery passphrase": "Enter recovery passphrase",
"Wrong file type": "Wrong file type",
"Looks good!": "Looks good!",
"Wrong Recovery Key": "Wrong Recovery Key",
"Invalid Recovery Key": "Invalid Recovery Key",
"Security Phrase": "Security Phrase",
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.",
"<b>Warning</b>: You should only do this on a trusted computer.": "<b>Warning</b>: You should only do this on a trusted computer.",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.",
"Enter recovery key": "Enter recovery key",
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.",
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Not a valid recovery key": "Not a valid recovery key",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "If you've forgotten your recovery key you can <button>set up new recovery options</button>.",
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Enter your Security Phrase or <button>Use your Security Key</button> to continue.",
"Security Key": "Security Key",
"Use your Security Key to continue.": "Use your Security Key to continue.",
"Go Back": "Go Back",
"Restoring keys from backup": "Restoring keys from backup",
"Fetching keys from server...": "Fetching keys from server...",
"%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored",
@ -1806,9 +1865,13 @@
"Keys restored": "Keys restored",
"Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
"Successfully restored %(sessionCount)s keys": "Successfully restored %(sessionCount)s keys",
"Enter recovery passphrase": "Enter recovery passphrase",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
"Enter recovery key": "Enter recovery key",
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Not a valid recovery key": "Not a valid recovery key",
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
@ -1837,10 +1900,11 @@
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
"Notification settings": "Notification settings",
"All messages (noisy)": "All messages (noisy)",
"All messages": "All messages",
"Mentions only": "Mentions only",
"Leave": "Leave",
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
@ -2056,7 +2120,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",
@ -2132,7 +2195,6 @@
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",
"Go Back": "Go Back",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Incorrect password": "Incorrect password",
"Failed to re-authenticate": "Failed to re-authenticate",
@ -2171,48 +2233,56 @@
"Import": "Import",
"Confirm encryption setup": "Confirm encryption setup",
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
"Generate a Security Key": "Generate a Security Key",
"Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.",
"Enter a Security Phrase": "Enter a Security Phrase",
"Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use a secret phrase only you know, and optionally save a Security Key to use for backup.",
"Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
"Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption",
"Restore": "Restore",
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
"Enter a security phrase only you know, as its used to safeguard your data. To be secure, you shouldnt re-use your account password.": "Enter a security phrase only you know, as its used to safeguard your data. To be secure, you shouldnt re-use your account password.",
"Enter a recovery passphrase": "Enter a recovery passphrase",
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
"Back up encrypted message keys": "Back up encrypted message keys",
"Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!",
"Use a different passphrase?": "Use a different passphrase?",
"That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.",
"Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.",
"Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Store your Security Key somewhere safe, like a password manager or a safe, as its used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as its used to safeguard your encrypted data.",
"Download": "Download",
"Copy": "Copy",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
"You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.",
"Set up Secure backup": "Set up Secure backup",
"Upgrade your encryption": "Upgrade your encryption",
"Set a Security Phrase": "Set a Security Phrase",
"Confirm Security Phrase": "Confirm Security Phrase",
"Save your Security Key": "Save your Security Key",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Set up with a recovery key": "Set up with a recovery key",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.",
"Your recovery key": "Your recovery key",
"Copy": "Copy",
"Download": "Download",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.",
"Upgrade your encryption": "Upgrade your encryption",
"Confirm recovery passphrase": "Confirm recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"You're done!": "You're done!",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"Starting backup...": "Starting backup...",
"Success!": "Success!",
"Create key backup": "Create key backup",

View file

@ -1179,7 +1179,7 @@
"Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo largo con más vueltas",
"Enable Community Filter Panel": "Habilitar el Panel de Filtro de Comunidad",
"Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.",
"Your Riot is misconfigured": "Tu Riot está mal configurado",
"Your Riot is misconfigured": "Tu Riot tiene un error de configuración",
"Whether or not you're logged in (we don't record your username)": "Hayas o no iniciado sesión (no guardamos tu nombre de usuario)",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Uses o no los 'breadcrumbs' (iconos sobre la lista de salas)",
"A conference call could not be started because the integrations server is not available": "No se pudo iniciar la conferencia porque el servidor de integraciones no está disponible",
@ -1556,7 +1556,7 @@
"Sign In or Create Account": "Iniciar sesión o Crear una cuenta",
"Use your account or create a new one to continue.": "Usa tu cuenta existente o crea una nueva para continuar.",
"Create Account": "Crear cuenta",
"Sign In": "Registrarse",
"Sign In": "Iniciar sesión",
"Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo en markdown",
"Failed to set topic": "No se ha podido establecer el tema",
"Command failed": "El comando falló",
@ -2191,5 +2191,44 @@
"This homeserver does not support login using email address.": "Este servidor doméstico no admite iniciar sesión con una dirección de correo electrónico.",
"This account has been deactivated.": "Esta cuenta ha sido desactivada.",
"Room name or address": "Nombre o dirección de la sala",
"Address (optional)": "Dirección (opcional)"
"Address (optional)": "Dirección (opcional)",
"Help us improve Riot": "Ayúdanos a mejorar Riot",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Enviar <UsageDataLink>información anónima de uso</UsageDataLink> nos ayudaría bastante a mejorar Riot. Esto cuenta como utilizar <PolicyLink>una cookie</PolicyLink>.",
"I want to help": "Quiero ayudar",
"Ok": "Ok",
"Set password": "Establecer contraseña",
"To return to your account in future you need to set a password": "Para poder regresar a tu cuenta en un futuro necesitas establecer una contraseña",
"Restart": "Reiniciar",
"Upgrade your Riot": "Actualiza tu Riot",
"A new version of Riot is available!": "¡Una nueva versión de Riot se encuentra disponible!",
"You joined the call": "Te has unido a la llamada",
"%(senderName)s joined the call": "%(senderName)s se ha unido a la llamada",
"Call in progress": "Llamada en progreso",
"You left the call": "Has abandonado la llamada",
"%(senderName)s left the call": "%(senderName)s dejo la llamada",
"Call ended": "La llamada ha finalizado",
"You started a call": "Has iniciado una llamada",
"%(senderName)s started a call": "%(senderName)s inicio una llamada",
"Waiting for answer": "Esperado por una respuesta",
"%(senderName)s is calling": "%(senderName)s está llamando",
"%(senderName)s created the room": "%(senderName)s creo la sala",
"You were invited": "Has sido invitado",
"%(targetName)s was invited": "%(targetName)s ha sido invitado",
"%(targetName)s left": "%(targetName)s se ha ido",
"You were kicked (%(reason)s)": "Has sido expulsado por %(reason)s",
"You rejected the invite": "Has rechazado la invitación",
"%(targetName)s rejected the invite": "%(targetName)s rechazo la invitación",
"You were banned (%(reason)s)": "Has sido baneado por %(reason)s",
"%(targetName)s was banned (%(reason)s)": "%(targetName)s fue baneado por %(reason)s",
"You were banned": "Has sido baneado",
"%(targetName)s was banned": "%(targetName)s fue baneado",
"You joined": "Te has unido",
"%(targetName)s joined": "%(targetName)s se ha unido",
"You changed your name": "Has cambiado tu nombre",
"%(targetName)s changed their name": "%(targetName)s cambio su nombre",
"You changed your avatar": "Ha cambiado su avatar",
"%(targetName)s changed their avatar": "%(targetName)s ha cambiado su avatar",
"You changed the room name": "Has cambiado el nombre de la sala",
"%(senderName)s changed the room name": "%(senderName)s cambio el nombre de la sala",
"You invited %(targetName)s": "Has invitado a %(targetName)s"
}

View file

@ -1592,5 +1592,257 @@
"We encountered an error trying to restore your previous session.": "Meil tekkis eelmise sessiooni taastamisel viga.",
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Kui sa varem oled kasutanud uuemat Riot'i versiooni, siis sinu pragune sessioon ei pruugi olla sellega ühilduv. Sulge see aken ja jätka selle uuema versiooni kasutamist.",
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Brauseri andmeruumi tühjendamine võib selle vea lahendada, kui samas logid sa ka välja ning kogu krüptitud vestlusajalugu muutub loetamatuks.",
"Verification Pending": "Verifikatsioon on ootel"
"Verification Pending": "Verifikatsioon on ootel",
"Back": "Tagasi",
"Send Custom Event": "Saada kohandatud sündmus",
"You must specify an event type!": "Sa pead määratlema sündmuse tüübi!",
"Event sent!": "Sündmus on saadetud!",
"Failed to send custom event.": "Kohandatud sündmuse saatmine ei õnnestunud.",
"Event Type": "Sündmuse tüüp",
"State Key": "Oleku võti",
"Event Content": "Sündmuse sisu",
"Send Account Data": "Saada kasutajakonto andmed",
"View Servers in Room": "Näita jututoas kasutatavaid servereid",
"Verification Requests": "Verifitseerimistaotlused",
"Toolbox": "Töövahendid",
"Developer Tools": "Arendusvahendid",
"An error has occurred.": "Tekkis viga.",
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Selle kasutaja usaldamiseks peaksid ta verifitseerima. Kui sa pruugid läbivalt krüptitud sõnumeid, siis kasutajate verifitseerimine tagab sulle täiendava meelerahu.",
"Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Selle kasutaja verifitseerimisel märgitakse tema sessioon usaldusväärseks ning samuti märgitakse sinu sessioon tema jaoks usaldusväärseks.",
"Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Selle seadme usaldamiseks peaksid ta verifitseerima. Kui sa pruugid läbivalt krüptitud sõnumeid, siis selle seadme usaldamine tagab sulle ja teistele kasutajatele täiendava meelerahu.",
"Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Selle seadme verifitseerimisel märgitakse ta usaldusväärseks ning kõik kasutajad, kes sinuga on verifitseerimise läbi teinud, loevad ka selle seadme usaldusväärseks.",
"Waiting for partner to confirm...": "Ootan teise osapoole kinnitust...",
"Incoming Verification Request": "Saabuv verifitseerimispalve",
"Integrations are disabled": "Lõimingud ei ole kasutusel",
"Enable 'Manage Integrations' in Settings to do this.": "Selle tegevuse jaoks määra seadetes \"Halda lõiminguid\" kasutuselevõetuks.",
"Integrations not allowed": "Lõimingute kasutamine ei ole lubatud",
"Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Sinu Riot ei võimalda selle tegevuse jaoks kasutada Lõimingute haldurit. Palun küsi lisateavet administraatorilt.",
"Failed to invite the following users to chat: %(csvUsers)s": "Järgnevate kasutajate vestlema kutsumine ei õnnestunud: %(csvUsers)s",
"We couldn't create your DM. Please check the users you want to invite and try again.": "Otsevestluse loomine ei õnnestunud. Palun kontrolli, et kasutajanimed oleks õiged ja proovi uuesti.",
"a new master key signature": "uus üldvõtme allkiri",
"a new cross-signing key signature": "uus risttunnustamise võtme allkiri",
"a device cross-signing signature": "seadme risttunnustamise allkiri",
"a key signature": "võtme allkiri",
"Riot encountered an error during upload of:": "Riot'is tekkis viga järgneva üleslaadimisel:",
"Cancelled signature upload": "Allkirja üleslaadimine on tühistatud",
"Unable to upload": "Üleslaadimine ei õnnestu",
"Signature upload success": "Allkirja üleslaadimine õnnestus",
"Signature upload failed": "Allkirja üleslaadimine ei õnnestunud",
"Address (optional)": "Aadress (valikuline)",
"Reject invitation": "Lükka kutse tagasi",
"Are you sure you want to reject the invitation?": "Kas sa oled kindel, et soovid lükata kutse tagasi?",
"Failed to set Direct Message status of room": "Jututoa otsevestluse oleku seadmine ei õnnestunud",
"Failed to forget room %(errCode)s": "Jututoa unustamine ei õnnestunud %(errCode)s",
"This homeserver would like to make sure you are not a robot.": "See server soovib kindlaks teha, et sa ei ole robot.",
"Country Dropdown": "Riikide valik",
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "Sa võid kasutada serveri kohandatud valikuid selleks, et määrates teise aadressi logida sisse teise Matrix'i serverisse. See võimaldab sul kasutada seda rakendust teises koduserveris hallatava olemasoleva Matrix'i kontoga.",
"Confirm your identity by entering your account password below.": "Tuvasta oma isik sisestades salasõna alljärgnevalt.",
"Please review and accept all of the homeserver's policies": "Palun vaata üle kõik koduserveri kasutustingimused ja nõustu nendega",
"Please review and accept the policies of this homeserver:": "Palun vaata üle selle koduserveri kasutustingimused ja nõustu nendega:",
"An email has been sent to %(emailAddress)s": "Saatsime e-kirja %(emailAddress)s aadressile",
"Please check your email to continue registration.": "Registreerimise jätkamiseks, palun vaata oma e-kirjad.",
"A text message has been sent to %(msisdn)s": "Saatsime tekstisõnumi telefoninumbrile %(msisdn)s",
"Please enter the code it contains:": "Palun sisesta seal kuvatud kood:",
"Code": "Kood",
"Submit": "Saada",
"Start authentication": "Alusta autentimist",
"Unable to validate homeserver/identity server": "Ei õnnestu valideerida koduserverit/isikutuvastusserverit",
"Your Modular server": "Sinu Modular-server",
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Sisesta oma Modular'i koduserveri aadress. See võib kasutada nii sinu oma domeeni, kui olla <a>modular.im</a> alamdomeen.",
"Sign in with SSO": "Logi sisse kasutades SSO'd ehk ühekordset autentimist",
"Failed to reject invitation": "Kutse tagasi lükkamine ei õnnestunud",
"This room is not public. You will not be able to rejoin without an invite.": "See ei ole avalik jututuba. Ilma kutseta sa ei saa uuesti liituda.",
"Failed to leave room": "Jututoast lahkumine ei õnnestunud",
"Can't leave Server Notices room": "Serveriteadete jututoast ei saa lahkuda",
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Seda jututuba kasutatakse sinu koduserveri oluliste teadete jaoks ja seega sa ei saa sealt lahkuda.",
"Signed Out": "Välja logitud",
"A username can only contain lower case letters, numbers and '=_-./'": "Kasutajanimes võivad olla vaid väiketähed, numbrid ja need viis tähemärki =_-./",
"Username not available": "Selline kasutajanimi ei ole saadaval",
"Username invalid: %(errMessage)s": "Vigane kasutajanimi: %(errMessage)s",
"An error occurred: %(error_string)s": "Tekkis viga: %(error_string)s",
"Checking...": "Kontrollin...",
"Username available": "Kasutajanimi on saadaval",
"To get started, please pick a username!": "Alustamiseks palun vali kasutajanimi!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "See saab olema sinu kasutajanimi <span></span> koduserveris, aga sa võid valida ka <a>mõne teise serveri</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Kui sul juba on olemas Matrix'i konto, siis võid lihtsalt <a>sisse logida</a>.",
"You have successfully set a password!": "Salasõna loomine õnnestus!",
"You have successfully set a password and an email address!": "Salasõna loomine ja e-posti aadressi salvestamine õnnestus!",
"You can now return to your account after signing out, and sign in on other devices.": "Nüüd sa saad peale väljalogimist pöörduda tagasi oma konto juurde või logida sisse muudest seadmetest.",
"Remember, you can always set an email address in user settings if you change your mind.": "Jäta meelde, et sa saad alati hiljem määrata kasutajaseadetest oma e-posti aadressi.",
"Use bots, bridges, widgets and sticker packs": "Kasuta roboteid, võrgusildu, vidinaid või kleepsupakke",
"Upload all": "Lae kõik üles",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "See fail on üleslaadimiseks <b>liiga suur</b>. Üleslaetavate failide mahupiir on %(limit)s, kuid selle faili suurus on %(sizeOfThisFile)s.",
"Appearance": "Välimus",
"Enter recovery passphrase": "Sisesta taastamise paroolifraas",
"For security, this session has been signed out. Please sign in again.": "Turvalisusega seotud põhjustel on see sessioon välja logitud. Palun logi uuesti sisse.",
"Review terms and conditions": "Vaata üle kasutustingimused",
"Old cryptography data detected": "Tuvastasin andmed, mille puhul on kasutatud vanemat tüüpi krüptimist",
"Self-verification request": "Päring enda verifitseerimiseks",
"Switch to light mode": "Kasuta heledat teemat",
"Switch to dark mode": "Kasuta tumedat teemat",
"Switch theme": "Vaheta teemat",
"Security & privacy": "Turvalisus ja privaatsus",
"All settings": "Kõik seadistused",
"Archived rooms": "Arhiveeritud jututoad",
"Feedback": "Tagasiside",
"Account settings": "Kasutajakonto seadistused",
"Use Single Sign On to continue": "Jätkamiseks kasuta ühekordset sisselogimist",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Kinnita selle e-posti aadressi lisamine kasutades ühekordset sisselogimist oma isiku tuvastamiseks.",
"Single Sign On": "SSO Ühekordne sisselogimine",
"Confirm adding email": "Kinnita e-posti aadressi lisamine",
"Click the button below to confirm adding this email address.": "Klõpsi järgnevat nuppu e-posti aadressi lisamise kinnitamiseks.",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Kinnita selle telefoninumbri lisamine kasutades ühekordset sisselogimist oma isiku tuvastamiseks.",
"Confirm adding phone number": "Kinnita telefoninumbri lisamine",
"Click the button below to confirm adding this phone number.": "Klõpsi järgnevat nuppu telefoninumbri lisamise kinnitamiseks.",
"Add Phone Number": "Lisa telefoninumber",
"Default": "Tavakasutaja",
"Restricted": "Piiratud õigustega kasutaja",
"Moderator": "Moderaator",
"Admin": "Peakasutaja",
"Custom (%(level)s)": "Kohandatud õigused (%(level)s)",
"Failed to invite": "Kutse saatmine ei õnnestunud",
"Operation failed": "Toiming ei õnnestunud",
"Failed to invite users to the room:": "Kasutajate kutsumine jututuppa ei õnnestunud:",
"Failed to invite the following users to the %(roomName)s room:": "Järgnevate kasutajate kutsumine %(roomName)s jututuppa ei õnnestunud:",
"You need to be logged in.": "Sa peaksid olema sisse loginud.",
"You need to be able to invite users to do that.": "Selle tegevuse jaoks peaks sul olema õigus teistele kasutajatele kutse saatmiseks.",
"Unable to create widget.": "Vidina loomine ei õnnestunud.",
"Missing roomId.": "Jututoa tunnus ehk roomId on puudu.",
"Failed to send request.": "Päringu saatmine ei õnnestunud.",
"This room is not recognised.": "Seda jututuba ei õnnestu ära tunda.",
"Power level must be positive integer.": "Õiguste tase peab olema positiivne täisarv.",
"You are not in this room.": "Sa ei asu selles jututoas.",
"You do not have permission to do that in this room.": "Sinul pole selle toimingu jaoks selles jututoas õigusi.",
"Missing room_id in request": "Päringus puudub jututoa tunnus ehk room_id",
"Room %(roomId)s not visible": "Jututuba %(roomId)s ei ole nähtav",
"Missing user_id in request": "Päringus puudub kasutaja tunnus ehk user_id",
"Messages": "Sõnumid",
"Actions": "Tegevused",
"Other": "Muud",
"Usage": "Kasutus",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Lisa ¯\\_(ツ)_/¯ smaili vormindamata teksti algusesse",
"Sends a message as plain text, without interpreting it as markdown": "Saadab sõnumi vormindamata tekstina ega tõlgenda seda markdown-vormindusena",
"Sends a message as html, without interpreting it as markdown": "Saadab sõnumi html'ina ega tõlgenda seda markdown-vormindusena",
"/ddg is not a command": "/ddg ei ole käsk",
"You joined the call": "Sina liitusid kõnega",
"%(senderName)s joined the call": "%(senderName)s liitus kõnega",
"Call in progress": "Kõne on pooleli",
"You left the call": "Sa lahkusid kõnest",
"%(senderName)s left the call": "%(senderName)s lahkus kõnest",
"Call ended": "Kõne lõppes",
"You started a call": "Sa alustasid kõnet",
"%(senderName)s started a call": "%(senderName)s alustas kõnet",
"Waiting for answer": "Ootan kõnele vastamist",
"%(senderName)s is calling": "%(senderName)s helistab",
"You created the room": "Sa lõid jututoa",
"%(senderName)s created the room": "%(senderName)s lõi jututoa",
"You made the chat encrypted": "Sina võtsid vestlusel kasutuse krüptimise",
"%(senderName)s made the chat encrypted": "%(senderName)s võttis vestlusel kasutuse krüptimise",
"You made history visible to new members": "Sina tegid jututoa ajaloo loetavaks uuetele liikmetele",
"%(senderName)s made history visible to new members": "%(senderName)s tegi jututoa ajaloo loetavaks uuetele liikmetele",
"You made history visible to anyone": "Sina tegi jututoa ajaloo loetavaks kõikidele",
"%(senderName)s made history visible to anyone": "%(senderName)s tegi jututoa ajaloo loetavaks kõikidele",
"You made history visible to future members": "Sina tegid jututoa ajaloo loetavaks tulevastele liikmetele",
"%(senderName)s made history visible to future members": "%(senderName)s tegi jututoa ajaloo loetavaks tulevastele liikmetele",
"You were invited": "Sina said kutse",
"%(targetName)s was invited": "%(targetName)s sai kutse",
"You left": "Sina lahkusid",
"%(targetName)s left": "%(targetName)s lahkus",
"You were kicked (%(reason)s)": "Sind müksati jututoast välja (%(reason)s)",
"%(targetName)s was kicked (%(reason)s)": "%(targetName)s müksati jututoast välja (%(reason)s)",
"You were kicked": "Sind müksati jututoast välja",
"%(targetName)s was kicked": "%(targetName)s müksati jututoast välja",
"You rejected the invite": "Sa lükkasid kutse tagasi",
"%(targetName)s rejected the invite": "%(targetName)s lükkas kutse tagasi",
"You were uninvited": "Sinult võeti kutse tagasi",
"%(targetName)s was uninvited": "%(targetName)s'lt võeti kutse tagasi",
"You joined": "Sina liitusid",
"%(targetName)s joined": "%(targetName)s liitus",
"You changed your name": "Sa muutsid oma nime",
"%(targetName)s changed their name": "%(targetName)s muutis oma nime",
"You changed your avatar": "Sa muutsid oma tunnuspilti",
"%(targetName)s changed their avatar": "%(targetName)s muutis oma tunnuspilti",
"%(senderName)s %(emote)s": "%(senderName)s %(emote)s",
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
"You changed the room name": "Sina muutsid jututoa nime",
"%(senderName)s changed the room name": "%(senderName)s muutis jututoa nime",
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"You uninvited %(targetName)s": "Sina võtsid tagasi kutse kasutajalt %(targetName)s",
"%(senderName)s uninvited %(targetName)s": "%(senderName)s võttis kutse tagasi kasutajalt %(targetName)s",
"You invited %(targetName)s": "Sina kutsusid kasutajat %(targetName)s",
"%(senderName)s invited %(targetName)s": "%(senderName)s kutsus kasutajat %(targetName)s",
"You changed the room topic": "Sina muutsid jututoa teemat",
"%(senderName)s changed the room topic": "%(senderName)s muutis jututoa teemat",
"Use the improved room list (will refresh to apply changes)": "Kasuta parandaatud jututubade loendit (muudatuse jõustamine eeldab andmete uuesti laadimist)",
"Enable IRC layout option in the appearance tab": "Näita välimuse seadistustes IRC-tüüpi paigutuse valikut",
"Use custom size": "Kasuta kohandatud suurust",
"Use a more compact Modern layout": "Kasuta veel kompaktsemat \"moodsat\" paigutust",
"Cross-signing public keys:": "Avalikud võtmed risttunnustamise jaoks:",
"in memory": "on mälus",
"not found": "pole leitavad",
"Delete %(count)s sessions|other": "Kustuta %(count)s sessiooni",
"Delete %(count)s sessions|one": "Kustuta %(count)s sessioon",
"ID": "Kasutaja ID",
"Public Name": "Avalik nimi",
"Last seen": "Viimati nähtud",
"Manage": "Halda",
"Enable": "Võta kasutusele",
"Error saving email notification preferences": "E-posti teel saadetavate teavituste eelistuste salvestamisel tekkis viga",
"An error occurred whilst saving your email notification preferences.": "E-posti teel saadetavate teavituste eelistuste salvestamisel tekkis viga.",
"Keywords": "Märksõnad",
"Enter keywords separated by a comma:": "Sisesta märksõnad ja kasuta eraldajaks koma:",
"Failed to change settings": "Seadistuste muutmine ei õnnestunud",
"Can't update user notification settings": "Kasutaja teavistuste eelistusi ei õnnestunud uuendada",
"Failed to update keywords": "Märksõnade uuendamine ei õnnestunud",
"Messages containing <span>keywords</span>": "Sõnumid, mis sisaldavad <span>märksõnu</span>",
"Notify me for anything else": "Teavita mind kõigest muust",
"Enable notifications for this account": "Võta sellel kasutajakontol kasutusele teavitused",
"Clear notifications": "Eemalda kõik teavitused",
"All notifications are currently disabled for all targets.": "Kõik teavituste liigid on välja lülitatud.",
"Add an email address to configure email notifications": "E-posti teel saadetavate teavituste seadistamiseks lisa e-posti aadress",
"Enable email notifications": "Võta kasutusele e-posti teel saadetavad teavitused",
"Notifications on the following keywords follow rules which cant be displayed here:": "Alljärgnevate märksõnadega seotud teavitused järgivad reegleid, misa siin ei saa kuvada:",
"Disinvite this user from community?": "Kas võtame sellelt kasutajalt tagasi kutse kogukonnaga liitumiseks?",
"Failed to withdraw invitation": "Kutse tühistamine ei õnnestunud",
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> jututoas %(roomName)s",
"Failed to change power level": "Õiguste muutmine ei õnnestunud",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Sa ei saa seda muudatust hiljem tagasi pöörata, sest annad teisele kasutajale samad õigused, mis sinul on.",
"Deactivate user?": "Kas blokeerime kasutaja?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Kasutaja blokeerimisel logitakse ta automaatselt välja ning ei lubata enam sisse logida. Lisaks lahkub ta kõikidest jututubadest, mille liige ta parasjagu on. Seda tegevust ei saa tagasi pöörata. Kas sa oled ikka kindel, et soovid selle kasutaja blokeerida?",
"Deactivate user": "Blokeeri kasutaja",
"Failed to deactivate user": "Kasutaja blokeerimine ei õnnestunud",
"This client does not support end-to-end encryption.": "See klient ei toeta läbivat krüptimist.",
"Security": "Turvalisus",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Selle vidina kasutamisel võidakse jagada andmeid <helpIcon /> saitidega %(widgetDomain)s ning sinu vidinahalduriga.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Selle vidina kasutamisel võidakse jagada andmeid <helpIcon /> saitidega %(widgetDomain)s.",
"Widgets do not use message encryption.": "Erinevalt sõnumitest vidinad ei kasuta krüptimist.",
"Widget added by": "Vidina lisaja",
"This widget may use cookies.": "See vidin võib kasutada küpsiseid.",
"Delete Widget": "Kustuta vidin",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Vidina kustutamisel eemaldatakse ta kõikide selle jututoa kasutajate jaoks. Kas sa kindlasti soovid seda vidinat eemaldada?",
"Delete widget": "Kustuta vidin",
"Minimize apps": "Vähenda rakendused",
"Maximize apps": "Suurenda rakendused",
"Popout widget": "Ava rakendus eraldi aknas",
"More options": "Täiendavad seadistused",
"Language Dropdown": "Keelevalik",
"Manage Integrations": "Halda lõiminguid",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s liitusid %(count)s korda",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s liitusid",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s liitus %(count)s korda",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s liitus",
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s lahkusid %(count)s korda",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s lahkusid",
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s lahkus %(count)s korda",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s lahkus",
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s liitusid ja lahkusid %(count)s korda",
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s liitusid ja lahkusid",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s liitus ja lahkus %(count)s korda",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s liitus ja lahkus",
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s lahkusid ja liitusid uuesti %(count)s korda",
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s lahkusid ja liitusid uuesti",
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s lahkus ja liitus uuesti %(count)s korda",
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s lahkus ja liitus uuesti"
}

Some files were not shown because too many files have changed in this diff Show more