Merge branches 'develop' and 't3chguy/room-list/3' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/3
Conflicts: src/components/structures/ContextMenu.tsx src/components/structures/UserMenu.tsx src/components/views/rooms/RoomSublist2.tsx src/components/views/rooms/RoomTile2.tsx
This commit is contained in:
commit
afac330143
42 changed files with 1084 additions and 248 deletions
156
CHANGELOG.md
156
CHANGELOG.md
|
@ -1,3 +1,159 @@
|
||||||
|
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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0...v2.8.1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "2.8.1",
|
"version": "2.9.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -120,6 +120,7 @@
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
"@peculiar/webcrypto": "^1.0.22",
|
"@peculiar/webcrypto": "^1.0.22",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/lodash": "^4.14.152",
|
"@types/lodash": "^4.14.152",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
|
|
|
@ -86,11 +86,15 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
|
|
||||||
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
|
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
|
||||||
// Cheaty way to return the occupied space to the filter input
|
// Cheaty way to return the occupied space to the filter input
|
||||||
|
flex-basis: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
||||||
// Don't forget to hide the masked ::before icon
|
// Don't forget to hide the masked ::before icon,
|
||||||
visibility: hidden;
|
// using display:none or visibility:hidden would break accessibility
|
||||||
|
&::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_exploreButton {
|
.mx_LeftPanel2_exploreButton {
|
||||||
|
|
|
@ -28,9 +28,6 @@ limitations under the License.
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
border: 1px solid $input-darker-bg-color;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
> .mx_RadioButton_content {
|
> .mx_RadioButton_content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
@ -110,6 +107,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_outlined {
|
||||||
|
border: 1px solid $input-darker-bg-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RadioButton_checked {
|
.mx_RadioButton_checked {
|
||||||
border-color: $accent-color;
|
border-color: $accent-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ limitations under the License.
|
||||||
|
|
||||||
// Apply the width and margin to the badge so the container doesn't occupy dead space
|
// Apply the width and margin to the badge so the container doesn't occupy dead space
|
||||||
.mx_NotificationBadge {
|
.mx_NotificationBadge {
|
||||||
width: 16px;
|
// Do not set a width so the badges get properly sized
|
||||||
margin-left: 8px; // same as menu+aux buttons
|
margin-left: 8px; // same as menu+aux buttons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,6 +278,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomSublist2_hasMenuOpen,
|
&.mx_RoomSublist2_hasMenuOpen,
|
||||||
|
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within,
|
||||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
||||||
.mx_RoomSublist2_menuButton {
|
.mx_RoomSublist2_menuButton {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
|
@ -24,7 +24,10 @@ limitations under the License.
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
|
&.mx_RoomTile2_selected,
|
||||||
|
&:hover,
|
||||||
|
&:focus-within,
|
||||||
|
&.mx_RoomTile2_hasMenuOpen {
|
||||||
background-color: $roomtile2-selected-bg-color;
|
background-color: $roomtile2-selected-bg-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
}
|
}
|
||||||
|
@ -74,7 +77,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_menuButton {
|
.mx_RoomTile2_notificationsButton {
|
||||||
margin-left: 4px; // spacing between buttons
|
margin-left: 4px; // spacing between buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +85,7 @@ limitations under the License.
|
||||||
height: 16px;
|
height: 16px;
|
||||||
// don't set width so that it takes no space when there is no badge to show
|
// don't set width so that it takes no space when there is no badge to show
|
||||||
margin: auto 0; // vertically align
|
margin: auto 0; // vertically align
|
||||||
|
position: relative; // fixes badge alignment in some scenarios
|
||||||
|
|
||||||
// Create a flexbox to make aligning dot badges easier
|
// Create a flexbox to make aligning dot badges easier
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -104,7 +108,8 @@ limitations under the License.
|
||||||
width: 20px;
|
width: 20px;
|
||||||
min-width: 20px; // yay flex
|
min-width: 20px; // yay flex
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: auto 0;
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@ -132,7 +137,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.mx_RoomTile2_minimized) {
|
&: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
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_RoomTile2_badgeContainer {
|
.mx_RoomTile2_badgeContainer {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -193,6 +193,10 @@ limitations under the License.
|
||||||
.mx_EventTile_content {
|
.mx_EventTile_content {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
|
||||||
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RadioButton {
|
.mx_RadioButton {
|
||||||
|
|
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
|
@ -20,6 +20,7 @@ import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import DeviceListener from "../DeviceListener";
|
import DeviceListener from "../DeviceListener";
|
||||||
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
||||||
|
import { PlatformPeg } from "../PlatformPeg";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -33,6 +34,7 @@ declare global {
|
||||||
mx_ToastStore: ToastStore;
|
mx_ToastStore: ToastStore;
|
||||||
mx_DeviceListener: DeviceListener;
|
mx_DeviceListener: DeviceListener;
|
||||||
mx_RoomListStore2: RoomListStore2;
|
mx_RoomListStore2: RoomListStore2;
|
||||||
|
mxPlatformPeg: PlatformPeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
||||||
|
@ -45,6 +47,10 @@ declare global {
|
||||||
hasStorageAccess?: () => Promise<boolean>;
|
hasStorageAccess?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Navigator {
|
||||||
|
userLanguage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface StorageEstimate {
|
interface StorageEstimate {
|
||||||
usageDetails?: {[key: string]: number};
|
usageDetails?: {[key: string]: number};
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ export default abstract class BasePlatform {
|
||||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract async getConfig(): Promise<{}>;
|
||||||
|
|
||||||
|
abstract getDefaultDeviceDisplayName(): string;
|
||||||
|
|
||||||
protected onAction = (payload: ActionPayload) => {
|
protected onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'on_client_not_viable':
|
case 'on_client_not_viable':
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import BasePlatform from "./BasePlatform";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Holds the current Platform object used by the code to do anything
|
* Holds the current Platform object used by the code to do anything
|
||||||
* specific to the platform we're running on (eg. web, electron)
|
* specific to the platform we're running on (eg. web, electron)
|
||||||
|
@ -21,10 +24,8 @@ limitations under the License.
|
||||||
* This allows the app layer to set a Platform without necessarily
|
* This allows the app layer to set a Platform without necessarily
|
||||||
* having to have a MatrixChat object
|
* having to have a MatrixChat object
|
||||||
*/
|
*/
|
||||||
class PlatformPeg {
|
export class PlatformPeg {
|
||||||
constructor() {
|
platform: BasePlatform = null;
|
||||||
this.platform = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current Platform object for the application.
|
* Returns the current Platform object for the application.
|
||||||
|
@ -39,12 +40,12 @@ class PlatformPeg {
|
||||||
* application.
|
* application.
|
||||||
* This should be an instance of a class extending BasePlatform.
|
* This should be an instance of a class extending BasePlatform.
|
||||||
*/
|
*/
|
||||||
set(plaf) {
|
set(plaf: BasePlatform) {
|
||||||
this.platform = plaf;
|
this.platform = plaf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.mxPlatformPeg) {
|
if (!window.mxPlatformPeg) {
|
||||||
global.mxPlatformPeg = new PlatformPeg();
|
window.mxPlatformPeg = new PlatformPeg();
|
||||||
}
|
}
|
||||||
export default global.mxPlatformPeg;
|
export default window.mxPlatformPeg;
|
24
src/RoomNotifsTypes.ts
Normal file
24
src/RoomNotifsTypes.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
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 {
|
||||||
|
ALL_MESSAGES,
|
||||||
|
ALL_MESSAGES_LOUD,
|
||||||
|
MENTIONS_ONLY,
|
||||||
|
MUTE,
|
||||||
|
} from "./RoomNotifs";
|
||||||
|
|
||||||
|
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
|
|
@ -444,6 +444,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
value={CREATE_STORAGE_OPTION_KEY}
|
value={CREATE_STORAGE_OPTION_KEY}
|
||||||
name="keyPassphrase"
|
name="keyPassphrase"
|
||||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
||||||
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
||||||
|
@ -456,6 +457,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||||
name="keyPassphrase"
|
name="keyPassphrase"
|
||||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||||
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
||||||
|
|
|
@ -23,6 +23,8 @@ import classNames from "classnames";
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import AccessibleButton, { IProps as IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton";
|
import AccessibleButton, { IProps as IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import {Writeable} from "../../@types/common";
|
import {Writeable} from "../../@types/common";
|
||||||
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
|
import StyledRadioButton from "../views/elements/StyledRadioButton";
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -455,6 +457,54 @@ export const MenuItemCheckbox: React.FC<IMenuItemCheckboxProps> = ({children, la
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IStyledMenuItemCheckboxProps extends IAccessibleButtonProps {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onChange();
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemcheckbox
|
||||||
|
export const StyledMenuItemCheckbox: React.FC<IStyledMenuItemCheckboxProps> = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledCheckbox
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
checked={checked}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IMenuItemRadioProps extends IAccessibleButtonProps {
|
interface IMenuItemRadioProps extends IAccessibleButtonProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
@ -472,6 +522,55 @@ export const MenuItemRadio: React.FC<IMenuItemRadioProps> = ({children, label, a
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface IStyledMenuItemRadioProps extends IAccessibleButtonProps {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onChange();
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemradio
|
||||||
|
export const StyledMenuItemRadio: React.FC<IStyledMenuItemRadioProps> = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledRadioButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={checked}
|
||||||
|
checked={checked}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledRadioButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
|
|
|
@ -30,7 +30,9 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStore, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
||||||
|
import {Key} from "../../Keyboard";
|
||||||
|
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// 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
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -54,9 +56,19 @@ interface IState {
|
||||||
showTagPanel: boolean;
|
showTagPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||||
|
const cssClasses = [
|
||||||
|
"mx_RoomSearch_input",
|
||||||
|
"mx_RoomSearch_icon", // minimized <RoomSearch />
|
||||||
|
"mx_RoomSublist2_headerText",
|
||||||
|
"mx_RoomTile2",
|
||||||
|
"mx_RoomSublist2_showNButton",
|
||||||
|
];
|
||||||
|
|
||||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private tagPanelWatcherRef: string;
|
private tagPanelWatcherRef: string;
|
||||||
|
private focusedElement = null;
|
||||||
|
|
||||||
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
||||||
|
|
||||||
|
@ -113,6 +125,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
const headerStickyWidth = rlRect.width - headerRightMargin;
|
const headerStickyWidth = rlRect.width - headerRightMargin;
|
||||||
|
|
||||||
let gotBottom = false;
|
let gotBottom = false;
|
||||||
|
let lastTopHeader;
|
||||||
for (const sublist of sublists) {
|
for (const sublist of sublists) {
|
||||||
const slRect = sublist.getBoundingClientRect();
|
const slRect = sublist.getBoundingClientRect();
|
||||||
|
|
||||||
|
@ -122,19 +135,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
header.style.width = `${headerStickyWidth}px`;
|
||||||
header.style.top = `unset`;
|
header.style.removeProperty("top");
|
||||||
gotBottom = true;
|
gotBottom = true;
|
||||||
} else if (slRect.top < top) {
|
} else if ((slRect.top - (headerHeight / 3)) < top) {
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
header.style.width = `${headerStickyWidth}px`;
|
||||||
header.style.top = `${rlRect.top}px`;
|
header.style.top = `${rlRect.top}px`;
|
||||||
|
if (lastTopHeader) {
|
||||||
|
lastTopHeader.style.display = "none";
|
||||||
|
}
|
||||||
|
// first unset it, if set in last iteration
|
||||||
|
header.style.removeProperty("display");
|
||||||
|
lastTopHeader = header;
|
||||||
} else {
|
} else {
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
header.style.width = `unset`;
|
header.style.removeProperty("width");
|
||||||
header.style.top = `unset`;
|
header.style.removeProperty("top");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,13 +169,76 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
this.handleStickyHeaders(this.listContainerRef.current);
|
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 && !cssClasses.some(c => classes.contains(c)));
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
this.focusedElement = element;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private renderHeader(): React.ReactNode {
|
private renderHeader(): React.ReactNode {
|
||||||
let breadcrumbs;
|
let breadcrumbs;
|
||||||
if (this.state.showBreadcrumbs) {
|
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
|
||||||
breadcrumbs = (
|
breadcrumbs = (
|
||||||
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar">
|
<IndicatorScrollbar
|
||||||
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
|
className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||||
</div>
|
verticalScrollsHorizontally={true}
|
||||||
|
>
|
||||||
|
<RoomBreadcrumbs2 />
|
||||||
|
</IndicatorScrollbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,13 +252,22 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderSearchExplore(): React.ReactNode {
|
private renderSearchExplore(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_LeftPanel2_filterContainer">
|
<div
|
||||||
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
|
className="mx_LeftPanel2_filterContainer"
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
>
|
||||||
|
<RoomSearch
|
||||||
|
onQueryUpdate={this.onSearch}
|
||||||
|
isMinimized={this.props.isMinimized}
|
||||||
|
onVerticalArrow={this.onKeyDown}
|
||||||
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
|
||||||
className='mx_LeftPanel2_exploreButton'
|
className="mx_LeftPanel2_exploreButton"
|
||||||
onClick={this.onExplore}
|
onClick={this.onExplore}
|
||||||
alt={_t("Explore rooms")}
|
title={_t("Explore rooms")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -189,15 +280,15 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Determine what these onWhatever handlers do: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
const roomList = <RoomList2
|
const roomList = <RoomList2
|
||||||
onKeyDown={() => {/*TODO*/}}
|
onKeyDown={this.onKeyDown}
|
||||||
resizeNotifier={null}
|
resizeNotifier={null}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
searchFilter={this.state.searchFilter}
|
searchFilter={this.state.searchFilter}
|
||||||
onFocus={() => {/*TODO*/}}
|
onFocus={this.onFocus}
|
||||||
onBlur={() => {/*TODO*/}}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
onResize={this.onResize}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
|
// TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
|
||||||
|
@ -223,7 +314,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
ref={this.listContainerRef}
|
ref={this.listContainerRef}
|
||||||
>{roomList}</div>
|
// Firefox sometimes makes this element focusable due to
|
||||||
|
// overflow:scroll;, so force it out of tab order.
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{roomList}
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -146,6 +146,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||||
protected readonly _sessionStore: sessionStore;
|
protected readonly _sessionStore: sessionStore;
|
||||||
protected readonly _sessionStoreToken: { remove: () => void };
|
protected readonly _sessionStoreToken: { remove: () => void };
|
||||||
|
protected readonly _compactLayoutWatcherRef: string;
|
||||||
protected resizer: Resizer;
|
protected resizer: Resizer;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -177,6 +178,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
this._matrixClient.on("sync", this.onSync);
|
this._matrixClient.on("sync", this.onSync);
|
||||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
|
||||||
|
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||||
|
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||||
|
);
|
||||||
|
|
||||||
fixupColorFonts();
|
fixupColorFonts();
|
||||||
|
|
||||||
this._roomView = React.createRef();
|
this._roomView = React.createRef();
|
||||||
|
@ -194,6 +199,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
|
||||||
if (this._sessionStoreToken) {
|
if (this._sessionStoreToken) {
|
||||||
this._sessionStoreToken.remove();
|
this._sessionStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -263,16 +269,17 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccountData = (event) => {
|
onAccountData = (event) => {
|
||||||
if (event.getType() === "im.vector.web.settings") {
|
|
||||||
this.setState({
|
|
||||||
useCompactLayout: event.getContent().useCompactLayout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (event.getType() === "m.ignored_user_list") {
|
if (event.getType() === "m.ignored_user_list") {
|
||||||
dis.dispatch({action: "ignore_state_changed"});
|
dis.dispatch({action: "ignore_state_changed"});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => {
|
||||||
|
this.setState({
|
||||||
|
useCompactLayout: valueAtLevel,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onSync = (syncState, oldSyncState, data) => {
|
onSync = (syncState, oldSyncState, data) => {
|
||||||
const oldErrCode = (
|
const oldErrCode = (
|
||||||
this.state.syncErrorData &&
|
this.state.syncErrorData &&
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { Action } from "../../dispatcher/actions";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onQueryUpdate: (newQuery: string) => void;
|
onQueryUpdate: (newQuery: string) => void;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
onVerticalArrow(ev: React.KeyboardEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -111,6 +112,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
defaultDispatcher.fire(Action.FocusComposer);
|
defaultDispatcher.fire(Action.FocusComposer);
|
||||||
|
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
|
||||||
|
this.props.onVerticalArrow(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
let clearButton = (
|
let clearButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className='mx_RoomSearch_clearButton'
|
title={_t("Clear filter")}
|
||||||
|
className="mx_RoomSearch_clearButton"
|
||||||
onClick={this.clearInput}
|
onClick={this.clearInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -154,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
if (this.props.isMinimized) {
|
if (this.props.isMinimized) {
|
||||||
icon = (
|
icon = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
title={_t("Search rooms")}
|
||||||
className='mx_RoomSearch_icon'
|
className="mx_RoomSearch_icon"
|
||||||
onClick={this.openSearch}
|
onClick={this.openSearch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React, { createRef } from "react";
|
||||||
import {createRef} from "react";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import {ChevronFace, ContextMenu, ContextMenuButton} from "./ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
|
||||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||||
|
@ -30,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
import {getCustomTheme} from "../../theme";
|
import {getCustomTheme} from "../../theme";
|
||||||
import {getHostingLink} from "../../utils/HostingLink";
|
import {getHostingLink} from "../../utils/HostingLink";
|
||||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import {getHomePageUrl} from "../../utils/pages";
|
import {getHomePageUrl} from "../../utils/pages";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
|
@ -50,6 +49,19 @@ interface IState {
|
||||||
isDarkTheme: boolean;
|
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> {
|
export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private themeWatcherRef: string;
|
private themeWatcherRef: string;
|
||||||
|
@ -102,8 +114,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
private onAction = (ev: ActionPayload) => {
|
private onAction = (ev: ActionPayload) => {
|
||||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
||||||
|
|
||||||
// For accessibility
|
if (this.state.contextMenuPosition) {
|
||||||
|
this.setState({contextMenuPosition: null});
|
||||||
|
} else {
|
||||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
|
@ -130,7 +145,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.setState({contextMenuPosition: null});
|
this.setState({contextMenuPosition: null});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSwitchThemeClick = () => {
|
private onSwitchThemeClick = (ev: React.MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
// Disable system theme matching if the user hits this button
|
// Disable system theme matching if the user hits this button
|
||||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||||
|
|
||||||
|
@ -206,10 +224,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
let homeButton = null;
|
let homeButton = null;
|
||||||
if (this.hasHomePage) {
|
if (this.hasHomePage) {
|
||||||
homeButton = (
|
homeButton = (
|
||||||
<AccessibleButton onClick={this.onHomeClick}>
|
<MenuButton
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
|
iconClassName="mx_UserMenu_iconHome"
|
||||||
<span>{_t("Home")}</span>
|
label={_t("Home")}
|
||||||
</AccessibleButton>
|
onClick={this.onHomeClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,32 +265,38 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
{hostingLink}
|
{hostingLink}
|
||||||
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||||
{homeButton}
|
{homeButton}
|
||||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
|
<MenuButton
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
|
iconClassName="mx_UserMenu_iconBell"
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Notification settings")}</span>
|
label={_t("Notification settings")}
|
||||||
</AccessibleButton>
|
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
||||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
|
/>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
|
<MenuButton
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Security & privacy")}</span>
|
iconClassName="mx_UserMenu_iconLock"
|
||||||
</AccessibleButton>
|
label={_t("Security & privacy")}
|
||||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
|
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
|
/>
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("All settings")}</span>
|
<MenuButton
|
||||||
</AccessibleButton>
|
iconClassName="mx_UserMenu_iconSettings"
|
||||||
<AccessibleButton onClick={this.onShowArchived}>
|
label={_t("All settings")}
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
|
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Archived rooms")}</span>
|
/>
|
||||||
</AccessibleButton>
|
<MenuButton
|
||||||
<AccessibleButton onClick={this.onProvideFeedback}>
|
iconClassName="mx_UserMenu_iconArchive"
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
|
label={_t("Archived rooms")}
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Feedback")}</span>
|
onClick={this.onShowArchived}
|
||||||
</AccessibleButton>
|
/>
|
||||||
|
<MenuButton
|
||||||
|
iconClassName="mx_UserMenu_iconMessage"
|
||||||
|
label={_t("Feedback")}
|
||||||
|
onClick={this.onProvideFeedback}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
|
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
|
||||||
<AccessibleButton onClick={this.onSignOutClick}>
|
<MenuButton
|
||||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
|
iconClassName="mx_UserMenu_iconSignOut"
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Sign out")}</span>
|
label={_t("Sign out")}
|
||||||
</AccessibleButton>
|
onClick={this.onSignOutClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -303,7 +328,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onOpenMenuClick}
|
onClick={this.onOpenMenuClick}
|
||||||
inputRef={this.buttonRef}
|
inputRef={this.buttonRef}
|
||||||
label={_t("Account settings")}
|
label={_t("User menu")}
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
>
|
>
|
||||||
|
|
|
@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {_t} from '../../../languageHandler.js';
|
import {_t} from '../../../languageHandler';
|
||||||
import Field from "./Field";
|
import Field from "./Field";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
outlined?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -29,7 +30,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { children, className, disabled, ...otherProps } = this.props;
|
const { children, className, disabled, outlined, ...otherProps } = this.props;
|
||||||
const _className = classnames(
|
const _className = classnames(
|
||||||
'mx_RadioButton',
|
'mx_RadioButton',
|
||||||
className,
|
className,
|
||||||
|
@ -37,11 +38,12 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
||||||
"mx_RadioButton_disabled": disabled,
|
"mx_RadioButton_disabled": disabled,
|
||||||
"mx_RadioButton_enabled": !disabled,
|
"mx_RadioButton_enabled": !disabled,
|
||||||
"mx_RadioButton_checked": this.props.checked,
|
"mx_RadioButton_checked": this.props.checked,
|
||||||
|
"mx_RadioButton_outlined": outlined,
|
||||||
});
|
});
|
||||||
return <label className={_className}>
|
return <label className={_className}>
|
||||||
<input type='radio' disabled={disabled} {...otherProps} />
|
<input type='radio' disabled={disabled} {...otherProps} />
|
||||||
{/* Used to render the radio button circle */}
|
{/* Used to render the radio button circle */}
|
||||||
<div><div></div></div>
|
<div><div /></div>
|
||||||
<div className="mx_RadioButton_content">{children}</div>
|
<div className="mx_RadioButton_content">{children}</div>
|
||||||
<div className="mx_RadioButton_spacer" />
|
<div className="mx_RadioButton_spacer" />
|
||||||
</label>;
|
</label>;
|
||||||
|
|
|
@ -32,10 +32,11 @@ interface IProps<T extends string> {
|
||||||
className?: string;
|
className?: string;
|
||||||
definitions: IDefinition<T>[];
|
definitions: IDefinition<T>[];
|
||||||
value?: T; // if not provided no options will be selected
|
value?: T; // if not provided no options will be selected
|
||||||
|
outlined?: boolean;
|
||||||
onChange(newValue: T);
|
onChange(newValue: T);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, onChange}: IProps<T>) {
|
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
||||||
const _onChange = e => {
|
const _onChange = e => {
|
||||||
onChange(e.target.value);
|
onChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
@ -49,6 +50,7 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
|
||||||
name={name}
|
name={name}
|
||||||
value={d.value}
|
value={d.value}
|
||||||
disabled={d.disabled}
|
disabled={d.disabled}
|
||||||
|
outlined={outlined}
|
||||||
>
|
>
|
||||||
{d.label}
|
{d.label}
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
|
|
|
@ -51,6 +51,7 @@ interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
onFocus: (ev: React.FocusEvent) => void;
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
|
onResize: () => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
|
@ -63,8 +64,6 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAG_ORDER: TagID[] = [
|
const TAG_ORDER: TagID[] = [
|
||||||
// -- Community Invites Placeholder --
|
|
||||||
|
|
||||||
DefaultTagID.Invite,
|
DefaultTagID.Invite,
|
||||||
DefaultTagID.Favourite,
|
DefaultTagID.Favourite,
|
||||||
DefaultTagID.DM,
|
DefaultTagID.DM,
|
||||||
|
@ -76,7 +75,6 @@ const TAG_ORDER: TagID[] = [
|
||||||
DefaultTagID.ServerNotice,
|
DefaultTagID.ServerNotice,
|
||||||
DefaultTagID.Archived,
|
DefaultTagID.Archived,
|
||||||
];
|
];
|
||||||
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
|
|
||||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||||
DefaultTagID.DM,
|
DefaultTagID.DM,
|
||||||
|
@ -183,14 +181,16 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
layoutMap.set(tagId, new ListLayout(tagId));
|
layoutMap.set(tagId, new ListLayout(tagId));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({sublists: newLists, layouts: layoutMap});
|
this.setState({sublists: newLists, layouts: layoutMap}, () => {
|
||||||
|
this.props.onResize();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderCommunityInvites(): React.ReactElement[] {
|
private renderCommunityInvites(): React.ReactElement[] {
|
||||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||||
if (g.myMembership !== 'invite') return false;
|
if (g.myMembership !== 'invite') return false;
|
||||||
return !this.searchFilter || this.searchFilter.matches(g.name);
|
return !this.searchFilter || this.searchFilter.matches(g.name || "");
|
||||||
}).map(g => {
|
}).map(g => {
|
||||||
const avatar = (
|
const avatar = (
|
||||||
<GroupAvatar
|
<GroupAvatar
|
||||||
|
@ -224,17 +224,15 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
const components: React.ReactElement[] = [];
|
const components: React.ReactElement[] = [];
|
||||||
|
|
||||||
for (const orderedTagId of TAG_ORDER) {
|
for (const orderedTagId of TAG_ORDER) {
|
||||||
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
|
|
||||||
// Populate community invites if we have the chance
|
|
||||||
// TODO: Community invites: https://github.com/vector-im/riot-web/issues/14179
|
|
||||||
}
|
|
||||||
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
||||||
// Populate custom tags if needed
|
// Populate custom tags if needed
|
||||||
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
|
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||||
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||||
|
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
||||||
|
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||||
continue; // skip tag - not needed
|
continue; // skip tag - not needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +240,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||||
|
|
||||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
|
||||||
components.push(
|
components.push(
|
||||||
<RoomSublist2
|
<RoomSublist2
|
||||||
key={`sublist-${orderedTagId}`}
|
key={`sublist-${orderedTagId}`}
|
||||||
|
@ -256,6 +253,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
isInvite={aesthetics.isInvite}
|
isInvite={aesthetics.isInvite}
|
||||||
layout={this.state.layouts.get(orderedTagId)}
|
layout={this.state.layouts.get(orderedTagId)}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
onResize={this.props.onResize}
|
||||||
extraBadTilesThatShouldntExist={extraTiles}
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -276,9 +274,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomList2"
|
className="mx_RoomList2"
|
||||||
role="tree"
|
role="tree"
|
||||||
aria-label={_t("Rooms")}
|
aria-label={_t("Rooms")}
|
||||||
// Firefox sometimes makes this element focusable due to
|
|
||||||
// overflow:scroll;, so force it out of tab order.
|
|
||||||
tabIndex={-1}
|
|
||||||
>{sublists}</div>
|
>{sublists}</div>
|
||||||
)}
|
)}
|
||||||
</RovingTabIndexProvider>
|
</RovingTabIndexProvider>
|
||||||
|
|
|
@ -26,17 +26,22 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
import RoomTile2 from "./RoomTile2";
|
import RoomTile2 from "./RoomTile2";
|
||||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import {
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
ChevronFace,
|
||||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
ContextMenu,
|
||||||
|
ContextMenuButton,
|
||||||
|
StyledMenuItemCheckbox,
|
||||||
|
StyledMenuItemRadio,
|
||||||
|
} from "../../structures/ContextMenu";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||||
import Tooltip from "../elements/Tooltip";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { Key } from "../../../Keyboard";
|
||||||
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// 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
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -62,9 +67,10 @@ interface IProps {
|
||||||
onAddRoom?: () => void;
|
onAddRoom?: () => void;
|
||||||
addRoomLabel: string;
|
addRoomLabel: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
layout: ListLayout;
|
layout?: ListLayout;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
|
onResize: () => void;
|
||||||
|
|
||||||
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
// 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.
|
// You should feel bad if you use this.
|
||||||
|
@ -82,6 +88,9 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
private headerButton = createRef<HTMLDivElement>();
|
||||||
|
private sublistRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -132,11 +141,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowAllClick = () => {
|
private onShowAllClick = () => {
|
||||||
|
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
|
||||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowLessClick = () => {
|
private onShowLessClick = () => {
|
||||||
|
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
|
||||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
@ -199,6 +210,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -217,8 +229,53 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
sublist.scrollIntoView({behavior: 'smooth'});
|
sublist.scrollIntoView({behavior: 'smooth'});
|
||||||
} else {
|
} else {
|
||||||
// on screen - toggle collapse
|
// on screen - toggle collapse
|
||||||
|
this.toggleCollapsed();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private toggleCollapsed = () => {
|
||||||
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger an update
|
this.forceUpdate(); // because the layout doesn't trigger an update
|
||||||
|
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -230,10 +287,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const tiles: React.ReactElement[] = [];
|
const tiles: React.ReactElement[] = [];
|
||||||
|
|
||||||
if (this.props.extraBadTilesThatShouldntExist) {
|
|
||||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.rooms) {
|
if (this.props.rooms) {
|
||||||
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||||
for (const room of visibleRooms) {
|
for (const room of visibleRooms) {
|
||||||
|
@ -249,6 +302,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.extraBadTilesThatShouldntExist) {
|
||||||
|
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||||
|
}
|
||||||
|
|
||||||
// We only have to do this because of the extra tiles. We do it conditionally
|
// 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
|
// 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
|
// as users are unlikely to have more than a handful of tiles when the extra
|
||||||
|
@ -261,15 +318,42 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderMenu(): React.ReactElement {
|
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;
|
let contextMenu = null;
|
||||||
if (this.state.contextMenuPosition) {
|
if (this.state.contextMenuPosition) {
|
||||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||||
|
|
||||||
|
// Invites don't get some nonsense options, so only add them if we have to.
|
||||||
|
let otherSections = null;
|
||||||
|
if (this.props.tagId !== DefaultTagID.Invite) {
|
||||||
|
otherSections = (
|
||||||
|
<React.Fragment>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||||
|
<StyledMenuItemCheckbox
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
|
onChange={this.onUnreadFirstChanged}
|
||||||
|
checked={isUnreadFirst}
|
||||||
|
>
|
||||||
|
{_t("Always show first")}
|
||||||
|
</StyledMenuItemCheckbox>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||||
|
<StyledMenuItemCheckbox
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
|
onChange={this.onMessagePreviewChanged}
|
||||||
|
checked={this.props.layout.showPreviews}
|
||||||
|
>
|
||||||
|
{_t("Message preview")}
|
||||||
|
</StyledMenuItemCheckbox>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronFace={ChevronFace.None}
|
chevronFace={ChevronFace.None}
|
||||||
|
@ -280,41 +364,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
<div className="mx_RoomSublist2_contextMenu">
|
<div className="mx_RoomSublist2_contextMenu">
|
||||||
<div>
|
<div>
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
||||||
<StyledRadioButton
|
<StyledMenuItemRadio
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||||
checked={!isAlphabetical}
|
checked={!isAlphabetical}
|
||||||
name={`mx_${this.props.tagId}_sortBy`}
|
name={`mx_${this.props.tagId}_sortBy`}
|
||||||
>
|
>
|
||||||
{_t("Activity")}
|
{_t("Activity")}
|
||||||
</StyledRadioButton>
|
</StyledMenuItemRadio>
|
||||||
<StyledRadioButton
|
<StyledMenuItemRadio
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
||||||
checked={isAlphabetical}
|
checked={isAlphabetical}
|
||||||
name={`mx_${this.props.tagId}_sortBy`}
|
name={`mx_${this.props.tagId}_sortBy`}
|
||||||
>
|
>
|
||||||
{_t("A-Z")}
|
{_t("A-Z")}
|
||||||
</StyledRadioButton>
|
</StyledMenuItemRadio>
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
|
||||||
<StyledCheckbox
|
|
||||||
onChange={this.onUnreadFirstChanged}
|
|
||||||
checked={isUnreadFirst}
|
|
||||||
>
|
|
||||||
{_t("Always show first")}
|
|
||||||
</StyledCheckbox>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
|
||||||
<StyledCheckbox
|
|
||||||
onChange={this.onMessagePreviewChanged}
|
|
||||||
checked={this.props.layout.showPreviews}
|
|
||||||
>
|
|
||||||
{_t("Message preview")}
|
|
||||||
</StyledCheckbox>
|
|
||||||
</div>
|
</div>
|
||||||
|
{otherSections}
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
|
@ -335,17 +402,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderHeader(): React.ReactElement {
|
private renderHeader(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||||
{({onFocus, isActive, ref}) => {
|
{({onFocus, isActive, ref}) => {
|
||||||
// TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
|
let ariaLabel = _t("Jump to first unread room.");
|
||||||
|
if (this.props.tagId === DefaultTagID.Invite) {
|
||||||
|
ariaLabel = _t("Jump to first invite.");
|
||||||
|
}
|
||||||
|
|
||||||
const badge = (
|
const badge = (
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
forceCount={true}
|
forceCount={true}
|
||||||
notification={this.state.notificationState}
|
notification={this.state.notificationState}
|
||||||
onClick={this.onBadgeClick}
|
onClick={this.onBadgeClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -386,14 +458,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
// doesn't become sticky.
|
// doesn't become sticky.
|
||||||
// The same applies to the notification badge.
|
// The same applies to the notification badge.
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
|
||||||
<div className='mx_RoomSublist2_stickable'>
|
<div className="mx_RoomSublist2_stickable">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={"mx_RoomSublist2_headerText"}
|
className="mx_RoomSublist2_headerText"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
|
||||||
aria-level={1}
|
aria-level={1}
|
||||||
onClick={this.onHeaderClick}
|
onClick={this.onHeaderClick}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
|
@ -449,12 +522,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) showMoreText = null;
|
if (this.props.isMinimized) showMoreText = null;
|
||||||
showNButton = (
|
showNButton = (
|
||||||
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
<AccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses} tabIndex={-1}>
|
||||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||||
{/* set by CSS masking */}
|
{/* set by CSS masking */}
|
||||||
</span>
|
</span>
|
||||||
{showMoreText}
|
{showMoreText}
|
||||||
</div>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
||||||
// we have all tiles visible - add a button to show less
|
// we have all tiles visible - add a button to show less
|
||||||
|
@ -464,13 +537,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) showLessText = null;
|
if (this.props.isMinimized) showLessText = null;
|
||||||
|
// TODO Roving tab index / treeitem?: https://github.com/vector-im/riot-web/issues/14180
|
||||||
showNButton = (
|
showNButton = (
|
||||||
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
<AccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses} tabIndex={-1}>
|
||||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||||
{/* set by CSS masking */}
|
{/* set by CSS masking */}
|
||||||
</span>
|
</span>
|
||||||
{showLessText}
|
{showLessText}
|
||||||
</div>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,12 +594,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: onKeyDown support: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.sublistRef}
|
||||||
className={classes}
|
className={classes}
|
||||||
role="group"
|
role="group"
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{content}
|
{content}
|
||||||
|
|
|
@ -26,17 +26,31 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {ChevronFace, ContextMenu, ContextMenuButton, MenuItemRadio} from "../../structures/ContextMenu";
|
import {
|
||||||
|
ChevronFace,
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuButton,
|
||||||
|
MenuItemRadio,
|
||||||
|
MenuItemCheckbox,
|
||||||
|
MenuItem,
|
||||||
|
} from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
import {
|
||||||
|
getRoomNotifsState,
|
||||||
|
setRoomNotifsState,
|
||||||
|
ALL_MESSAGES,
|
||||||
|
ALL_MESSAGES_LOUD,
|
||||||
|
MENTIONS_ONLY,
|
||||||
|
MUTE,
|
||||||
|
} from "../../../RoomNotifs";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { setRoomNotifsState } from "../../../RoomNotifs";
|
|
||||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import { Volume } from "../../../RoomNotifsTypes";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// 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
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -68,6 +82,8 @@ interface IState {
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
|
||||||
|
|
||||||
const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||||
// align the context menu's icons with the icon which opened the context menu
|
// align the context menu's icons with the icon which opened the context menu
|
||||||
const left = elementRect.left + window.pageXOffset - 9;
|
const left = elementRect.left + window.pageXOffset - 9;
|
||||||
|
@ -123,6 +139,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get showMessagePreview(): boolean {
|
||||||
|
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
@ -195,6 +215,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
// 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
|
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
||||||
|
|
||||||
|
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
this.setState({generalMenuPosition: null}); // hide the menu
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
||||||
|
@ -219,11 +244,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({generalMenuPosition: null}); // hide the menu
|
this.setState({generalMenuPosition: null}); // hide the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
|
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (MatrixClientPeg.get().isGuest()) return;
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
|
// get key before we go async and React discards the nativeEvent
|
||||||
|
const key = (ev as React.KeyboardEvent).key;
|
||||||
try {
|
try {
|
||||||
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
||||||
await setRoomNotifsState(this.props.room.roomId, newState);
|
await setRoomNotifsState(this.props.room.roomId, newState);
|
||||||
|
@ -233,7 +260,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({notificationsMenuPosition: null}); // Close the context menu
|
if (key === Key.ENTER) {
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
||||||
|
@ -241,7 +271,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
|
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
|
||||||
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
||||||
|
|
||||||
private renderNotificationsMenu(): React.ReactElement {
|
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||||
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
|
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
|
||||||
// the menu makes no sense in these cases so do not show one
|
// the menu makes no sense in these cases so do not show one
|
||||||
return null;
|
return null;
|
||||||
|
@ -287,7 +317,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const classes = classNames("mx_RoomTile2_notificationsButton", {
|
const classes = classNames("mx_RoomTile2_notificationsButton", {
|
||||||
// Show bell icon for the default case too.
|
// Show bell icon for the default case too.
|
||||||
mx_RoomTile2_iconBell: state === state === ALL_MESSAGES,
|
mx_RoomTile2_iconBell: state === ALL_MESSAGES,
|
||||||
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
|
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
|
||||||
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
|
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
|
||||||
mx_RoomTile2_iconBellCrossed: state === MUTE,
|
mx_RoomTile2_iconBellCrossed: state === MUTE,
|
||||||
|
@ -304,6 +334,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
onClick={this.onNotificationsMenuOpenClick}
|
onClick={this.onNotificationsMenuOpenClick}
|
||||||
label={_t("Notification options")}
|
label={_t("Notification options")}
|
||||||
isExpanded={!!this.state.notificationsMenuPosition}
|
isExpanded={!!this.state.notificationsMenuPosition}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -321,20 +352,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
||||||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
||||||
<div className="mx_IconizedContextMenu_optionList">
|
<div className="mx_IconizedContextMenu_optionList">
|
||||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
|
<MenuItemCheckbox
|
||||||
|
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||||
|
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
|
||||||
|
label={_t("Favourite")}
|
||||||
|
>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
||||||
</AccessibleButton>
|
</MenuItemCheckbox>
|
||||||
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
||||||
</AccessibleButton>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
|
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
|
||||||
<AccessibleButton onClick={this.onLeaveRoomClick}>
|
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
|
||||||
</AccessibleButton>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -374,8 +409,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let badge: React.ReactNode;
|
let badge: React.ReactNode;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized) {
|
||||||
|
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||||
badge = (
|
badge = (
|
||||||
<div className="mx_RoomTile2_badgeContainer">
|
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
notification={this.state.notificationState}
|
notification={this.state.notificationState}
|
||||||
forceCount={false}
|
forceCount={false}
|
||||||
|
@ -391,24 +427,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.props.showMessagePreview && !this.props.isMinimized) {
|
if (this.showMessagePreview) {
|
||||||
// The preview store heavily caches this info, so should be safe to hammer.
|
// The preview store heavily caches this info, so should be safe to hammer.
|
||||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||||
|
|
||||||
// Only show the preview if there is one to show.
|
// Only show the preview if there is one to show.
|
||||||
if (text) {
|
if (text) {
|
||||||
messagePreview = (
|
messagePreview = (
|
||||||
<div className="mx_RoomTile2_messagePreview">
|
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notificationColor = this.state.notificationState.color;
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
"mx_RoomTile2_name": true,
|
"mx_RoomTile2_name": true,
|
||||||
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
||||||
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
|
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nameContainer = (
|
let nameContainer = (
|
||||||
|
@ -421,6 +458,27 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) nameContainer = null;
|
if (this.props.isMinimized) nameContainer = null;
|
||||||
|
|
||||||
|
let ariaLabel = name;
|
||||||
|
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||||
|
if (this.props.tag === DefaultTagID.Invite) {
|
||||||
|
// append nothing
|
||||||
|
} else if (notificationColor >= NotificationColor.Red) {
|
||||||
|
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||||
|
count: this.state.notificationState.count,
|
||||||
|
});
|
||||||
|
} else if (notificationColor >= NotificationColor.Grey) {
|
||||||
|
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||||
|
count: this.state.notificationState.count,
|
||||||
|
});
|
||||||
|
} else if (notificationColor >= NotificationColor.Bold) {
|
||||||
|
ariaLabel += " " + _t("Unread messages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ariaDescribedBy: string;
|
||||||
|
if (this.showMessagePreview) {
|
||||||
|
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper>
|
||||||
|
@ -433,14 +491,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
onMouseEnter={this.onTileMouseEnter}
|
onMouseEnter={this.onTileMouseEnter}
|
||||||
onMouseLeave={this.onTileMouseLeave}
|
onMouseLeave={this.onTileMouseLeave}
|
||||||
onClick={this.onTileClick}
|
onClick={this.onTileClick}
|
||||||
role="treeitem"
|
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
|
role="treeitem"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-selected={this.state.selected}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
>
|
>
|
||||||
{roomAvatar}
|
{roomAvatar}
|
||||||
{nameContainer}
|
{nameContainer}
|
||||||
{badge}
|
{badge}
|
||||||
{this.renderNotificationsMenu()}
|
|
||||||
{this.renderGeneralMenu()}
|
{this.renderGeneralMenu()}
|
||||||
|
{this.renderNotificationsMenu(isActive)}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
}
|
}
|
||||||
</RovingTabIndexWrapper>
|
</RovingTabIndexWrapper>
|
||||||
|
|
|
@ -34,6 +34,7 @@ import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import Field from '../../../elements/Field';
|
import Field from '../../../elements/Field';
|
||||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -288,10 +289,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
}))}
|
}))}
|
||||||
onChange={this.onThemeChange}
|
onChange={this.onThemeChange}
|
||||||
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
||||||
|
outlined
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{customThemeForm}
|
{customThemeForm}
|
||||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} useCheckbox={true} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -344,7 +345,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
|
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
|
||||||
|
|
||||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButton">
|
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||||
|
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.useIRCLayout,
|
||||||
|
})}>
|
||||||
<EventTilePreview
|
<EventTilePreview
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
message={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
@ -360,7 +363,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
<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
|
<EventTilePreview
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
message={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ReactChild} from "react";
|
import React, {ReactNode} from "react";
|
||||||
|
|
||||||
import FormButton from "../elements/FormButton";
|
import FormButton from "../elements/FormButton";
|
||||||
import {XOR} from "../../../@types/common";
|
import {XOR} from "../../../@types/common";
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
description: ReactChild;
|
description: ReactNode;
|
||||||
acceptLabel: string;
|
acceptLabel: string;
|
||||||
|
|
||||||
onAccept();
|
onAccept();
|
||||||
|
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
const MatrixClientContext = createContext(undefined);
|
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||||
MatrixClientContext.displayName = "MatrixClientContext";
|
MatrixClientContext.displayName = "MatrixClientContext";
|
||||||
export default MatrixClientContext;
|
export default MatrixClientContext;
|
|
@ -15,6 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
|
@ -26,6 +29,56 @@ import {getAddressType} from "./UserAddress";
|
||||||
|
|
||||||
const E2EE_WK_KEY = "im.vector.riot.e2ee";
|
const E2EE_WK_KEY = "im.vector.riot.e2ee";
|
||||||
|
|
||||||
|
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
|
||||||
|
enum Visibility {
|
||||||
|
Public = "public",
|
||||||
|
Private = "private",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Preset {
|
||||||
|
PrivateChat = "private_chat",
|
||||||
|
TrustedPrivateChat = "trusted_private_chat",
|
||||||
|
PublicChat = "public_chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Invite3PID {
|
||||||
|
id_server: string;
|
||||||
|
id_access_token?: string; // this gets injected by the js-sdk
|
||||||
|
medium: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStateEvent {
|
||||||
|
type: string;
|
||||||
|
state_key?: string; // defaults to an empty string
|
||||||
|
content: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICreateOpts {
|
||||||
|
visibility?: Visibility;
|
||||||
|
room_alias_name?: string;
|
||||||
|
name?: string;
|
||||||
|
topic?: string;
|
||||||
|
invite?: string[];
|
||||||
|
invite_3pid?: Invite3PID[];
|
||||||
|
room_version?: string;
|
||||||
|
creation_content?: object;
|
||||||
|
initial_state?: IStateEvent[];
|
||||||
|
preset?: Preset;
|
||||||
|
is_direct?: boolean;
|
||||||
|
power_level_content_override?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
dmUserId?: string;
|
||||||
|
createOpts?: ICreateOpts;
|
||||||
|
spinner?: boolean;
|
||||||
|
guestAccess?: boolean;
|
||||||
|
encryption?: boolean;
|
||||||
|
inlineErrors?: boolean;
|
||||||
|
andView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new room, and switch to it.
|
* Create a new room, and switch to it.
|
||||||
*
|
*
|
||||||
|
@ -40,11 +93,12 @@ const E2EE_WK_KEY = "im.vector.riot.e2ee";
|
||||||
* Default: False
|
* Default: False
|
||||||
* @param {bool=} opts.inlineErrors True to raise errors off the promise instead of resolving to null.
|
* @param {bool=} opts.inlineErrors True to raise errors off the promise instead of resolving to null.
|
||||||
* Default: False
|
* Default: False
|
||||||
|
* @param {bool=} opts.andView True to dispatch an action to view the room once it has been created.
|
||||||
*
|
*
|
||||||
* @returns {Promise} which resolves to the room id, or null if the
|
* @returns {Promise} which resolves to the room id, or null if the
|
||||||
* action was aborted or failed.
|
* action was aborted or failed.
|
||||||
*/
|
*/
|
||||||
export default function createRoom(opts) {
|
export default function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
if (opts.spinner === undefined) opts.spinner = true;
|
if (opts.spinner === undefined) opts.spinner = true;
|
||||||
if (opts.guestAccess === undefined) opts.guestAccess = true;
|
if (opts.guestAccess === undefined) opts.guestAccess = true;
|
||||||
|
@ -59,12 +113,12 @@ export default function createRoom(opts) {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPreset = opts.dmUserId ? 'trusted_private_chat' : 'private_chat';
|
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
|
||||||
|
|
||||||
// set some defaults for the creation
|
// set some defaults for the creation
|
||||||
const createOpts = opts.createOpts || {};
|
const createOpts = opts.createOpts || {};
|
||||||
createOpts.preset = createOpts.preset || defaultPreset;
|
createOpts.preset = createOpts.preset || defaultPreset;
|
||||||
createOpts.visibility = createOpts.visibility || 'private';
|
createOpts.visibility = createOpts.visibility || Visibility.Private;
|
||||||
if (opts.dmUserId && createOpts.invite === undefined) {
|
if (opts.dmUserId && createOpts.invite === undefined) {
|
||||||
switch (getAddressType(opts.dmUserId)) {
|
switch (getAddressType(opts.dmUserId)) {
|
||||||
case 'mx-user-id':
|
case 'mx-user-id':
|
||||||
|
@ -166,7 +220,7 @@ export default function createRoom(opts) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findDMForUser(client, userId) {
|
export function findDMForUser(client: MatrixClient, userId: string): Room {
|
||||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||||
const rooms = roomIds.map(id => client.getRoom(id));
|
const rooms = roomIds.map(id => client.getRoom(id));
|
||||||
const suitableDMRooms = rooms.filter(r => {
|
const suitableDMRooms = rooms.filter(r => {
|
||||||
|
@ -189,7 +243,7 @@ export function findDMForUser(client, userId) {
|
||||||
* NOTE: this assumes you've just created the room and there's not been an opportunity
|
* NOTE: this assumes you've just created the room and there's not been an opportunity
|
||||||
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
|
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
|
||||||
*/
|
*/
|
||||||
export async function _waitForMember(client, roomId, userId, opts = { timeout: 1500 }) {
|
export async function _waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
|
||||||
const { timeout } = opts;
|
const { timeout } = opts;
|
||||||
let handler;
|
let handler;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -212,7 +266,7 @@ export async function _waitForMember(client, roomId, userId, opts = { timeout: 1
|
||||||
* Ensure that for every user in a room, there is at least one device that we
|
* Ensure that for every user in a room, there is at least one device that we
|
||||||
* can encrypt to.
|
* can encrypt to.
|
||||||
*/
|
*/
|
||||||
export async function canEncryptToAllUsers(client, userIds) {
|
export async function canEncryptToAllUsers(client: MatrixClient, userIds: string[]) {
|
||||||
const usersDeviceMap = await client.downloadKeys(userIds);
|
const usersDeviceMap = await client.downloadKeys(userIds);
|
||||||
// { "@user:host": { "DEVICE": {...}, ... }, ... }
|
// { "@user:host": { "DEVICE": {...}, ... }, ... }
|
||||||
return Object.values(usersDeviceMap).every((userDevices) =>
|
return Object.values(usersDeviceMap).every((userDevices) =>
|
||||||
|
@ -221,7 +275,7 @@ export async function canEncryptToAllUsers(client, userIds) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDMExists(client, userId) {
|
export async function ensureDMExists(client: MatrixClient, userId: string): Promise<string> {
|
||||||
const existingDMRoom = findDMForUser(client, userId);
|
const existingDMRoom = findDMForUser(client, userId);
|
||||||
let roomId;
|
let roomId;
|
||||||
if (existingDMRoom) {
|
if (existingDMRoom) {
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from './languageHandler.js';
|
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export const GroupMemberType = PropTypes.shape({
|
export const GroupMemberType = PropTypes.shape({
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -1200,14 +1200,16 @@
|
||||||
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
||||||
"Not now": "Not now",
|
"Not now": "Not now",
|
||||||
"Don't ask me again": "Don't ask me again",
|
"Don't ask me again": "Don't ask me again",
|
||||||
"Sort by": "Sort by",
|
|
||||||
"Activity": "Activity",
|
|
||||||
"A-Z": "A-Z",
|
|
||||||
"Unread rooms": "Unread rooms",
|
"Unread rooms": "Unread rooms",
|
||||||
"Always show first": "Always show first",
|
"Always show first": "Always show first",
|
||||||
"Show": "Show",
|
"Show": "Show",
|
||||||
"Message preview": "Message preview",
|
"Message preview": "Message preview",
|
||||||
|
"Sort by": "Sort by",
|
||||||
|
"Activity": "Activity",
|
||||||
|
"A-Z": "A-Z",
|
||||||
"List options": "List options",
|
"List options": "List options",
|
||||||
|
"Jump to first unread room.": "Jump to first unread room.",
|
||||||
|
"Jump to first invite.": "Jump to first invite.",
|
||||||
"Add room": "Add room",
|
"Add room": "Add room",
|
||||||
"Show %(count)s more|other": "Show %(count)s more",
|
"Show %(count)s more|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
|
@ -2089,6 +2091,8 @@
|
||||||
"Find a room…": "Find a room…",
|
"Find a room…": "Find a room…",
|
||||||
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
||||||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||||
|
"Clear filter": "Clear filter",
|
||||||
|
"Search rooms": "Search rooms",
|
||||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
|
@ -2100,8 +2104,6 @@
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"Active call": "Active call",
|
"Active call": "Active call",
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"Jump to first unread room.": "Jump to first unread room.",
|
|
||||||
"Jump to first invite.": "Jump to first invite.",
|
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
"Search failed": "Search failed",
|
"Search failed": "Search failed",
|
||||||
|
@ -2116,7 +2118,6 @@
|
||||||
"Click to mute video": "Click to mute video",
|
"Click to mute video": "Click to mute video",
|
||||||
"Click to unmute audio": "Click to unmute audio",
|
"Click to unmute audio": "Click to unmute audio",
|
||||||
"Click to mute audio": "Click to mute audio",
|
"Click to mute audio": "Click to mute audio",
|
||||||
"Clear filter": "Clear filter",
|
|
||||||
"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 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.",
|
"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",
|
"Failed to load timeline position": "Failed to load timeline position",
|
||||||
|
@ -2131,7 +2132,7 @@
|
||||||
"All settings": "All settings",
|
"All settings": "All settings",
|
||||||
"Archived rooms": "Archived rooms",
|
"Archived rooms": "Archived rooms",
|
||||||
"Feedback": "Feedback",
|
"Feedback": "Feedback",
|
||||||
"Account settings": "Account settings",
|
"User menu": "User menu",
|
||||||
"Could not load user profile": "Could not load user profile",
|
"Could not load user profile": "Could not load user profile",
|
||||||
"Verify this login": "Verify this login",
|
"Verify this login": "Verify this login",
|
||||||
"Session verified": "Session verified",
|
"Session verified": "Session verified",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 MTRNord and Cooperative EITA
|
Copyright 2017 MTRNord and Cooperative EITA
|
||||||
Copyright 2017 Vector Creations Ltd.
|
Copyright 2017 Vector Creations Ltd.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -20,10 +20,11 @@ limitations under the License.
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import counterpart from 'counterpart';
|
import counterpart from 'counterpart';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||||
import PlatformPeg from "./PlatformPeg";
|
import PlatformPeg from "./PlatformPeg";
|
||||||
|
|
||||||
// $webapp is a webpack resolve alias pointing to the output directory, see webpack config
|
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
|
||||||
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
|
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
|
||||||
|
|
||||||
const i18nFolder = 'i18n/';
|
const i18nFolder = 'i18n/';
|
||||||
|
@ -37,27 +38,31 @@ counterpart.setSeparator('|');
|
||||||
// Fall back to English
|
// Fall back to English
|
||||||
counterpart.setFallbackLocale('en');
|
counterpart.setFallbackLocale('en');
|
||||||
|
|
||||||
|
interface ITranslatableError extends Error {
|
||||||
|
translatedMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to create an error which has an English message
|
* Helper function to create an error which has an English message
|
||||||
* with a translatedMessage property for use by the consumer.
|
* with a translatedMessage property for use by the consumer.
|
||||||
* @param {string} message Message to translate.
|
* @param {string} message Message to translate.
|
||||||
* @returns {Error} The constructed error.
|
* @returns {Error} The constructed error.
|
||||||
*/
|
*/
|
||||||
export function newTranslatableError(message) {
|
export function newTranslatableError(message: string) {
|
||||||
const error = new Error(message);
|
const error = new Error(message) as ITranslatableError;
|
||||||
error.translatedMessage = _t(message);
|
error.translatedMessage = _t(message);
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function which only purpose is to mark that a string is translatable
|
// Function which only purpose is to mark that a string is translatable
|
||||||
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
||||||
export function _td(s) {
|
export function _td(s: string): string {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
|
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
|
||||||
// Takes the same arguments as counterpart.translate()
|
// Takes the same arguments as counterpart.translate()
|
||||||
function safeCounterpartTranslate(text, options) {
|
function safeCounterpartTranslate(text: string, options?: object) {
|
||||||
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
|
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
|
||||||
// The interpolation library that counterpart uses does not support undefined/null
|
// The interpolation library that counterpart uses does not support undefined/null
|
||||||
// values and instead will throw an error. This is a problem since everywhere else
|
// values and instead will throw an error. This is a problem since everywhere else
|
||||||
|
@ -89,6 +94,13 @@ function safeCounterpartTranslate(text, options) {
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IVariables {
|
||||||
|
count?: number;
|
||||||
|
[key: string]: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tags = Record<string, (sub: string) => React.ReactNode>;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
|
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
|
||||||
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
|
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
|
||||||
|
@ -105,7 +117,9 @@ function safeCounterpartTranslate(text, options) {
|
||||||
*
|
*
|
||||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||||
*/
|
*/
|
||||||
export function _t(text, variables, tags) {
|
export function _t(text: string, variables?: IVariables): string;
|
||||||
|
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
|
||||||
|
export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
|
||||||
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
|
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
|
||||||
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
|
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
|
||||||
// It is enough to pass the count variable, but in the future counterpart might make use of other information too
|
// It is enough to pass the count variable, but in the future counterpart might make use of other information too
|
||||||
|
@ -141,23 +155,25 @@ export function _t(text, variables, tags) {
|
||||||
*
|
*
|
||||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||||
*/
|
*/
|
||||||
export function substitute(text, variables, tags) {
|
export function substitute(text: string, variables?: IVariables): string;
|
||||||
let result = text;
|
export function substitute(text: string, variables: IVariables, tags: Tags): string;
|
||||||
|
export function substitute(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
|
||||||
|
let result: React.ReactNode | string = text;
|
||||||
|
|
||||||
if (variables !== undefined) {
|
if (variables !== undefined) {
|
||||||
const regexpMapping = {};
|
const regexpMapping: IVariables = {};
|
||||||
for (const variable in variables) {
|
for (const variable in variables) {
|
||||||
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
|
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
|
||||||
}
|
}
|
||||||
result = replaceByRegexes(result, regexpMapping);
|
result = replaceByRegexes(result as string, regexpMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags !== undefined) {
|
if (tags !== undefined) {
|
||||||
const regexpMapping = {};
|
const regexpMapping: Tags = {};
|
||||||
for (const tag in tags) {
|
for (const tag in tags) {
|
||||||
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
|
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
|
||||||
}
|
}
|
||||||
result = replaceByRegexes(result, regexpMapping);
|
result = replaceByRegexes(result as string, regexpMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -172,7 +188,9 @@ export function substitute(text, variables, tags) {
|
||||||
*
|
*
|
||||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||||
*/
|
*/
|
||||||
export function replaceByRegexes(text, mapping) {
|
export function replaceByRegexes(text: string, mapping: IVariables): string;
|
||||||
|
export function replaceByRegexes(text: string, mapping: Tags): React.ReactNode;
|
||||||
|
export function replaceByRegexes(text: string, mapping: IVariables | Tags): string | React.ReactNode {
|
||||||
// We initially store our output as an array of strings and objects (e.g. React components).
|
// We initially store our output as an array of strings and objects (e.g. React components).
|
||||||
// This will then be converted to a string or a <span> at the end
|
// This will then be converted to a string or a <span> at the end
|
||||||
const output = [text];
|
const output = [text];
|
||||||
|
@ -189,7 +207,7 @@ export function replaceByRegexes(text, mapping) {
|
||||||
// and everything after the match. Insert all three into the output. We need to do this because we can insert objects.
|
// and everything after the match. Insert all three into the output. We need to do this because we can insert objects.
|
||||||
// Otherwise there would be no need for the splitting and we could do simple replacement.
|
// Otherwise there would be no need for the splitting and we could do simple replacement.
|
||||||
let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it
|
let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it
|
||||||
for (const outputIndex in output) {
|
for (let outputIndex = 0; outputIndex < output.length; outputIndex++) {
|
||||||
const inputText = output[outputIndex];
|
const inputText = output[outputIndex];
|
||||||
if (typeof inputText !== 'string') { // We might have inserted objects earlier, don't try to replace them
|
if (typeof inputText !== 'string') { // We might have inserted objects earlier, don't try to replace them
|
||||||
continue;
|
continue;
|
||||||
|
@ -216,7 +234,7 @@ export function replaceByRegexes(text, mapping) {
|
||||||
let replaced;
|
let replaced;
|
||||||
// If substitution is a function, call it
|
// If substitution is a function, call it
|
||||||
if (mapping[regexpString] instanceof Function) {
|
if (mapping[regexpString] instanceof Function) {
|
||||||
replaced = mapping[regexpString].apply(null, capturedGroups);
|
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups);
|
||||||
} else {
|
} else {
|
||||||
replaced = mapping[regexpString];
|
replaced = mapping[regexpString];
|
||||||
}
|
}
|
||||||
|
@ -277,11 +295,11 @@ export function replaceByRegexes(text, mapping) {
|
||||||
// Allow overriding the text displayed when no translation exists
|
// Allow overriding the text displayed when no translation exists
|
||||||
// Currently only used in unit tests to avoid having to load
|
// Currently only used in unit tests to avoid having to load
|
||||||
// the translations in riot-web
|
// the translations in riot-web
|
||||||
export function setMissingEntryGenerator(f) {
|
export function setMissingEntryGenerator(f: (value: string) => void) {
|
||||||
counterpart.setMissingEntryGenerator(f);
|
counterpart.setMissingEntryGenerator(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLanguage(preferredLangs) {
|
export function setLanguage(preferredLangs: string | string[]) {
|
||||||
if (!Array.isArray(preferredLangs)) {
|
if (!Array.isArray(preferredLangs)) {
|
||||||
preferredLangs = [preferredLangs];
|
preferredLangs = [preferredLangs];
|
||||||
}
|
}
|
||||||
|
@ -358,8 +376,8 @@ export function getLanguageFromBrowser() {
|
||||||
* @param {string} language The input language string
|
* @param {string} language The input language string
|
||||||
* @return {string[]} List of normalised languages
|
* @return {string[]} List of normalised languages
|
||||||
*/
|
*/
|
||||||
export function getNormalizedLanguageKeys(language) {
|
export function getNormalizedLanguageKeys(language: string) {
|
||||||
const languageKeys = [];
|
const languageKeys: string[] = [];
|
||||||
const normalizedLanguage = normalizeLanguageKey(language);
|
const normalizedLanguage = normalizeLanguageKey(language);
|
||||||
const languageParts = normalizedLanguage.split('-');
|
const languageParts = normalizedLanguage.split('-');
|
||||||
if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
|
if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
|
||||||
|
@ -380,7 +398,7 @@ export function getNormalizedLanguageKeys(language) {
|
||||||
* @param {string} language The language string to be normalized
|
* @param {string} language The language string to be normalized
|
||||||
* @returns {string} The normalized language string
|
* @returns {string} The normalized language string
|
||||||
*/
|
*/
|
||||||
export function normalizeLanguageKey(language) {
|
export function normalizeLanguageKey(language: string) {
|
||||||
return language.toLowerCase().replace("_", "-");
|
return language.toLowerCase().replace("_", "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +414,7 @@ export function getCurrentLanguage() {
|
||||||
* @param {string[]} langs List of language codes to pick from
|
* @param {string[]} langs List of language codes to pick from
|
||||||
* @returns {string} The most appropriate language code from langs
|
* @returns {string} The most appropriate language code from langs
|
||||||
*/
|
*/
|
||||||
export function pickBestLanguage(langs) {
|
export function pickBestLanguage(langs: string[]): string {
|
||||||
const currentLang = getCurrentLanguage();
|
const currentLang = getCurrentLanguage();
|
||||||
const normalisedLangs = langs.map(normalizeLanguageKey);
|
const normalisedLangs = langs.map(normalizeLanguageKey);
|
||||||
|
|
||||||
|
@ -408,13 +426,13 @@ export function pickBestLanguage(langs) {
|
||||||
|
|
||||||
{
|
{
|
||||||
// Failing that, a different dialect of the same language
|
// Failing that, a different dialect of the same language
|
||||||
const closeLangIndex = normalisedLangs.find((l) => l.substr(0, 2) === currentLang.substr(0, 2));
|
const closeLangIndex = normalisedLangs.findIndex((l) => l.substr(0, 2) === currentLang.substr(0, 2));
|
||||||
if (closeLangIndex > -1) return langs[closeLangIndex];
|
if (closeLangIndex > -1) return langs[closeLangIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Neither of those? Try an english variant.
|
// Neither of those? Try an english variant.
|
||||||
const enIndex = normalisedLangs.find((l) => l.startsWith('en'));
|
const enIndex = normalisedLangs.findIndex((l) => l.startsWith('en'));
|
||||||
if (enIndex > -1) return langs[enIndex];
|
if (enIndex > -1) return langs[enIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,7 +440,7 @@ export function pickBestLanguage(langs) {
|
||||||
return langs[0];
|
return langs[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLangsJson() {
|
function getLangsJson(): Promise<object> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
let url;
|
let url;
|
||||||
if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
|
if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
|
||||||
|
@ -443,7 +461,7 @@ function getLangsJson() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function weblateToCounterpart(inTrs) {
|
function weblateToCounterpart(inTrs: object): object {
|
||||||
const outTrs = {};
|
const outTrs = {};
|
||||||
|
|
||||||
for (const key of Object.keys(inTrs)) {
|
for (const key of Object.keys(inTrs)) {
|
||||||
|
@ -463,7 +481,7 @@ function weblateToCounterpart(inTrs) {
|
||||||
return outTrs;
|
return outTrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLanguage(langPath) {
|
function getLanguage(langPath: string): object {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url: langPath },
|
{ method: "GET", url: langPath },
|
|
@ -202,7 +202,7 @@ export const SETTINGS = {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"useCompactLayout": {
|
"useCompactLayout": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
displayName: _td('Use a more compact ‘Modern’ layout'),
|
displayName: _td('Use a more compact ‘Modern’ layout'),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -85,8 +85,8 @@ export class ListLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get defaultVisibleTiles(): number {
|
public get defaultVisibleTiles(): number {
|
||||||
// 10 is what "feels right", and mostly subject to design's opinion.
|
// This number is what "feels right", and mostly subject to design's opinion.
|
||||||
return 10 + RESIZER_BOX_FACTOR;
|
return 5 + RESIZER_BOX_FACTOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setVisibleTilesWithin(diff: number, maxPossible: number) {
|
public setVisibleTilesWithin(diff: number, maxPossible: number) {
|
||||||
|
|
|
@ -101,6 +101,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
||||||
this.algorithm.stickyRoom = null;
|
this.algorithm.stickyRoom = null;
|
||||||
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Changing sticky room to ${activeRoomId}`);
|
||||||
this.algorithm.stickyRoom = activeRoom;
|
this.algorithm.stickyRoom = activeRoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,6 +299,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
||||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,6 +367,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAlgorithmListUpdated = () => {
|
private onAlgorithmListUpdated = () => {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log("Underlying algorithm has triggered a list update - refiring");
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -402,6 +408,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public addFilter(filter: IFilterCondition): void {
|
public addFilter(filter: IFilterCondition): void {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log("Adding filter condition:", filter);
|
||||||
this.filterConditions.push(filter);
|
this.filterConditions.push(filter);
|
||||||
if (this.algorithm) {
|
if (this.algorithm) {
|
||||||
this.algorithm.addFilterCondition(filter);
|
this.algorithm.addFilterCondition(filter);
|
||||||
|
@ -409,6 +417,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFilter(filter: IFilterCondition): void {
|
public removeFilter(filter: IFilterCondition): void {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log("Removing filter condition:", filter);
|
||||||
const idx = this.filterConditions.indexOf(filter);
|
const idx = this.filterConditions.indexOf(filter);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.filterConditions.splice(idx, 1);
|
this.filterConditions.splice(idx, 1);
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
||||||
import { getEnumValues } from "../../../utils/enums";
|
import { getEnumValues } from "../../../utils/enums";
|
||||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||||
import {
|
import {
|
||||||
|
@ -57,6 +57,7 @@ export class Algorithm extends EventEmitter {
|
||||||
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
|
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
|
||||||
private filteredRooms: ITagMap = {};
|
private filteredRooms: ITagMap = {};
|
||||||
private _stickyRoom: IStickyRoom = null;
|
private _stickyRoom: IStickyRoom = null;
|
||||||
|
private _lastStickyRoom: IStickyRoom = null; // only not-null when changing the sticky room
|
||||||
private sortAlgorithms: ITagSortingMap;
|
private sortAlgorithms: ITagSortingMap;
|
||||||
private listAlgorithms: IListOrderingMap;
|
private listAlgorithms: IListOrderingMap;
|
||||||
private algorithms: IOrderingAlgorithmMap;
|
private algorithms: IOrderingAlgorithmMap;
|
||||||
|
@ -162,9 +163,21 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateStickyRoom(val: Room) {
|
private async updateStickyRoom(val: Room) {
|
||||||
|
try {
|
||||||
|
return await this.doUpdateStickyRoom(val);
|
||||||
|
} finally {
|
||||||
|
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doUpdateStickyRoom(val: Room) {
|
||||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||||
// otherwise we risk duplicating rooms.
|
// otherwise we risk duplicating rooms.
|
||||||
|
|
||||||
|
// Set the last sticky room to indicate that we're in a change. The code throughout the
|
||||||
|
// class can safely handle a null room, so this should be safe to do as a backup.
|
||||||
|
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
|
||||||
|
|
||||||
// It's possible to have no selected room. In that case, clear the sticky room
|
// It's possible to have no selected room. In that case, clear the sticky room
|
||||||
if (!val) {
|
if (!val) {
|
||||||
if (this._stickyRoom) {
|
if (this._stickyRoom) {
|
||||||
|
@ -179,7 +192,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we do have a room though, we expect to be able to find it
|
// When we do have a room though, we expect to be able to find it
|
||||||
const tag = this.roomIdsToTags[val.roomId][0];
|
let tag = this.roomIdsToTags[val.roomId][0];
|
||||||
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
|
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
|
||||||
|
|
||||||
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
|
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
|
||||||
|
@ -196,19 +209,41 @@ export class Algorithm extends EventEmitter {
|
||||||
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
|
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
|
||||||
// a new update for ourselves.
|
// a new update for ourselves.
|
||||||
const lastStickyRoom = this._stickyRoom;
|
const lastStickyRoom = this._stickyRoom;
|
||||||
this._stickyRoom = null;
|
this._stickyRoom = null; // clear before we update the algorithm
|
||||||
this.recalculateStickyRoom();
|
this.recalculateStickyRoom();
|
||||||
|
|
||||||
// When we do have the room, re-add the old room (if needed) to the algorithm
|
// When we do have the room, re-add the old room (if needed) to the algorithm
|
||||||
// and remove the sticky room from the algorithm. This is so the underlying
|
// and remove the sticky room from the algorithm. This is so the underlying
|
||||||
// algorithm doesn't try and confuse itself with the sticky room concept.
|
// algorithm doesn't try and confuse itself with the sticky room concept.
|
||||||
if (lastStickyRoom) {
|
// We don't add the new room if the sticky room isn't changing because that's
|
||||||
|
// an easy way to cause duplication. We have to do room ID checks instead of
|
||||||
|
// referential checks as the references can differ through the lifecycle.
|
||||||
|
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
||||||
// Lie to the algorithm and re-add the room to the algorithm
|
// Lie to the algorithm and re-add the room to the algorithm
|
||||||
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
||||||
}
|
}
|
||||||
// Lie to the algorithm and remove the room from it's field of view
|
// Lie to the algorithm and remove the room from it's field of view
|
||||||
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
||||||
|
|
||||||
|
// Check for tag & position changes while we're here. We also check the room to ensure
|
||||||
|
// it is still the same room.
|
||||||
|
if (this._stickyRoom) {
|
||||||
|
if (this._stickyRoom.room !== val) {
|
||||||
|
// Check the room IDs just in case
|
||||||
|
if (this._stickyRoom.room.roomId === val.roomId) {
|
||||||
|
console.warn("Sticky room changed references");
|
||||||
|
} else {
|
||||||
|
throw new Error("Sticky room changed while the sticky room was changing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Sticky room changed tag & position from ${tag} / ${position} `
|
||||||
|
+ `to ${this._stickyRoom.tag} / ${this._stickyRoom.position}`);
|
||||||
|
|
||||||
|
tag = this._stickyRoom.tag;
|
||||||
|
position = this._stickyRoom.position;
|
||||||
|
}
|
||||||
|
|
||||||
// Now that we're done lying to the algorithm, we need to update our position
|
// Now that we're done lying to the algorithm, we need to update our position
|
||||||
// marker only if the user is moving further down the same list. If they're switching
|
// marker only if the user is moving further down the same list. If they're switching
|
||||||
// lists, or moving upwards, the position marker will splice in just fine but if
|
// lists, or moving upwards, the position marker will splice in just fine but if
|
||||||
|
@ -272,6 +307,9 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newMap[tagId] = allowedRoomsInThisTag;
|
newMap[tagId] = allowedRoomsInThisTag;
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
||||||
|
@ -307,6 +345,9 @@ export class Algorithm extends EventEmitter {
|
||||||
if (filteredRooms.length > 0) {
|
if (filteredRooms.length > 0) {
|
||||||
this.filteredRooms[tagId] = filteredRooms;
|
this.filteredRooms[tagId] = filteredRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
|
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
|
||||||
|
@ -345,6 +386,8 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._cachedStickyRooms || !updatedTag) {
|
if (!this._cachedStickyRooms || !updatedTag) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Generating clone of cached rooms for sticky room handling`);
|
||||||
const stickiedTagMap: ITagMap = {};
|
const stickiedTagMap: ITagMap = {};
|
||||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||||
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
|
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
|
||||||
|
@ -355,6 +398,8 @@ export class Algorithm extends EventEmitter {
|
||||||
if (updatedTag) {
|
if (updatedTag) {
|
||||||
// Update the tag indicated by the caller, if possible. This is mostly to ensure
|
// Update the tag indicated by the caller, if possible. This is mostly to ensure
|
||||||
// our cache is up to date.
|
// our cache is up to date.
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Replacing cached sticky rooms for ${updatedTag}`);
|
||||||
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
|
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +408,8 @@ export class Algorithm extends EventEmitter {
|
||||||
// we might have updated from the cache is also our sticky room.
|
// we might have updated from the cache is also our sticky room.
|
||||||
const sticky = this._stickyRoom;
|
const sticky = this._stickyRoom;
|
||||||
if (!updatedTag || updatedTag === sticky.tag) {
|
if (!updatedTag || updatedTag === sticky.tag) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
|
||||||
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
|
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,9 +501,13 @@ export class Algorithm extends EventEmitter {
|
||||||
// Split out the easy rooms first (leave and invite)
|
// Split out the easy rooms first (leave and invite)
|
||||||
const memberships = splitRoomsByMembership(rooms);
|
const memberships = splitRoomsByMembership(rooms);
|
||||||
for (const room of memberships[EffectiveMembership.Invite]) {
|
for (const room of memberships[EffectiveMembership.Invite]) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
|
||||||
newTags[DefaultTagID.Invite].push(room);
|
newTags[DefaultTagID.Invite].push(room);
|
||||||
}
|
}
|
||||||
for (const room of memberships[EffectiveMembership.Leave]) {
|
for (const room of memberships[EffectiveMembership.Leave]) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
|
||||||
newTags[DefaultTagID.Archived].push(room);
|
newTags[DefaultTagID.Archived].push(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,7 +518,11 @@ export class Algorithm extends EventEmitter {
|
||||||
let inTag = false;
|
let inTag = false;
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
|
||||||
if (!isNullOrUndefined(newTags[tag])) {
|
if (!isNullOrUndefined(newTags[tag])) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
|
||||||
newTags[tag].push(room);
|
newTags[tag].push(room);
|
||||||
inTag = true;
|
inTag = true;
|
||||||
}
|
}
|
||||||
|
@ -477,6 +532,9 @@ export class Algorithm extends EventEmitter {
|
||||||
if (!inTag) {
|
if (!inTag) {
|
||||||
// TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236
|
// TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236
|
||||||
newTags[DefaultTagID.Untagged].push(room);
|
newTags[DefaultTagID.Untagged].push(room);
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -537,7 +595,7 @@ export class Algorithm extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Updates the roomsToTags map
|
* Updates the roomsToTags map
|
||||||
*/
|
*/
|
||||||
protected updateTagsFromCache() {
|
private updateTagsFromCache() {
|
||||||
const newMap = {};
|
const newMap = {};
|
||||||
|
|
||||||
const tags = Object.keys(this.cachedRooms);
|
const tags = Object.keys(this.cachedRooms);
|
||||||
|
@ -584,21 +642,94 @@ export class Algorithm extends EventEmitter {
|
||||||
* processing.
|
* processing.
|
||||||
*/
|
*/
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||||
|
|
||||||
|
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||||
|
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
||||||
if (cause === RoomUpdateCause.NewRoom) {
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
|
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||||
const roomTags = this.roomIdsToTags[room.roomId];
|
const roomTags = this.roomIdsToTags[room.roomId];
|
||||||
if (roomTags && roomTags.length > 0) {
|
const hasTags = roomTags && roomTags.length > 0;
|
||||||
|
|
||||||
|
// Don't change the cause if the last sticky room is being re-added. If we fail to
|
||||||
|
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
|
||||||
|
// lose the room.
|
||||||
|
if (hasTags && !isForLastSticky) {
|
||||||
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
|
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
|
||||||
cause = RoomUpdateCause.PossibleTagChange;
|
cause = RoomUpdateCause.PossibleTagChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have tags for a room and don't have the room referenced, the room reference
|
||||||
|
// probably changed. We need to swap out the problematic reference.
|
||||||
|
if (hasTags && !this.rooms.includes(room) && !isSticky) {
|
||||||
|
console.warn(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
||||||
|
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (!this.rooms.includes(room)) {
|
||||||
|
throw new Error(`Failed to replace ${room.roomId} with an updated reference`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like above, update the reference to the sticky room if we need to
|
||||||
|
if (hasTags && isSticky) {
|
||||||
|
// Go directly in and set the sticky room's new reference, being careful not
|
||||||
|
// to trigger a sticky room update ourselves.
|
||||||
|
this._stickyRoom.room = room;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cause === RoomUpdateCause.PossibleTagChange) {
|
if (cause === RoomUpdateCause.PossibleTagChange) {
|
||||||
// TODO: Be smarter and splice rather than regen the planet. https://github.com/vector-im/riot-web/issues/14035
|
let didTagChange = false;
|
||||||
// TODO: No-op if no change. https://github.com/vector-im/riot-web/issues/14035
|
const oldTags = this.roomIdsToTags[room.roomId] || [];
|
||||||
await this.setKnownRooms(this.rooms);
|
const newTags = this.getTagsForRoom(room);
|
||||||
return true;
|
const diff = arrayDiff(oldTags, newTags);
|
||||||
|
if (diff.removed.length > 0 || diff.added.length > 0) {
|
||||||
|
for (const rmTag of diff.removed) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Removing ${room.roomId} from ${rmTag}`);
|
||||||
|
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||||
|
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||||
|
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||||
|
}
|
||||||
|
for (const addTag of diff.added) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Adding ${room.roomId} to ${addTag}`);
|
||||||
|
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||||
|
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||||
|
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the tag map so we don't regen it in a moment
|
||||||
|
this.roomIdsToTags[room.roomId] = newTags;
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
|
||||||
|
cause = RoomUpdateCause.Timeline;
|
||||||
|
didTagChange = true;
|
||||||
|
} else {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.warn(`Received no-op update for ${room.roomId} - changing to Timeline update`);
|
||||||
|
cause = RoomUpdateCause.Timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didTagChange && isSticky) {
|
||||||
|
// Manually update the tag for the sticky room without triggering a sticky room
|
||||||
|
// update. The update will be handled implicitly by the sticky room handling and
|
||||||
|
// requires no changes on our part, if we're in the middle of a sticky room change.
|
||||||
|
if (this._lastStickyRoom) {
|
||||||
|
this._stickyRoom = {
|
||||||
|
room,
|
||||||
|
tag: this.roomIdsToTags[room.roomId][0],
|
||||||
|
position: 0, // right at the top as it changed tags
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// We have to clear the lock as the sticky room change will trigger updates.
|
||||||
|
await this.setStickyRoomAsync(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the update is for a room change which might be the sticky room, prevent it. We
|
// If the update is for a room change which might be the sticky room, prevent it. We
|
||||||
|
@ -612,8 +743,9 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cause === RoomUpdateCause.NewRoom && !this.roomIdsToTags[room.roomId]) {
|
if (!this.roomIdsToTags[room.roomId]) {
|
||||||
console.log(`[RoomListDebug] Updating tags for new room ${room.roomId} (${room.name})`);
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
|
||||||
|
|
||||||
// Get the tags for the room and populate the cache
|
// Get the tags for the room and populate the cache
|
||||||
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
|
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
|
||||||
|
@ -623,9 +755,15 @@ export class Algorithm extends EventEmitter {
|
||||||
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
|
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
|
||||||
|
|
||||||
this.roomIdsToTags[room.roomId] = roomTags;
|
this.roomIdsToTags[room.roomId] = roomTags;
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags = this.roomIdsToTags[room.roomId];
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
|
||||||
|
|
||||||
|
const tags = this.roomIdsToTags[room.roomId];
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||||
return false;
|
return false;
|
||||||
|
@ -645,6 +783,8 @@ export class Algorithm extends EventEmitter {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,9 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
|
|
||||||
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||||
super(tagId, initialSortingAlgorithm);
|
super(tagId, initialSortingAlgorithm);
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSMethodCanBeStatic
|
// noinspection JSMethodCanBeStatic
|
||||||
|
|
|
@ -28,6 +28,9 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
|
||||||
|
|
||||||
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||||
super(tagId, initialSortingAlgorithm);
|
super(tagId, initialSortingAlgorithm);
|
||||||
|
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRooms(rooms: Room[]): Promise<any> {
|
public async setRooms(rooms: Room[]): Promise<any> {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { TagID } from "../../models";
|
||||||
import { IAlgorithm } from "./IAlgorithm";
|
import { IAlgorithm } from "./IAlgorithm";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import * as Unread from "../../../../Unread";
|
import * as Unread from "../../../../Unread";
|
||||||
|
import { EffectiveMembership, getEffectiveMembership } from "../../membership";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts rooms according to the last event's timestamp in each room that seems
|
* Sorts rooms according to the last event's timestamp in each room that seems
|
||||||
|
@ -37,6 +38,8 @@ export class RecentAlgorithm implements IAlgorithm {
|
||||||
// actually changed (probably needs to be done higher up?) then we could do an
|
// actually changed (probably needs to be done higher up?) then we could do an
|
||||||
// insertion sort or similar on the limited set of changes.
|
// insertion sort or similar on the limited set of changes.
|
||||||
|
|
||||||
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
const tsCache: { [roomId: string]: number } = {};
|
const tsCache: { [roomId: string]: number } = {};
|
||||||
const getLastTs = (r: Room) => {
|
const getLastTs = (r: Room) => {
|
||||||
if (tsCache[r.roomId]) {
|
if (tsCache[r.roomId]) {
|
||||||
|
@ -50,13 +53,23 @@ export class RecentAlgorithm implements IAlgorithm {
|
||||||
return Number.MAX_SAFE_INTEGER;
|
return Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the room hasn't been joined yet, it probably won't have a timeline to
|
||||||
|
// parse. We'll still fall back to the timeline if this fails, but chances
|
||||||
|
// are we'll at least have our own membership event to go off of.
|
||||||
|
const effectiveMembership = getEffectiveMembership(r.getMyMembership());
|
||||||
|
if (effectiveMembership !== EffectiveMembership.Join) {
|
||||||
|
const membershipEvent = r.currentState.getStateEvents("m.room.member", myUserId);
|
||||||
|
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
||||||
|
return membershipEvent.getTs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
||||||
const ev = r.timeline[i];
|
const ev = r.timeline[i];
|
||||||
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
|
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
|
||||||
|
|
||||||
// TODO: Don't assume we're using the same client as the peg
|
// TODO: Don't assume we're using the same client as the peg
|
||||||
if (ev.getSender() === MatrixClientPeg.get().getUserId()
|
if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) {
|
||||||
|| Unread.eventTriggersUnreadCount(ev)) {
|
|
||||||
return ev.getTs();
|
return ev.getTs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,8 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
|
||||||
const beforeRoomIds = this.roomIds;
|
const beforeRoomIds = this.roomIds;
|
||||||
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
||||||
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log("Updating filter for group: ", this.community.groupId);
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,6 +41,8 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
|
|
||||||
public set search(val: string) {
|
public set search(val: string) {
|
||||||
this._search = val;
|
this._search = val;
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
|
console.log("Updating filter for room name search:", this._search);
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Returns a promise which resolves with a given value after the given number of ms
|
// Returns a promise which resolves with a given value after the given number of ms
|
||||||
export function sleep<T>(ms: number, value: T): Promise<T> {
|
export function sleep<T>(ms: number, value?: T): Promise<T> {
|
||||||
return new Promise((resolve => { setTimeout(resolve, ms, value); }));
|
return new Promise((resolve => { setTimeout(resolve, ms, value); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1257,6 +1257,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
|
||||||
integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
|
integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
|
||||||
|
|
||||||
|
"@types/counterpart@^0.18.1":
|
||||||
|
version "0.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
|
||||||
|
integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
|
||||||
|
|
||||||
"@types/fbemitter@*":
|
"@types/fbemitter@*":
|
||||||
version "2.0.32"
|
version "2.0.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"
|
resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"
|
||||||
|
@ -5821,8 +5826,8 @@ mathml-tag-names@^2.0.1:
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "7.0.0"
|
version "7.1.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f683f4544aa5da150836b01c754062809119fa97"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2a688bdac828dc62916437d83c72cef1e525d5f9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.8.3"
|
"@babel/runtime" "^7.8.3"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
|
Loading…
Reference in a new issue