Merge remote-tracking branch 'origin/develop' into dbkr/recovery_keys_over_passphrases
commit
b82a893a79
res
css
structures
views
fonts/Twemoji_Mozilla
img
src
@types
autocomplete
components
views
elements
emojipicker
right_panel
settings
i18n/strings
integrations
stores/room-list
widgets
195
CHANGELOG.md
195
CHANGELOG.md
|
@ -1,3 +1,198 @@
|
|||
Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1)
|
||||
|
||||
* Upgrade to JS SDK 6.2.1
|
||||
* Fix exceptions from Tooltip
|
||||
[\#4716](https://github.com/matrix-org/matrix-react-sdk/pull/4716)
|
||||
* Fix not being able to dismiss new login toasts
|
||||
[\#4715](https://github.com/matrix-org/matrix-react-sdk/pull/4715)
|
||||
* Fix compact layout regression
|
||||
[\#4714](https://github.com/matrix-org/matrix-react-sdk/pull/4714)
|
||||
|
||||
Changes in [2.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0) (2020-06-04)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.2...v2.7.0)
|
||||
|
||||
* Upgrade to JS SDK 6.2.0
|
||||
* Prevent (double) 4S bootstrap from RestoreKeyBackupDialog
|
||||
[\#4703](https://github.com/matrix-org/matrix-react-sdk/pull/4703)
|
||||
* Fix checkbox bleed
|
||||
[\#4702](https://github.com/matrix-org/matrix-react-sdk/pull/4702)
|
||||
* Fix login loop where the sso flow returns to `#/login` to release
|
||||
[\#4693](https://github.com/matrix-org/matrix-react-sdk/pull/4693)
|
||||
|
||||
Changes in [2.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.2) (2020-06-02)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.1...v2.7.0-rc.2)
|
||||
|
||||
* Rewire the Sticker button to be an Emoji Picker
|
||||
[\#3747](https://github.com/matrix-org/matrix-react-sdk/pull/3747)
|
||||
|
||||
Changes in [2.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.1) (2020-06-02)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.1...v2.7.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 6.2.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#4683](https://github.com/matrix-org/matrix-react-sdk/pull/4683)
|
||||
* Make auth argument in the register request compliant with r0.6.0
|
||||
[\#4347](https://github.com/matrix-org/matrix-react-sdk/pull/4347)
|
||||
* Revert "Prevent PersistedElements overflowing scrolled areas"
|
||||
[\#4682](https://github.com/matrix-org/matrix-react-sdk/pull/4682)
|
||||
* Remove unused TagPanelButtons
|
||||
[\#4680](https://github.com/matrix-org/matrix-react-sdk/pull/4680)
|
||||
* Pass roomId to IRCTimelineProfileResizer
|
||||
[\#4679](https://github.com/matrix-org/matrix-react-sdk/pull/4679)
|
||||
* Remove logging to console for irc name resize
|
||||
[\#4678](https://github.com/matrix-org/matrix-react-sdk/pull/4678)
|
||||
* Use arrow functions instead of binding `this`
|
||||
[\#4677](https://github.com/matrix-org/matrix-react-sdk/pull/4677)
|
||||
* Increase specificity of compact layout selectors
|
||||
[\#4675](https://github.com/matrix-org/matrix-react-sdk/pull/4675)
|
||||
* Create and use stylised checkboxes
|
||||
[\#4665](https://github.com/matrix-org/matrix-react-sdk/pull/4665)
|
||||
* useIRCLayout moved to props
|
||||
[\#4676](https://github.com/matrix-org/matrix-react-sdk/pull/4676)
|
||||
* Fix paste image to upload
|
||||
[\#4674](https://github.com/matrix-org/matrix-react-sdk/pull/4674)
|
||||
* Fix FilePanel and NotificationsPanel regression
|
||||
[\#4647](https://github.com/matrix-org/matrix-react-sdk/pull/4647)
|
||||
* Allow deferring of Update Toast until the next morning
|
||||
[\#4669](https://github.com/matrix-org/matrix-react-sdk/pull/4669)
|
||||
* Give contextual feedback for manual update check instead of banner
|
||||
[\#4668](https://github.com/matrix-org/matrix-react-sdk/pull/4668)
|
||||
* Dialog wrap title instead of taking same space as the close/cancel button
|
||||
[\#4659](https://github.com/matrix-org/matrix-react-sdk/pull/4659)
|
||||
* Update Modular hosting link
|
||||
[\#4627](https://github.com/matrix-org/matrix-react-sdk/pull/4627)
|
||||
* Fix field placeholder regression
|
||||
[\#4663](https://github.com/matrix-org/matrix-react-sdk/pull/4663)
|
||||
* Fix/document a number of UIA oddities
|
||||
[\#4667](https://github.com/matrix-org/matrix-react-sdk/pull/4667)
|
||||
* Stop copy icon repeating weirdly
|
||||
[\#4662](https://github.com/matrix-org/matrix-react-sdk/pull/4662)
|
||||
* Try and fix the Notifier race
|
||||
[\#4661](https://github.com/matrix-org/matrix-react-sdk/pull/4661)
|
||||
* set the client's pickle key if the platform can store one
|
||||
[\#4657](https://github.com/matrix-org/matrix-react-sdk/pull/4657)
|
||||
* Migrate Banners to Toasts
|
||||
[\#4624](https://github.com/matrix-org/matrix-react-sdk/pull/4624)
|
||||
* Move Appearance tab to ts
|
||||
[\#4658](https://github.com/matrix-org/matrix-react-sdk/pull/4658)
|
||||
* Fix room alias lookup vs peeking race condition
|
||||
[\#4606](https://github.com/matrix-org/matrix-react-sdk/pull/4606)
|
||||
* Fix encryption icon miss-alignment
|
||||
[\#4651](https://github.com/matrix-org/matrix-react-sdk/pull/4651)
|
||||
* Fix sublist sizing regression
|
||||
[\#4649](https://github.com/matrix-org/matrix-react-sdk/pull/4649)
|
||||
* Fix lines overflowing room list width
|
||||
[\#4650](https://github.com/matrix-org/matrix-react-sdk/pull/4650)
|
||||
* Remove the keyshare dialog
|
||||
[\#4648](https://github.com/matrix-org/matrix-react-sdk/pull/4648)
|
||||
* Update badge counts in new room list as needed
|
||||
[\#4654](https://github.com/matrix-org/matrix-react-sdk/pull/4654)
|
||||
* EventIndex: Handle invalid m.room.redaction events correctly.
|
||||
[\#4653](https://github.com/matrix-org/matrix-react-sdk/pull/4653)
|
||||
* EventIndex: Print out the checkpoint if there was an error during a crawl
|
||||
[\#4652](https://github.com/matrix-org/matrix-react-sdk/pull/4652)
|
||||
* Move Field to Typescript
|
||||
[\#4635](https://github.com/matrix-org/matrix-react-sdk/pull/4635)
|
||||
* Use connection error to detect network problem
|
||||
[\#4646](https://github.com/matrix-org/matrix-react-sdk/pull/4646)
|
||||
* Revert default font size to 15px
|
||||
[\#4641](https://github.com/matrix-org/matrix-react-sdk/pull/4641)
|
||||
* Add logging when room join fails
|
||||
[\#4645](https://github.com/matrix-org/matrix-react-sdk/pull/4645)
|
||||
* Remove EncryptedEventDialog
|
||||
[\#4644](https://github.com/matrix-org/matrix-react-sdk/pull/4644)
|
||||
* Migrate Toasts to Typescript and to granular priority system
|
||||
[\#4618](https://github.com/matrix-org/matrix-react-sdk/pull/4618)
|
||||
* Update Crypto Store Too New copy
|
||||
[\#4632](https://github.com/matrix-org/matrix-react-sdk/pull/4632)
|
||||
* MemberAvatar should not have its own letter fallback, it should use
|
||||
BaseAvatar
|
||||
[\#4643](https://github.com/matrix-org/matrix-react-sdk/pull/4643)
|
||||
* Fix media upload issues with abort and status bar
|
||||
[\#4630](https://github.com/matrix-org/matrix-react-sdk/pull/4630)
|
||||
* fix viewGroup to actually show the group if possible
|
||||
[\#4633](https://github.com/matrix-org/matrix-react-sdk/pull/4633)
|
||||
* Update confirm passphrase copy
|
||||
[\#4634](https://github.com/matrix-org/matrix-react-sdk/pull/4634)
|
||||
* Improve accessibility of the emoji picker
|
||||
[\#4636](https://github.com/matrix-org/matrix-react-sdk/pull/4636)
|
||||
* Fix Emoji Picker footer being too small if text overflows
|
||||
[\#4631](https://github.com/matrix-org/matrix-react-sdk/pull/4631)
|
||||
* Improve style of toasts to match Figma
|
||||
[\#4613](https://github.com/matrix-org/matrix-react-sdk/pull/4613)
|
||||
* Iterate toast count indicator more logically
|
||||
[\#4620](https://github.com/matrix-org/matrix-react-sdk/pull/4620)
|
||||
* Fix reacting to redactions
|
||||
[\#4626](https://github.com/matrix-org/matrix-react-sdk/pull/4626)
|
||||
* Fix sentMessageAndIsAlone by dispatching `message_sent` more consistently
|
||||
[\#4628](https://github.com/matrix-org/matrix-react-sdk/pull/4628)
|
||||
* Update from Weblate
|
||||
[\#4640](https://github.com/matrix-org/matrix-react-sdk/pull/4640)
|
||||
* Replace `alias` with `address` in copy for consistency
|
||||
[\#4402](https://github.com/matrix-org/matrix-react-sdk/pull/4402)
|
||||
* Convert MatrixClientPeg to TypeScript
|
||||
[\#4638](https://github.com/matrix-org/matrix-react-sdk/pull/4638)
|
||||
* Fix BaseAvatar wrongly retrying urls
|
||||
[\#4629](https://github.com/matrix-org/matrix-react-sdk/pull/4629)
|
||||
* Fix event highlights not being updated to reflect edits
|
||||
[\#4637](https://github.com/matrix-org/matrix-react-sdk/pull/4637)
|
||||
* Calculate badges in the new room list more reliably
|
||||
[\#4625](https://github.com/matrix-org/matrix-react-sdk/pull/4625)
|
||||
* Transition BaseAvatar to hooks
|
||||
[\#4101](https://github.com/matrix-org/matrix-react-sdk/pull/4101)
|
||||
* Convert BasePlatform and BaseEventIndexManager to Typescript
|
||||
[\#4614](https://github.com/matrix-org/matrix-react-sdk/pull/4614)
|
||||
* Fix: Tag_DM is not defined
|
||||
[\#4619](https://github.com/matrix-org/matrix-react-sdk/pull/4619)
|
||||
* Fix visibility of message timestamps
|
||||
[\#4615](https://github.com/matrix-org/matrix-react-sdk/pull/4615)
|
||||
* Rewrite the room list store
|
||||
[\#4253](https://github.com/matrix-org/matrix-react-sdk/pull/4253)
|
||||
* Update code style to mention switch statements
|
||||
[\#4610](https://github.com/matrix-org/matrix-react-sdk/pull/4610)
|
||||
* Fix key backup restore with SSSS
|
||||
[\#4612](https://github.com/matrix-org/matrix-react-sdk/pull/4612)
|
||||
* Handle null tokens in the crawler loop.
|
||||
[\#4608](https://github.com/matrix-org/matrix-react-sdk/pull/4608)
|
||||
* Font scaling settings and slider
|
||||
[\#4424](https://github.com/matrix-org/matrix-react-sdk/pull/4424)
|
||||
* Prevent PersistedElements overflowing scrolled areas
|
||||
[\#4494](https://github.com/matrix-org/matrix-react-sdk/pull/4494)
|
||||
* IRC ui layout
|
||||
[\#4531](https://github.com/matrix-org/matrix-react-sdk/pull/4531)
|
||||
* Remove SSSS key upgrade check from rageshake
|
||||
[\#4607](https://github.com/matrix-org/matrix-react-sdk/pull/4607)
|
||||
* Label the create room button better than "Add room"
|
||||
[\#4603](https://github.com/matrix-org/matrix-react-sdk/pull/4603)
|
||||
* Convert the dispatcher to TypeScript
|
||||
[\#4593](https://github.com/matrix-org/matrix-react-sdk/pull/4593)
|
||||
* Consolidate password/passphrase fields into a component & add dynamic colour
|
||||
to progress
|
||||
[\#4599](https://github.com/matrix-org/matrix-react-sdk/pull/4599)
|
||||
* UserView, show Welcome page in the mid panel instead of empty space
|
||||
[\#4590](https://github.com/matrix-org/matrix-react-sdk/pull/4590)
|
||||
* Update from Weblate
|
||||
[\#4601](https://github.com/matrix-org/matrix-react-sdk/pull/4601)
|
||||
* Make email auth component fail better if server claims email isn't validated
|
||||
[\#4600](https://github.com/matrix-org/matrix-react-sdk/pull/4600)
|
||||
* Add new keyboard shortcuts for jump to unread and upload file
|
||||
[\#4588](https://github.com/matrix-org/matrix-react-sdk/pull/4588)
|
||||
* accept and linkify local domains like those from mDNS
|
||||
[\#4594](https://github.com/matrix-org/matrix-react-sdk/pull/4594)
|
||||
* Revert "ImageView make clicking off it easier"
|
||||
[\#4586](https://github.com/matrix-org/matrix-react-sdk/pull/4586)
|
||||
* wrap node-qrcode in a React FC and use it for ShareDialog
|
||||
[\#4394](https://github.com/matrix-org/matrix-react-sdk/pull/4394)
|
||||
* Pass screenAfterLogin through SSO in the callback url
|
||||
[\#4585](https://github.com/matrix-org/matrix-react-sdk/pull/4585)
|
||||
* Remove debugging that causes email addresses to load forever
|
||||
[\#4597](https://github.com/matrix-org/matrix-react-sdk/pull/4597)
|
||||
|
||||
Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -164,7 +164,9 @@
|
|||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
"setupFiles": ["jest-canvas-mock"],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
/* LeftPanel 260px */
|
||||
min-width: 260px;
|
||||
max-width: 50%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
min-width: 264px;
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -67,22 +68,27 @@ limitations under the License.
|
|||
|
||||
.mx_RightPanel_membersButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/user.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_filesButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/files.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_notifsButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/notifications.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_groupMembersButton::before {
|
||||
mask-image: url('$(res)/img/icons-people.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_roomsButton::before {
|
||||
mask-image: url('$(res)/img/icons-room-nobg.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_headerButton_highlight::after {
|
||||
|
|
|
@ -23,6 +23,7 @@ limitations under the License.
|
|||
border-radius: 3px;
|
||||
border: solid 1px $accent-color;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mx_AddressSelector.mx_AddressSelector_empty {
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
$left-gutter: 65px;
|
||||
|
||||
.mx_GroupLayout {
|
||||
|
||||
.mx_EventTile {
|
||||
> .mx_SenderProfile {
|
||||
line-height: $font-17px;
|
||||
|
@ -53,14 +52,14 @@ $left-gutter: 65px;
|
|||
/* Compact layout overrides */
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.mx_EventTile {
|
||||
padding-top: 4px;
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.mx_EventTile_info {
|
||||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||
padding-top: 0px;
|
||||
|
|
|
@ -63,4 +63,25 @@ limitations under the License.
|
|||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_warning {
|
||||
color: $notice-primary-color;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
margin-top: 30px;
|
||||
|
||||
&::before {
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: $font-24px;
|
||||
position: absolute;
|
||||
width: $font-24px;
|
||||
height: $font-24px;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $notice-primary-color;
|
||||
mask-image: url('$(res)/img/feather-customised/alert-triangle.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.29 3.86002L1.82002 18C1.46466 18.6154 1.46254 19.3732 1.81445 19.9905C2.16635 20.6079 2.81943 20.9922 3.53002 21H20.47C21.1806 20.9922 21.8337 20.6079 22.1856 19.9905C22.5375 19.3732 22.5354 18.6154 22.18 18L13.71 3.86002C13.3475 3.2623 12.6991 2.89728 12 2.89728C11.3009 2.89728 10.6526 3.2623 10.29 3.86002Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 9V13" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="17" r="1" fill="#2E2F32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 665 B |
BIN
res/img/file.png
BIN
res/img/file.png
Binary file not shown.
Before Width: | Height: | Size: 482 B |
Binary file not shown.
Before Width: | Height: | Size: 503 B |
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="17px" height="22px" viewBox="0 0 17 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: bin/sketchtool 1.4 (305) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icons_browse_files</title>
|
||||
<desc>Created with bin/sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="02-Chat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="02_13-Chat-member-profile" sketch:type="MSArtboardGroup" transform="translate(-1025.000000, -33.000000)">
|
||||
<g id="icons_browse_files" sketch:type="MSLayerGroup" transform="translate(1025.000000, 32.000000)">
|
||||
<g id="Rectangle-5-+-Rectangle-6-Copy" transform="translate(0.000000, 1.000000)" sketch:type="MSShapeGroup">
|
||||
<path d="M0,4.00955791 C0,1.79514022 1.78163126,0 3.99825563,0 L9.59161955,0 C9.59161955,0 16.3225806,6.49234232 16.3225806,6.49234232 L16.3225806,18.0063928 C16.3225806,20.2120012 14.5290874,22 12.3296282,22 L3.99295243,22 C1.7877057,22 0,20.1996477 0,17.9904421 L0,4.00955791 Z" id="Rectangle-5" stroke="#76CFA6"></path>
|
||||
<path d="M15.6804916,7.49527496 L11.5273266,7.49527496 C10.3308881,7.49527496 9.3609831,6.52527676 9.3609831,5.3289315 L9.3609831,1.88544393 L15.6804916,7.49527496 Z" id="Rectangle-6-Copy" fill="#FFFFFF"></path>
|
||||
<path d="M16.3225806,7.09677419 L11.4129801,7.09677419 C10.2050375,7.09677419 9.22580645,6.11744908 9.22580645,4.90960051 L9.22580645,0" id="Rectangle-6" stroke="#76CFA6"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -19,6 +19,7 @@ import ContentMessages from "../ContentMessages";
|
|||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import DeviceListener from "../DeviceListener";
|
||||
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -31,6 +32,7 @@ declare global {
|
|||
mx_ContentMessages: ContentMessages;
|
||||
mx_ToastStore: ToastStore;
|
||||
mx_DeviceListener: DeviceListener;
|
||||
mx_RoomListStore2: RoomListStore2;
|
||||
}
|
||||
|
||||
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
||||
|
|
|
@ -25,6 +25,9 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
|||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
|
||||
export const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
export const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
Error = "ERROR",
|
||||
|
@ -47,6 +50,7 @@ export default abstract class BasePlatform {
|
|||
|
||||
constructor() {
|
||||
dis.register(this.onAction);
|
||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||
}
|
||||
|
||||
protected onAction = (payload: ActionPayload) => {
|
||||
|
@ -217,11 +221,9 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL {
|
||||
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
url.searchParams.set("homeserver", hsUrl);
|
||||
url.searchParams.set("identityServer", isUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
@ -232,8 +234,12 @@ export default abstract class BasePlatform {
|
|||
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
|
||||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
|
||||
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(),
|
||||
fragmentAfterLogin);
|
||||
// persist hs url and is url for when the user is returned to the app with the login token
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
}
|
||||
|
||||
|
|
|
@ -22,13 +22,13 @@ import {
|
|||
import {
|
||||
hideToast as hideSetupEncryptionToast,
|
||||
Kind as SetupKind,
|
||||
Kind,
|
||||
showToast as showSetupEncryptionToast
|
||||
} from "./toasts/SetupEncryptionToast";
|
||||
import {
|
||||
hideToast as hideUnverifiedSessionsToast,
|
||||
showToast as showUnverifiedSessionsToast
|
||||
} from "./toasts/UnverifiedSessionToast";
|
||||
import {privateShouldBeEncrypted} from "./createRoom";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
|
@ -169,6 +169,14 @@ export default class DeviceListener {
|
|||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
private shouldShowSetupEncryptionToast() {
|
||||
// In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
|
||||
// then do not show the toasts until user is in at least one encrypted room.
|
||||
if (privateShouldBeEncrypted()) return true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -184,7 +192,7 @@ export default class DeviceListener {
|
|||
|
||||
if (this.dismissedThisDeviceToast || crossSigningReady) {
|
||||
hideSetupEncryptionToast();
|
||||
} else {
|
||||
} else if (this.shouldShowSetupEncryptionToast()) {
|
||||
// make sure our keys are finished downloading
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
|
@ -196,10 +204,10 @@ export default class DeviceListener {
|
|||
const backupInfo = await this._getKeyBackupInfo();
|
||||
if (backupInfo) {
|
||||
// No cross-signing on account but key backup available (upgrade encryption)
|
||||
showSetupEncryptionToast(Kind.UPGRADE_ENCRYPTION);
|
||||
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
||||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION);
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -163,14 +164,16 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (!queryParams.homeserver) {
|
||||
const homeserver = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
if (!homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return sendLoginRequest(
|
||||
queryParams.homeserver,
|
||||
queryParams.identityServer,
|
||||
homeserver,
|
||||
identityServer,
|
||||
"m.login.token", {
|
||||
token: queryParams.loginToken,
|
||||
initial_device_display_name: defaultDeviceDisplayName,
|
||||
|
@ -256,8 +259,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
|||
* @returns {Object} Information about the session - see implementation for variables.
|
||||
*/
|
||||
export function getLocalStorageSessionVars() {
|
||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||
const isUrl = localStorage.getItem("mx_is_url");
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
|
@ -486,9 +489,9 @@ function _showStorageEvictedDialog() {
|
|||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||
if (credentials.identityServerUrl) {
|
||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||
}
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
|
@ -619,7 +622,7 @@ async function startMatrixClient(startSyncing=true) {
|
|||
}
|
||||
|
||||
// Now that we have a MatrixClientPeg, update the Jitsi info
|
||||
await Jitsi.getInstance().update();
|
||||
await Jitsi.getInstance().start();
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
|
|
|
@ -49,6 +49,7 @@ export interface IOpts {
|
|||
initialSyncLimit?: number;
|
||||
pendingEventOrdering?: "detached" | "chronological";
|
||||
lazyLoadMembers?: boolean;
|
||||
clientWellKnownPollPeriod?: number;
|
||||
}
|
||||
|
||||
export interface IMatrixClientPeg {
|
||||
|
@ -209,6 +210,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
opts.lazyLoadMembers = true;
|
||||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||
|
||||
// Connect the matrix client to the dispatcher and setting handlers
|
||||
MatrixActionCreators.start(this.matrixClient);
|
||||
|
|
|
@ -122,7 +122,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} aria-label={unicode}>
|
||||
<span style={{maxWidth: '1em'}}>{ unicode }</span>
|
||||
<span>{ unicode }</span>
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
|
|
|
@ -92,12 +92,23 @@ interface IProps {
|
|||
currentGroupIsNew?: boolean;
|
||||
}
|
||||
|
||||
interface IUsageLimit {
|
||||
limit_type: "monthly_active_user" | string;
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mouseDown?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
syncErrorData: any;
|
||||
syncErrorData?: {
|
||||
error: {
|
||||
data: IUsageLimit;
|
||||
errcode: string;
|
||||
};
|
||||
};
|
||||
usageLimitEventContent?: IUsageLimit;
|
||||
useCompactLayout: boolean;
|
||||
}
|
||||
|
||||
|
@ -282,7 +293,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(data);
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -293,7 +304,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
|
||||
_calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncErrorData.error.data;
|
||||
|
@ -330,8 +341,9 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
|
||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||
this.setState({ usageLimitEventContent });
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
|
|
|
@ -1900,7 +1900,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
return setLoggedInPromise;
|
||||
}
|
||||
|
||||
if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
// Test for the master cross-signing key in SSSS as a quick proxy for
|
||||
// whether cross-signing has been set up on the account.
|
||||
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
|
||||
if (masterKeyInStorage) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
|
@ -1919,7 +1924,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
let fragmentAfterLogin = "";
|
||||
if (this.props.initialScreenAfterLogin) {
|
||||
if (this.props.initialScreenAfterLogin &&
|
||||
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
|
||||
!["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen)
|
||||
) {
|
||||
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,30 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
|
|||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(prevEvent, mxEvent) {
|
||||
// sanity check inputs
|
||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||
// check if within the max continuation period
|
||||
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
|
||||
|
||||
// Some events should appear as continuations from previous events of different types.
|
||||
if (mxEvent.getType() !== prevEvent.getType() &&
|
||||
(!continuedTypes.includes(mxEvent.getType()) ||
|
||||
!continuedTypes.includes(prevEvent.getType()))) return false;
|
||||
|
||||
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
|
||||
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
|
||||
mxEvent.sender.name !== prevEvent.sender.name ||
|
||||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
||||
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
if (!haveTileForEvent(prevEvent)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
|
@ -515,39 +539,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
/*
|
||||
// Work out if this is still a continuation, as we are now showing commands
|
||||
// and /me messages with their own little avatar. The case of a change of
|
||||
// event type (commands) is handled above, but we need to handle the /me
|
||||
// messages seperately as they have a msgtype of 'm.emote' but are classed
|
||||
// as normal messages
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()
|
||||
&& prevEvent.getContent().msgtype === 'm.emote') {
|
||||
continuation = false;
|
||||
}
|
||||
*/
|
||||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
|
@ -559,12 +550,15 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||
if (wantsDateSeparator) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = {
|
|||
export default class SoftLogout extends React.Component {
|
||||
static propTypes = {
|
||||
// Query parameters from MatrixChat
|
||||
realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken}
|
||||
realQueryParams: PropTypes.object, // {loginToken}
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
|
@ -90,7 +91,7 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
async _initLogin() {
|
||||
const queryParams = this.props.realQueryParams;
|
||||
const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken'];
|
||||
const hasAllParams = queryParams && queryParams['loginToken'];
|
||||
if (hasAllParams) {
|
||||
this.setState({loginView: LOGIN_VIEW.LOADING});
|
||||
this.trySsoLogin();
|
||||
|
@ -157,8 +158,8 @@ export default class SoftLogout extends React.Component {
|
|||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = this.props.realQueryParams['homeserver'];
|
||||
const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
|
|
|
@ -24,6 +24,7 @@ import withValidation from '../elements/Validation';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
|
@ -36,7 +37,7 @@ export default createReactClass({
|
|||
const config = SdkConfig.get();
|
||||
return {
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: true,
|
||||
isEncrypted: privateShouldBeEncrypted(),
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
|
@ -193,6 +194,13 @@ export default createReactClass({
|
|||
|
||||
let e2eeSection;
|
||||
if (!this.state.isPublic) {
|
||||
let microcopy;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
} else {
|
||||
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
|
||||
"in private rooms & Direct Messages.");
|
||||
}
|
||||
e2eeSection = <React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={ _t("Enable end-to-end encryption")}
|
||||
|
@ -200,7 +208,7 @@ export default createReactClass({
|
|||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
/>
|
||||
<p>{ _t("You can’t disable this later. Bridges & most bots won’t work yet.") }</p>
|
||||
<p>{ microcopy }</p>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
@ -575,14 +575,16 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
preview = <div>
|
||||
<div>
|
||||
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||
src={require("../../../../res/img/files.png")}
|
||||
src={require("../../../../res/img/feather-customised/files.svg")}
|
||||
/>
|
||||
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||
</div>
|
||||
|
|
|
@ -227,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
loadError: null,
|
||||
});
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const has4S = await cli.hasSecretStorageKey();
|
||||
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
|
|
|
@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import Modal from "../../../Modal";
|
||||
import * as sdk from "../../../index";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
export default class ImageView extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -50,16 +49,6 @@ export default class ImageView extends React.Component {
|
|||
this.state = { rotationDegrees: 0 };
|
||||
}
|
||||
|
||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
||||
// dialog base class somehow, surely...
|
||||
componentDidMount() {
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
|
@ -195,7 +184,14 @@ export default class ImageView extends React.Component {
|
|||
const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style};
|
||||
|
||||
return (
|
||||
<div className="mx_ImageView">
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this.onKeyDown,
|
||||
role: "dialog",
|
||||
}}
|
||||
className="mx_ImageView"
|
||||
>
|
||||
<div className="mx_ImageView_lhs">
|
||||
</div>
|
||||
<div className="mx_ImageView_content">
|
||||
|
@ -231,7 +227,7 @@ export default class ImageView extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ImageView_rhs">
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
return style;
|
||||
}
|
||||
|
||||
private renderTooltip() {
|
||||
private renderTooltip = () => {
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
|
|
|
@ -27,8 +27,7 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
|
|||
if (!data) {
|
||||
throw new Error(`Emoji ${emoji} doesn't exist in emojibase`);
|
||||
}
|
||||
// Prefer our unicode value for quick reactions as we sometimes use variation selectors.
|
||||
return Object.assign({}, data, { unicode: emoji });
|
||||
return data;
|
||||
});
|
||||
|
||||
class QuickReactions extends React.Component {
|
||||
|
|
|
@ -25,7 +25,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom from '../../../createRoom';
|
||||
import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
@ -108,15 +108,17 @@ async function openDMForUser(matrixClient, userId) {
|
|||
dmUserId: userId,
|
||||
};
|
||||
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
|
||||
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
|
||||
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
|
||||
return Object.keys(devices).length > 0;
|
||||
});
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
|
||||
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
|
||||
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
|
||||
return Object.keys(devices).length > 0;
|
||||
});
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
}
|
||||
}
|
||||
|
||||
createRoom(createRoomOptions);
|
||||
|
|
|
@ -260,7 +260,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
behavior: "auto",
|
||||
block: "nearest",
|
||||
});
|
||||
} else {
|
||||
} else if (this.containerRef.current) {
|
||||
this.containerRef.current.scrollTo({ top: 0 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ export function getHandlerTile(ev) {
|
|||
// fall back to showing hidden events, if we're viewing hidden events
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
return;
|
||||
|
|
|
@ -28,6 +28,8 @@ import { Dispatcher } from "flux";
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist2 from "./RoomSublist2";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
|
||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -130,6 +132,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
||||
private unfilteredLayout: Layout;
|
||||
private filteredLayout: Layout;
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -139,6 +142,21 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
this.prepareLayouts();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
if (prevProps.searchFilter !== this.props.searchFilter) {
|
||||
const hadSearch = !!this.searchFilter.search.trim();
|
||||
const haveSearch = !!this.props.searchFilter.trim();
|
||||
this.searchFilter.search = this.props.searchFilter;
|
||||
if (!hadSearch && haveSearch) {
|
||||
// started a new filter - add the condition
|
||||
RoomListStore.instance.addFilter(this.searchFilter);
|
||||
} else if (hadSearch && !haveSearch) {
|
||||
// cleared a filter - remove the condition
|
||||
RoomListStore.instance.removeFilter(this.searchFilter);
|
||||
} // else the filter hasn't changed enough for us to care here
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
|
||||
console.log("new lists", store.orderedLists);
|
||||
|
|
|
@ -40,8 +40,8 @@ export default class ProfileSettings extends React.Component {
|
|||
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
|
||||
this.state = {
|
||||
userId: user.userId,
|
||||
originalDisplayName: user.displayName,
|
||||
displayName: user.displayName,
|
||||
originalDisplayName: user.rawDisplayName,
|
||||
displayName: user.rawDisplayName,
|
||||
originalAvatarUrl: avatarUrl,
|
||||
avatarUrl: avatarUrl,
|
||||
avatarFile: null,
|
||||
|
|
|
@ -26,6 +26,7 @@ import Modal from "../../../../../Modal";
|
|||
import * as sdk from "../../../../..";
|
||||
import {sleep} from "../../../../../utils/promise";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import {privateShouldBeEncrypted} from "../../../../../createRoom";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -317,8 +318,17 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
||||
|
||||
let warning;
|
||||
if (!privateShouldBeEncrypted()) {
|
||||
warning = <div className="mx_SecurityUserSettingsTab_warning">
|
||||
{ _t("Your server admin has disabled end-to-end encryption by default " +
|
||||
"in private rooms & Direct Messages.") }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{warning}
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Where you’re logged in")}</span>
|
||||
|
|
|
@ -24,6 +24,8 @@ import * as Rooms from "./Rooms";
|
|||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import {getAddressType} from "./UserAddress";
|
||||
|
||||
const E2EE_WK_KEY = "im.vector.riot.e2ee";
|
||||
|
||||
/**
|
||||
* Create a new room, and switch to it.
|
||||
*
|
||||
|
@ -225,9 +227,22 @@ export async function ensureDMExists(client, userId) {
|
|||
if (existingDMRoom) {
|
||||
roomId = existingDMRoom.roomId;
|
||||
} else {
|
||||
const encryption = canEncryptToAllUsers(client, [userId]);
|
||||
let encryption;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
encryption = canEncryptToAllUsers(client, [userId]);
|
||||
}
|
||||
roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false});
|
||||
await _waitForMember(client, roomId, userId);
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
export function privateShouldBeEncrypted() {
|
||||
const clientWellKnown = MatrixClientPeg.get().getClientWellKnown();
|
||||
if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) {
|
||||
const defaultDisabled = clientWellKnown[E2EE_WK_KEY]["default"] === false;
|
||||
return !defaultDisabled;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -871,6 +871,7 @@
|
|||
"Key backup": "Key backup",
|
||||
"Message search": "Message search",
|
||||
"Cross-signing": "Cross-signing",
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Where you’re logged in": "Where you’re logged in",
|
||||
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
|
||||
|
@ -1559,8 +1560,8 @@
|
|||
"Please enter a name for the room": "Please enter a name for the room",
|
||||
"Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.",
|
||||
"This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.",
|
||||
"Enable end-to-end encryption": "Enable end-to-end encryption",
|
||||
"You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
|
||||
"Enable end-to-end encryption": "Enable end-to-end encryption",
|
||||
"Create a public room": "Create a public room",
|
||||
"Create a private room": "Create a private room",
|
||||
"Name": "Name",
|
||||
|
|
|
@ -21,10 +21,8 @@ import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER}
|
|||
import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import {AutoDiscovery} from "matrix-js-sdk";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours
|
||||
const KIND_PREFERENCE = [
|
||||
// Ordered: first is most preferred, last is least preferred.
|
||||
KIND_ACCOUNT,
|
||||
|
@ -44,7 +42,6 @@ export class IntegrationManagers {
|
|||
|
||||
_managers: IntegrationManagerInstance[] = [];
|
||||
_client: MatrixClient;
|
||||
_wellknownRefreshTimerId: number = null;
|
||||
_primaryManager: IntegrationManagerInstance;
|
||||
|
||||
constructor() {
|
||||
|
@ -55,20 +52,19 @@ export class IntegrationManagers {
|
|||
this.stopWatching();
|
||||
this._client = MatrixClientPeg.get();
|
||||
this._client.on("accountData", this._onAccountData);
|
||||
this._client.on("WellKnown.client", this._setupHomeserverManagers);
|
||||
this._compileManagers();
|
||||
setInterval(() => this._setupHomeserverManagers(), HS_MANAGERS_REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
stopWatching(): void {
|
||||
if (!this._client) return;
|
||||
this._client.removeListener("accountData", this._onAccountData);
|
||||
if (this._wellknownRefreshTimerId !== null) clearInterval(this._wellknownRefreshTimerId);
|
||||
this._client.removeListener("WellKnown.client", this._setupHomeserverManagers);
|
||||
}
|
||||
|
||||
_compileManagers() {
|
||||
this._managers = [];
|
||||
this._setupConfiguredManager();
|
||||
this._setupHomeserverManagers();
|
||||
this._setupAccountManagers();
|
||||
}
|
||||
|
||||
|
@ -82,39 +78,31 @@ export class IntegrationManagers {
|
|||
}
|
||||
}
|
||||
|
||||
async _setupHomeserverManagers() {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
try {
|
||||
console.log("Updating homeserver-configured integration managers...");
|
||||
const homeserverDomain = MatrixClientPeg.getHomeserverName();
|
||||
const discoveryResponse = await AutoDiscovery.getRawClientConfig(homeserverDomain);
|
||||
if (discoveryResponse && discoveryResponse['m.integrations']) {
|
||||
let managers = discoveryResponse['m.integrations']['managers'];
|
||||
if (!Array.isArray(managers)) managers = []; // make it an array so we can wipe the HS managers
|
||||
async _setupHomeserverManagers(discoveryResponse) {
|
||||
console.log("Updating homeserver-configured integration managers...");
|
||||
if (discoveryResponse && discoveryResponse['m.integrations']) {
|
||||
let managers = discoveryResponse['m.integrations']['managers'];
|
||||
if (!Array.isArray(managers)) managers = []; // make it an array so we can wipe the HS managers
|
||||
|
||||
console.log(`Homeserver has ${managers.length} integration managers`);
|
||||
console.log(`Homeserver has ${managers.length} integration managers`);
|
||||
|
||||
// Clear out any known managers for the homeserver
|
||||
// TODO: Log out of the scalar clients
|
||||
this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER);
|
||||
// Clear out any known managers for the homeserver
|
||||
// TODO: Log out of the scalar clients
|
||||
this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER);
|
||||
|
||||
// Now add all the managers the homeserver wants us to have
|
||||
for (const hsManager of managers) {
|
||||
if (!hsManager["api_url"]) continue;
|
||||
this._managers.push(new IntegrationManagerInstance(
|
||||
KIND_HOMESERVER,
|
||||
hsManager["api_url"],
|
||||
hsManager["ui_url"], // optional
|
||||
));
|
||||
}
|
||||
|
||||
this._primaryManager = null; // reset primary
|
||||
} else {
|
||||
console.log("Homeserver has no integration managers");
|
||||
// Now add all the managers the homeserver wants us to have
|
||||
for (const hsManager of managers) {
|
||||
if (!hsManager["api_url"]) continue;
|
||||
this._managers.push(new IntegrationManagerInstance(
|
||||
KIND_HOMESERVER,
|
||||
hsManager["api_url"],
|
||||
hsManager["ui_url"], // optional
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Errors during discovery are non-fatal
|
||||
|
||||
this._primaryManager = null; // reset primary
|
||||
} else {
|
||||
console.log("Homeserver has no integration managers");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,6 +111,21 @@ an object containing the tags it needs to worry about and the rooms within. The
|
|||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||
all kinds of filtering.
|
||||
|
||||
## Filtering
|
||||
|
||||
Filters are provided to the store as condition classes, which are then passed along to the algorithm
|
||||
implementations. The implementations then get to decide how to actually filter the rooms, however in
|
||||
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
|
||||
|
||||
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
|
||||
as the old room list store does. When a filter condition changes, it emits an update which (in this
|
||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
||||
minor subset where possible to avoid over-iterating rooms.
|
||||
|
||||
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
|
||||
expect a change in the condition unless the condition says it has changed. This is intentional to
|
||||
maintain the caching behaviour described above.
|
||||
|
||||
## Class breakdowns
|
||||
|
||||
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||
import { Algorithm } from "./algorithms/list-ordering/Algorithm";
|
||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm";
|
||||
import TagOrderStore from "../TagOrderStore";
|
||||
import { AsyncStore } from "../AsyncStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
@ -27,6 +27,8 @@ import { getListAlgorithmInstance } from "./algorithms/list-ordering";
|
|||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import { IFilterCondition } from "./filters/IFilterCondition";
|
||||
import { TagWatcher } from "./TagWatcher";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -41,11 +43,13 @@ interface IState {
|
|||
*/
|
||||
export const LISTS_UPDATE_EVENT = "lists_update";
|
||||
|
||||
class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||
private matrixClient: MatrixClient;
|
||||
export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||
private _matrixClient: MatrixClient;
|
||||
private initialListsGenerated = false;
|
||||
private enabled = false;
|
||||
private algorithm: Algorithm;
|
||||
private filterConditions: IFilterCondition[] = [];
|
||||
private tagWatcher = new TagWatcher(this);
|
||||
|
||||
private readonly watchedSettings = [
|
||||
'RoomList.orderAlphabetically',
|
||||
|
@ -65,6 +69,10 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
return this.algorithm.getOrderedRooms();
|
||||
}
|
||||
|
||||
public get matrixClient(): MatrixClient {
|
||||
return this._matrixClient;
|
||||
}
|
||||
|
||||
// TODO: Remove enabled flag when the old RoomListStore goes away
|
||||
private checkEnabled() {
|
||||
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
|
||||
|
@ -96,7 +104,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
this.checkEnabled();
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.matrixClient = payload.matrixClient;
|
||||
this._matrixClient = payload.matrixClient;
|
||||
|
||||
// Update any settings here, as some may have happened before we were logically ready.
|
||||
console.log("Regenerating room lists: Startup");
|
||||
|
@ -111,7 +119,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
// Reset state without causing updates as the client will have been destroyed
|
||||
// and downstream code will throw NPE errors.
|
||||
this.reset(null, true);
|
||||
this.matrixClient = null;
|
||||
this._matrixClient = null;
|
||||
this.initialListsGenerated = false; // we'll want to regenerate them
|
||||
}
|
||||
|
||||
|
@ -152,8 +160,21 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
|
||||
const roomId = eventPayload.event.getRoomId();
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
|
||||
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
||||
const tryUpdate = async (updatedRoom: Room) => {
|
||||
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
|
||||
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
|
||||
};
|
||||
if (!room) {
|
||||
console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
||||
console.warn(`Queuing failed room update for retry as a result.`);
|
||||
setTimeout(async () => {
|
||||
const updatedRoom = this.matrixClient.getRoom(roomId);
|
||||
await tryUpdate(updatedRoom);
|
||||
}, 100); // 100ms should be enough for the room to show up
|
||||
return;
|
||||
} else {
|
||||
await tryUpdate(room);
|
||||
}
|
||||
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||
const roomId = eventPayload.event.getRoomId();
|
||||
|
@ -171,11 +192,20 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
// TODO: Update DMs
|
||||
console.log(payload);
|
||||
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
||||
// TODO: Improve new room check
|
||||
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
|
||||
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
|
||||
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
||||
}
|
||||
|
||||
// TODO: Update room from membership change
|
||||
console.log(payload);
|
||||
} else if (payload.action === 'MatrixActions.Room') {
|
||||
// TODO: Update room from creation/join
|
||||
console.log(payload);
|
||||
// TODO: Improve new room check
|
||||
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
|
||||
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
|
||||
} else if (payload.action === 'view_room') {
|
||||
// TODO: Update sticky room
|
||||
console.log(payload);
|
||||
|
@ -211,11 +241,22 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
}
|
||||
|
||||
private setAlgorithmClass() {
|
||||
if (this.algorithm) {
|
||||
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
}
|
||||
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
|
||||
this.algorithm.setFilterConditions(this.filterConditions);
|
||||
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||
}
|
||||
|
||||
private onAlgorithmListUpdated = () => {
|
||||
console.log("Underlying algorithm has triggered a list update - refiring");
|
||||
this.emit(LISTS_UPDATE_EVENT, this);
|
||||
};
|
||||
|
||||
private async regenerateAllLists() {
|
||||
console.warn("Regenerating all room lists");
|
||||
|
||||
const tags: ITagSortingMap = {};
|
||||
for (const tagId of OrderedDefaultTagIDs) {
|
||||
tags[tagId] = this.getSortAlgorithmFor(tagId);
|
||||
|
@ -234,16 +275,38 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
|||
|
||||
this.emit(LISTS_UPDATE_EVENT, this);
|
||||
}
|
||||
|
||||
public addFilter(filter: IFilterCondition): void {
|
||||
console.log("Adding filter condition:", filter);
|
||||
this.filterConditions.push(filter);
|
||||
if (this.algorithm) {
|
||||
this.algorithm.addFilterCondition(filter);
|
||||
}
|
||||
}
|
||||
|
||||
public removeFilter(filter: IFilterCondition): void {
|
||||
console.log("Removing filter condition:", filter);
|
||||
const idx = this.filterConditions.indexOf(filter);
|
||||
if (idx >= 0) {
|
||||
this.filterConditions.splice(idx, 1);
|
||||
|
||||
if (this.algorithm) {
|
||||
this.algorithm.removeFilterCondition(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class RoomListStore {
|
||||
private static internalInstance: _RoomListStore;
|
||||
private static internalInstance: RoomListStore2;
|
||||
|
||||
public static get instance(): _RoomListStore {
|
||||
public static get instance(): RoomListStore2 {
|
||||
if (!RoomListStore.internalInstance) {
|
||||
RoomListStore.internalInstance = new _RoomListStore();
|
||||
RoomListStore.internalInstance = new RoomListStore2();
|
||||
}
|
||||
|
||||
return RoomListStore.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
window.mx_RoomListStore2 = RoomListStore.instance;
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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 { RoomListStore2 } from "./RoomListStore2";
|
||||
import TagOrderStore from "../TagOrderStore";
|
||||
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
|
||||
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
|
||||
|
||||
/**
|
||||
* Watches for changes in tags/groups to manage filters on the provided RoomListStore
|
||||
*/
|
||||
export class TagWatcher {
|
||||
// TODO: Support custom tags, somehow (deferred to later work - need support elsewhere)
|
||||
private filters = new Map<string, CommunityFilterCondition>();
|
||||
|
||||
constructor(private store: RoomListStore2) {
|
||||
TagOrderStore.addListener(this.onTagsUpdated);
|
||||
}
|
||||
|
||||
private onTagsUpdated = () => {
|
||||
const lastTags = Array.from(this.filters.keys());
|
||||
const newTags = TagOrderStore.getSelectedTags();
|
||||
|
||||
if (arrayHasDiff(lastTags, newTags)) {
|
||||
// Selected tags changed, do some filtering
|
||||
|
||||
if (!this.store.matrixClient) {
|
||||
console.warn("Tag update without an associated matrix client - ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilters = new Map<string, CommunityFilterCondition>();
|
||||
|
||||
// TODO: Support custom tags properly
|
||||
const filterableTags = newTags.filter(t => t.startsWith("+"));
|
||||
|
||||
for (const tag of filterableTags) {
|
||||
const group = this.store.matrixClient.getGroup(tag);
|
||||
if (!group) {
|
||||
console.warn(`Group selected with no group object available: ${tag}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
newFilters.set(tag, new CommunityFilterCondition(group));
|
||||
}
|
||||
|
||||
// Update the room list store's filters
|
||||
const diff = arrayDiff(lastTags, newTags);
|
||||
for (const tag of diff.added) {
|
||||
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
|
||||
const filter = newFilters.get(tag);
|
||||
if (!filter) continue;
|
||||
|
||||
this.store.addFilter(filter);
|
||||
}
|
||||
for (const tag of diff.removed) {
|
||||
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
|
||||
const filter = this.filters.get(tag);
|
||||
if (!filter) continue;
|
||||
|
||||
this.store.removeFilter(filter);
|
||||
}
|
||||
|
||||
this.filters = newFilters;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -20,24 +20,138 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
|||
import { EffectiveMembership, splitRoomsByMembership } from "../../membership";
|
||||
import { ITagMap, ITagSortingMap } from "../models";
|
||||
import DMRoomMap from "../../../../utils/DMRoomMap";
|
||||
import { FILTER_CHANGED, IFilterCondition } from "../../filters/IFilterCondition";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
// TODO: Add locking support to avoid concurrent writes?
|
||||
// TODO: EventEmitter support? Might not be needed.
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
*/
|
||||
export const LIST_UPDATED_EVENT = "list_updated_event";
|
||||
|
||||
/**
|
||||
* Represents a list ordering algorithm. This class will take care of tag
|
||||
* management (which rooms go in which tags) and ask the implementation to
|
||||
* deal with ordering mechanics.
|
||||
*/
|
||||
export abstract class Algorithm {
|
||||
protected cached: ITagMap = {};
|
||||
export abstract class Algorithm extends EventEmitter {
|
||||
private _cachedRooms: ITagMap = {};
|
||||
private filteredRooms: ITagMap = {};
|
||||
|
||||
protected sortAlgorithms: ITagSortingMap;
|
||||
protected rooms: Room[] = [];
|
||||
protected roomIdsToTags: {
|
||||
[roomId: string]: TagID[];
|
||||
} = {};
|
||||
protected allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||
protected allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected get hasFilters(): boolean {
|
||||
return this.allowedByFilter.size > 0;
|
||||
}
|
||||
|
||||
protected set cachedRooms(val: ITagMap) {
|
||||
this._cachedRooms = val;
|
||||
this.recalculateFilteredRooms();
|
||||
}
|
||||
|
||||
protected get cachedRooms(): ITagMap {
|
||||
return this._cachedRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filter conditions the Algorithm should use.
|
||||
* @param filterConditions The filter conditions to use.
|
||||
*/
|
||||
public setFilterConditions(filterConditions: IFilterCondition[]): void {
|
||||
for (const filter of filterConditions) {
|
||||
this.addFilterCondition(filter);
|
||||
}
|
||||
}
|
||||
|
||||
public addFilterCondition(filterCondition: IFilterCondition): void {
|
||||
// Populate the cache of the new filter
|
||||
this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r)));
|
||||
this.recalculateFilteredRooms();
|
||||
filterCondition.on(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
|
||||
}
|
||||
|
||||
public removeFilterCondition(filterCondition: IFilterCondition): void {
|
||||
filterCondition.off(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
|
||||
if (this.allowedByFilter.has(filterCondition)) {
|
||||
this.allowedByFilter.delete(filterCondition);
|
||||
|
||||
// If we removed the last filter, tell consumers that we've "updated" our filtered
|
||||
// view. This will trick them into getting the complete room list.
|
||||
if (!this.hasFilters) {
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected recalculateFilteredRooms() {
|
||||
if (!this.hasFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn("Recalculating filtered room list");
|
||||
const allowedByFilters = new Set<Room>();
|
||||
const filters = Array.from(this.allowedByFilter.keys());
|
||||
const newMap: ITagMap = {};
|
||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||
// Cheaply clone the rooms so we can more easily do operations on the list.
|
||||
// We optimize our lookups by trying to reduce sample size as much as possible
|
||||
// to the rooms we know will be deduped by the Set.
|
||||
const rooms = this.cachedRooms[tagId];
|
||||
const remainingRooms = rooms.map(r => r).filter(r => !allowedByFilters.has(r));
|
||||
const allowedRoomsInThisTag = [];
|
||||
for (const filter of filters) {
|
||||
const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
|
||||
for (const room of filteredRooms) {
|
||||
const idx = remainingRooms.indexOf(room);
|
||||
if (idx >= 0) remainingRooms.splice(idx, 1);
|
||||
allowedByFilters.add(room);
|
||||
allowedRoomsInThisTag.push(room);
|
||||
}
|
||||
}
|
||||
newMap[tagId] = allowedRoomsInThisTag;
|
||||
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||
}
|
||||
|
||||
this.allowedRoomsByFilters = allowedByFilters;
|
||||
this.filteredRooms = newMap;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
|
||||
const filters = this.allowedByFilter.keys();
|
||||
for (const room of added) {
|
||||
for (const filter of filters) {
|
||||
if (filter.isVisible(room)) {
|
||||
this.allowedRoomsByFilters.add(room);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we've updated the allowed rooms, recalculate the tag
|
||||
this.recalculateFilteredRoomsForTag(tagId);
|
||||
}
|
||||
|
||||
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
|
||||
console.log(`Recalculating filtered rooms for ${tagId}`);
|
||||
delete this.filteredRooms[tagId];
|
||||
const rooms = this.cachedRooms[tagId];
|
||||
const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
|
||||
if (filteredRooms.length > 0) {
|
||||
this.filteredRooms[tagId] = filteredRooms;
|
||||
}
|
||||
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,12 +168,15 @@ export abstract class Algorithm {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets an ordered set of rooms for the all known tags.
|
||||
* Gets an ordered set of rooms for the all known tags, filtered.
|
||||
* @returns {ITagMap} The cached list of rooms, ordered,
|
||||
* for each tag. May be empty, but never null/undefined.
|
||||
*/
|
||||
public getOrderedRooms(): ITagMap {
|
||||
return this.cached;
|
||||
if (!this.hasFilters) {
|
||||
return this.cachedRooms;
|
||||
}
|
||||
return this.filteredRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +200,7 @@ export abstract class Algorithm {
|
|||
// If we can avoid doing work, do so.
|
||||
if (!rooms.length) {
|
||||
await this.generateFreshTags(newTags); // just in case it wants to do something
|
||||
this.cached = newTags;
|
||||
this.cachedRooms = newTags;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -130,7 +247,7 @@ export abstract class Algorithm {
|
|||
|
||||
await this.generateFreshTags(newTags);
|
||||
|
||||
this.cached = newTags;
|
||||
this.cachedRooms = newTags;
|
||||
this.updateTagsFromCache();
|
||||
}
|
||||
|
||||
|
@ -140,9 +257,9 @@ export abstract class Algorithm {
|
|||
protected updateTagsFromCache() {
|
||||
const newMap = {};
|
||||
|
||||
const tags = Object.keys(this.cached);
|
||||
const tags = Object.keys(this.cachedRooms);
|
||||
for (const tagId of tags) {
|
||||
const rooms = this.cached[tagId];
|
||||
const rooms = this.cachedRooms[tagId];
|
||||
for (const room of rooms) {
|
||||
if (!newMap[room.roomId]) newMap[room.roomId] = [];
|
||||
newMap[room.roomId].push(tagId);
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import { Algorithm } from "./Algorithm";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomUpdateCause, TagID } from "../../models";
|
||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../../models";
|
||||
import { ITagMap, SortAlgorithm } from "../models";
|
||||
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
||||
import * as Unread from '../../../../Unread';
|
||||
|
@ -92,9 +92,9 @@ export class ImportanceAlgorithm extends Algorithm {
|
|||
// can be found from `this.indices[tag][category]` and the sticky room information
|
||||
// from `this.stickyRoom`.
|
||||
//
|
||||
// The room list store is always provided with the `this.cached` results, which are
|
||||
// The room list store is always provided with the `this.cachedRooms` results, which are
|
||||
// updated as needed and not recalculated often. For example, when a room needs to
|
||||
// move within a tag, the array in `this.cached` will be spliced instead of iterated.
|
||||
// move within a tag, the array in `this.cachedRooms` will be spliced instead of iterated.
|
||||
// The `indices` help track the positions of each category to make splicing easier.
|
||||
|
||||
private indices: {
|
||||
|
@ -189,7 +189,13 @@ export class ImportanceAlgorithm extends Algorithm {
|
|||
}
|
||||
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
// TODO: Be smarter and insert rather than regen the planet.
|
||||
await this.setKnownRooms([room, ...this.rooms]);
|
||||
return;
|
||||
}
|
||||
|
||||
let tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
|
@ -201,7 +207,7 @@ export class ImportanceAlgorithm extends Algorithm {
|
|||
continue; // Nothing to do here.
|
||||
}
|
||||
|
||||
const taggedRooms = this.cached[tag];
|
||||
const taggedRooms = this.cachedRooms[tag];
|
||||
const indices = this.indices[tag];
|
||||
let roomIdx = taggedRooms.indexOf(room);
|
||||
if (roomIdx === -1) {
|
||||
|
|
|
@ -49,7 +49,7 @@ export class NaturalAlgorithm extends Algorithm {
|
|||
for (const tag of tags) {
|
||||
// TODO: Optimize this loop to avoid useless operations
|
||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||
this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]);
|
||||
this.cachedRooms[tag] = await sortRoomsWithAlgorithm(this.cachedRooms[tag], tag, this.sortAlgorithms[tag]);
|
||||
}
|
||||
return true; // assume we changed something
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition";
|
||||
import { Group } from "matrix-js-sdk/src/models/group";
|
||||
import { EventEmitter } from "events";
|
||||
import GroupStore from "../../GroupStore";
|
||||
import { arrayHasDiff } from "../../../utils/arrays";
|
||||
import { IDisposable } from "../../../utils/IDisposable";
|
||||
|
||||
/**
|
||||
* A filter condition for the room list which reveals rooms which
|
||||
* are a member of a given community.
|
||||
*/
|
||||
export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDisposable {
|
||||
private roomIds: string[] = [];
|
||||
|
||||
constructor(private community: Group) {
|
||||
super();
|
||||
GroupStore.on("update", this.onStoreUpdate);
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onStoreUpdate(); // trigger a false update to seed the store
|
||||
}
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
return this.roomIds.includes(room.roomId);
|
||||
}
|
||||
|
||||
private onStoreUpdate = async (): Promise<any> => {
|
||||
// We don't actually know if the room list changed for the community, so just
|
||||
// check it again.
|
||||
const beforeRoomIds = this.roomIds;
|
||||
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
||||
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
||||
console.log("Updating filter for group: ", this.community.groupId);
|
||||
this.emit(FILTER_CHANGED);
|
||||
}
|
||||
};
|
||||
|
||||
public dispose(): void {
|
||||
GroupStore.off("update", this.onStoreUpdate);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export const FILTER_CHANGED = "filter_changed";
|
||||
|
||||
/**
|
||||
* A filter condition for the room list, determining if a room
|
||||
* should be shown or not.
|
||||
*
|
||||
* All filter conditions are expected to be stable executions,
|
||||
* meaning that given the same input the same answer will be
|
||||
* returned (thus allowing caching). As such, filter conditions
|
||||
* can, but shouldn't, do heavier logic and not worry about being
|
||||
* called constantly by the room list. When the condition changes
|
||||
* such that different inputs lead to different answers (such
|
||||
* as a change in the user's input), this emits FILTER_CHANGED.
|
||||
*/
|
||||
export interface IFilterCondition extends EventEmitter {
|
||||
/**
|
||||
* Determines if a given room should be visible under this
|
||||
* condition.
|
||||
* @param room The room to check.
|
||||
* @returns True if the room should be visible.
|
||||
*/
|
||||
isVisible(room: Room): boolean;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* A filter condition for the room list which reveals rooms of a particular
|
||||
* name, or associated name (like a room alias).
|
||||
*/
|
||||
export class NameFilterCondition extends EventEmitter implements IFilterCondition {
|
||||
private _search = "";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public get search(): string {
|
||||
return this._search;
|
||||
}
|
||||
|
||||
public set search(val: string) {
|
||||
this._search = val;
|
||||
console.log("Updating filter for room name search:", this._search);
|
||||
this.emit(FILTER_CHANGED);
|
||||
}
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
// TODO: Improve this filter to include aliases and such
|
||||
return room.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
|
||||
}
|
||||
}
|
|
@ -39,4 +39,5 @@ export type TagID = string | DefaultTagID;
|
|||
export enum RoomUpdateCause {
|
||||
Timeline = "TIMELINE",
|
||||
RoomRead = "ROOM_READ", // TODO: Use this.
|
||||
NewRoom = "NEW_ROOM",
|
||||
}
|
||||
|
|
|
@ -66,5 +66,5 @@ export const showToast = (deviceId: string) => {
|
|||
};
|
||||
|
||||
export const hideToast = (deviceId: string) => {
|
||||
ToastStore.sharedInstance().dismissToast(deviceId);
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if two arrays are different through a shallow comparison.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns True if they are the same, false otherwise.
|
||||
*/
|
||||
export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||
if (a.length === b.length) {
|
||||
// When the lengths are equal, check to see if either array is missing
|
||||
// an element from the other.
|
||||
if (b.some(i => !a.includes(i))) return true;
|
||||
if (a.some(i => !b.includes(i))) return true;
|
||||
} else {
|
||||
return true; // different lengths means they are naturally diverged
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a diff on two arrays. The result is what is different with the
|
||||
* first array (`added` in the returned object means objects in B that aren't
|
||||
* in A). Shallow comparisons are used to perform the diff.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns The diff between the arrays.
|
||||
*/
|
||||
export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
|
||||
return {
|
||||
added: b.filter(i => !a.includes(i)),
|
||||
removed: a.filter(i => !b.includes(i)),
|
||||
};
|
||||
}
|
|
@ -16,10 +16,8 @@ limitations under the License.
|
|||
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
|
||||
|
||||
const JITSI_WK_PROPERTY = "im.vector.riot.jitsi";
|
||||
const JITSI_WK_CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours, arbitrarily selected
|
||||
|
||||
export interface JitsiWidgetData {
|
||||
conferenceId: string;
|
||||
|
@ -36,39 +34,27 @@ export class Jitsi {
|
|||
return this.domain || 'jitsi.riot.im';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// We rely on the first call to be an .update() instead of doing one here. Doing one
|
||||
// here could result in duplicate calls to the homeserver.
|
||||
|
||||
// Start a timer to update the server info regularly
|
||||
setInterval(() => this.update(), JITSI_WK_CHECK_INTERVAL);
|
||||
public start() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("WellKnown.client", this.update);
|
||||
// call update initially in case we missed the first WellKnown.client event and for if no well-known present
|
||||
this.update(cli.getClientWellKnown());
|
||||
}
|
||||
|
||||
public async update(): Promise<any> {
|
||||
private update = async (discoveryResponse): Promise<any> => {
|
||||
// Start with a default of the config's domain
|
||||
let domain = (SdkConfig.get()['jitsi'] || {})['preferredDomain'] || 'jitsi.riot.im';
|
||||
|
||||
// Now request the .well-known config to see if it changed
|
||||
if (MatrixClientPeg.get()) {
|
||||
try {
|
||||
console.log("Attempting to get Jitsi conference information from homeserver");
|
||||
|
||||
const homeserverDomain = MatrixClientPeg.getHomeserverName();
|
||||
const discoveryResponse = await AutoDiscovery.getRawClientConfig(homeserverDomain);
|
||||
if (discoveryResponse && discoveryResponse[JITSI_WK_PROPERTY]) {
|
||||
const wkPreferredDomain = discoveryResponse[JITSI_WK_PROPERTY]['preferredDomain'];
|
||||
if (wkPreferredDomain) domain = wkPreferredDomain;
|
||||
}
|
||||
} catch (e) {
|
||||
// These are non-fatal errors
|
||||
console.error(e);
|
||||
}
|
||||
console.log("Attempting to get Jitsi conference information from homeserver");
|
||||
if (discoveryResponse && discoveryResponse[JITSI_WK_PROPERTY]) {
|
||||
const wkPreferredDomain = discoveryResponse[JITSI_WK_PROPERTY]['preferredDomain'];
|
||||
if (wkPreferredDomain) domain = wkPreferredDomain;
|
||||
}
|
||||
|
||||
// Put the result into memory for us to use later
|
||||
this.domain = domain;
|
||||
console.log("Jitsi conference domain:", this.preferredDomain);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the given URL into the data needed for a Jitsi widget, if the widget
|
||||
|
|
|
@ -5802,8 +5802,8 @@ mathml-tag-names@^2.0.1:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "6.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a4a7097c103da42075f2c70e070fd01fa6fb0d48"
|
||||
version "6.2.1"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ebe66bdd6e0f6edbc60be1612c5a1fc0c9ea092c"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.3"
|
||||
another-json "^0.2.0"
|
||||
|
|
Loading…
Reference in New Issue