Merge branch 'develop' into Discovery

This commit is contained in:
Travis Ralston 2021-03-22 23:04:41 -06:00
commit edcd7c4426
86 changed files with 1340 additions and 941 deletions

View file

@ -1,3 +1,126 @@
Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0)
* Upgrade to JS SDK 9.9.0
* [Release] Change read receipt drift to be non-fractional
[\#5746](https://github.com/matrix-org/matrix-react-sdk/pull/5746)
* [Release] Properly gate SpaceRoomView behind labs
[\#5750](https://github.com/matrix-org/matrix-react-sdk/pull/5750)
Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2)
* Fixed incorrect build output in rc.1
Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1)
* Upgrade to JS SDK 9.9.0-rc.1
* Translations update from Weblate
[\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743)
* Document behaviour of showReadReceipts=false for sent receipts
[\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739)
* Tweak sent marker code style
[\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741)
* Fix sent markers disappearing for edits/reactions
[\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737)
* Ignore to-device decryption in the room list store
[\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740)
* Spaces suggested rooms support
[\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736)
* Add tooltips to sent/sending receipts
[\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738)
* Remove a bunch of useless 'use strict' definitions
[\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735)
* [SK-1] Fix types for replaceableComponent
[\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732)
* [SK-2] Make debugging skinning problems easier
[\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733)
* Support sending invite reasons with /invite command
[\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695)
* Fix clicking on the avatar for opening member info requires pixel-perfect
accuracy
[\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717)
* Display decrypted and encrypted event source on the same dialog
[\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713)
* Fix units of TURN server expiry time
[\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730)
* Display room name in pills instead of address
[\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624)
* Refresh UI for file uploads
[\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723)
* UI refresh for uploaded files
[\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719)
* Improve message sending states to match new designs
[\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699)
* Add clipboard write permission for widgets
[\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725)
* Fix widget resizing
[\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722)
* Option for audio streaming
[\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707)
* Show a specific error for hs_disabled
[\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576)
* Add Edge to the targets list
[\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721)
* File drop UI fixes and improvements
[\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505)
* Fix Bottom border of state counters is white on the dark theme
[\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715)
* Trim spurious whitespace of nicknames
[\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332)
* Ensure HostSignupDialog border colour matches light theme
[\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716)
* Don't place another call if there's already one ongoing
[\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712)
* Space room hierarchies
[\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706)
* Iterate Space view and right panel
[\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705)
* Add a scroll to bottom on message sent setting
[\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692)
* Add .tmp files to gitignore
[\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708)
* Initial Space Room View and Creation UX
[\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704)
* Add multi language spell check
[\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452)
* Fix tetris effect (holes) in read receipts
[\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697)
* Fixed edit for markdown images
[\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703)
* Iterate Space Panel
[\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702)
* Fix read receipts for compact layout
[\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700)
* Space Store and Space Panel for Room List filtering
[\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689)
* Log when turn creds expire
[\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691)
* Null check for maxHeight in call view
[\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690)
* Autocomplete invited users
[\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687)
* Add send message button
[\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535)
* Move call buttons to the room header
[\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693)
* Use the default SSSS key if the default is set
[\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638)
* Initial Spaces feature flag
[\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668)
* Clean up code edge cases and add helpers
[\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667)
* Clean up widgets when leaving the room
[\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684)
* Fix read receipts?
[\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567)
* Fix MAU usage alerts
[\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678)
Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0)

View file

@ -21,14 +21,14 @@ caret nodes (more on that later).
For these reasons it doesn't use `innerText`, `textContent` or anything similar. For these reasons it doesn't use `innerText`, `textContent` or anything similar.
The model addresses any content in the editor within as an offset within this string. The model addresses any content in the editor within as an offset within this string.
The caret position is thus also converted from a position in the DOM tree The caret position is thus also converted from a position in the DOM tree
to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.ts`.
Once the content string and caret offset is calculated, it is passed to the `update()` Once the content string and caret offset is calculated, it is passed to the `update()`
method of the model. The model first calculates the same content string of its current parts, method of the model. The model first calculates the same content string of its current parts,
basically just concatenating their text. It then looks for differences between basically just concatenating their text. It then looks for differences between
the current and the new content string. The diffing algorithm is very basic, the current and the new content string. The diffing algorithm is very basic,
and assumes there is only one change around the caret offset, and assumes there is only one change around the caret offset,
so this should be very inexpensive. See `diff.js` for details. so this should be very inexpensive. See `diff.ts` for details.
The result of the diffing is the strings that were added and/or removed from The result of the diffing is the strings that were added and/or removed from
the current content. These differences are then applied to the parts, the current content. These differences are then applied to the parts,
@ -51,7 +51,7 @@ which relate poorly to text input or changes, and don't need the `beforeinput` e
which isn't broadly supported yet. which isn't broadly supported yet.
Once the parts of the model are updated, the DOM of the editor is then reconciled Once the parts of the model are updated, the DOM of the editor is then reconciled
with the new model state, see `renderModel` in `render.js` for this. with the new model state, see `renderModel` in `render.ts` for this.
If the model didn't reject the input and didn't make any additional changes, If the model didn't reject the input and didn't make any additional changes,
this won't make any changes to the DOM at all, and should thus be fairly efficient. this won't make any changes to the DOM at all, and should thus be fairly efficient.

View file

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

View file

@ -395,6 +395,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
border: 1px solid $accent-color; border: 1px solid $accent-color;
color: $accent-color; color: $accent-color;
background-color: $button-secondary-bg-color; background-color: $button-secondary-bg-color;
font-family: inherit;
} }
.mx_Dialog button:last-child { .mx_Dialog button:last-child {

View file

@ -19,7 +19,8 @@ $roomListCollapsedWidth: 68px;
.mx_LeftPanel { .mx_LeftPanel {
background-color: $roomlist-bg-color; background-color: $roomlist-bg-color;
min-width: 260px; // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
min-width: 206px;
max-width: 50%; max-width: 50%;
// Create a row-based flexbox for the GroupFilterPanel and the room list // Create a row-based flexbox for the GroupFilterPanel and the room list

View file

@ -66,7 +66,7 @@ limitations under the License.
} }
/* not the left panel, and not the resize handle, so the roomview/groupview/... */ /* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) { .mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle) {
background-color: $primary-bg-color; background-color: $primary-bg-color;
flex: 1 1 0; flex: 1 1 0;

View file

@ -16,9 +16,8 @@ limitations under the License.
$topLevelHeight: 32px; $topLevelHeight: 32px;
$nestedHeight: 24px; $nestedHeight: 24px;
$gutterSize: 17px; $gutterSize: 16px;
$activeStripeSize: 4px; $activeBorderTransparentGap: 1px;
$activeBorderTransparentGap: 2px;
$activeBackgroundColor: $roomtile-selected-bg-color; $activeBackgroundColor: $roomtile-selected-bg-color;
$activeBorderColor: $secondary-fg-color; $activeBorderColor: $secondary-fg-color;
@ -36,6 +35,7 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpacePanel_spaceTreeWrapper { .mx_SpacePanel_spaceTreeWrapper {
flex: 1; flex: 1;
overflow-y: scroll;
} }
.mx_SpacePanel_toggleCollapse { .mx_SpacePanel_toggleCollapse {
@ -63,21 +63,26 @@ $activeBorderColor: $secondary-fg-color;
} }
.mx_AutoHideScrollbar { .mx_AutoHideScrollbar {
padding: 16px 12px 16px 0; padding: 8px 0 16px;
} }
.mx_SpaceButton_toggleCollapse { .mx_SpaceButton_toggleCollapse {
cursor: pointer; cursor: pointer;
} }
.mx_SpaceItem.collapsed { .mx_SpaceTreeLevel {
.mx_SpaceButton { display: flex;
.mx_NotificationBadge { flex-direction: column;
right: -4px; max-width: 250px;
top: -4px; flex-grow: 1;
}
} }
.mx_SpaceItem {
display: inline-flex;
flex-flow: wrap;
}
.mx_SpaceItem.collapsed {
& > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
@ -89,34 +94,42 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
margin-left: $gutterSize; margin-left: $gutterSize;
min-width: 40px;
} }
.mx_SpaceButton { .mx_SpaceButton {
border-radius: 8px; border-radius: 8px;
position: relative;
margin-bottom: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px; padding: 4px 4px 4px 0;
width: 100%;
&.mx_SpaceButton_active { &.mx_SpaceButton_active {
&:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper {
background-color: $activeBackgroundColor; background-color: $activeBackgroundColor;
border-radius: 8px;
} }
&.mx_SpaceButton_narrow { &.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper {
.mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder { padding: $activeBorderTransparentGap;
border: 2px $activeBorderColor solid; border: 3px $activeBorderColor solid;
border-radius: 11px;
}
} }
} }
.mx_SpaceButton_selectionWrapper { .mx_SpaceButton_selectionWrapper {
position: relative;
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-items: center;
border-radius: 12px;
padding: 4px;
}
&:not(.mx_SpaceButton_narrow) {
.mx_SpaceButton_selectionWrapper {
width: 100%;
padding-right: 16px;
overflow: hidden;
}
} }
.mx_SpaceButton_name { .mx_SpaceButton_name {
@ -124,7 +137,6 @@ $activeBorderColor: $secondary-fg-color;
margin-left: 8px; margin-left: 8px;
white-space: nowrap; white-space: nowrap;
display: block; display: block;
max-width: 150px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
padding-right: 8px; padding-right: 8px;
@ -133,8 +145,10 @@ $activeBorderColor: $secondary-fg-color;
} }
.mx_SpaceButton_toggleCollapse { .mx_SpaceButton_toggleCollapse {
width: calc($gutterSize - $activeStripeSize); width: $gutterSize;
margin-left: 1px; // negative margin to place it correctly even with the complex
// 4px selection border each space button has when active
margin-right: -4px;
height: 20px; height: 20px;
mask-position: center; mask-position: center;
mask-size: 20px; mask-size: 20px;
@ -172,11 +186,6 @@ $activeBorderColor: $secondary-fg-color;
} }
} }
.mx_SpaceButton_avatarPlaceholder {
border: $activeBorderTransparentGap transparent solid;
padding: $activeBorderTransparentGap;
}
&.mx_SpaceButton_new .mx_SpaceButton_icon { &.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $accent-color; background-color: $accent-color;
transition: all .1s ease-in-out; // TODO transition transition: all .1s ease-in-out; // TODO transition
@ -196,22 +205,9 @@ $activeBorderColor: $secondary-fg-color;
} }
} }
.mx_BaseAvatar {
/* moving the border-radius to this element from _image
element so we can add a border to it without the initials being displaced */
overflow: hidden;
border: 2px transparent solid;
padding: $activeBorderTransparentGap;
.mx_BaseAvatar_initial {
top: $activeBorderTransparentGap;
left: $activeBorderTransparentGap;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
} }
}
.mx_SpaceButton_menuButton { .mx_SpaceButton_menuButton {
width: 20px; width: 20px;
@ -219,8 +215,9 @@ $activeBorderColor: $secondary-fg-color;
height: 20px; height: 20px;
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
position: relative;
display: none; display: none;
position: absolute;
right: 4px;
&::before { &::before {
top: 2px; top: 2px;
@ -239,9 +236,8 @@ $activeBorderColor: $secondary-fg-color;
} }
.mx_SpacePanel_badgeContainer { .mx_SpacePanel_badgeContainer {
position: absolute;
height: 16px; height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
// Create a flexbox to make aligning dot badges easier // Create a flexbox to make aligning dot badges easier
display: flex; display: flex;
@ -261,14 +257,25 @@ $activeBorderColor: $secondary-fg-color;
&.collapsed { &.collapsed {
.mx_SpaceButton { .mx_SpaceButton {
.mx_SpacePanel_badgeContainer { .mx_SpacePanel_badgeContainer {
position: absolute; right: -3px;
right: 0px; top: -3px;
top: 2px; }
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
// when we draw the selection border we move the relative bounds of our parent
// so update our position within the bounds of the parent to maintain position overall
right: -6px;
top: -6px;
} }
} }
} }
&:not(.collapsed) { &:not(.collapsed) {
.mx_SpacePanel_badgeContainer {
position: absolute;
right: 4px;
}
.mx_SpaceButton:hover, .mx_SpaceButton:hover,
.mx_SpaceButton:focus-within, .mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen { .mx_SpaceButton_hasMenuOpen {

View file

@ -31,7 +31,8 @@ limitations under the License.
display: flex; display: flex;
.mx_BaseAvatar { .mx_BaseAvatar {
margin-right: 16px; margin-right: 12px;
align-self: center;
} }
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
@ -47,6 +48,7 @@ limitations under the License.
} }
> div { > div {
font-weight: 400;
color: $secondary-fg-color; color: $secondary-fg-color;
font-size: $font-15px; font-size: $font-15px;
line-height: $font-24px; line-height: $font-24px;
@ -55,38 +57,71 @@ limitations under the License.
} }
.mx_Dialog_content { .mx_Dialog_content {
// TODO fix scrollbar
//display: flex;
//flex-direction: column;
//height: calc(100% - 80px);
.mx_AccessibleButton_kind_link { .mx_AccessibleButton_kind_link {
padding: 0; padding: 0;
} }
.mx_SearchBox { .mx_SearchBox {
margin: 24px 0 28px; margin: 24px 0 16px;
}
.mx_SpaceRoomDirectory_noResults {
text-align: center;
> div {
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
}
} }
.mx_SpaceRoomDirectory_listHeader { .mx_SpaceRoomDirectory_listHeader {
display: flex; display: flex;
font-size: $font-12px; min-height: 32px;
line-height: $font-15px; align-items: center;
color: $secondary-fg-color; font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
.mx_FormButton { .mx_AccessibleButton {
margin-bottom: 8px; padding: 2px 8px;
font-weight: normal;
& + .mx_AccessibleButton {
margin-left: 16px;
}
} }
> span { > span {
margin: auto 0 0 auto; margin-left: auto;
}
}
.mx_SpaceRoomDirectory_error {
position: relative;
font-weight: $font-semi-bold;
color: $notice-primary-color;
font-size: $font-15px;
line-height: $font-18px;
margin: 20px auto 12px;
padding-left: 24px;
width: max-content;
&::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
left: 0;
background-image: url("$(res)/img/element-icons/warning-badge.svg");
} }
} }
} }
} }
.mx_SpaceRoomDirectory_list { .mx_SpaceRoomDirectory_list {
margin-top: 8px; margin-top: 16px;
padding-bottom: 40px;
.mx_SpaceRoomDirectory_roomCount { .mx_SpaceRoomDirectory_roomCount {
> h3 { > h3 {
@ -106,114 +141,128 @@ limitations under the License.
} }
.mx_SpaceRoomDirectory_subspace { .mx_SpaceRoomDirectory_subspace {
margin-top: 8px;
.mx_SpaceRoomDirectory_subspace_info {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
.mx_BaseAvatar {
margin-right: 12px;
vertical-align: middle;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
} }
}
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_subspace_toggle {
text-align: right; position: absolute;
height: min-content; left: -1px;
margin-left: auto; top: 10px;
margin-right: 16px; height: 16px;
width: 16px;
border-radius: 4px;
background-color: $primary-bg-color;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $tertiary-fg-color;
mask-size: 16px;
transform: rotate(270deg);
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
&.mx_SpaceRoomDirectory_subspace_toggle_shown::before {
transform: rotate(0deg);
} }
} }
.mx_SpaceRoomDirectory_subspace_children { .mx_SpaceRoomDirectory_subspace_children {
margin-left: 12px; position: relative;
border-left: 2px solid $space-button-outline-color; padding-left: 12px;
padding-left: 24px;
}
} }
.mx_SpaceRoomDirectory_roomTile { .mx_SpaceRoomDirectory_roomTile {
padding: 16px; position: relative;
padding: 6px 16px;
border-radius: 8px; border-radius: 8px;
border: 1px solid $space-button-outline-color; min-height: 56px;
margin: 8px 0 16px;
display: flex;
min-height: 76px;
box-sizing: border-box; box-sizing: border-box;
&.mx_AccessibleButton:hover { display: grid;
background-color: rgba(141, 151, 165, 0.1); grid-template-columns: 20px auto max-content;
} grid-column-gap: 8px;
align-items: center;
.mx_BaseAvatar { .mx_BaseAvatar {
margin-right: 16px; grid-row: 1;
margin-top: 6px; grid-column: 1;
} }
.mx_SpaceRoomDirectory_roomTile_info {
display: inline-block;
font-size: $font-15px;
flex-grow: 1;
height: min-content;
margin: auto 0;
.mx_SpaceRoomDirectory_roomTile_name { .mx_SpaceRoomDirectory_roomTile_name {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px; line-height: $font-18px;
grid-row: 1;
grid-column: 2;
.mx_InfoTooltip {
display: inline;
margin-left: 12px;
color: $tertiary-fg-color;
font-size: $font-12px;
line-height: $font-15px;
.mx_InfoTooltip_icon {
margin-right: 4px;
} }
.mx_SpaceRoomDirectory_roomTile_topic {
line-height: $font-24px;
color: $secondary-fg-color;
} }
} }
.mx_SpaceRoomDirectory_roomTile_memberCount { .mx_SpaceRoomDirectory_roomTile_info {
position: relative; font-size: $font-12px;
margin: auto 0 auto 24px; line-height: $font-15px;
padding: 0 0 0 28px; color: $tertiary-fg-color;
line-height: $font-24px; grid-row: 2;
display: inline-block; grid-column: 1/3;
width: 32px;
&::before {
position: absolute;
content: '';
width: 24px;
height: 24px;
top: 0;
left: 0;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
background-color: $secondary-fg-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
}
} }
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
width: 180px;
text-align: right; text-align: right;
margin-left: 28px; margin-left: 20px;
display: inline-flex; grid-column: 3;
align-items: center; grid-row: 1/3;
.mx_AccessibleButton { .mx_AccessibleButton {
vertical-align: middle; padding: 6px 18px;
& + .mx_AccessibleButton { display: none;
margin-left: 24px; }
.mx_Checkbox {
display: inline-flex;
vertical-align: middle;
margin-left: 12px;
} }
} }
&:hover {
background-color: $groupFilterPanel-bg-color;
.mx_AccessibleButton {
display: inline-block;
}
}
}
.mx_SpaceRoomDirectory_roomTile,
.mx_SpaceRoomDirectory_subspace_children {
&::before {
content: "";
position: absolute;
background-color: $groupFilterPanel-bg-color;
width: 1px;
height: 100%;
left: 6px;
top: 0;
} }
} }
@ -225,4 +274,17 @@ limitations under the License.
color: $secondary-fg-color; color: $secondary-fg-color;
} }
} }
> hr {
border: none;
height: 1px;
background-color: rgba(141, 151, 165, 0.2);
margin: 20px 0;
}
.mx_SpaceRoomDirectory_createRoom {
display: block;
margin: 16px auto 0;
width: max-content;
}
} }

View file

@ -16,6 +16,51 @@ limitations under the License.
$SpaceRoomViewInnerWidth: 428px; $SpaceRoomViewInnerWidth: 428px;
@define-mixin SpacePillButton {
position: relative;
padding: 16px 32px 16px 72px;
width: 432px;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid $input-darker-bg-color;
font-size: $font-15px;
margin: 20px 0;
> h3 {
font-weight: $font-semi-bold;
margin: 0 0 4px;
}
> span {
color: $secondary-fg-color;
}
&::before {
position: absolute;
content: '';
width: 32px;
height: 32px;
top: 24px;
left: 20px;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 24px;
background-color: $tertiary-fg-color;
}
&:hover {
border-color: $accent-color;
&::before {
background-color: $accent-color;
}
> span {
color: $primary-fg-color;
}
}
}
.mx_SpaceRoomView { .mx_SpaceRoomView {
.mx_MainSplit > div:first-child { .mx_MainSplit > div:first-child {
padding: 80px 60px; padding: 80px 60px;
@ -331,64 +376,8 @@ $SpaceRoomViewInnerWidth: 428px;
} }
.mx_SpaceRoomView_privateScope { .mx_SpaceRoomView_privateScope {
.mx_RadioButton { .mx_AccessibleButton {
width: $SpaceRoomViewInnerWidth; @mixin SpacePillButton;
border-radius: 8px;
border: 1px solid $space-button-outline-color;
padding: 16px 16px 16px 72px;
margin-top: 36px;
cursor: pointer;
box-sizing: border-box;
position: relative;
> div:first-of-type {
// hide radio dot
display: none;
}
.mx_RadioButton_content {
margin: 0;
> h3 {
margin: 0 0 4px;
font-size: $font-15px;
font-weight: $font-semi-bold;
line-height: $font-18px;
}
> div {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
}
}
&::before {
content: "";
position: absolute;
height: 32px;
width: 32px;
top: 24px;
left: 20px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
}
.mx_RadioButton_checked {
border-color: $accent-color;
.mx_RadioButton_content {
> div {
color: $primary-fg-color;
}
}
&::before {
background-color: $accent-color;
}
} }
.mx_SpaceRoomView_privateScope_justMeButton::before { .mx_SpaceRoomView_privateScope_justMeButton::before {

View file

@ -26,50 +26,6 @@ limitations under the License.
position: relative; position: relative;
} }
.mx_CompleteSecurity_clients {
width: max-content;
margin: 36px auto 0;
.mx_CompleteSecurity_clients_desktop, .mx_CompleteSecurity_clients_mobile {
position: relative;
width: 160px;
text-align: center;
padding-top: 64px;
display: inline-block;
&::before {
content: '';
position: absolute;
height: 48px;
width: 48px;
left: 56px;
top: 0;
background-color: $muted-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
}
.mx_CompleteSecurity_clients_desktop {
margin-right: 56px;
}
.mx_CompleteSecurity_clients_desktop::before {
mask-image: url('$(res)/img/feather-customised/monitor.svg');
}
.mx_CompleteSecurity_clients_mobile::before {
mask-image: url('$(res)/img/feather-customised/smartphone.svg');
}
p {
margin-top: 16px;
font-size: $font-12px;
color: $muted-fg-color;
text-align: center;
}
}
.mx_CompleteSecurity_heroIcon { .mx_CompleteSecurity_heroIcon {
width: 128px; width: 128px;
height: 128px; height: 128px;

View file

@ -60,6 +60,8 @@ limitations under the License.
width: 27px; width: 27px;
height: 24px; height: 24px;
box-sizing: border-box; box-sizing: border-box;
background: none;
vertical-align: middle;
} }
.mx_MessageComposerFormatBar_button::after { .mx_MessageComposerFormatBar_button::after {

View file

@ -189,6 +189,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/settings.svg'); mask-image: url('$(res)/img/element-icons/settings.svg');
} }
.mx_RoomTile_iconInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
.mx_RoomTile_iconSignOut::before { .mx_RoomTile_iconSignOut::before {
mask-image: url('$(res)/img/element-icons/leave.svg'); mask-image: url('$(res)/img/element-icons/leave.svg');
} }

View file

@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: the space panel currently does not have a fixed width, $spacePanelWidth: 71px;
// just the headers at each level have a max-width of 150px
// so this will look slightly off for now. We should probably use css grid for the whole main layout...
$spacePanelWidth: 200px;
.mx_SpaceCreateMenu_wrapper { .mx_SpaceCreateMenu_wrapper {
// background blur everything except SpacePanel // background blur everything except SpacePanel
@ -48,53 +45,11 @@ $spacePanelWidth: 200px;
} }
.mx_SpaceCreateMenuType { .mx_SpaceCreateMenuType {
position: relative; @mixin SpacePillButton;
padding: 16px 32px 16px 72px;
width: 432px;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid $input-darker-bg-color;
font-size: $font-15px;
margin: 20px 0;
> h3 {
font-weight: $font-semi-bold;
margin: 0 0 4px;
}
> span {
color: $secondary-fg-color;
}
&::before {
position: absolute;
content: '';
width: 32px;
height: 32px;
top: 24px;
left: 20px;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 32px;
background-color: $tertiary-fg-color;
}
&:hover {
border-color: $accent-color;
&::before {
background-color: $accent-color;
}
> span {
color: $primary-fg-color;
}
}
} }
.mx_SpaceCreateMenuType_public::before { .mx_SpaceCreateMenuType_public::before {
mask-image: url('$(res)/img/globe.svg'); mask-image: url('$(res)/img/globe.svg');
mask-size: 26px;
} }
.mx_SpaceCreateMenuType_private::before { .mx_SpaceCreateMenuType_private::before {
mask-image: url('$(res)/img/element-icons/lock.svg'); mask-image: url('$(res)/img/element-icons/lock.svg');

View file

@ -16,38 +16,7 @@ limitations under the License.
.mx_SpacePublicShare { .mx_SpacePublicShare {
.mx_AccessibleButton { .mx_AccessibleButton {
border: 1px solid $space-button-outline-color; @mixin SpacePillButton;
box-sizing: border-box;
border-radius: 8px;
padding: 12px 24px 12px 52px;
margin-top: 16px;
width: $SpaceRoomViewInnerWidth;
font-size: $font-15px;
line-height: $font-24px;
position: relative;
display: flex;
> span {
color: #368bd6;
margin-left: auto;
}
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::before {
content: "";
position: absolute;
width: 30px;
height: 30px;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
background: $muted-fg-color;
left: 12px;
top: 9px;
}
&.mx_SpacePublicShare_shareButton::before { &.mx_SpacePublicShare_shareButton::before {
mask-image: url('$(res)/img/element-icons/link.svg'); mask-image: url('$(res)/img/element-icons/link.svg');

View file

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#737D8C" style="mix-blend-mode:multiply"/> <circle cx="8" cy="8" r="8" fill="#FF4B55"/>
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/> <rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/> <rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 283 B

View file

@ -1,5 +0,0 @@
<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="M2 5C2 3.89543 2.89543 3 4 3H20C21.1046 3 22 3.89543 22 5V15C22 16.1046 21.1046 17 20 17H4C2.89543 17 2 16.1046 2 15V5Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 21H16" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17V21" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 510 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4C5 2.89543 5.89543 2 7 2H17C18.1046 2 19 2.89543 19 4V20C19 21.1046 18.1046 22 17 22H7C5.89543 22 5 21.1046 5 20V4Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="18" r="1" fill="#2E2F32"/>
</svg>

Before

Width:  |  Height:  |  Size: 386 B

View file

@ -788,6 +788,11 @@ export default class CallHandler {
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw // don't remove the call yet: let the hangup event handler do it (otherwise it will throw
// the hangup event away) // the hangup event away)
break; break;
case 'hangup_all':
for (const call of this.calls.values()) {
call.hangup(CallErrorCode.UserHangup, false);
}
break;
case 'answer': { case 'answer': {
if (!this.calls.has(payload.room_id)) { if (!this.calls.has(payload.room_id)) {
return; // no call to answer return; // no call to answer

View file

@ -14,9 +14,9 @@
limitations under the License. limitations under the License.
*/ */
import * as Matrix from 'matrix-js-sdk';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {SettingLevel} from "./settings/SettingLevel"; import {SettingLevel} from "./settings/SettingLevel";
import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
export default { export default {
hasAnyLabeledDevices: async function() { hasAnyLabeledDevices: async function() {
@ -54,24 +54,24 @@ export default {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
Matrix.setMatrixCallAudioOutput(audioOutDeviceId); setMatrixCallAudioOutput(audioOutDeviceId);
Matrix.setMatrixCallAudioInput(audioDeviceId); setMatrixCallAudioInput(audioDeviceId);
Matrix.setMatrixCallVideoInput(videoDeviceId); setMatrixCallVideoInput(videoDeviceId);
}, },
setAudioOutput: function(deviceId) { setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioOutput(deviceId); setMatrixCallAudioOutput(deviceId);
}, },
setAudioInput: function(deviceId) { setAudioInput: function(deviceId) {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId); setMatrixCallAudioInput(deviceId);
}, },
setVideoInput: function(deviceId) { setVideoInput: function(deviceId) {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallVideoInput(deviceId); setMatrixCallVideoInput(deviceId);
}, },
getAudioOutput: function() { getAudioOutput: function() {

View file

@ -237,6 +237,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
'details', 'summary',
], ],
allowedAttributes: { allowedAttributes: {
// custom ones first: // custom ones first:

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { createClient } from 'matrix-js-sdk/src/matrix';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import Modal from './Modal'; import Modal from './Modal';

View file

@ -17,8 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising import { createClient } from 'matrix-js-sdk/src/matrix';
import Matrix from 'matrix-js-sdk';
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes"; import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
@ -219,7 +218,7 @@ export function attemptTokenLogin(
button: _t("Try again"), button: _t("Try again"),
onFinished: tryAgain => { onFinished: tryAgain => {
if (tryAgain) { if (tryAgain) {
const cli = Matrix.createClient({ const cli = createClient({
baseUrl: homeserver, baseUrl: homeserver,
idBaseUrl: identityServer, idBaseUrl: identityServer,
}); });
@ -276,7 +275,7 @@ function registerAsGuest(
console.log(`Doing guest login on ${hsUrl}`); console.log(`Doing guest login on ${hsUrl}`);
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
const client = Matrix.createClient({ const client = createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
}); });

View file

@ -19,7 +19,7 @@ limitations under the License.
*/ */
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising // @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
import Matrix from "matrix-js-sdk"; import {createClient} from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { IMatrixClientCreds } from "./MatrixClientPeg"; import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
@ -115,7 +115,7 @@ export default class Login {
*/ */
public createTemporaryClient(): MatrixClient { public createTemporaryClient(): MatrixClient {
if (this.tempClient) return this.tempClient; // use memoization if (this.tempClient) return this.tempClient; // use memoization
return this.tempClient = Matrix.createClient({ return this.tempClient = createClient({
baseUrl: this.hsUrl, baseUrl: this.hsUrl,
idBaseUrl: this.isUrl, idBaseUrl: this.isUrl,
}); });
@ -210,7 +210,7 @@ export async function sendLoginRequest(
loginType: string, loginType: string,
loginParams: ILoginParams, loginParams: ILoginParams,
): Promise<IMatrixClientCreds> { ): Promise<IMatrixClientCreds> {
const client = Matrix.createClient({ const client = createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
idBaseUrl: isUrl, idBaseUrl: isUrl,
}); });

View file

@ -261,7 +261,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
} }
public getHomeserverName(): string { public getHomeserverName(): string {
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); const matches = /^@[^:]+:(.+)$/.exec(this.matrixClient.credentials.userId);
if (matches === null || matches.length < 1) { if (matches === null || matches.length < 1) {
throw new Error("Failed to derive homeserver name from user ID!"); throw new Error("Failed to derive homeserver name from user ID!");
} }
@ -296,10 +296,11 @@ class _MatrixClientPeg implements IMatrixClientPeg {
// These are always installed regardless of the labs flag so that // These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be // cross-signing features can toggle on without reloading and also be
// accessed immediately after login. // accessed immediately after login.
const customisedCallbacks = { Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
getDehydrationKey: SecurityCustomisations.getDehydrationKey, if (SecurityCustomisations.getDehydrationKey) {
}; opts.cryptoCallbacks.getDehydrationKey =
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks); SecurityCustomisations.getDehydrationKey;
}
this.matrixClient = createMatrixClient(opts); this.matrixClient = createMatrixClient(opts);

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as Matrix from 'matrix-js-sdk'; import { createClient } from 'matrix-js-sdk/src/matrix';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
/** /**
@ -32,7 +32,7 @@ export default class PasswordReset {
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
*/ */
constructor(homeserverUrl, identityUrl) { constructor(homeserverUrl, identityUrl) {
this.client = Matrix.createClient({ this.client = createClient({
baseUrl: homeserverUrl, baseUrl: homeserverUrl,
idBaseUrl: identityUrl, idBaseUrl: identityUrl,
}); });

View file

@ -17,7 +17,7 @@ limitations under the License.
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { EventStatus } from 'matrix-js-sdk'; import { EventStatus } from 'matrix-js-sdk/src/models/event';
export default class Resend { export default class Resend {
static resendUnsentEvents(room) { static resendUnsentEvents(room) {

View file

@ -21,9 +21,9 @@ import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import request from "browser-request"; import request from "browser-request";
import * as Matrix from 'matrix-js-sdk';
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import {WidgetType} from "./widgets/WidgetType"; import {WidgetType} from "./widgets/WidgetType";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
@ -153,7 +153,7 @@ export default class ScalarAuthClient {
parsedImRestUrl.path = ''; parsedImRestUrl.path = '';
parsedImRestUrl.pathname = ''; parsedImRestUrl.pathname = '';
return startTermsFlow([new Service( return startTermsFlow([new Service(
Matrix.SERVICE_TYPES.IM, SERVICE_TYPES.IM,
parsedImRestUrl.format(), parsedImRestUrl.format(),
token, token,
)], this.termsInteractionCallback).then(() => { )], this.termsInteractionCallback).then(() => {

View file

@ -237,7 +237,7 @@ Example:
*/ */
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import RoomViewStore from './stores/RoomViewStore'; import RoomViewStore from './stores/RoomViewStore';

View file

@ -19,7 +19,7 @@ import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Filter} from 'matrix-js-sdk'; import {Filter} from 'matrix-js-sdk/src/filter';
import * as sdk from '../../index'; import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg"; import EventIndexPeg from "../../indexing/EventIndexPeg";

View file

@ -35,7 +35,7 @@ import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore'; import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk"; import {Group} from "matrix-js-sdk/src/models/group";
import {allSettled, sleep} from "../../utils/promise"; import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {InteractiveAuth} from "matrix-js-sdk"; import {InteractiveAuth} from "matrix-js-sdk/src/interactive-auth";
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';

View file

@ -38,7 +38,6 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
import RoomListNumResults from "../views/rooms/RoomListNumResults"; import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media"; import {mediaFromMxc} from "../../customisations/Media";
@ -392,11 +391,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
let leftLeftPanel; let leftLeftPanel;
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now if (this.state.showGroupFilterPanel) {
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
if (SettingsStore.getValue("feature_spaces")) {
leftLeftPanel = <SpacePanel />;
} else if (this.state.showGroupFilterPanel) {
leftLeftPanel = ( leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer"> <div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel /> <GroupFilterPanel />

View file

@ -56,6 +56,7 @@ import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
@ -229,21 +230,15 @@ class LoggedInView extends React.Component<IProps, IState> {
let size; let size;
let collapsed; let collapsed;
const collapseConfig: ICollapseConfig = { const collapseConfig: ICollapseConfig = {
// TODO: the space panel currently does not have a fixed width, // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
// just the headers at each level have a max-width of 150px toggleSize: 206 - 50,
// Taking 222px for the space panel for now,
// so this will look slightly off for now,
// depending on the depth of your space tree.
// To fix this, we'll need to turn toggleSize
// into a callback so it can be measured when starting the resize operation
toggleSize: 222 + 68,
onCollapsed: (_collapsed) => { onCollapsed: (_collapsed) => {
collapsed = _collapsed; collapsed = _collapsed;
if (_collapsed) { if (_collapsed) {
dis.dispatch({action: "hide_left_panel"}, true); dis.dispatch({action: "hide_left_panel"});
window.localStorage.setItem("mx_lhs_size", '0'); window.localStorage.setItem("mx_lhs_size", '0');
} else { } else {
dis.dispatch({action: "show_left_panel"}, true); dis.dispatch({action: "show_left_panel"});
} }
}, },
onResized: (_size) => { onResized: (_size) => {
@ -670,13 +665,6 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout'; bodyClasses += ' mx_MatrixChat_useCompactLayout';
} }
const leftPanel = (
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
);
return ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>
<div <div
@ -688,7 +676,11 @@ class LoggedInView extends React.Component<IProps, IState> {
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
{ leftPanel } { SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
<ResizeHandle /> <ResizeHandle />
{ pageElement } { pageElement }
</div> </div>

View file

@ -1,8 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2017-2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,8 +15,7 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
// @ts-ignore - XXX: no idea why this import fails import { createClient } from "matrix-js-sdk/src/matrix";
import * as Matrix from "matrix-js-sdk";
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -82,9 +78,12 @@ import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal"; import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory"; import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -582,6 +581,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
break; break;
case 'logout': case 'logout':
dis.dispatch({action: "hangup_all"});
Lifecycle.logout(); Lifecycle.logout();
break; break;
case 'require_registration': case 'require_registration':
@ -606,12 +606,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (payload.screenAfterLogin) { if (payload.screenAfterLogin) {
this.screenAfterLogin = payload.screenAfterLogin; this.screenAfterLogin = payload.screenAfterLogin;
} }
this.setStateForNewView({ this.viewLogin();
view: Views.LOGIN,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
break; break;
case 'start_password_recovery': case 'start_password_recovery':
this.setStateForNewView({ this.setStateForNewView({
@ -975,6 +970,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
private viewWelcome() { private viewWelcome() {
if (shouldUseLoginForWelcome(SdkConfig.get())) {
return this.viewLogin();
}
this.setStateForNewView({ this.setStateForNewView({
view: Views.WELCOME, view: Views.WELCOME,
}); });
@ -983,6 +981,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.themeWatcher.recheck(); this.themeWatcher.recheck();
} }
private viewLogin(otherState?: any) {
this.setStateForNewView({
view: Views.LOGIN,
...otherState,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
}
private viewHome(justRegistered = false) { private viewHome(justRegistered = false) {
// The home page requires the "logged in" view, so we'll set that. // The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({ this.setStateForNewView({
@ -1139,11 +1147,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
private forgetRoom(roomId: string) { private forgetRoom(roomId: string) {
const room = MatrixClientPeg.get().getRoom(roomId);
MatrixClientPeg.get().forget(roomId).then(() => { MatrixClientPeg.get().forget(roomId).then(() => {
// Switch to home page if we're currently viewing the forgotten room // Switch to home page if we're currently viewing the forgotten room
if (this.state.currentRoomId === roomId) { if (this.state.currentRoomId === roomId) {
dis.dispatch({ action: "view_home_page" }); dis.dispatch({ action: "view_home_page" });
} }
// We have to manually update the room list because the forgotten room will not
// be notified to us, therefore the room list will have no other way of knowing
// the room is forgotten.
RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved);
}).catch((err) => { }).catch((err) => {
const errCode = err.errcode || _td("unknown error code"); const errCode = err.errcode || _td("unknown error code");
Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, { Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, {
@ -1298,17 +1312,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* Called when the session is logged out * Called when the session is logged out
*/ */
private onLoggedOut() { private onLoggedOut() {
this.notifyNewScreen('login'); this.viewLogin({
this.setStateForNewView({
view: Views.LOGIN,
ready: false, ready: false,
collapseLhs: false, collapseLhs: false,
currentRoomId: null, currentRoomId: null,
}); });
this.subTitleStatus = ''; this.subTitleStatus = '';
this.setPageSubtitle(); this.setPageSubtitle();
ThemeController.isLogin = true;
this.themeWatcher.recheck();
} }
/** /**
@ -1648,7 +1658,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let cli = MatrixClientPeg.get(); let cli = MatrixClientPeg.get();
if (!cli) { if (!cli) {
const {hsUrl, isUrl} = this.props.serverConfig; const {hsUrl, isUrl} = this.props.serverConfig;
cli = Matrix.createClient({ cli = createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
idBaseUrl: isUrl, idBaseUrl: isUrl,
}); });

View file

@ -23,7 +23,6 @@ import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils'; import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from "../../dispatcher/dispatcher";
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
@ -210,13 +209,11 @@ export default class MessagePanel extends React.Component {
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
dis.unregister(this.dispatcherRef);
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@ -229,14 +226,6 @@ export default class MessagePanel extends React.Component {
} }
} }
onAction = (payload) => {
switch (payload.action) {
case "scroll_to_bottom":
this.scrollToBottom();
break;
}
}
onShowTypingNotificationsChange = () => { onShowTypingNotificationsChange = () => {
this.setState({ this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@ -1038,6 +1027,100 @@ class CreationGrouper {
} }
} }
class RedactionGrouper {
static canStartGroup = function(panel, ev) {
return panel._shouldShowEvent(ev) && ev.isRedacted();
}
constructor(panel, ev, prevEvent, lastShownEvent) {
this.panel = panel;
this.readMarker = panel._readMarkerForEvent(
ev.getId(),
ev === lastShownEvent,
);
this.events = [ev];
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
}
shouldGroup(ev) {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
if (!this.panel._shouldShowEvent(ev)) {
return true;
}
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
add(ev) {
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
if (!this.panel._shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
getTiles() {
if (!this.events || !this.events.length) return [];
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
const key = "redactioneventlistsummary-" + (
this.prevEvent ? this.events[0].getId() : "initial"
);
const senders = new Set();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
return panel._getTilesForEvent(i === 0 ? this.prevEvent : this.events[i - 1], e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<EventListSummary
key={key}
threshold={2}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.events[0];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted // Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper { class MemberGrouper {
static canStartGroup = function(panel, ev) { static canStartGroup = function(panel, ev) {
@ -1148,4 +1231,4 @@ class MemberGrouper {
} }
// all the grouper classes that we use // all the grouper classes that we use
const groupers = [CreationGrouper, MemberGrouper]; const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper];

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
@ -24,6 +23,7 @@ import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {EventStatus} from "matrix-js-sdk/src/models/event";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -32,7 +32,7 @@ const STATUS_BAR_EXPANDED_LARGE = 2;
function getUnsentMessages(room) { function getUnsentMessages(room) {
if (!room) { return []; } if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) { return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT; return ev.status === EventStatus.NOT_SENT;
}); });
} }

View file

@ -14,27 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useMemo, useRef, useState} from "react"; import React, {useMemo, useState} from "react";
import Room from "matrix-js-sdk/src/models/room"; import Room from "matrix-js-sdk/src/models/room";
import MatrixEvent from "matrix-js-sdk/src/models/event";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import classNames from "classnames";
import {sortBy} from "lodash";
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog"; import BaseDialog from "../views/dialogs/BaseDialog";
import FormButton from "../views/elements/FormButton"; import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox"; import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import {useAsyncMemo} from "../../hooks/useAsyncMemo"; import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {shouldShowSpaceSettings} from "../../utils/space";
import {EnhancedMap} from "../../utils/maps"; import {EnhancedMap} from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox"; import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar"; import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media"; import {mediaFromMxc} from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle";
interface IProps { interface IProps {
space: Room; space: Room;
@ -72,215 +75,98 @@ export interface ISpaceSummaryEvent {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
interface ISubspaceProps { interface ITileProps {
space: ISpaceSummaryRoom; room: ISpaceSummaryRoom;
event?: MatrixEvent;
editing?: boolean; editing?: boolean;
onPreviewClick?(): void; suggested?: boolean;
queueAction?(action: IAction): void; selected?: boolean;
onJoinClick?(): void; numChildRooms?: number;
hasPermissions?: boolean;
onViewRoomClick(autoJoin: boolean): void;
onToggleClick?(): void;
} }
const SubSpace: React.FC<ISubspaceProps> = ({ const Tile: React.FC<ITileProps> = ({
space, room,
editing, editing,
event, suggested,
queueAction, selected,
onJoinClick, hasPermissions,
onPreviewClick, onToggleClick,
onViewRoomClick,
numChildRooms,
children, children,
}) => { }) => {
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); const name = room.name || room.canonical_alias || room.aliases?.[0]
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
const evContent = event?.getContent(); const [showChildren, toggleShowChildren] = useStateToggle(true);
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(space.room_id);
const myMembership = cliRoom?.getMyMembership();
// TODO DRY code
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
suggested: !v,
});
return !v;
});
};
const setRemoved = () => {
_setRemoved(v => {
queueAction({
event,
removed: !v,
suggested,
});
return !v;
});
};
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("You're in this space")}
</span>;
} else if (onJoinClick) {
actions = <React.Fragment>
<AccessibleButton onClick={onPreviewClick} kind="link">
{ _t("Preview") }
</AccessibleButton>
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment>
}
}
let url: string;
if (space.avatar_url) {
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
}
return <div className="mx_SpaceRoomDirectory_subspace">
<div className="mx_SpaceRoomDirectory_subspace_info">
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
{ name }
<div className="mx_SpaceRoomDirectory_actions">
{ actions }
</div>
</div>
<div className="mx_SpaceRoomDirectory_subspace_children">
{ children }
</div>
</div>
};
interface IAction {
event: MatrixEvent;
suggested: boolean;
removed: boolean;
}
interface IRoomTileProps {
room: ISpaceSummaryRoom;
event?: MatrixEvent;
editing?: boolean;
onPreviewClick(): void;
queueAction?(action: IAction): void;
onJoinClick?(): void;
}
const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent();
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(room.room_id); const cliRoom = cli.getRoom(room.room_id);
const myMembership = cliRoom?.getMyMembership(); const myMembership = cliRoom?.getMyMembership();
let actions; const onPreviewClick = () => onViewRoomClick(false);
if (editing && queueAction) { const onJoinClick = () => onViewRoomClick(true);
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
suggested: !v,
});
return !v;
});
};
const setRemoved = () => { let button;
_setRemoved(v => {
queueAction({
event,
removed: !v,
suggested,
});
return !v;
});
};
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") { if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText"> button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("You're in this room")} { _t("Open") }
</span>; </AccessibleButton>;
} else if (onJoinClick) { } else if (onJoinClick) {
actions = <React.Fragment> button = <AccessibleButton onClick={onJoinClick} kind="primary">
<AccessibleButton onClick={onPreviewClick} kind="link"> { _t("Join") }
{ _t("Preview") } </AccessibleButton>;
</AccessibleButton> }
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment> let checkbox;
if (onToggleClick) {
if (hasPermissions) {
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
} else {
checkbox = <TextWithTooltip
tooltip={_t("You don't have permission")}
onClick={ev => { ev.stopPropagation() }}
>
<StyledCheckbox disabled={true} />
</TextWithTooltip>;
} }
} }
let url: string; let url: string;
if (room.avatar_url) { if (room.avatar_url) {
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio)); url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio));
}
let description = _t("%(count)s members", { count: room.num_joined_members });
if (numChildRooms) {
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
}
if (room.topic) {
description += " · " + room.topic;
}
let suggestedSection;
if (suggested) {
suggestedSection = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
{ _t("Suggested") }
</InfoTooltip>;
} }
const content = <React.Fragment> const content = <React.Fragment>
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} /> <BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
<div className="mx_SpaceRoomDirectory_roomTile_info">
<div className="mx_SpaceRoomDirectory_roomTile_name"> <div className="mx_SpaceRoomDirectory_roomTile_name">
{ name } { name }
</div> { suggestedSection }
<div className="mx_SpaceRoomDirectory_roomTile_topic">
{ room.topic }
</div>
</div>
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
{ room.num_joined_members }
</div> </div>
<div className="mx_SpaceRoomDirectory_roomTile_info">
{ description }
</div>
<div className="mx_SpaceRoomDirectory_actions"> <div className="mx_SpaceRoomDirectory_actions">
{ actions } { button }
{ checkbox }
</div> </div>
</React.Fragment>; </React.Fragment>;
@ -290,9 +176,38 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
</div> </div>
} }
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}> let childToggle;
let childSection;
if (children) {
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
childToggle = <div
className={classNames("mx_SpaceRoomDirectory_subspace_toggle", {
mx_SpaceRoomDirectory_subspace_toggle_shown: showChildren,
})}
onClick={ev => {
ev.stopPropagation();
toggleShowChildren();
}}
/>;
if (showChildren) {
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
{ children }
</div>;
}
}
return <>
<AccessibleButton
className={classNames("mx_SpaceRoomDirectory_roomTile", {
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
})}
onClick={hasPermissions ? onToggleClick : onPreviewClick}
>
{ content } { content }
</AccessibleButton>; { childToggle }
</AccessibleButton>
{ childSection }
</>;
}; };
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
interface IHierarchyLevelProps { interface IHierarchyLevelProps {
spaceId: string; spaceId: string;
rooms: Map<string, ISpaceSummaryRoom>; rooms: Map<string, ISpaceSummaryRoom>;
editing?: boolean; relations: EnhancedMap<string, Map<string, ISpaceSummaryEvent>>;
relations: EnhancedMap<string, string[]>;
parents: Set<string>; parents: Set<string>;
queueAction?(action: IAction): void; selectedMap?: Map<string, Set<string>>;
onPreviewClick(roomId: string): void; onViewRoomClick(roomId: string, autoJoin: boolean): void;
onRemoveFromSpaceClick?(roomId: string): void; onToggleClick?(parentId: string, childId: string): void;
onJoinClick?(roomId: string): void;
} }
export const HierarchyLevel = ({ export const HierarchyLevel = ({
spaceId, spaceId,
rooms, rooms,
editing,
relations, relations,
parents, parents,
onPreviewClick, selectedMap,
onJoinClick, onViewRoomClick,
queueAction, onToggleClick,
}: IHierarchyLevelProps) => { }: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const space = cli.getRoom(spaceId); const space = cli.getRoom(spaceId);
// TODO respect order const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
if (!rooms.has(roomId)) return result; // TODO wat const sortedChildren = sortBy([...relations.get(spaceId)?.values()], ev => ev.content.order || null);
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
if (!rooms.has(roomId)) return result;
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
return result; return result;
}, [[], []]) || [[], []]; }, [[], []]) || [[], []];
// Don't render this subspace if it has no rooms we can show
// TODO this is broken - as a space may have subspaces we still need to show
// if (!childRooms.length) return null;
const userId = cli.getUserId();
const newParents = new Set(parents).add(spaceId); const newParents = new Set(parents).add(spaceId);
return <React.Fragment> return <React.Fragment>
{ {
childRooms.map(roomId => ( childRooms.map(roomId => (
<RoomTile <Tile
key={roomId} key={roomId}
room={rooms.get(roomId)} room={rooms.get(roomId)}
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId) suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId) selected={selectedMap?.get(spaceId)?.has(roomId)}
: undefined} onViewRoomClick={(autoJoin) => {
editing={editing} onViewRoomClick(roomId, autoJoin);
queueAction={queueAction}
onPreviewClick={() => {
onPreviewClick(roomId);
}} }}
onJoinClick={onJoinClick ? () => { hasPermissions={hasPermissions}
onJoinClick(roomId); onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
} : undefined}
/> />
)) ))
} }
{ {
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
<SubSpace <Tile
key={roomId} key={roomId}
space={rooms.get(roomId)} room={rooms.get(roomId)}
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)} numChildRooms={Array.from(relations.get(roomId)?.values() || [])
editing={editing} .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
queueAction={queueAction} suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
onPreviewClick={() => { selected={selectedMap?.get(spaceId)?.has(roomId)}
onPreviewClick(roomId); onViewRoomClick={(autoJoin) => {
}} onViewRoomClick(roomId, autoJoin);
onJoinClick={() => {
onJoinClick(roomId);
}} }}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
> >
<HierarchyLevel <HierarchyLevel
spaceId={roomId} spaceId={roomId}
rooms={rooms} rooms={rooms}
editing={editing}
relations={relations} relations={relations}
parents={newParents} parents={newParents}
onPreviewClick={onPreviewClick} selectedMap={selectedMap}
onJoinClick={onJoinClick} onViewRoomClick={onViewRoomClick}
queueAction={queueAction} onToggleClick={onToggleClick}
/> />
</SubSpace> </Tile>
)) ))
} }
</React.Fragment> </React.Fragment>
@ -415,8 +319,8 @@ export const HierarchyLevel = ({
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => { const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
// TODO pagination // TODO pagination
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const [query, setQuery] = useState(initialText); const [query, setQuery] = useState(initialText);
const [isEditing, setIsEditing] = useState(false);
const onCreateRoomClick = () => { const onCreateRoomClick = () => {
dis.dispatch({ dis.dispatch({
@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
onFinished(); onFinished();
}; };
// stored within a ref as we don't need to re-render when it changes const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const pendingActions = useRef(new Map<string, IAction>());
let adminButton; const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
const onManageButtonClicked = () => {
setIsEditing(true);
};
const onSaveButtonClicked = () => {
// TODO setBusy
pendingActions.current.forEach(({event, suggested, removed}) => {
const content = {
...event.getContent(),
suggested,
};
if (removed) {
delete content["via"];
}
cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
});
setIsEditing(false);
};
if (isEditing) {
adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("Promoted to users") }</span>
</React.Fragment>;
} else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
}
}
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
try { try {
const data = await cli.getSpaceSummary(space.roomId); const data = await cli.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap<string, string[]>(); const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
const childParentRelations = new EnhancedMap<string, Set<string>>();
const viaMap = new EnhancedMap<string, Set<string>>(); const viaMap = new EnhancedMap<string, Set<string>>();
data.events.map((ev: ISpaceSummaryEvent) => { data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) { if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
} }
if (Array.isArray(ev.content["via"])) { if (Array.isArray(ev.content["via"])) {
const set = viaMap.getOrCreate(ev.state_key, new Set()); const set = viaMap.getOrCreate(ev.state_key, new Set());
@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
} }
}); });
return [data.rooms, parentChildRelations, viaMap]; return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap];
} catch (e) { } catch (e) {
console.error(e); // TODO console.error(e); // TODO
} }
@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase().trim();
const filteredRooms = rooms.filter(r => { const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms if (!lcQuery) return roomsMap;
|| r.name?.toLowerCase().includes(lcQuery)
|| r.topic?.toLowerCase().includes(lcQuery); const directMatches = rooms.filter(r => {
return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery);
}); });
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r])); // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy
// const root = rooms.get(space.roomId); const visited = new Set<string>();
}, [rooms, query]); const queue = [...directMatches.map(r => r.room_id)];
while (queue.length) {
const roomId = queue.pop();
visited.add(roomId);
childParentMap.get(roomId)?.forEach(parentId => {
if (!visited.has(parentId)) {
queue.push(parentId);
}
});
}
// Remove any mappings for rooms which were not visited in the walk
Array.from(roomsMap.keys()).forEach(roomId => {
if (!visited.has(roomId)) {
roomsMap.delete(roomId);
}
});
return roomsMap;
}, [rooms, childParentMap, query]);
const title = <React.Fragment> const title = <React.Fragment>
<RoomAvatar room={space} height={40} width={40} /> <RoomAvatar room={space} height={32} width={32} />
<div> <div>
<h1>{ _t("Explore rooms") }</h1> <h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div> <div><RoomName room={space} /></div>
</div> </div>
</React.Fragment>; </React.Fragment>;
const explanation = const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null, _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", null,
{a: sub => { {a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>; return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}}, }},
); );
const [error, setError] = useState("");
const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false);
let content; let content;
if (roomsMap) { if (roomsMap) {
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list"> const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
let countsStr;
if (numSpaces > 1) {
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
} else if (numSpaces > 0) {
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
} else {
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
}
let editSection;
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
});
let buttons;
if (selectedRelations.length) {
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
});
const disabled = removing || saving;
buttons = <>
<AccessibleButton
onClick={async () => {
setRemoving(true);
try {
for (const [parentId, childId] of selectedRelations) {
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
parentChildMap.get(parentId).get(childId).content = {};
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</AccessibleButton>
<AccessibleButton
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
parentChildMap.get(parentId).get(childId).content = content;
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</AccessibleButton>
</>;
}
editSection = <span>
{ buttons }
</span>;
}
let results;
if (roomsMap.size) {
results = <>
<HierarchyLevel <HierarchyLevel
spaceId={space.roomId} spaceId={space.roomId}
rooms={roomsMap} rooms={roomsMap}
editing={isEditing} relations={parentChildMap}
relations={relations}
parents={new Set()} parents={new Set()}
queueAction={action => { selectedMap={selected}
pendingActions.current.set(action.event.room_id, action); onToggleClick={(parentId, childId) => {
setError("");
if (!selected.has(parentId)) {
setSelected(new Map(selected.set(parentId, new Set([childId]))));
return;
}
const parentSet = selected.get(parentId);
if (!parentSet.has(childId)) {
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
return;
}
parentSet.delete(childId);
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
}} }}
onPreviewClick={roomId => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false); showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
onFinished();
}}
onJoinClick={(roomId) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
onFinished(); onFinished();
}} }}
/> />
</AutoHideScrollbar>; <hr />
</>;
} else {
results = <div className="mx_SpaceRoomDirectory_noResults">
<h3>{ _t("No results found") }</h3>
<div>{ _t("You may want to try a different search or check for typos.") }</div>
</div>;
}
content = <>
<div className="mx_SpaceRoomDirectory_listHeader">
{ countsStr }
{ editSection }
</div>
{ error && <div className="mx_SpaceRoomDirectory_error">
{ error }
</div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
{ results }
<AccessibleButton
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</AutoHideScrollbar>
</>;
} else {
content = <Spinner />;
} }
// TODO loading state/error state // TODO loading state/error state
@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Find a room...") } placeholder={ _t("Search names and description") }
onSearch={setQuery} onSearch={setQuery}
/> />
<div className="mx_SpaceRoomDirectory_listHeader">
{ adminButton }
</div>
{ content } { content }
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, {RefObject, useContext, useRef, useState} from "react"; import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {EventSubscription} from "fbemitter";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomAvatar from "../views/avatars/RoomAvatar";
@ -31,7 +32,6 @@ import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom"; import createRoom, {IOpts, Preset} from "../../createRoom";
import Field from "../views/elements/Field"; import Field from "../views/elements/Field";
import {useEventEmitter} from "../../hooks/useEventEmitter"; import {useEventEmitter} from "../../hooks/useEventEmitter";
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
import withValidation from "../views/elements/Validation"; import withValidation from "../views/elements/Validation";
import * as Email from "../../email"; import * as Email from "../../email";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
@ -42,7 +42,6 @@ import ErrorBoundary from "../views/elements/ErrorBoundary";
import {ActionPayload} from "../../dispatcher/payloads"; import {ActionPayload} from "../../dispatcher/payloads";
import RightPanel from "./RightPanel"; import RightPanel from "./RightPanel";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import {EventSubscription} from "fbemitter";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import {useStateArray} from "../../hooks/useStateArray"; import {useStateArray} from "../../hooks/useStateArray";
@ -54,6 +53,7 @@ import {EnhancedMap} from "../../utils/maps";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar"; import MemberAvatar from "../views/avatars/MemberAvatar";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore";
interface IProps { interface IProps {
space: Room; space: Room;
@ -66,6 +66,7 @@ interface IProps {
interface IState { interface IState {
phase: Phase; phase: Phase;
showRightPanel: boolean; showRightPanel: boolean;
myMembership: string;
} }
enum Phase { enum Phase {
@ -98,6 +99,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
const [busy, setBusy] = useState(false);
let inviterSection; let inviterSection;
let joinButtons; let joinButtons;
if (myMembership === "invite") { if (myMembership === "invite") {
@ -121,11 +124,35 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
} }
joinButtons = <> joinButtons = <>
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} /> <FormButton
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} /> label={_t("Reject")}
kind="secondary"
onClick={() => {
setBusy(true);
onRejectButtonClicked();
}} />
<FormButton
label={_t("Accept")}
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
/>
</>; </>;
} else { } else {
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} /> joinButtons = (
<FormButton
label={_t("Join")}
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
/>
)
}
if (busy) {
joinButtons = <InlineSpinner />;
} }
let visibilitySection; let visibilitySection;
@ -230,10 +257,10 @@ const SpaceLanding = ({ space }) => {
try { try {
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
const parentChildRelations = new EnhancedMap<string, string[]>(); const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
data.events.map((ev: ISpaceSummaryEvent) => { data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) { if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
} }
}); });
@ -257,11 +284,10 @@ const SpaceLanding = ({ space }) => {
<HierarchyLevel <HierarchyLevel
spaceId={space.roomId} spaceId={space.roomId}
rooms={roomsMap} rooms={roomsMap}
editing={false}
relations={relations} relations={relations}
parents={new Set()} parents={new Set()}
onPreviewClick={roomId => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), [], false); // TODO showRoom(roomsMap.get(roomId), [], autoJoin);
}} }}
/> />
</AutoHideScrollbar>; </AutoHideScrollbar>;
@ -337,6 +363,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
placeholder={placeholders[i]} placeholder={placeholders[i]}
value={roomNames[i]} value={roomNames[i]}
onChange={ev => setRoomName(i, ev.target.value)} onChange={ev => setRoomName(i, ev.target.value)}
autoFocus={i === 2}
/>; />;
}); });
@ -369,7 +396,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) { if (roomNames.some(name => name.trim())) {
onClick = onNextClick; onClick = onNextClick;
buttonLabel = busy ? _t("Creating rooms...") : _t("Next") buttonLabel = busy ? _t("Creating rooms...") : _t("Continue")
} }
return <div> return <div>
@ -391,50 +418,40 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const SpaceSetupPublicShare = ({ space, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share your public space") }</h1> <h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div> <div className="mx_SpacePublicShare_description">
{ _t("It's just you at the moment, it will be even better with others.") }
</div>
<SpacePublicShare space={space} onFinished={onFinished} /> <SpacePublicShare space={space} onFinished={onFinished} />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Finish")} onClick={onFinished} /> <FormButton label={_t("Go to my first room")} onClick={onFinished} />
</div> </div>
</div>; </div>;
}; };
const SpaceSetupPrivateScope = ({ onFinished }) => { const SpaceSetupPrivateScope = ({ space, onFinished }) => {
const [option, setOption] = useState<string>(null);
return <div className="mx_SpaceRoomView_privateScope"> return <div className="mx_SpaceRoomView_privateScope">
<h1>{ _t("Who are you working with?") }</h1> <h1>{ _t("Who are you working with?") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div> <div className="mx_SpaceRoomView_description">
{ _t("Make sure the right people have access to %(name)s", { name: space.name }) }
</div>
<StyledRadioGroup <AccessibleButton
name="privateSpaceScope" className="mx_SpaceRoomView_privateScope_justMeButton"
value={option} onClick={() => { onFinished(false) }}
onChange={setOption} >
definitions={[ <h3>{ _t("Just me") }</h3>
{ <div>{ _t("A private space to organise your rooms") }</div>
value: "justMe", </AccessibleButton>
className: "mx_SpaceRoomView_privateScope_justMeButton", <AccessibleButton
label: <React.Fragment> className="mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
<h3>{ _t("Just Me") }</h3> onClick={() => { onFinished(true) }}
<div>{ _t("A private space just for you") }</div> >
</React.Fragment>,
}, {
value: "meAndMyTeammates",
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
label: <React.Fragment>
<h3>{ _t("Me and my teammates") }</h3> <h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div> <div>{ _t("A private space for you and your teammates") }</div>
</React.Fragment>, </AccessibleButton>
},
]}
/>
<div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
</div>
</div>; </div>;
}; };
@ -464,6 +481,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
onChange={ev => setEmailAddress(i, ev.target.value)} onChange={ev => setEmailAddress(i, ev.target.value)}
ref={fieldRefs[i]} ref={fieldRefs[i]}
onValidate={validateEmailRules} onValidate={validateEmailRules}
autoFocus={i === 0}
/>; />;
}); });
@ -501,9 +519,18 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
setBusy(false); setBusy(false);
}; };
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick;
buttonLabel = busy ? _t("Inviting...") : _t("Continue")
}
return <div className="mx_SpaceRoomView_inviteTeammates"> return <div className="mx_SpaceRoomView_inviteTeammates">
<h1>{ _t("Invite your teammates") }</h1> <h1>{ _t("Invite your teammates") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div> <div className="mx_SpaceRoomView_description">
{ _t("Make sure the right people have access. You can invite more later.") }
</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields } { fields }
@ -518,8 +545,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div> </div>
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton> <FormButton label={buttonLabel} disabled={busy} onClick={onClick} />
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
</div> </div>
</div>; </div>;
}; };
@ -547,17 +573,26 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
phase, phase,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
myMembership: this.props.space.getMyMembership(),
}; };
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
this.context.on("Room.myMembership", this.onMyMembership);
} }
componentWillUnmount() { componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
this.rightPanelStoreToken.remove(); this.rightPanelStoreToken.remove();
this.context.off("Room.myMembership", this.onMyMembership);
} }
private onMyMembership = (room: Room, myMembership: string) => {
if (room.roomId === this.props.space.roomId) {
this.setState({ myMembership });
}
};
private onRightPanelStoreUpdate = () => { private onRightPanelStoreUpdate = () => {
this.setState({ this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
@ -594,10 +629,43 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
} }
}; };
private goToFirstRoom = async () => {
const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId);
if (childRooms.length) {
const room = childRooms[0];
defaultDispatcher.dispatch({
action: "view_room",
room_id: room.roomId,
});
return;
}
let suggestedRooms = SpaceStore.instance.suggestedRooms;
if (SpaceStore.instance.activeSpace !== this.props.space) {
// the space store has the suggested rooms loaded for a different space, fetch the right ones
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms;
}
if (suggestedRooms.length) {
const room = suggestedRooms[0];
defaultDispatcher.dispatch({
action: "view_room",
room_id: room.room_id,
oobData: {
avatarUrl: room.avatar_url,
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
},
});
return;
}
this.setState({ phase: Phase.Landing });
};
private renderBody() { private renderBody() {
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Landing: case Phase.Landing:
if (this.props.space.getMyMembership() === "join") { if (this.state.myMembership === "join") {
return <SpaceLanding space={this.props.space} />; return <SpaceLanding space={this.props.space} />;
} else { } else {
return <SpacePreview return <SpacePreview
@ -610,17 +678,16 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What are some things you want to discuss?")} title={_t("What are some things you want to discuss?")}
description={_t("We'll create rooms for each topic.")} description={_t("Let's create a room for each of them. " +
"You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:
return <SpaceSetupPublicShare return <SpaceSetupPublicShare space={this.props.space} onFinished={this.goToFirstRoom} />;
space={this.props.space}
onFinished={() => this.setState({ phase: Phase.Landing })}
/>;
case Phase.PrivateScope: case Phase.PrivateScope:
return <SpaceSetupPrivateScope return <SpaceSetupPrivateScope
space={this.props.space}
onFinished={(invite: boolean) => { onFinished={(invite: boolean) => {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
}} }}
@ -634,7 +701,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What projects are you working on?")} title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")} description={_t("We'll create rooms for each of them. " +
"You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.Landing })} onFinished={() => this.setState({ phase: Phase.Landing })}
/>; />;
} }

View file

@ -22,8 +22,8 @@ import {LayoutPropType} from "../../settings/Layout";
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {EventTimeline} from "matrix-js-sdk"; import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
import * as Matrix from "matrix-js-sdk"; import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
@ -463,6 +463,9 @@ class TimelinePanel extends React.Component {
} }
}); });
} }
if (payload.action === "scroll_to_bottom") {
this.jumpToLiveTimeline();
}
}; };
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
@ -1007,7 +1010,7 @@ class TimelinePanel extends React.Component {
* returns a promise which will resolve when the load completes. * returns a promise which will resolve when the load completes.
*/ */
_loadTimeline(eventId, pixelOffset, offsetBase) { _loadTimeline(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow( this._timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet, MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap}); {windowLimit: this.props.timelineCap});

View file

@ -17,13 +17,14 @@ limitations under the License.
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import Matrix from "matrix-js-sdk";
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index"; import * as sdk from "../../index";
import Modal from '../../Modal'; import Modal from '../../Modal';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
@replaceableComponent("structures.UserView") @replaceableComponent("structures.UserView")
export default class UserView extends React.Component { export default class UserView extends React.Component {
@ -68,8 +69,8 @@ export default class UserView extends React.Component {
this.setState({loading: false}); this.setState({loading: false});
return; return;
} }
const fakeEvent = new Matrix.MatrixEvent({type: "m.room.member", content: profileInfo}); const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo});
const member = new Matrix.RoomMember(null, this.props.userId); const member = new RoomMember(null, this.props.userId);
member.setMembershipEvent(fakeEvent); member.setMembershipEvent(fakeEvent);
this.setState({member, loading: false}); this.setState({member, loading: false});
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk'; import {createClient} from 'matrix-js-sdk/src/matrix';
import React, {ReactNode} from 'react'; import React, {ReactNode} from 'react';
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -181,7 +181,7 @@ export default class Registration extends React.Component<IProps, IState> {
} }
const {hsUrl, isUrl} = serverConfig; const {hsUrl, isUrl} = serverConfig;
const cli = Matrix.createClient({ const cli = createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
idBaseUrl: isUrl, idBaseUrl: isUrl,
}); });

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk'; import {Group} from 'matrix-js-sdk/src/models/group';
import GroupStore from "../../../stores/GroupStore"; import GroupStore from "../../../stores/GroupStore";
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";

View file

@ -19,7 +19,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk/src/models/event';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';

View file

@ -69,6 +69,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingRoomsSet = new Set(existingRooms); const existingRoomsSet = new Set(existingRooms);
const rooms = cli.getVisibleRooms().filter(room => { const rooms = cli.getVisibleRooms().filter(room => {
return !existingRoomsSet.has(room) // not already in space return !existingRoomsSet.has(room) // not already in space
&& !room.isSpaceRoom() // not a space itself
&& room.name.toLowerCase().includes(lcQuery) // contains query && room.name.toLowerCase().includes(lcQuery) // contains query
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM && !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
}); });

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';

View file

@ -19,7 +19,6 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight'; import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Room, MatrixEvent } from "matrix-js-sdk";
import Field from "../elements/Field"; import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
@ -39,6 +38,8 @@ import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
class GenericEditor extends React.PureComponent { class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired}; // static propTypes = {onBack: PropTypes.func.isRequired};

View file

@ -100,6 +100,20 @@ export default (props) => {
); );
} }
let bugReports = null;
if (SdkConfig.get().bug_report_endpoint_url) {
bugReports = (
<p>{
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
"to help us track down the problem.", {}, {
debugLogsLink: sub => (
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
),
})
}</p>
);
}
return (<QuestionDialog return (<QuestionDialog
className="mx_FeedbackDialog" className="mx_FeedbackDialog"
hasCancelButton={!!hasFeedback} hasCancelButton={!!hasFeedback}
@ -120,14 +134,7 @@ export default (props) => {
}, },
}) })
}</p> }</p>
<p>{ {bugReports}
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
"to help us track down the problem.", {}, {
debugLogsLink: sub => (
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
),
})
}</p>
</div> </div>
{ countlyFeedbackSection } { countlyFeedbackSection }
</React.Fragment>} </React.Fragment>}

View file

@ -1256,7 +1256,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
? _t("Invite to %(spaceName)s", { ? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"), spaceName: room.name || _t("Unnamed Space"),
}) })
: _t("Invite to this room"); : _t("Invite to %(roomName)s", {
roomName: room.name || _t("Unnamed Room"),
});
let helpTextUntranslated; let helpTextUntranslated;
if (isSpace) { if (isSpace) {

View file

@ -18,7 +18,7 @@ import React, {PureComponent} from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {MatrixEvent} from "matrix-js-sdk"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown'; import Markdown from '../../../Markdown';

View file

@ -82,6 +82,33 @@ export default class RoomUpgradeWarningDialog extends React.Component {
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
let bugReports = (
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please report a bug.", {brand},
)}
</p>
);
if (SdkConfig.get().bug_report_endpoint_url) {
bugReports = (
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},
},
)}
</p>
);
}
return ( return (
<BaseDialog <BaseDialog
className='mx_RoomUpgradeWarningDialog' className='mx_RoomUpgradeWarningDialog'
@ -97,20 +124,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
"is unstable due to bugs, missing features or security vulnerabilities.", "is unstable due to bugs, missing features or security vulnerabilities.",
)} )}
</p> </p>
<p> {bugReports}
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},
},
)}
</p>
<p> <p>
{_t( {_t(
"You'll upgrade this room from <oldVersion /> to <newVersion />.", "You'll upgrade this room from <oldVersion /> to <newVersion />.",

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {Room} from "matrix-js-sdk"; import {Room} from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms"; import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -20,8 +20,8 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler'; import { _t, pickBestLanguage } from '../../../languageHandler';
import Matrix from 'matrix-js-sdk';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
class TermsCheckbox extends React.PureComponent { class TermsCheckbox extends React.PureComponent {
static propTypes = { static propTypes = {
@ -85,22 +85,22 @@ export default class TermsDialog extends React.PureComponent {
_nameForServiceType(serviceType, host) { _nameForServiceType(serviceType, host) {
switch (serviceType) { switch (serviceType) {
case Matrix.SERVICE_TYPES.IS: case SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>; return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICE_TYPES.IM: case SERVICE_TYPES.IM:
return <div>{_t("Integration Manager")}<br />({host})</div>; return <div>{_t("Integration Manager")}<br />({host})</div>;
} }
} }
_summaryForServiceType(serviceType) { _summaryForServiceType(serviceType) {
switch (serviceType) { switch (serviceType) {
case Matrix.SERVICE_TYPES.IS: case SERVICE_TYPES.IS:
return <div> return <div>
{_t("Find others by phone or email")} {_t("Find others by phone or email")}
<br /> <br />
{_t("Be found by phone or email")} {_t("Be found by phone or email")}
</div>; </div>;
case Matrix.SERVICE_TYPES.IM: case SERVICE_TYPES.IM:
return <div> return <div>
{_t("Use bots, bridges, widgets and sticker packs")} {_t("Use bots, bridges, widgets and sticker packs")}
</div>; </div>;

View file

@ -19,7 +19,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager'; import { accessSecretStorage } from '../../../../SecurityManager';

View file

@ -17,7 +17,8 @@ import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore"; import FlairStore from "../../../stores/FlairStore";

View file

@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils'; import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk'; import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout"; import {LayoutPropType} from "../../../settings/Layout";

View file

@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
render() { render() {
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
return ( return (
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}> <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
{this.props.children} {children}
{this.state.hover && <Tooltip {this.state.hover && <Tooltip
label={this.props.tooltip} label={tooltip}
tooltipClassName={this.props.tooltipClass} tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> } className={"mx_TextWithTooltip_tooltip"} /> }
</span> </span>
); );

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils'; import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import {formatTime} from '../../../DateUtils'; import {formatTime} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk'; import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {pillifyLinks, unmountPills} from '../../../utils/pillify'; import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';

View file

@ -18,7 +18,7 @@ limitations under the License.
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk'; import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixEvent} from 'matrix-js-sdk'; import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';

View file

@ -27,7 +27,7 @@ import {parseEvent} from '../../../editor/deserialize';
import {PartCreator} from '../../../editor/parts'; import {PartCreator} from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import classNames from 'classnames'; import classNames from 'classnames';
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk/src/models/event';
import BasicMessageComposer from "./BasicMessageComposer"; import BasicMessageComposer from "./BasicMessageComposer";
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -28,7 +28,7 @@ import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {Layout, LayoutPropType} from "../../../settings/Layout"; import {Layout, LayoutPropType} from "../../../settings/Layout";
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk/src/models/event';
import {formatTime} from "../../../DateUtils"; import {formatTime} from "../../../DateUtils";
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";

View file

@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent {
return ( return (
<AccessibleTooltipButton <AccessibleTooltipButton
as="span" element="button"
role="button" type="button"
onClick={this.props.onClick} onClick={this.props.onClick}
title={this.props.label} title={this.props.label}
tooltip={tooltip} tooltip={tooltip}

View file

@ -333,6 +333,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({generalMenuPosition: null}); // hide the menu this.setState({generalMenuPosition: null}); // hide the menu
}; };
private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'view_invite',
roomId: this.props.room.roomId,
});
this.setState({generalMenuPosition: null}); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: Volume) { private async saveNotifState(ev: ButtonEvent, newState: Volume) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -453,6 +464,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
const lowPriorityLabel = _t("Low Priority"); const lowPriorityLabel = _t("Low Priority");
const userId = MatrixClientPeg.get().getUserId();
const canInvite = this.props.room.canInvite(userId);
contextMenu = <IconizedContextMenu contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)} {...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu} onFinished={this.onCloseGeneralMenu}
@ -472,7 +485,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
label={lowPriorityLabel} label={lowPriorityLabel}
iconClassName="mx_RoomTile_iconArrowDown" iconClassName="mx_RoomTile_iconArrowDown"
/> />
{canInvite ? (
<IconizedContextMenuOption
onClick={this.onInviteClick}
label={_t("Invite People")}
iconClassName="mx_RoomTile_iconInvite"
/>
) : null}
<IconizedContextMenuOption <IconizedContextMenuOption
onClick={this.onOpenRoomSettings} onClick={this.onOpenRoomSettings}
label={_t("Settings")} label={_t("Settings")}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {MatrixEvent} from "matrix-js-sdk"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index"; import * as sdk from "../../../index";

View file

@ -32,7 +32,7 @@ import * as sdk from "../../../../..";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import {Service, startTermsFlow} from "../../../../../Terms"; import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk"; import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import IdentityAuthClient from "../../../../../IdentityAuthClient"; import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils"; import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';

View file

@ -84,6 +84,7 @@ export default class VoiceUserSettingsTab extends React.Component {
} }
} }
if (error) { if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {

View file

@ -108,7 +108,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
body = <React.Fragment> body = <React.Fragment>
<h2>{ _t("Create a space") }</h2> <h2>{ _t("Create a space") }</h2>
<p>{ _t("Spaces are new ways to group rooms and people. " + <p>{ _t("Spaces are new ways to group rooms and people. " +
"To join an existing space youll need an invite") }</p> "To join an existing space you'll need an invite.") }</p>
<SpaceCreateMenuType <SpaceCreateMenuType
title={_t("Public")} title={_t("Public")}
@ -140,9 +140,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
</h2> </h2>
<p> <p>
{ {
_t("Give it a photo, name and description to help you identify it.") _t("Add some details to help people recognise it.")
} { } {
_t("You can change these at any point.") _t("You can change these anytime.")
} }
</p> </p>

View file

@ -220,13 +220,19 @@ const SpacePanel = () => {
<SpaceButton <SpaceButton
className={newClasses} className={newClasses}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")} tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={menuDisplayed ? closeMenu : openMenu} onClick={menuDisplayed ? closeMenu : () => {
openMenu();
if (!isPanelCollapsed) setPanelCollapsed(true);
}}
isNarrow={isPanelCollapsed} isNarrow={isPanelCollapsed}
/> />
</AutoHideScrollbar> </AutoHideScrollbar>
<AccessibleTooltipButton <AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})} className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
onClick={evt => setPanelCollapsed(!isPanelCollapsed)} onClick={() => {
setPanelCollapsed(!isPanelCollapsed);
if (menuDisplayed) closeMenu();
}}
title={expandCollapseButtonTitle} title={expandCollapseButtonTitle}
/> />
{ contextMenu } { contextMenu }

View file

@ -41,13 +41,13 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
const success = await copyPlaintext(permalinkCreator.forRoom()); const success = await copyPlaintext(permalinkCreator.forRoom());
const text = success ? _t("Copied!") : _t("Failed to copy"); const text = success ? _t("Copied!") : _t("Failed to copy");
setCopiedText(text); setCopiedText(text);
await sleep(10); await sleep(5000);
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
setCopiedText(_t("Click to copy")); setCopiedText(_t("Click to copy"));
} }
}} }}
> >
{ _t("Share invite link") } <h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span> <span>{ copiedText }</span>
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
@ -57,7 +57,8 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
onFinished(); onFinished();
}} }}
> >
{ _t("Invite by email or username") } <h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span>
</AccessibleButton> </AccessibleButton>
</div>; </div>;
}; };

View file

@ -51,6 +51,7 @@ interface IItemProps {
isNested?: boolean; isNested?: boolean;
isPanelCollapsed?: boolean; isPanelCollapsed?: boolean;
onExpand?: Function; onExpand?: Function;
parents?: Set<string>;
} }
interface IItemState { interface IItemState {
@ -299,7 +300,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const isNarrow = this.props.isPanelCollapsed; const isNarrow = this.props.isPanelCollapsed;
const collapsed = this.state.collapsed || forceCollapsed; const collapsed = this.state.collapsed || forceCollapsed;
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId); const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
.filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space); const isActive = activeSpaces.includes(space);
const itemClasses = classNames({ const itemClasses = classNames({
"mx_SpaceItem": true, "mx_SpaceItem": true,
@ -312,11 +314,17 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
mx_SpaceButton_narrow: isNarrow, mx_SpaceButton_narrow: isNarrow,
}); });
const notificationState = SpaceStore.instance.getNotificationState(space.roomId); const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
let childItems;
if (childSpaces && !collapsed) {
childItems = <SpaceTreeLevel
spaces={childSpaces} spaces={childSpaces}
activeSpaces={activeSpaces} activeSpaces={activeSpaces}
isNested={true} isNested={true}
/> : null; parents={new Set(this.props.parents).add(this.props.space.roomId)}
/>;
}
let notifBadge; let notifBadge;
if (notificationState) { if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer"> notifBadge = <div className="mx_SpacePanel_badgeContainer">
@ -383,12 +391,14 @@ interface ITreeLevelProps {
spaces: Room[]; spaces: Room[];
activeSpaces: Room[]; activeSpaces: Room[];
isNested?: boolean; isNested?: boolean;
parents: Set<string>;
} }
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
spaces, spaces,
activeSpaces, activeSpaces,
isNested, isNested,
parents,
}) => { }) => {
return <ul className="mx_SpaceTreeLevel"> return <ul className="mx_SpaceTreeLevel">
{spaces.map(s => { {spaces.map(s => {
@ -397,6 +407,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
activeSpaces={activeSpaces} activeSpaces={activeSpaces}
space={s} space={s}
isNested={isNested} isNested={isNested}
parents={parents}
/>); />);
})} })}
</ul>; </ul>;

View file

@ -989,7 +989,7 @@
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Create a space": "Create a space", "Create a space": "Create a space",
"Spaces are new ways to group rooms and people. To join an existing space youll need an invite": "Spaces are new ways to group rooms and people. To join an existing space youll need an invite", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.",
"Public": "Public", "Public": "Public",
"Open space for anyone, best for communities": "Open space for anyone, best for communities", "Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private", "Private": "Private",
@ -998,8 +998,8 @@
"Go back": "Go back", "Go back": "Go back",
"Your public space": "Your public space", "Your public space": "Your public space",
"Your private space": "Your private space", "Your private space": "Your private space",
"Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.", "Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these at any point.": "You can change these at any point.", "You can change these anytime.": "You can change these anytime.",
"Creating...": "Creating...", "Creating...": "Creating...",
"Create": "Create", "Create": "Create",
"Expand space panel": "Expand space panel", "Expand space panel": "Expand space panel",
@ -1009,10 +1009,10 @@
"Copied!": "Copied!", "Copied!": "Copied!",
"Failed to copy": "Failed to copy", "Failed to copy": "Failed to copy",
"Share invite link": "Share invite link", "Share invite link": "Share invite link",
"Invite by email or username": "Invite by email or username", "Invite people": "Invite people",
"Invite with email or username": "Invite with email or username",
"Invite members": "Invite members", "Invite members": "Invite members",
"Share your public space": "Share your public space", "Share your public space": "Share your public space",
"Invite people": "Invite people",
"Settings": "Settings", "Settings": "Settings",
"Leave space": "Leave space", "Leave space": "Leave space",
"New room": "New room", "New room": "New room",
@ -1601,6 +1601,7 @@
"Favourited": "Favourited", "Favourited": "Favourited",
"Favourite": "Favourite", "Favourite": "Favourite",
"Low Priority": "Low Priority", "Low Priority": "Low Priority",
"Invite People": "Invite People",
"Leave Room": "Leave Room", "Leave Room": "Leave Room",
"Room options": "Room options", "Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@ -2150,10 +2151,10 @@
"Add comment": "Add comment", "Add comment": "Add comment",
"Comment": "Comment", "Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback", "Feedback": "Feedback",
"Report a bug": "Report a bug", "Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.", "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Send feedback": "Send feedback", "Send feedback": "Send feedback",
"Confirm abort of host creation": "Confirm abort of host creation", "Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
@ -2201,6 +2202,7 @@
"Go": "Go", "Go": "Go",
"Invite to %(spaceName)s": "Invite to %(spaceName)s", "Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Unnamed Space": "Unnamed Space", "Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
@ -2269,8 +2271,9 @@
"Automatically invite users": "Automatically invite users", "Automatically invite users": "Automatically invite users",
"Upgrade private room": "Upgrade private room", "Upgrade private room": "Upgrade private room",
"Upgrade public room": "Upgrade public room", "Upgrade public room": "Upgrade public room",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.",
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.", "You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
"Resend": "Resend", "Resend": "Resend",
"You're all caught up.": "You're all caught up.", "You're all caught up.": "You're all caught up.",
@ -2550,6 +2553,8 @@
"Logout": "Logout", "Logout": "Logout",
"%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created this DM.": "%(creator)s created this DM.",
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
"%(count)s messages deleted.|other": "%(count)s messages deleted.",
"%(count)s messages deleted.|one": "%(count)s message deleted.",
"Your Communities": "Your Communities", "Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
@ -2602,20 +2607,30 @@
"Drop file here to upload": "Drop file here to upload", "Drop file here to upload": "Drop file here to upload",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"Undo": "Undo", "Open": "Open",
"Remove from Space": "Remove from Space", "You don't have permission": "You don't have permission",
"No permissions": "No permissions", "%(count)s members|other": "%(count)s members",
"You're in this space": "You're in this space", "%(count)s members|one": "%(count)s member",
"You're in this room": "You're in this room", "%(count)s rooms|other": "%(count)s rooms",
"Save changes": "Save changes", "%(count)s rooms|one": "%(count)s room",
"Promoted to users": "Promoted to users", "This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Manage rooms": "Manage rooms", "Suggested": "Suggested",
"Find a room...": "Find a room...", "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces",
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
"%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
"Removing...": "Removing...",
"Mark as not suggested": "Mark as not suggested",
"Mark as suggested": "Mark as suggested",
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Create room": "Create room",
"Search names and description": "Search names and description",
"<inviter/> invites you": "<inviter/> invites you", "<inviter/> invites you": "<inviter/> invites you",
"Public space": "Public space", "Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member",
"Add existing rooms & spaces": "Add existing rooms & spaces", "Add existing rooms & spaces": "Add existing rooms & spaces",
"Default Rooms": "Default Rooms", "Default Rooms": "Default Rooms",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
@ -2628,22 +2643,24 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"At the moment only you can see it.": "At the moment only you can see it.", "Share %(name)s": "Share %(name)s",
"Finish": "Finish", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room",
"Who are you working with?": "Who are you working with?", "Who are you working with?": "Who are you working with?",
"Ensure the right people have access to the space.": "Ensure the right people have access to the space.", "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
"Just Me": "Just Me", "Just me": "Just me",
"A private space just for you": "A private space just for you", "A private space to organise your rooms": "A private space to organise your rooms",
"Me and my teammates": "Me and my teammates", "Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates", "A private space for you and your teammates": "A private space for you and your teammates",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Invite your teammates": "Invite your teammates",
"Invite by username": "Invite by username",
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
"Invite by username": "Invite by username",
"What are some things you want to discuss?": "What are some things you want to discuss?", "What are some things you want to discuss?": "What are some things you want to discuss?",
"We'll create rooms for each topic.": "We'll create rooms for each topic.", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.",
"What projects are you working on?": "What projects are you working on?", "What projects are you working on?": "What projects are you working on?",
"We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position", "Failed to load timeline position": "Failed to load timeline position",

View file

@ -28,3 +28,7 @@ export function resetSkin() {
export function getComponent(componentName) { export function getComponent(componentName) {
return Skinner.getComponent(componentName); return Skinner.getComponent(componentName);
} }
// Import the js-sdk so the proper `request` object can be set. This does some
// magic with the browser injection to make all subsequent imports work fine.
import "matrix-js-sdk";

View file

@ -16,7 +16,8 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg"; import PlatformPeg from "../PlatformPeg";
import {MatrixClientPeg} from "../MatrixClientPeg"; import {MatrixClientPeg} from "../MatrixClientPeg";
import {EventTimeline, RoomMember} from 'matrix-js-sdk'; import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {sleep} from "../utils/promise"; import {sleep} from "../utils/promise";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import {EventEmitter} from "events"; import {EventEmitter} from "events";

View file

@ -434,15 +434,37 @@ function selectQuery(store, keyRange, resultMapper) {
/** /**
* Configure rage shaking support for sending bug reports. * Configure rage shaking support for sending bug reports.
* Modifies globals. * Modifies globals.
* @param {boolean} setUpPersistence When true (default), the persistence will
* be set up immediately for the logs.
* @return {Promise} Resolves when set up. * @return {Promise} Resolves when set up.
*/ */
export function init() { export function init(setUpPersistence = true) {
if (global.mx_rage_initPromise) { if (global.mx_rage_initPromise) {
return global.mx_rage_initPromise; return global.mx_rage_initPromise;
} }
global.mx_rage_logger = new ConsoleLogger(); global.mx_rage_logger = new ConsoleLogger();
global.mx_rage_logger.monkeyPatch(window.console); global.mx_rage_logger.monkeyPatch(window.console);
if (setUpPersistence) {
return tryInitStorage();
}
global.mx_rage_initPromise = Promise.resolve();
return global.mx_rage_initPromise;
}
/**
* Try to start up the rageshake storage for logs. If not possible (client unsupported)
* then this no-ops.
* @return {Promise} Resolves when complete.
*/
export function tryInitStorage() {
if (global.mx_rage_initStoragePromise) {
return global.mx_rage_initStoragePromise;
}
console.log("Configuring rageshake persistence...");
// just *accessing* indexedDB throws an exception in firefox with // just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled. // indexeddb disabled.
let indexedDB; let indexedDB;
@ -452,11 +474,11 @@ export function init() {
if (indexedDB) { if (indexedDB) {
global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger); global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger);
global.mx_rage_initPromise = global.mx_rage_store.connect(); global.mx_rage_initStoragePromise = global.mx_rage_store.connect();
return global.mx_rage_initPromise; return global.mx_rage_initStoragePromise;
} }
global.mx_rage_initPromise = Promise.resolve(); global.mx_rage_initStoragePromise = Promise.resolve();
return global.mx_rage_initPromise; return global.mx_rage_initStoragePromise;
} }
export function flush() { export function flush() {

View file

@ -29,13 +29,22 @@ interface IState {
avatarUrl?: string; avatarUrl?: string;
} }
const KEY_DISPLAY_NAME = "mx_profile_displayname";
const KEY_AVATAR_URL = "mx_profile_avatar_url";
export class OwnProfileStore extends AsyncStoreWithClient<IState> { export class OwnProfileStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new OwnProfileStore(); private static internalInstance = new OwnProfileStore();
private monitoredUser: User; private monitoredUser: User;
private constructor() { private constructor() {
super(defaultDispatcher, {}); // seed from localstorage because otherwise we won't get these values until a whole network
// round-trip after the client is ready, and we often load widgets in that time, and we'd
// and up passing them an incorrect display name
super(defaultDispatcher, {
displayName: window.localStorage.getItem(KEY_DISPLAY_NAME),
avatarUrl: window.localStorage.getItem(KEY_AVATAR_URL),
});
} }
public static get instance(): OwnProfileStore { public static get instance(): OwnProfileStore {
@ -115,6 +124,16 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
// We specifically do not use the User object we stored for profile info as it // We specifically do not use the User object we stored for profile info as it
// could easily be wrong (such as per-room instead of global profile). // could easily be wrong (such as per-room instead of global profile).
const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId()); const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId());
if (profileInfo.displayname) {
window.localStorage.setItem(KEY_DISPLAY_NAME, profileInfo.displayname);
} else {
window.localStorage.removeItem(KEY_DISPLAY_NAME);
}
if (profileInfo.avatar_url) {
window.localStorage.setItem(KEY_AVATAR_URL, profileInfo.avatar_url);
} else {
window.localStorage.removeItem(KEY_AVATAR_URL);
}
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url}); await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
}; };

View file

@ -118,22 +118,31 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
if (space) { if (space) {
try { const data = await this.fetchSuggestedRooms(space);
const data: {
rooms: ISpaceSummaryRoom[];
events: ISpaceSummaryEvent[];
} = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, MAX_SUGGESTED_ROOMS);
if (this._activeSpace === space) { if (this._activeSpace === space) {
this._suggestedRooms = data.rooms.filter(roomInfo => { this._suggestedRooms = data.rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id);
}); });
this.emit(SUGGESTED_ROOMS, this._suggestedRooms); this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
} }
}
}
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => {
try {
const data: {
rooms: ISpaceSummaryRoom[];
events: ISpaceSummaryEvent[];
} = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit);
return data;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} return {
} rooms: [],
events: [],
};
};
public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, { return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, {
@ -385,7 +394,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => { private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => {
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees // If the room was in favourites and now isn't or the opposite then update its position in the trees
if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) { const oldTags = lastEvent.getContent()?.tags;
const newTags = ev.getContent()?.tags;
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
this.onRoomUpdate(room); this.onRoomUpdate(room);
} }
} }

View file

@ -655,6 +655,18 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
if (!algorithmTags) return [DefaultTagID.Untagged]; if (!algorithmTags) return [DefaultTagID.Untagged];
return algorithmTags; return algorithmTags;
} }
/**
* Manually update a room with a given cause. This should only be used if the
* room list store would otherwise be incapable of doing the update itself. Note
* that this may race with the room list's regular operation.
* @param {Room} room The room to update.
* @param {RoomUpdateCause} cause The cause to update for.
*/
public async manualRoomUpdate(room: Room, cause: RoomUpdateCause) {
await this.handleRoomUpdate(room, cause);
this.updateFn.trigger();
}
} }
export default class RoomListStore { export default class RoomListStore {

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {AutoDiscovery} from "matrix-js-sdk"; import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import {_t, _td, newTranslatableError} from "../languageHandler"; import {_t, _td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils"; import {makeType} from "./TypeUtils";
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventStatus } from 'matrix-js-sdk'; import { EventStatus } from 'matrix-js-sdk/src/models/event';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
import shouldHideEvent from "../shouldHideEvent"; import shouldHideEvent from "../shouldHideEvent";
/** /**

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { SERVICE_TYPES } from 'matrix-js-sdk'; import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk';
import {LocalStorageCryptoStore} from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store'; import {LocalStorageCryptoStore} from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store';
import Analytics from '../Analytics'; import Analytics from '../Analytics';
import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb";
import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
const localStorage = window.localStorage; const localStorage = window.localStorage;
@ -132,7 +133,7 @@ export async function checkConsistency() {
async function checkSyncStore() { async function checkSyncStore() {
let exists = false; let exists = false;
try { try {
exists = await Matrix.IndexedDBStore.exists( exists = await IndexedDBStore.exists(
indexedDB, SYNC_STORE_NAME, indexedDB, SYNC_STORE_NAME,
); );
log(`Sync store using IndexedDB contains data? ${exists}`); log(`Sync store using IndexedDB contains data? ${exists}`);
@ -148,7 +149,7 @@ async function checkSyncStore() {
async function checkCryptoStore() { async function checkCryptoStore() {
let exists = false; let exists = false;
try { try {
exists = await Matrix.IndexedDBCryptoStore.exists( exists = await IndexedDBCryptoStore.exists(
indexedDB, CRYPTO_STORE_NAME, indexedDB, CRYPTO_STORE_NAME,
); );
log(`Crypto store using IndexedDB contains data? ${exists}`); log(`Crypto store using IndexedDB contains data? ${exists}`);

View file

@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as Matrix from 'matrix-js-sdk'; import {createClient} from "matrix-js-sdk/src/matrix";
import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage";
import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb";
const localStorage = window.localStorage; const localStorage = window.localStorage;
@ -44,7 +47,7 @@ export default function createMatrixClient(opts) {
}; };
if (indexedDB && localStorage) { if (indexedDB && localStorage) {
storeOpts.store = new Matrix.IndexedDBStore({ storeOpts.store = new IndexedDBStore({
indexedDB: indexedDB, indexedDB: indexedDB,
dbName: "riot-web-sync", dbName: "riot-web-sync",
localStorage: localStorage, localStorage: localStorage,
@ -53,18 +56,18 @@ export default function createMatrixClient(opts) {
} }
if (localStorage) { if (localStorage) {
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); storeOpts.sessionStore = new WebStorageSessionStore(localStorage);
} }
if (indexedDB) { if (indexedDB) {
storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore( storeOpts.cryptoStore = new IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto", indexedDB, "matrix-js-sdk:crypto",
); );
} }
opts = Object.assign(storeOpts, opts); opts = Object.assign(storeOpts, opts);
return Matrix.createClient(opts); return createClient(opts);
} }
createMatrixClient.indexedDbWorkerScript = null; createMatrixClient.indexedDbWorkerScript = null;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export function getHomePageUrl(appConfig) { import { ConfigOptions } from "../SdkConfig";
export function getHomePageUrl(appConfig: ConfigOptions): string | null {
const pagesConfig = appConfig.embeddedPages; const pagesConfig = appConfig.embeddedPages;
let pageUrl = null; let pageUrl = pagesConfig?.homeUrl;
if (pagesConfig) {
pageUrl = pagesConfig.homeUrl;
}
if (!pageUrl) { if (!pageUrl) {
// This is a deprecated config option for the home page // This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome // (despite the name, given we also now have a welcome
@ -29,3 +29,8 @@ export function getHomePageUrl(appConfig) {
return pageUrl; return pageUrl;
} }
export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean {
const pagesConfig = appConfig.embeddedPages;
return pagesConfig?.loginForWelcome === true;
}

View file

@ -5588,8 +5588,8 @@ mathml-tag-names@^2.1.3:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "9.8.0" version "9.9.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/cd38fb9b4c349eb31feac14e806e710bf6431b72"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"