Merge branch 'develop' into export-conversations
This commit is contained in:
commit
dbb3614374
55 changed files with 1236 additions and 569 deletions
|
@ -18,7 +18,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [{
|
overrides: [{
|
||||||
"files": ["src/**/*.{ts,tsx}"],
|
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
||||||
"extends": ["matrix-org/ts"],
|
"extends": ["matrix-org/ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// We're okay being explicit at the moment
|
// We're okay being explicit at the moment
|
||||||
|
|
106
CHANGELOG.md
106
CHANGELOG.md
|
@ -1,3 +1,109 @@
|
||||||
|
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0
|
||||||
|
* [Release] Fix notif panel timestamp padding
|
||||||
|
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
|
||||||
|
|
||||||
|
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
|
||||||
|
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
|
||||||
|
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
|
||||||
|
* Update way of checking for registration disabled
|
||||||
|
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
|
||||||
|
* Fix the ability to remove avatar from a space via settings
|
||||||
|
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
|
||||||
|
* Switch to stable endpoint/fields for MSC2858
|
||||||
|
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
|
||||||
|
* Clear stored editor state when canceling editing using a shortcut
|
||||||
|
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
|
||||||
|
* Respect newlines in space topics
|
||||||
|
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
|
||||||
|
* Add url param `defaultUsername` to prefill the login username field
|
||||||
|
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
|
||||||
|
* Bump ws from 7.4.2 to 7.4.6
|
||||||
|
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
|
||||||
|
* Sticky headers repositioning without layout trashing
|
||||||
|
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
|
||||||
|
* Handle user_busy in voip calls
|
||||||
|
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
|
||||||
|
* Avoid showing warning modals from the invite dialog after it unmounts
|
||||||
|
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
|
||||||
|
* Fix misleading child counts in spaces
|
||||||
|
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
|
||||||
|
* Close creation menu when expanding space panel via expand hierarchy
|
||||||
|
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
|
||||||
|
* Prevent having duplicates in pending room state
|
||||||
|
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
|
||||||
|
* Update reactions row on event decryption
|
||||||
|
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
|
||||||
|
* Destroy playback instance on voice message unmount
|
||||||
|
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
|
||||||
|
* Fix message preview not up to date
|
||||||
|
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
|
||||||
|
* Convert some Flow typed files to TS (round 2)
|
||||||
|
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
|
||||||
|
* Remove unused middlePanelResized event listener
|
||||||
|
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
|
||||||
|
* Fix accessing currentState on an invalid joinedRoom
|
||||||
|
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
|
||||||
|
* Remove Promise allSettled polyfill as js-sdk uses it directly
|
||||||
|
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
|
||||||
|
* Prevent DecoratedRoomAvatar to update its state for the same value
|
||||||
|
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
|
||||||
|
* Skip generatePreview if event is not part of the live timeline
|
||||||
|
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
|
||||||
|
* fix sticky headers when results num get displayed
|
||||||
|
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
|
||||||
|
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
|
||||||
|
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
|
||||||
|
* Safeguards to prevent layout trashing for window dimensions
|
||||||
|
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
|
||||||
|
* Use local room state to render space hierarchy if the room is known
|
||||||
|
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
|
||||||
|
* Add spinner in UserMenu to list pending long running actions
|
||||||
|
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
|
||||||
|
* Stop overscroll in Firefox Nightly for macOS
|
||||||
|
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
|
||||||
|
* Move SettingsStore watchers/monitors over to ES6 maps for performance
|
||||||
|
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
|
||||||
|
* Bump libolm version.
|
||||||
|
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
|
||||||
|
* Improve styling of the message action bar
|
||||||
|
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
|
||||||
|
* Improve explore rooms when no results are found
|
||||||
|
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
|
||||||
|
* Remove logo spinner
|
||||||
|
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
|
||||||
|
* Fix add reaction prompt showing even when user is not joined to room
|
||||||
|
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
|
||||||
|
* Vectorize spinners
|
||||||
|
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
|
||||||
|
* Fix handling of via servers for suggested rooms
|
||||||
|
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
|
||||||
|
* Upgrade showChatEffects to room-level setting exposure
|
||||||
|
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
|
||||||
|
* Reduce noise in tests
|
||||||
|
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
|
||||||
|
* Fix room name issues in right panel summary card
|
||||||
|
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
|
||||||
|
* Cache normalized room name
|
||||||
|
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
|
||||||
|
* Update MemberList to reflect changes for invite permission change
|
||||||
|
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
|
||||||
|
* Show subspace rooms count even if it is 0 for consistency
|
||||||
|
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
|
||||||
|
|
||||||
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.22.0",
|
"version": "3.23.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||||
|
@import "./views/dialogs/_ForwardDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_HostSignupDialog.scss";
|
@import "./views/dialogs/_HostSignupDialog.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
|
|
|
@ -82,7 +82,6 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
||||||
|
@ -103,6 +102,7 @@ limitations under the License.
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
position: initial;
|
position: initial;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_line {
|
.mx_NotificationPanel .mx_EventTile_line {
|
||||||
|
|
|
@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_betaWarning {
|
||||||
|
padding: 12px 12px 12px 54px;
|
||||||
|
position: relative;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
width: 432px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $info-plinth-bg-color;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_inviteTeammates {
|
.mx_SpaceRoomView_inviteTeammates {
|
||||||
// XXX remove this when spaces leaves Beta
|
// XXX remove this when spaces leaves Beta
|
||||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||||
|
|
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ForwardDialog {
|
||||||
|
width: 520px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
height: 80vh;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardDialog_preview {
|
||||||
|
max-height: 30%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
div {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When forwarding messages from encrypted rooms, EventTile will complain
|
||||||
|
// that our preview is unencrypted, which doesn't actually matter
|
||||||
|
.mx_EventTile_e2eIcon_unencrypted {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also hide download links to not encourage users to try interacting
|
||||||
|
.mx_MFileBody_download {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> hr {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid $input-border-color;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardList {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_results {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_roomButton {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.mx_DecoratedRoomAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry_name {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
|
||||||
|
// Hide the "Send" label while preserving button size
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
// Match the failed to send indicator's color with the disabled button
|
||||||
|
background-color: $button-danger-disabled-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image_wrapper {
|
.mx_ImageView_image_wrapper {
|
||||||
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -30,7 +31,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image {
|
.mx_ImageView_image {
|
||||||
pointer-events: all;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_info_wrapper {
|
.mx_ImageView_info_wrapper {
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -63,7 +63,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ImageView_toolbar {
|
.mx_ImageView_toolbar {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,12 +85,11 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
visibility: hidden !important;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
.mx_EventTile .mx_MessageTimestamp {
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -142,29 +141,11 @@ $left-gutter: 64px;
|
||||||
line-height: 57px !important;
|
line-height: 57px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||||
left: 3px;
|
left: 3px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
|
|
||||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
||||||
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,40 +14,49 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ComponentType } from "react";
|
||||||
|
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
|
||||||
|
|
||||||
|
type AsyncImport<T> = { default: T };
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
// A promise which resolves with the real component
|
||||||
|
prom: Promise<ComponentType | AsyncImport<ComponentType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
component?: ComponentType;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap an asynchronous loader function with a react component which shows a
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
* spinner until the real component loads.
|
* spinner until the real component loads.
|
||||||
*/
|
*/
|
||||||
export default class AsyncWrapper extends React.Component {
|
export default class AsyncWrapper extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private unmounted = false;
|
||||||
/** A promise which resolves with the real component
|
|
||||||
*/
|
|
||||||
prom: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
public state = {
|
||||||
component: null,
|
component: null,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._unmounted = false;
|
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Starting load of AsyncWrapper for modal');
|
console.log('Starting load of AsyncWrapper for modal');
|
||||||
this.props.prom.then((result) => {
|
this.props.prom.then((result) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Take the 'default' member if it's there, then we support
|
// Take the 'default' member if it's there, then we support
|
||||||
// passing in just an import()ed module, since ES6 async import
|
// passing in just an import()ed module, since ES6 async import
|
||||||
// always returns a module *namespace*.
|
// always returns a module *namespace*.
|
||||||
const component = result.default ? result.default : result;
|
const component = (result as AsyncImport<ComponentType>).default
|
||||||
|
? (result as AsyncImport<ComponentType>).default
|
||||||
|
: result as ComponentType;
|
||||||
this.setState({ component });
|
this.setState({ component });
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.warn('AsyncWrapper promise failed', e);
|
console.warn('AsyncWrapper promise failed', e);
|
||||||
|
@ -57,10 +65,10 @@ export default class AsyncWrapper extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWrapperCancelClick = () => {
|
private onWrapperCancelClick = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <BaseDialog onFinished={this.props.onFinished}
|
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
|
||||||
title={_t("Error")}
|
|
||||||
>
|
|
||||||
{ _t("Unable to load! Check your network connectivity and try again.") }
|
{ _t("Unable to load! Check your network connectivity and try again.") }
|
||||||
<DialogButtons primaryButton={_t("Dismiss")}
|
<DialogButtons primaryButton={_t("Dismiss")}
|
||||||
onPrimaryButtonClick={this._onWrapperCancelClick}
|
onPrimaryButtonClick={this.onWrapperCancelClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 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.
|
||||||
|
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
|
||||||
|
|
||||||
export default class Resend {
|
export default class Resend {
|
||||||
static resendUnsentEvents(room) {
|
static resendUnsentEvents(room: Room): Promise<void[]> {
|
||||||
return Promise.all(room.getPendingEvents().filter(function(ev) {
|
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).map(function(event) {
|
}).map(function(event: MatrixEvent) {
|
||||||
return Resend.resend(event);
|
return Resend.resend(event);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelUnsentEvents(room) {
|
static cancelUnsentEvents(room: Room): void {
|
||||||
room.getPendingEvents().filter(function(ev) {
|
room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).forEach(function(event) {
|
}).forEach(function(event: MatrixEvent) {
|
||||||
Resend.removeFromQueue(event);
|
Resend.removeFromQueue(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static resend(event) {
|
static resend(event: MatrixEvent): Promise<void> {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event,
|
event: event,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err: Error) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
@ -55,7 +56,7 @@ export default class Resend {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeFromQueue(event) {
|
static removeFromQueue(event: MatrixEvent): void {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
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.
|
||||||
|
@ -24,12 +24,12 @@ limitations under the License.
|
||||||
* A similar thing could also be achieved via `pushState` with a state object,
|
* A similar thing could also be achieved via `pushState` with a state object,
|
||||||
* but keeping it separate like this seems easier in case we do want to extend.
|
* but keeping it separate like this seems easier in case we do want to extend.
|
||||||
*/
|
*/
|
||||||
const aliasToIDMap = new Map();
|
const aliasToIDMap = new Map<string, string>();
|
||||||
|
|
||||||
export function storeRoomAliasInCache(alias, id) {
|
export function storeRoomAliasInCache(alias: string, id: string): void {
|
||||||
aliasToIDMap.set(alias, id);
|
aliasToIDMap.set(alias, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCachedRoomIDForAlias(alias) {
|
export function getCachedRoomIDForAlias(alias: string): string {
|
||||||
return aliasToIDMap.get(alias);
|
return aliasToIDMap.get(alias);
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 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,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dis from '../dispatcher/dispatcher';
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
|
|
||||||
|
import dis from "../dispatcher/dispatcher";
|
||||||
|
import {ActionPayload} from "../dispatcher/payloads";
|
||||||
|
|
||||||
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
||||||
// become dispatches in the same place.
|
// become dispatches in the same place.
|
||||||
|
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
|
||||||
* @param {string} prevState the previous sync state.
|
* @param {string} prevState the previous sync state.
|
||||||
* @returns {Object} an action of type MatrixActions.sync.
|
* @returns {Object} an action of type MatrixActions.sync.
|
||||||
*/
|
*/
|
||||||
function createSyncAction(matrixClient, state, prevState) {
|
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.sync',
|
action: 'MatrixActions.sync',
|
||||||
state,
|
state,
|
||||||
|
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
|
||||||
* @param {MatrixEvent} accountDataEvent the account data event.
|
* @param {MatrixEvent} accountDataEvent the account data event.
|
||||||
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
||||||
*/
|
*/
|
||||||
function createAccountDataAction(matrixClient, accountDataEvent) {
|
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.accountData',
|
action: 'MatrixActions.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||||
* @param {Room} room the room where account data was changed
|
* @param {Room} room the room where account data was changed
|
||||||
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
||||||
*/
|
*/
|
||||||
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
function createRoomAccountDataAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
accountDataEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.accountData',
|
action: 'MatrixActions.Room.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
||||||
* @param {Room} room the Room that was stored.
|
* @param {Room} room the Room that was stored.
|
||||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||||
*/
|
*/
|
||||||
function createRoomAction(matrixClient, room) {
|
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room', room };
|
return { action: 'MatrixActions.Room', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
|
||||||
* @param {Room} room the Room whose tags were changed.
|
* @param {Room} room the Room whose tags were changed.
|
||||||
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||||
*/
|
*/
|
||||||
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room.tags', room };
|
return { action: 'MatrixActions.Room.tags', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||||
* @param {Room} room the room the receipt happened in.
|
* @param {Room} room the room the receipt happened in.
|
||||||
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
||||||
*/
|
*/
|
||||||
function createRoomReceiptAction(matrixClient, event, room) {
|
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.receipt',
|
action: 'MatrixActions.Room.receipt',
|
||||||
event,
|
event,
|
||||||
|
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
|
||||||
* @param {EventTimeline} data.timeline the timeline being altered.
|
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||||
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||||
*/
|
*/
|
||||||
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
function createRoomTimelineAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
timelineEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
removed: boolean,
|
||||||
|
data: {
|
||||||
|
liveEvent: boolean;
|
||||||
|
timeline: EventTimeline;
|
||||||
|
},
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.timeline',
|
action: 'MatrixActions.Room.timeline',
|
||||||
event: timelineEvent,
|
event: timelineEvent,
|
||||||
|
@ -208,7 +228,12 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
|
||||||
* @param {string} oldMembership the previous membership, can be null.
|
* @param {string} oldMembership the previous membership, can be null.
|
||||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
||||||
*/
|
*/
|
||||||
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
|
function createSelfMembershipAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
membership: string,
|
||||||
|
oldMembership: string,
|
||||||
|
): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
|
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,34 +253,15 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi
|
||||||
* @param {MatrixEvent} event the matrix event that was decrypted.
|
* @param {MatrixEvent} event the matrix event that was decrypted.
|
||||||
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
||||||
*/
|
*/
|
||||||
function createEventDecryptedAction(matrixClient, event) {
|
function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
|
||||||
return { action: 'MatrixActions.Event.decrypted', event };
|
return { action: 'MatrixActions.Event.decrypted', event };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type Listener = () => void;
|
||||||
* This object is responsible for dispatching actions when certain events are emitted by
|
type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload;
|
||||||
* the given MatrixClient.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
// A list of callbacks to call to unregister all listeners added
|
|
||||||
_matrixClientListenersStop: [],
|
|
||||||
|
|
||||||
/**
|
// A list of callbacks to call to unregister all listeners added
|
||||||
* Start listening to certain events from the MatrixClient and dispatch actions when
|
let matrixClientListenersStop: Listener[] = [];
|
||||||
* they are emitted.
|
|
||||||
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
|
||||||
*/
|
|
||||||
start(matrixClient) {
|
|
||||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
|
||||||
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
||||||
|
@ -266,23 +272,46 @@ export default {
|
||||||
* when given the MatrixClient as an argument as well as
|
* when given the MatrixClient as an argument as well as
|
||||||
* arguments emitted in the MatrixClient event.
|
* arguments emitted in the MatrixClient event.
|
||||||
*/
|
*/
|
||||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void {
|
||||||
const listener = (...args) => {
|
const listener: Listener = (...args) => {
|
||||||
const payload = actionCreator(matrixClient, ...args);
|
const payload = actionCreator(matrixClient, ...args);
|
||||||
if (payload) {
|
if (payload) {
|
||||||
dis.dispatch(payload, true);
|
dis.dispatch(payload, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
matrixClient.on(eventName, listener);
|
matrixClient.on(eventName, listener);
|
||||||
this._matrixClientListenersStop.push(() => {
|
matrixClientListenersStop.push(() => {
|
||||||
matrixClient.removeListener(eventName, listener);
|
matrixClient.removeListener(eventName, listener);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object is responsible for dispatching actions when certain events are emitted by
|
||||||
|
* the given MatrixClient.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Start listening to certain events from the MatrixClient and dispatch actions when
|
||||||
|
* they are emitted.
|
||||||
|
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
||||||
|
*/
|
||||||
|
start(matrixClient: MatrixClient) {
|
||||||
|
addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||||
|
addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop listening to events.
|
* Stop listening to events.
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
|
matrixClientListenersStop.forEach((stopListener) => stopListener());
|
||||||
|
matrixClientListenersStop = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||||
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 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';
|
||||||
|
@ -616,10 +615,6 @@ export default class MessagePanel extends React.Component {
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
|
||||||
// Local echos have a send "status".
|
|
||||||
const scrollToken = mxEv.status ? undefined : eventId;
|
|
||||||
|
|
||||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||||
|
|
||||||
let isLastSuccessful = false;
|
let isLastSuccessful = false;
|
||||||
|
@ -651,7 +646,6 @@ export default class MessagePanel extends React.Component {
|
||||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
<EventTile
|
<EventTile
|
||||||
as="li"
|
as="li"
|
||||||
data-scroll-tokens={scrollToken}
|
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
mxEvent={mxEv}
|
mxEvent={mxEv}
|
||||||
|
@ -854,13 +848,6 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
const style = this.props.hidden ? { display: 'none' } : {};
|
const style = this.props.hidden ? { display: 'none' } : {};
|
||||||
|
|
||||||
const className = classNames(
|
|
||||||
this.props.className,
|
|
||||||
{
|
|
||||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let whoIsTyping;
|
let whoIsTyping;
|
||||||
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||||
whoIsTyping = (<WhoIsTypingTile
|
whoIsTyping = (<WhoIsTypingTile
|
||||||
|
@ -884,7 +871,7 @@ export default class MessagePanel extends React.Component {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ScrollPanel
|
<ScrollPanel
|
||||||
ref={this._scrollPanel}
|
ref={this._scrollPanel}
|
||||||
className={className}
|
className={this.props.className}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
onUserScroll={this.props.onUserScroll}
|
onUserScroll={this.props.onUserScroll}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
showUrlPreview={false}
|
showUrlPreview={false}
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
|
alwaysShowTimestamps={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -59,7 +59,6 @@ import ScrollPanel from "./ScrollPanel";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
|
||||||
import SearchBar from "../views/rooms/SearchBar";
|
import SearchBar from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
|
@ -136,7 +135,6 @@ export interface IState {
|
||||||
// Whether to highlight the event scrolled to
|
// Whether to highlight the event scrolled to
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
forwardingEvent?: MatrixEvent;
|
|
||||||
numUnreadMessages: number;
|
numUnreadMessages: number;
|
||||||
draggingFile: boolean;
|
draggingFile: boolean;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
|
@ -323,7 +321,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
initialEventId: RoomViewStore.getInitialEventId(),
|
initialEventId: RoomViewStore.getInitialEventId(),
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
|
||||||
// we should only peek once we have a ready client
|
// we should only peek once we have a ready client
|
||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
@ -1410,18 +1407,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({ action: "open_room_settings" });
|
dis.dispatch({ action: "open_room_settings" });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCancelClick = () => {
|
|
||||||
console.log("updateTint from onCancelClick");
|
|
||||||
this.updateTint();
|
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'forward_event',
|
|
||||||
event: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dis.fire(Action.FocusComposer);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAppsClick = () => {
|
private onAppsClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "appsDrawer",
|
action: "appsDrawer",
|
||||||
|
@ -1526,10 +1511,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
private jumpToLiveTimeline = () => {
|
private jumpToLiveTimeline = () => {
|
||||||
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
|
// If we were viewing a highlighted event, firing view_room without
|
||||||
|
// an event will take care of both clearing the URL fragment and
|
||||||
|
// jumping to the bottom
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: this.state.room.roomId,
|
room_id: this.state.room.roomId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Otherwise we have to jump manually
|
||||||
|
this.messagePanel.jumpToLiveTimeline();
|
||||||
|
dis.fire(Action.FocusComposer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1828,11 +1822,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let aux = null;
|
let aux = null;
|
||||||
let previewBar;
|
let previewBar;
|
||||||
let hideCancel = false;
|
if (this.state.searching) {
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
|
||||||
} else if (this.state.searching) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <SearchBar
|
aux = <SearchBar
|
||||||
searchInProgress={this.state.searchInProgress}
|
searchInProgress={this.state.searchInProgress}
|
||||||
onCancelClick={this.onCancelSearchClick}
|
onCancelClick={this.onCancelSearchClick}
|
||||||
|
@ -1841,7 +1831,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
|
||||||
} else if (myMembership !== "join") {
|
} else if (myMembership !== "join") {
|
||||||
// We do have a room object for this room, but we're not currently in it.
|
// We do have a room object for this room, but we're not currently in it.
|
||||||
// We may have a 3rd party invite to it.
|
// We may have a 3rd party invite to it.
|
||||||
|
@ -1850,7 +1839,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inviterName = this.props.oobData.inviterName;
|
inviterName = this.props.oobData.inviterName;
|
||||||
}
|
}
|
||||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||||
hideCancel = true;
|
|
||||||
previewBar = (
|
previewBar = (
|
||||||
<RoomPreviewBar
|
<RoomPreviewBar
|
||||||
onJoinClick={this.onJoinButtonClicked}
|
onJoinClick={this.onJoinButtonClicked}
|
||||||
|
@ -1968,11 +1956,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
|
||||||
let highlightedEventId = null;
|
let highlightedEventId = null;
|
||||||
if (this.state.forwardingEvent) {
|
if (this.state.isInitialEventHighlighted) {
|
||||||
highlightedEventId = this.state.forwardingEvent.getId();
|
|
||||||
} else if (shouldHighlight) {
|
|
||||||
highlightedEventId = this.state.initialEventId;
|
highlightedEventId = this.state.initialEventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2061,7 +2046,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
|
|
@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
||||||
<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>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
<div className="mx_SpaceRoomView_betaWarning">
|
||||||
|
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||||
|
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
|
||||||
|
</div>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,6 +120,9 @@ class TimelinePanel extends React.Component {
|
||||||
|
|
||||||
// which layout to use
|
// which layout to use
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
|
|
||||||
|
// whether to always show timestamps for an event
|
||||||
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// a map from room id to read marker event timestamp
|
// a map from room id to read marker event timestamp
|
||||||
|
@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component {
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.state.isTwelveHour}
|
isTwelveHour={this.state.isTwelveHour}
|
||||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { MenuItem } from "../../structures/ContextMenu";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
|
@ -157,10 +158,10 @@ export default class MessageContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onForwardClick = () => {
|
onForwardClick = () => {
|
||||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||||
dis.dispatch({
|
matrixClient: MatrixClientPeg.get(),
|
||||||
action: 'forward_event',
|
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||||
showUnpin?: boolean;
|
showUnpin?: boolean;
|
||||||
// override delete handler
|
// override delete handler
|
||||||
onDeleteClick?(): void;
|
onDeleteClick?(): void;
|
||||||
|
// override edit handler
|
||||||
|
onEditClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetContextMenu: React.FC<IProps> = ({
|
const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
app,
|
app,
|
||||||
userWidget,
|
userWidget,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
|
onEditClick,
|
||||||
showUnpin,
|
showUnpin,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let editButton;
|
let editButton;
|
||||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||||
const onEditClick = () => {
|
const _onEditClick = () => {
|
||||||
|
if (onEditClick) {
|
||||||
|
onEditClick();
|
||||||
|
} else {
|
||||||
WidgetUtils.editWidget(room, app);
|
WidgetUtils.editWidget(room, app);
|
||||||
|
}
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshotButton;
|
let snapshotButton;
|
||||||
|
@ -116,7 +123,10 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (onDeleteClick || canModify) {
|
if (onDeleteClick || canModify) {
|
||||||
const onDeleteClickDefault = () => {
|
const _onDeleteClick = () => {
|
||||||
|
if (onDeleteClick) {
|
||||||
|
onDeleteClick();
|
||||||
|
} else {
|
||||||
// Show delete confirmation dialog
|
// Show delete confirmation dialog
|
||||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
title: _t("Delete Widget"),
|
title: _t("Delete Widget"),
|
||||||
|
@ -129,11 +139,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteButton = <IconizedContextMenuOption
|
deleteButton = <IconizedContextMenuOption
|
||||||
onClick={onDeleteClick || onDeleteClickDefault}
|
onClick={_onDeleteClick}
|
||||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useMemo, useState, useEffect} from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
|
||||||
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
|
import {Layout} from "../../../settings/Layout";
|
||||||
|
import {IDialogProps} from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import {avatarUrlForUser} from "../../../Avatar";
|
||||||
|
import EventTile from "../rooms/EventTile";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import {Alignment} from '../elements/Tooltip';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 30;
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
// The event to forward
|
||||||
|
event: MatrixEvent;
|
||||||
|
// We need a permalink creator for the source room to pass through to EventTile
|
||||||
|
// in case the event is a reply (even though the user can't get at the link)
|
||||||
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEntryProps {
|
||||||
|
room: Room;
|
||||||
|
event: MatrixEvent;
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
onFinished(success: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SendState {
|
||||||
|
CanSend,
|
||||||
|
Sending,
|
||||||
|
Sent,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
|
||||||
|
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
|
||||||
|
|
||||||
|
const jumpToRoom = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
onFinished(true);
|
||||||
|
};
|
||||||
|
const send = async () => {
|
||||||
|
setSendState(SendState.Sending);
|
||||||
|
try {
|
||||||
|
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
|
||||||
|
setSendState(SendState.Sent);
|
||||||
|
} catch (e) {
|
||||||
|
setSendState(SendState.Failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let className;
|
||||||
|
let disabled = false;
|
||||||
|
let title;
|
||||||
|
let icon;
|
||||||
|
if (sendState === SendState.CanSend) {
|
||||||
|
className = "mx_ForwardList_canSend";
|
||||||
|
if (room.maySendMessage()) {
|
||||||
|
title = _t("Send");
|
||||||
|
} else {
|
||||||
|
disabled = true;
|
||||||
|
title = _t("You don't have permission to do this");
|
||||||
|
}
|
||||||
|
} else if (sendState === SendState.Sending) {
|
||||||
|
className = "mx_ForwardList_sending";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sending");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else if (sendState === SendState.Sent) {
|
||||||
|
className = "mx_ForwardList_sent";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sent");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else {
|
||||||
|
className = "mx_ForwardList_sendFailed";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Failed to send");
|
||||||
|
icon = <NotificationBadge
|
||||||
|
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_ForwardList_entry">
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ForwardList_roomButton"
|
||||||
|
onClick={jumpToRoom}
|
||||||
|
title={_t("Open link")}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||||
|
<span className="mx_ForwardList_entry_name">{ room.name }</span>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||||
|
className={`mx_ForwardList_sendButton ${className}`}
|
||||||
|
onClick={send}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
|
||||||
|
{ icon }
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const [profileInfo, setProfileInfo] = useState<any>({});
|
||||||
|
useEffect(() => {
|
||||||
|
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
|
||||||
|
}, [cli, userId]);
|
||||||
|
|
||||||
|
// For the message preview we fake the sender as ourselves
|
||||||
|
const mockEvent = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: userId,
|
||||||
|
content: event.getContent(),
|
||||||
|
unsigned: {
|
||||||
|
age: 97,
|
||||||
|
},
|
||||||
|
event_id: "$9999999999999999999999999999999999999999999",
|
||||||
|
room_id: event.getRoomId(),
|
||||||
|
});
|
||||||
|
mockEvent.sender = {
|
||||||
|
name: profileInfo.displayname || userId,
|
||||||
|
userId,
|
||||||
|
getAvatarUrl: (..._) => {
|
||||||
|
return avatarUrlForUser(
|
||||||
|
{ avatarUrl: profileInfo.avatar_url },
|
||||||
|
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const lcQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||||
|
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||||
|
const previewLayout = useSettingValue<Layout>("layout");
|
||||||
|
|
||||||
|
let rooms = useMemo(() => sortRooms(
|
||||||
|
cli.getVisibleRooms().filter(
|
||||||
|
room => room.getMyMembership() === "join" &&
|
||||||
|
!(spacesEnabled && room.isSpaceRoom()),
|
||||||
|
),
|
||||||
|
), [cli, spacesEnabled]);
|
||||||
|
|
||||||
|
if (lcQuery) {
|
||||||
|
rooms = new QueryMatcher<Room>(rooms, {
|
||||||
|
keys: ["name"],
|
||||||
|
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||||
|
shouldMatchWordsOnly: false,
|
||||||
|
}).match(lcQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Forward message")}
|
||||||
|
className="mx_ForwardDialog"
|
||||||
|
contentId="mx_ForwardList"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<h3>{ _t("Message preview") }</h3>
|
||||||
|
<div className={classnames("mx_ForwardDialog_preview", {
|
||||||
|
"mx_IRCLayout": previewLayout == Layout.IRC,
|
||||||
|
"mx_GroupLayout": previewLayout == Layout.Group,
|
||||||
|
})}>
|
||||||
|
<EventTile
|
||||||
|
mxEvent={mockEvent}
|
||||||
|
layout={previewLayout}
|
||||||
|
enableFlair={flairEnabled}
|
||||||
|
permalinkCreator={permalinkCreator}
|
||||||
|
as="div"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className="mx_ForwardList" id="mx_ForwardList">
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search for rooms or people")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
{ rooms.length > 0 ? (
|
||||||
|
<div className="mx_ForwardList_results">
|
||||||
|
{ rooms.map(room =>
|
||||||
|
<Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
event={event}
|
||||||
|
matrixClient={cli}
|
||||||
|
onFinished={onFinished}
|
||||||
|
/>,
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
) : <span className="mx_ForwardList_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span> }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForwardDialog;
|
|
@ -15,5 +15,5 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface IDialogProps {
|
export interface IDialogProps {
|
||||||
onFinished: (bool) => void;
|
onFinished(...args: any): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||||
stickyBottom={false}
|
stickyBottom={false}
|
||||||
startAtBottom={false}
|
startAtBottom={false}
|
||||||
>
|
>
|
||||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||||
</ScrollPanel>);
|
</ScrollPanel>);
|
||||||
}
|
}
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip, {Alignment} from './Tooltip';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
|
alignment?: Alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
yOffset={yOffset}
|
yOffset={yOffset}
|
||||||
|
alignment={alignment}
|
||||||
/> : null;
|
/> : null;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -417,6 +417,8 @@ export default class AppTile extends React.Component {
|
||||||
onFinished={this._closeContextMenu}
|
onFinished={this._closeContextMenu}
|
||||||
showUnpin={!this.props.userWidget}
|
showUnpin={!this.props.userWidget}
|
||||||
userWidget={this.props.userWidget}
|
userWidget={this.props.userWidget}
|
||||||
|
onEditClick={this.props.onEditClick}
|
||||||
|
onDeleteClick={this.props.onDeleteClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
|
||||||
// If we are only given few events then just pass them through
|
// If we are only given few events then just pass them through
|
||||||
if (events.length < threshold) {
|
if (events.length < threshold) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||||
{ children }
|
{ children }
|
||||||
</div>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private initX = 0;
|
private initX = 0;
|
||||||
private initY = 0;
|
private initY = 0;
|
||||||
private lastX = 0;
|
|
||||||
private lastY = 0;
|
|
||||||
private previousX = 0;
|
private previousX = 0;
|
||||||
private previousY = 0;
|
private previousY = 0;
|
||||||
|
|
||||||
|
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
// needs to be passive in order to work with Chromium
|
// needs to be passive in order to work with Chromium
|
||||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||||
// We want to recalculate zoom whenever the window's size changes
|
// We want to recalculate zoom whenever the window's size changes
|
||||||
window.addEventListener("resize", this.calculateZoom);
|
window.addEventListener("resize", this.recalculateZoom);
|
||||||
// After the image loads for the first time we want to calculate the zoom
|
// After the image loads for the first time we want to calculate the zoom
|
||||||
this.image.current.addEventListener("load", this.calculateZoom);
|
this.image.current.addEventListener("load", this.recalculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||||
window.removeEventListener("resize", this.calculateZoom);
|
window.removeEventListener("resize", this.recalculateZoom);
|
||||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateZoom = () => {
|
private recalculateZoom = () => {
|
||||||
|
this.setZoomAndRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setZoomAndRotation = (inputRotation?: number) => {
|
||||||
const image = this.image.current;
|
const image = this.image.current;
|
||||||
const imageWrapper = this.imageWrapper.current;
|
const imageWrapper = this.imageWrapper.current;
|
||||||
|
|
||||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
const rotation = inputRotation || this.state.rotation;
|
||||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
|
||||||
|
const imageIsNotFlipped = rotation % 180 === 0;
|
||||||
|
|
||||||
|
// If the image is rotated take it into account
|
||||||
|
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
|
||||||
|
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
|
||||||
|
|
||||||
|
const zoomX = imageWrapper.clientWidth / width;
|
||||||
|
const zoomY = imageWrapper.clientHeight / height;
|
||||||
|
|
||||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||||
// display it in its original size
|
// display it in its original size
|
||||||
|
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: 1,
|
maxZoom: 1,
|
||||||
|
rotation: rotation,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
// image by default
|
// image by default
|
||||||
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
||||||
|
|
||||||
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
|
// If zoom is smaller than minZoom don't go below that value
|
||||||
|
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
minZoom: minZoom,
|
minZoom: minZoom,
|
||||||
maxZoom: 1,
|
maxZoom: 1,
|
||||||
|
rotation: rotation,
|
||||||
|
zoom: zoom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onRotateCounterClockwiseClick = () => {
|
private onRotateCounterClockwiseClick = () => {
|
||||||
const cur = this.state.rotation;
|
const cur = this.state.rotation;
|
||||||
const rotationDegrees = cur - 90;
|
this.setZoomAndRotation(cur - 90);
|
||||||
this.setState({ rotation: rotationDegrees });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRotateClockwiseClick = () => {
|
private onRotateClockwiseClick = () => {
|
||||||
const cur = this.state.rotation;
|
const cur = this.state.rotation;
|
||||||
const rotationDegrees = cur + 90;
|
this.setZoomAndRotation(cur + 90);
|
||||||
this.setState({ rotation: rotationDegrees });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDownloadClick = () => {
|
private onDownloadClick = () => {
|
||||||
|
@ -253,8 +266,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
this.setState({ moving: true });
|
this.setState({ moving: true });
|
||||||
this.previousX = this.state.translationX;
|
this.previousX = this.state.translationX;
|
||||||
this.previousY = this.state.translationY;
|
this.previousY = this.state.translationY;
|
||||||
this.initX = ev.pageX - this.lastX;
|
this.initX = ev.pageX - this.state.translationX;
|
||||||
this.initY = ev.pageY - this.lastY;
|
this.initY = ev.pageY - this.state.translationY;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMoving = (ev: React.MouseEvent) => {
|
private onMoving = (ev: React.MouseEvent) => {
|
||||||
|
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!this.state.moving) return;
|
if (!this.state.moving) return;
|
||||||
|
|
||||||
this.lastX = ev.pageX - this.initX;
|
|
||||||
this.lastY = ev.pageY - this.initY;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
translationX: this.lastX,
|
translationX: ev.pageX - this.initX,
|
||||||
translationY: this.lastY,
|
translationY: ev.pageY - this.initY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -283,6 +294,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
translationX: 0,
|
translationX: 0,
|
||||||
translationY: 0,
|
translationY: 0,
|
||||||
});
|
});
|
||||||
|
this.initX = 0;
|
||||||
|
this.initY = 0;
|
||||||
}
|
}
|
||||||
this.setState({ moving: false });
|
this.setState({ moving: false });
|
||||||
};
|
};
|
||||||
|
@ -471,7 +484,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="mx_ImageView_image_wrapper"
|
className="mx_ImageView_image_wrapper"
|
||||||
ref={this.imageWrapper}>
|
ref={this.imageWrapper}
|
||||||
|
onMouseDown={this.props.onFinished}
|
||||||
|
onMouseMove={this.onMoving}
|
||||||
|
onMouseUp={this.onEndMoving}
|
||||||
|
onMouseLeave={this.onEndMoving}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={this.props.src}
|
||||||
title={this.props.name}
|
title={this.props.name}
|
||||||
|
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
className="mx_ImageView_image"
|
className="mx_ImageView_image"
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onMouseDown={this.onStartMoving}
|
onMouseDown={this.onStartMoving}
|
||||||
onMouseMove={this.onMoving}
|
|
||||||
onMouseUp={this.onEndMoving}
|
|
||||||
onMouseLeave={this.onEndMoving}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FocusLock>
|
</FocusLock>
|
||||||
|
|
|
@ -47,6 +47,10 @@ export default class ReplyThread extends React.Component {
|
||||||
// Specifies which layout to use.
|
// Specifies which layout to use.
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
forExport: PropTypes.bool,
|
forExport: PropTypes.bool,
|
||||||
|
|
||||||
|
// Whether to always show a timestamp
|
||||||
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
@ -216,7 +220,7 @@ export default class ReplyThread extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, forExport) {
|
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, forExport, alwaysShowTimestamps) {
|
||||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -227,6 +231,7 @@ export default class ReplyThread extends React.Component {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
permalinkCreator={permalinkCreator}
|
permalinkCreator={permalinkCreator}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,6 +401,7 @@ export default class ReplyThread extends React.Component {
|
||||||
isRedacted={ev.isRedacted()}
|
isRedacted={ev.isRedacted()}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
replacingEventId={ev.replacingEventId()}
|
replacingEventId={ev.replacingEventId()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -916,6 +916,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
|
// Local echos have a send "status".
|
||||||
|
const scrollToken = this.props.mxEvent.status
|
||||||
|
? undefined
|
||||||
|
: this.props.mxEvent.getId();
|
||||||
|
|
||||||
let avatar;
|
let avatar;
|
||||||
let sender;
|
let sender;
|
||||||
let avatarSize;
|
let avatarSize;
|
||||||
|
@ -989,7 +995,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
|
||||||
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
|
const showTimestamp = this.props.mxEvent.getTs() &&
|
||||||
|
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
|
||||||
const timestamp = showTimestamp ?
|
const timestamp = showTimestamp ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
|
||||||
|
@ -1060,7 +1067,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
case 'notif': {
|
case 'notif': {
|
||||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return (
|
return (
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||||
<div className="mx_EventTile_roomName">
|
<div className="mx_EventTile_roomName">
|
||||||
<RoomAvatar room={room} width={28} height={28} />
|
<RoomAvatar room={room} width={28} height={28} />
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
|
@ -1083,12 +1090,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'file_grid': {
|
case 'file_grid': {
|
||||||
return (
|
return (
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||||
<div className="mx_EventTile_line">
|
<div className="mx_EventTile_line">
|
||||||
<EventTileType ref={this.tile}
|
<EventTileType ref={this.tile}
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
|
@ -1109,7 +1116,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1122,10 +1129,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.onHeightChanged,
|
this.props.onHeightChanged,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
this.replyThread,
|
this.replyThread,
|
||||||
|
null,
|
||||||
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||||
{ ircTimestamp }
|
{ ircTimestamp }
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
|
@ -1143,7 +1152,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={false}
|
showUrlPreview={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@ -1154,17 +1163,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.replyThread,
|
this.replyThread,
|
||||||
this.props.layout,
|
this.props.layout,
|
||||||
this.props.forExport,
|
this.props.forExport,
|
||||||
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
|
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
return (
|
return (
|
||||||
React.createElement(this.props.as || "div", {
|
React.createElement(this.props.as || "li", {
|
||||||
"ref": this.ref,
|
"ref": this.ref,
|
||||||
"className": classes,
|
"className": classes,
|
||||||
"tabIndex": -1,
|
"tabIndex": -1,
|
||||||
"aria-live": ariaLive,
|
"aria-live": ariaLive,
|
||||||
"aria-atomic": "true",
|
"aria-atomic": "true",
|
||||||
"data-scroll-tokens": this.props["data-scroll-tokens"],
|
"data-scroll-tokens": scrollToken,
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
}, [
|
}, [
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2017 Michael Telatynski
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {Key} from '../../../Keyboard';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ForwardMessage")
|
|
||||||
export default class ForwardMessage extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onCancelClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('keydown', this._onKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onKeyDown = ev => {
|
|
||||||
switch (ev.key) {
|
|
||||||
case Key.ESCAPE:
|
|
||||||
this.props.onCancelClick();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="mx_ForwardMessage">
|
|
||||||
<h1>{ _t('Please select the destination room for this message') }</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ReplyPreview_clear" />
|
<div className="mx_ReplyPreview_clear" />
|
||||||
<EventTile
|
<EventTile
|
||||||
last={true}
|
alwaysShowTimestamps={true}
|
||||||
tileShape="reply_preview"
|
tileShape="reply_preview"
|
||||||
mxEvent={this.state.event}
|
mxEvent={this.state.event}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
import { CancelButton } from './SimpleRoomHeader';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
@ -44,7 +43,6 @@ export default class RoomHeader extends React.Component {
|
||||||
onSettingsClick: PropTypes.func,
|
onSettingsClick: PropTypes.func,
|
||||||
onSearchClick: PropTypes.func,
|
onSearchClick: PropTypes.func,
|
||||||
onLeaveClick: PropTypes.func,
|
onLeaveClick: PropTypes.func,
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
e2eStatus: PropTypes.string,
|
e2eStatus: PropTypes.string,
|
||||||
onAppsClick: PropTypes.func,
|
onAppsClick: PropTypes.func,
|
||||||
appsShown: PropTypes.bool,
|
appsShown: PropTypes.bool,
|
||||||
|
@ -54,7 +52,6 @@ export default class RoomHeader extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
onCancelClick: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -90,11 +87,6 @@ export default class RoomHeader extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
let cancelButton = null;
|
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a valid (possibly zero) searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
|
@ -220,7 +212,6 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||||
{ name }
|
{ name }
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ cancelButton }
|
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons room={this.props.room} />
|
<RoomHeaderButtons room={this.props.room} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
|
||||||
|
|
||||||
const ts1 = mxEv.getTs();
|
const ts1 = mxEv.getTs();
|
||||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||||
|
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||||
|
|
||||||
const timeline = result.context.getTimeline();
|
const timeline = result.context.getTimeline();
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
|
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
|
||||||
highlightLink={this.props.resultLink}
|
highlightLink={this.props.resultLink}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -16,23 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// cancel button which is shared between room header and simple room header
|
|
||||||
export function CancelButton(props) {
|
|
||||||
const {onClick} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
|
||||||
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
|
|
||||||
width="18" height="18" alt={_t("Cancel")} />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A stripped-down room header used for things like the user settings
|
* A stripped-down room header used for things like the user settings
|
||||||
* and room directory.
|
* and room directory.
|
||||||
|
@ -41,18 +27,13 @@ export function CancelButton(props) {
|
||||||
export default class SimpleRoomHeader extends React.Component {
|
export default class SimpleRoomHeader extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
|
|
||||||
// `src` to a TintableSvg. Optional.
|
// `src` to a TintableSvg. Optional.
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let cancelButton;
|
|
||||||
let icon;
|
let icon;
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
|
||||||
}
|
|
||||||
if (this.props.icon) {
|
if (this.props.icon) {
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
icon = <TintableSvg
|
icon = <TintableSvg
|
||||||
|
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_simpleHeader">
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
{ icon }
|
{ icon }
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
{ cancelButton }
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
/**
|
/**
|
||||||
* Launch the integration manager on the stickers integration page
|
* Launch the integration manager on the stickers integration page
|
||||||
*/
|
*/
|
||||||
_launchManageIntegrations() {
|
_launchManageIntegrations = () => {
|
||||||
// TODO: Open the right integration manager for the widget
|
// TODO: Open the right integration manager for the widget
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||||
IntegrationManagers.sharedInstance().openAll(
|
IntegrationManagers.sharedInstance().openAll(
|
||||||
|
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
this.state.widgetId,
|
this.state.widgetId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let stickerPicker;
|
let stickerPicker;
|
||||||
|
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
key="controls_hide_stickers"
|
key="controls_hide_stickers"
|
||||||
className={className}
|
className={className}
|
||||||
onClick={this._onHideStickersClick}
|
onClick={this._onHideStickersClick}
|
||||||
active={this.state.showStickers}
|
active={this.state.showStickers.toString()}
|
||||||
title={_t("Hide Stickers")}
|
title={_t("Hide Stickers")}
|
||||||
>
|
>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
|
|
@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hook to fetch whether a feature is enabled and dynamically update when that changes
|
// Hook to fetch whether a feature is enabled and dynamically update when that changes
|
||||||
export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
|
export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
|
||||||
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
|
const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
|
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
|
||||||
|
|
|
@ -1477,7 +1477,6 @@
|
||||||
"Encrypting your message...": "Encrypting your message...",
|
"Encrypting your message...": "Encrypting your message...",
|
||||||
"Your message was sent": "Your message was sent",
|
"Your message was sent": "Your message was sent",
|
||||||
"Failed to send": "Failed to send",
|
"Failed to send": "Failed to send",
|
||||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
|
||||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||||
"Close preview": "Close preview",
|
"Close preview": "Close preview",
|
||||||
"and %(count)s others...|other": "and %(count)s others...",
|
"and %(count)s others...|other": "and %(count)s others...",
|
||||||
|
@ -2209,6 +2208,13 @@
|
||||||
"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.",
|
"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.",
|
||||||
"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>.",
|
||||||
|
"You don't have permission to do this": "You don't have permission to do this",
|
||||||
|
"Sending": "Sending",
|
||||||
|
"Sent": "Sent",
|
||||||
|
"Open link": "Open link",
|
||||||
|
"Forward message": "Forward message",
|
||||||
|
"Message preview": "Message preview",
|
||||||
|
"Search for rooms or people": "Search for rooms or people",
|
||||||
"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.",
|
||||||
"Abort": "Abort",
|
"Abort": "Abort",
|
||||||
|
@ -2667,7 +2673,6 @@
|
||||||
"Some of your messages have not been sent": "Some of your messages have not been sent",
|
"Some of your messages have not been sent": "Some of your messages have not been sent",
|
||||||
"Delete all": "Delete all",
|
"Delete all": "Delete all",
|
||||||
"Retry all": "Retry all",
|
"Retry all": "Retry all",
|
||||||
"Sending": "Sending",
|
|
||||||
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
|
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
|
@ -2728,6 +2733,8 @@
|
||||||
"A private space to organise your rooms": "A private space to organise your rooms",
|
"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",
|
||||||
|
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
|
||||||
|
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
|
||||||
"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",
|
||||||
"Inviting...": "Inviting...",
|
"Inviting...": "Inviting...",
|
||||||
"Invite your teammates": "Invite your teammates",
|
"Invite your teammates": "Invite your teammates",
|
||||||
|
|
|
@ -35,7 +35,7 @@ export interface MatrixProfile {
|
||||||
export interface CrawlerCheckpoint {
|
export interface CrawlerCheckpoint {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
token: string;
|
token: string;
|
||||||
fullCrawl: boolean;
|
fullCrawl?: boolean;
|
||||||
direction: string;
|
direction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,14 +73,14 @@ export interface EventAndProfile {
|
||||||
export interface LoadArgs {
|
export interface LoadArgs {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
fromEvent: string;
|
fromEvent?: string;
|
||||||
direction: string;
|
direction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexStats {
|
export interface IndexStats {
|
||||||
size: number;
|
size: number;
|
||||||
event_count: number;
|
eventCount: number;
|
||||||
room_count: number;
|
roomCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
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,33 +14,42 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import PlatformPeg from "../PlatformPeg";
|
import { EventEmitter } from "events";
|
||||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
|
||||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||||
|
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||||
|
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||||
|
|
||||||
|
import PlatformPeg from "../PlatformPeg";
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
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 { SettingLevel } from "../settings/SettingLevel";
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
|
import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
|
||||||
|
|
||||||
|
// The time in ms that the crawler will wait loop iterations if there
|
||||||
|
// have not been any checkpoints to consume in the last iteration.
|
||||||
|
const CRAWLER_IDLE_TIME = 5000;
|
||||||
|
|
||||||
|
// The maximum number of events our crawler should fetch in a single crawl.
|
||||||
|
const EVENTS_PER_CRAWL = 100;
|
||||||
|
|
||||||
|
interface ICrawler {
|
||||||
|
cancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Event indexing class that wraps the platform specific event indexing.
|
* Event indexing class that wraps the platform specific event indexing.
|
||||||
*/
|
*/
|
||||||
export default class EventIndex extends EventEmitter {
|
export default class EventIndex extends EventEmitter {
|
||||||
constructor() {
|
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
|
||||||
super();
|
private crawler: ICrawler = null;
|
||||||
this.crawlerCheckpoints = [];
|
private currentCheckpoint: CrawlerCheckpoint = null;
|
||||||
// The time in ms that the crawler will wait loop iterations if there
|
|
||||||
// have not been any checkpoints to consume in the last iteration.
|
|
||||||
this._crawlerIdleTime = 5000;
|
|
||||||
// The maximum number of events our crawler should fetch in a single
|
|
||||||
// crawl.
|
|
||||||
this._eventsPerCrawl = 100;
|
|
||||||
this._crawler = null;
|
|
||||||
this._currentCheckpoint = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
public async init() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
|
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
|
||||||
|
@ -52,7 +61,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Register event listeners that are necessary for the event index to work.
|
* Register event listeners that are necessary for the event index to work.
|
||||||
*/
|
*/
|
||||||
registerListeners() {
|
public registerListeners() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
client.on('sync', this.onSync);
|
client.on('sync', this.onSync);
|
||||||
|
@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Remove the event index specific event listeners.
|
* Remove the event index specific event listeners.
|
||||||
*/
|
*/
|
||||||
removeListeners() {
|
public removeListeners() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client === null) return;
|
if (client === null) return;
|
||||||
|
|
||||||
|
@ -81,7 +90,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Get crawler checkpoints for the encrypted rooms and store them in the index.
|
* Get crawler checkpoints for the encrypted rooms and store them in the index.
|
||||||
*/
|
*/
|
||||||
async addInitialCheckpoints() {
|
public async addInitialCheckpoints() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const rooms = client.getRooms();
|
const rooms = client.getRooms();
|
||||||
|
@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter {
|
||||||
const timeline = room.getLiveTimeline();
|
const timeline = room.getLiveTimeline();
|
||||||
const token = timeline.getPaginationToken("b");
|
const token = timeline.getPaginationToken("b");
|
||||||
|
|
||||||
const backCheckpoint = {
|
const backCheckpoint: CrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "b",
|
direction: "b",
|
||||||
fullCrawl: true,
|
fullCrawl: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardCheckpoint = {
|
const forwardCheckpoint: CrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "f",
|
direction: "f",
|
||||||
|
@ -146,7 +155,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* - Every other sync, tell the event index to commit all the queued up
|
* - Every other sync, tell the event index to commit all the queued up
|
||||||
* live events
|
* live events
|
||||||
*/
|
*/
|
||||||
onSync = async (state, prevState, data) => {
|
private onSync = async (state: string, prevState: string, data: object) => {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
if (prevState === "PREPARED" && state === "SYNCING") {
|
if (prevState === "PREPARED" && state === "SYNCING") {
|
||||||
|
@ -176,7 +185,15 @@ export default class EventIndex extends EventEmitter {
|
||||||
* otherwise we save their event id and wait for them in the Event.decrypted
|
* otherwise we save their event id and wait for them in the Event.decrypted
|
||||||
* listener.
|
* listener.
|
||||||
*/
|
*/
|
||||||
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
|
private onRoomTimeline = async (
|
||||||
|
ev: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
removed: boolean,
|
||||||
|
data: {
|
||||||
|
liveEvent: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
// We only index encrypted rooms locally.
|
// We only index encrypted rooms locally.
|
||||||
|
@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
await this.addLiveEventToIndex(ev);
|
await this.addLiveEventToIndex(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomStateEvent = async (ev, state) => {
|
private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => {
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
|
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
|
||||||
|
|
||||||
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) {
|
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) {
|
||||||
|
@ -209,7 +226,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* Checks if the event was marked for addition in the Room.timeline
|
* Checks if the event was marked for addition in the Room.timeline
|
||||||
* listener, if so queues it up to be added to the index.
|
* listener, if so queues it up to be added to the index.
|
||||||
*/
|
*/
|
||||||
onEventDecrypted = async (ev, err) => {
|
private onEventDecrypted = async (ev: MatrixEvent, err: Error) => {
|
||||||
// If the event isn't in our live event set, ignore it.
|
// If the event isn't in our live event set, ignore it.
|
||||||
if (err) return;
|
if (err) return;
|
||||||
await this.addLiveEventToIndex(ev);
|
await this.addLiveEventToIndex(ev);
|
||||||
|
@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
*
|
*
|
||||||
* Removes a redacted event from our event index.
|
* Removes a redacted event from our event index.
|
||||||
*/
|
*/
|
||||||
onRedaction = async (ev, room) => {
|
private onRedaction = async (ev: MatrixEvent, room: Room) => {
|
||||||
// We only index encrypted rooms locally.
|
// We only index encrypted rooms locally.
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
@ -238,7 +255,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* Listens for timeline resets that are caused by a limited timeline to
|
* Listens for timeline resets that are caused by a limited timeline to
|
||||||
* re-add checkpoints for rooms that need to be crawled again.
|
* re-add checkpoints for rooms that need to be crawled again.
|
||||||
*/
|
*/
|
||||||
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
|
private onTimelineReset = async (room: Room, timelineSet: EventTimelineSet, resetAllTimelines: boolean) => {
|
||||||
if (room === null) return;
|
if (room === null) return;
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
||||||
|
|
||||||
|
@ -258,7 +275,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @returns {bool} Returns true if the event can be indexed, false
|
* @returns {bool} Returns true if the event can be indexed, false
|
||||||
* otherwise.
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
isValidEvent(ev) {
|
private isValidEvent(ev: MatrixEvent) {
|
||||||
const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType());
|
const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType());
|
||||||
const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure();
|
const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure();
|
||||||
|
|
||||||
|
@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
return validEventType && validMsgType && hasContentValue;
|
return validEventType && validMsgType && hasContentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventToJson(ev) {
|
private eventToJson(ev: MatrixEvent) {
|
||||||
const jsonEvent = ev.toJSON();
|
const jsonEvent = ev.toJSON();
|
||||||
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
||||||
|
|
||||||
|
@ -314,7 +331,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
*
|
*
|
||||||
* @param {MatrixEvent} ev The event that should be added to the index.
|
* @param {MatrixEvent} ev The event that should be added to the index.
|
||||||
*/
|
*/
|
||||||
async addLiveEventToIndex(ev) {
|
private async addLiveEventToIndex(ev: MatrixEvent) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
if (!this.isValidEvent(ev)) return;
|
if (!this.isValidEvent(ev)) return;
|
||||||
|
@ -333,11 +350,11 @@ export default class EventIndex extends EventEmitter {
|
||||||
* Emmit that the crawler has changed the checkpoint that it's currently
|
* Emmit that the crawler has changed the checkpoint that it's currently
|
||||||
* handling.
|
* handling.
|
||||||
*/
|
*/
|
||||||
emitNewCheckpoint() {
|
private emitNewCheckpoint() {
|
||||||
this.emit("changedCheckpoint", this.currentRoom());
|
this.emit("changedCheckpoint", this.currentRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEventsFromLiveTimeline(timeline) {
|
private async addEventsFromLiveTimeline(timeline: EventTimeline) {
|
||||||
const events = timeline.getEvents();
|
const events = timeline.getEvents();
|
||||||
|
|
||||||
for (let i = 0; i < events.length; i++) {
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
@ -346,7 +363,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addRoomCheckpoint(roomId, fullCrawl = false) {
|
private async addRoomCheckpoint(roomId: string, fullCrawl = false) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
|
@ -396,16 +413,16 @@ export default class EventIndex extends EventEmitter {
|
||||||
* crawl, otherwise create a new checkpoint and push it to the
|
* crawl, otherwise create a new checkpoint and push it to the
|
||||||
* crawlerCheckpoints queue so we go through them in a round-robin way.
|
* crawlerCheckpoints queue so we go through them in a round-robin way.
|
||||||
*/
|
*/
|
||||||
async crawlerFunc() {
|
private async crawlerFunc() {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
this._crawler = {};
|
this.crawler = {
|
||||||
|
cancel: () => {
|
||||||
this._crawler.cancel = () => {
|
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let idle = false;
|
let idle = false;
|
||||||
|
@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter {
|
||||||
sleepTime = Math.max(sleepTime, 100);
|
sleepTime = Math.max(sleepTime, 100);
|
||||||
|
|
||||||
if (idle) {
|
if (idle) {
|
||||||
sleepTime = this._crawlerIdleTime;
|
sleepTime = CRAWLER_IDLE_TIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._currentCheckpoint !== null) {
|
if (this.currentCheckpoint !== null) {
|
||||||
this._currentCheckpoint = null;
|
this.currentCheckpoint = null;
|
||||||
this.emitNewCheckpoint();
|
this.emitNewCheckpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._currentCheckpoint = checkpoint;
|
this.currentCheckpoint = checkpoint;
|
||||||
this.emitNewCheckpoint();
|
this.emitNewCheckpoint();
|
||||||
|
|
||||||
idle = false;
|
idle = false;
|
||||||
|
@ -454,8 +471,11 @@ export default class EventIndex extends EventEmitter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await client.createMessagesRequest(
|
res = await client.createMessagesRequest(
|
||||||
checkpoint.roomId, checkpoint.token, this._eventsPerCrawl,
|
checkpoint.roomId,
|
||||||
checkpoint.direction);
|
checkpoint.token,
|
||||||
|
EVENTS_PER_CRAWL,
|
||||||
|
checkpoint.direction,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.httpStatus === 403) {
|
if (e.httpStatus === 403) {
|
||||||
console.log("EventIndex: Removing checkpoint as we don't have ",
|
console.log("EventIndex: Removing checkpoint as we don't have ",
|
||||||
|
@ -612,23 +632,23 @@ export default class EventIndex extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._crawler = null;
|
this.crawler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the crawler background task.
|
* Start the crawler background task.
|
||||||
*/
|
*/
|
||||||
startCrawler() {
|
public startCrawler() {
|
||||||
if (this._crawler !== null) return;
|
if (this.crawler !== null) return;
|
||||||
this.crawlerFunc();
|
this.crawlerFunc();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the crawler background task.
|
* Stop the crawler background task.
|
||||||
*/
|
*/
|
||||||
stopCrawler() {
|
public stopCrawler() {
|
||||||
if (this._crawler === null) return;
|
if (this.crawler === null) return;
|
||||||
this._crawler.cancel();
|
this.crawler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* This removes all the MatrixClient event listeners, stops the crawler
|
* This removes all the MatrixClient event listeners, stops the crawler
|
||||||
* task, and closes the index.
|
* task, and closes the index.
|
||||||
*/
|
*/
|
||||||
async close() {
|
public async close() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
this.stopCrawler();
|
this.stopCrawler();
|
||||||
|
@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||||
* of search results once the search is done.
|
* of search results once the search is done.
|
||||||
*/
|
*/
|
||||||
async search(searchArgs) {
|
public async search(searchArgs: SearchArgs) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
return indexManager.searchEventIndex(searchArgs);
|
return indexManager.searchEventIndex(searchArgs);
|
||||||
}
|
}
|
||||||
|
@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
|
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
|
||||||
* contain URLs.
|
* contain URLs.
|
||||||
*/
|
*/
|
||||||
async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) {
|
public async loadFileEvents(
|
||||||
|
room: Room,
|
||||||
|
limit = 10,
|
||||||
|
fromEvent: string = null,
|
||||||
|
direction: string = EventTimeline.BACKWARDS,
|
||||||
|
) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
const loadArgs = {
|
const loadArgs: LoadArgs = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
};
|
};
|
||||||
|
@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @returns {Promise<boolean>} Resolves to true if events were added to the
|
* @returns {Promise<boolean>} Resolves to true if events were added to the
|
||||||
* timeline, false otherwise.
|
* timeline, false otherwise.
|
||||||
*/
|
*/
|
||||||
async populateFileTimeline(
|
public async populateFileTimeline(
|
||||||
timelineSet,
|
timelineSet: EventTimelineSet,
|
||||||
timeline,
|
timeline: EventTimeline,
|
||||||
room,
|
room: Room,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
fromEvent = null,
|
fromEvent: string = null,
|
||||||
direction = EventTimeline.BACKWARDS,
|
direction: string = EventTimeline.BACKWARDS,
|
||||||
) {
|
) {
|
||||||
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
|
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
|
||||||
|
|
||||||
|
@ -837,7 +862,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @returns {Promise<boolean>} Resolves to a boolean which is true if more
|
* @returns {Promise<boolean>} Resolves to a boolean which is true if more
|
||||||
* events were successfully retrieved.
|
* events were successfully retrieved.
|
||||||
*/
|
*/
|
||||||
paginateTimelineWindow(room, timelineWindow, direction, limit) {
|
public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) {
|
||||||
const tl = timelineWindow.getTimelineIndex(direction);
|
const tl = timelineWindow.getTimelineIndex(direction);
|
||||||
|
|
||||||
if (!tl) return Promise.resolve(false);
|
if (!tl) return Promise.resolve(false);
|
||||||
|
@ -871,7 +896,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @return {Promise<IndexStats>} A promise that will resolve to the index
|
* @return {Promise<IndexStats>} A promise that will resolve to the index
|
||||||
* statistics.
|
* statistics.
|
||||||
*/
|
*/
|
||||||
async getStats() {
|
public async getStats() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
return indexManager.getStats();
|
return indexManager.getStats();
|
||||||
}
|
}
|
||||||
|
@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @return {Promise<boolean>} Returns true if the index contains events for
|
* @return {Promise<boolean>} Returns true if the index contains events for
|
||||||
* the given room, false otherwise.
|
* the given room, false otherwise.
|
||||||
*/
|
*/
|
||||||
async isRoomIndexed(roomId) {
|
public async isRoomIndexed(roomId) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
return indexManager.isRoomIndexed(roomId);
|
return indexManager.isRoomIndexed(roomId);
|
||||||
}
|
}
|
||||||
|
@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter {
|
||||||
* @returns {Room} A MatrixRoom that is being currently crawled, null
|
* @returns {Room} A MatrixRoom that is being currently crawled, null
|
||||||
* if no room is currently being crawled.
|
* if no room is currently being crawled.
|
||||||
*/
|
*/
|
||||||
currentRoom() {
|
public currentRoom() {
|
||||||
if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
|
if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (this._currentCheckpoint !== null) {
|
if (this.currentCheckpoint !== null) {
|
||||||
return client.getRoom(this._currentCheckpoint.roomId);
|
return client.getRoom(this.currentCheckpoint.roomId);
|
||||||
} else {
|
} else {
|
||||||
return client.getRoom(this.crawlerCheckpoints[0].roomId);
|
return client.getRoom(this.crawlerCheckpoints[0].roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crawlingRooms() {
|
public crawlingRooms() {
|
||||||
const totalRooms = new Set();
|
const totalRooms = new Set();
|
||||||
const crawlingRooms = new Set();
|
const crawlingRooms = new Set();
|
||||||
|
|
||||||
|
@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter {
|
||||||
crawlingRooms.add(checkpoint.roomId);
|
crawlingRooms.add(checkpoint.roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._currentCheckpoint !== null) {
|
if (this.currentCheckpoint !== null) {
|
||||||
crawlingRooms.add(this._currentCheckpoint.roomId);
|
crawlingRooms.add(this.currentCheckpoint.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const rooms = client.getRooms();
|
const rooms = client.getRooms();
|
||||||
|
|
||||||
const isRoomEncrypted = (room) => {
|
const isRoomEncrypted = (room: Room) => {
|
||||||
return client.isRoomEncrypted(room.roomId);
|
return client.isRoomEncrypted(room.roomId);
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,8 +63,7 @@ export class WatchManager {
|
||||||
|
|
||||||
if (!inRoomId) {
|
if (!inRoomId) {
|
||||||
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
||||||
const callbacks = Array.from(roomWatchers.values()).flat(1);
|
callbacks.push(...Array.from(roomWatchers.values()).flat(1));
|
||||||
callbacks.push(...callbacks);
|
|
||||||
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
||||||
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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.
|
||||||
|
@ -15,11 +13,18 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import dis from '../dispatcher/dispatcher';
|
|
||||||
import { Store } from 'flux/utils';
|
import { Store } from 'flux/utils';
|
||||||
|
|
||||||
|
import dis from '../dispatcher/dispatcher';
|
||||||
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
deferredAction: any;
|
||||||
|
}
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
const INITIAL_STATE = {
|
||||||
deferred_action: null,
|
deferredAction: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,39 +32,38 @@ const INITIAL_STATE = {
|
||||||
* store that listens for actions and updates its state accordingly, informing any
|
* store that listens for actions and updates its state accordingly, informing any
|
||||||
* listeners (views) of state changes.
|
* listeners (views) of state changes.
|
||||||
*/
|
*/
|
||||||
class LifecycleStore extends Store {
|
class LifecycleStore extends Store<ActionPayload> {
|
||||||
|
private state: IState = INITIAL_STATE;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(dis);
|
super(dis);
|
||||||
|
|
||||||
// Initialise state
|
|
||||||
this._state = INITIAL_STATE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setState(newState) {
|
private setState(newState: Partial<IState>) {
|
||||||
this._state = Object.assign(this._state, newState);
|
this.state = Object.assign(this.state, newState);
|
||||||
this.__emitChange();
|
this.__emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
__onDispatch(payload) {
|
protected __onDispatch(payload: ActionPayload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'do_after_sync_prepared':
|
case 'do_after_sync_prepared':
|
||||||
this._setState({
|
this.setState({
|
||||||
deferred_action: payload.deferred_action,
|
deferredAction: payload.deferred_action,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'cancel_after_sync_prepared':
|
case 'cancel_after_sync_prepared':
|
||||||
this._setState({
|
this.setState({
|
||||||
deferred_action: null,
|
deferredAction: null,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'sync_state': {
|
case 'syncstate': {
|
||||||
if (payload.state !== 'PREPARED') {
|
if (payload.state !== 'PREPARED') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!this._state.deferred_action) break;
|
if (!this.state.deferredAction) break;
|
||||||
const deferredAction = Object.assign({}, this._state.deferred_action);
|
const deferredAction = Object.assign({}, this.state.deferredAction);
|
||||||
this._setState({
|
this.setState({
|
||||||
deferred_action: null,
|
deferredAction: null,
|
||||||
});
|
});
|
||||||
dis.dispatch(deferredAction);
|
dis.dispatch(deferredAction);
|
||||||
break;
|
break;
|
||||||
|
@ -71,8 +75,8 @@ class LifecycleStore extends Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
private reset() {
|
||||||
this._state = Object.assign({}, INITIAL_STATE);
|
this.state = Object.assign({}, INITIAL_STATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,6 @@ const INITIAL_STATE = {
|
||||||
// Any error that has occurred during loading
|
// Any error that has occurred during loading
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
|
|
||||||
forwardingEvent: null,
|
|
||||||
|
|
||||||
quotingEvent: null,
|
quotingEvent: null,
|
||||||
|
|
||||||
replyingToEvent: null,
|
replyingToEvent: null,
|
||||||
|
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
case 'on_logged_out':
|
case 'on_logged_out':
|
||||||
this.reset();
|
this.reset();
|
||||||
break;
|
break;
|
||||||
case 'forward_event':
|
|
||||||
this.setState({
|
|
||||||
forwardingEvent: payload.event,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'reply_to_event':
|
case 'reply_to_event':
|
||||||
// If currently viewed room does not match the room in which we wish to reply then change rooms
|
// If currently viewed room does not match the room in which we wish to reply then change rooms
|
||||||
// this can happen when performing a search across all rooms
|
// this can happen when performing a search across all rooms
|
||||||
|
@ -187,7 +180,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
roomAlias: payload.room_alias,
|
roomAlias: payload.room_alias,
|
||||||
initialEventId: payload.event_id,
|
initialEventId: payload.event_id,
|
||||||
isInitialEventHighlighted: payload.highlighted,
|
isInitialEventHighlighted: payload.highlighted,
|
||||||
forwardingEvent: null,
|
|
||||||
roomLoading: false,
|
roomLoading: false,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
// should peek by default
|
// should peek by default
|
||||||
|
@ -207,14 +199,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
newState.replyingToEvent = payload.replyingToEvent;
|
newState.replyingToEvent = payload.replyingToEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'send_event',
|
|
||||||
room_id: newState.roomId,
|
|
||||||
event: this.state.forwardingEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
|
|
||||||
if (payload.auto_join) {
|
if (payload.auto_join) {
|
||||||
|
@ -428,11 +412,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
return this.state.joinError;
|
return this.state.joinError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The mxEvent if one is about to be forwarded
|
|
||||||
public getForwardingEvent() {
|
|
||||||
return this.state.forwardingEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The mxEvent if one is currently being replied to/quoted
|
// The mxEvent if one is currently being replied to/quoted
|
||||||
public getQuotingEvent() {
|
public getQuotingEvent() {
|
||||||
return this.state.replyingToEvent;
|
return this.state.replyingToEvent;
|
||||||
|
|
|
@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainerWidgets(room: Room, container: Container): IApp[] {
|
public getContainerWidgets(room: Room, container: Container): IApp[] {
|
||||||
return this.byRoom[room.roomId]?.[container]?.ordered || [];
|
return this.byRoom[room?.roomId]?.[container]?.ordered || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInContainer(room: Room, widget: IApp, container: Container): boolean {
|
public isInContainer(room: Room, widget: IApp, container: Container): boolean {
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically focuses the captured reference when receiving a non-null
|
|
||||||
* object. Useful in scenarios where componentDidMount does not have a
|
|
||||||
* useful reference to an element, but one needs to focus the element on
|
|
||||||
* first render. Example usage: ref={focusCapturedRef}
|
|
||||||
* @param {function} ref The React reference to focus on, if not null
|
|
||||||
*/
|
|
||||||
export function focusCapturedRef(ref) {
|
|
||||||
if (ref) {
|
|
||||||
ref.focus();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
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.
|
||||||
|
@ -22,7 +22,7 @@ import url from "url";
|
||||||
* @param {string} u The url to be abbreviated
|
* @param {string} u The url to be abbreviated
|
||||||
* @returns {string} The abbreviated url
|
* @returns {string} The abbreviated url
|
||||||
*/
|
*/
|
||||||
export function abbreviateUrl(u) {
|
export function abbreviateUrl(u: string): string {
|
||||||
if (!u) return '';
|
if (!u) return '';
|
||||||
|
|
||||||
const parsedUrl = url.parse(u);
|
const parsedUrl = url.parse(u);
|
||||||
|
@ -37,7 +37,7 @@ export function abbreviateUrl(u) {
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unabbreviateUrl(u) {
|
export function unabbreviateUrl(u: string): string {
|
||||||
if (!u) return '';
|
if (!u) return '';
|
||||||
|
|
||||||
let longUrl = u;
|
let longUrl = u;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 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.
|
||||||
|
@ -16,12 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a timestamp into human-readable, translated, text.
|
|
||||||
* @param {number} timeMillis The time in millis to compare against.
|
|
||||||
* @returns {string} The humanized time.
|
|
||||||
*/
|
|
||||||
export function humanizeTime(timeMillis) {
|
|
||||||
// These are the constants we use for when to break the text
|
// These are the constants we use for when to break the text
|
||||||
const MILLISECONDS_RECENT = 15000;
|
const MILLISECONDS_RECENT = 15000;
|
||||||
const MILLISECONDS_1_MIN = 75000;
|
const MILLISECONDS_1_MIN = 75000;
|
||||||
|
@ -30,6 +24,12 @@ export function humanizeTime(timeMillis) {
|
||||||
const HOURS_UNDER_1_DAY = 23;
|
const HOURS_UNDER_1_DAY = 23;
|
||||||
const HOURS_1_DAY = 26;
|
const HOURS_1_DAY = 26;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a timestamp into human-readable, translated, text.
|
||||||
|
* @param {number} timeMillis The time in millis to compare against.
|
||||||
|
* @returns {string} The humanized time.
|
||||||
|
*/
|
||||||
|
export function humanizeTime(timeMillis: number): string {
|
||||||
const now = (new Date()).getTime();
|
const now = (new Date()).getTime();
|
||||||
let msAgo = now - timeMillis;
|
let msAgo = now - timeMillis;
|
||||||
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
|
@ -19,11 +19,11 @@ limitations under the License.
|
||||||
* TODO: Convert this to a real TypeScript interface
|
* TODO: Convert this to a real TypeScript interface
|
||||||
*/
|
*/
|
||||||
export default class PermalinkConstructor {
|
export default class PermalinkConstructor {
|
||||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,12 +73,12 @@ export class PermalinkParts {
|
||||||
return new PermalinkParts(null, null, null, groupId, null);
|
return new PermalinkParts(null, null, null, groupId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
|
static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
|
||||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
|
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
|
static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
|
||||||
return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
|
return new PermalinkParts(roomId, eventId, null, null, viaServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
get primaryEntityId(): string {
|
get primaryEntityId(): string {
|
||||||
|
|
|
@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
|
||||||
// Prefer to use canonical alias for permalink if possible
|
// Prefer to use canonical alias for permalink if possible
|
||||||
const alias = this.room.getCanonicalAlias();
|
const alias = this.room.getCanonicalAlias();
|
||||||
if (alias) {
|
if (alias) {
|
||||||
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
|
return getPermalinkConstructor().forRoom(alias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
||||||
|
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
|
||||||
}
|
}
|
||||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||||
permalinkCreator.load();
|
permalinkCreator.load();
|
||||||
return permalinkCreator.forRoom();
|
return permalinkCreator.forShareableRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeGroupPermalink(groupId: string): string {
|
export function makeGroupPermalink(groupId: string): string {
|
||||||
|
|
|
@ -25,7 +25,6 @@ import DMRoomMap from '../src/utils/DMRoomMap';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from '../src/SdkConfig';
|
import SdkConfig from '../src/SdkConfig';
|
||||||
import { ActionPayload } from '../src/dispatcher/payloads';
|
import { ActionPayload } from '../src/dispatcher/payloads';
|
||||||
import { Actions } from '../src/notifications/types';
|
|
||||||
import { Action } from '../src/dispatcher/actions';
|
import { Action } from '../src/dispatcher/actions';
|
||||||
|
|
||||||
const REAL_ROOM_ID = '$room1:example.org';
|
const REAL_ROOM_ID = '$room1:example.org';
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
function mockKeyEvent(key: string, modifiers?: {
|
function mockKeyEvent(key: string, modifiers?: {
|
||||||
ctrlKey?: boolean,
|
ctrlKey?: boolean,
|
||||||
|
@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: {
|
||||||
ctrlKey: modifiers?.ctrlKey ?? false,
|
ctrlKey: modifiers?.ctrlKey ?? false,
|
||||||
altKey: modifiers?.altKey ?? false,
|
altKey: modifiers?.altKey ?? false,
|
||||||
shiftKey: modifiers?.shiftKey ?? false,
|
shiftKey: modifiers?.shiftKey ?? false,
|
||||||
metaKey: modifiers?.metaKey ?? false
|
metaKey: modifiers?.metaKey ?? false,
|
||||||
} as KeyboardEvent;
|
} as KeyboardEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => {
|
||||||
const combo1: KeyCombo = {
|
const combo1: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match key + modifier key combo', () => {
|
it('should match key + modifier key combo', () => {
|
||||||
|
@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false);
|
||||||
|
|
||||||
const combo2: KeyCombo = {
|
const combo2: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
metaKey: true,
|
metaKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||||
|
|
||||||
const combo3: KeyCombo = {
|
const combo3: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
altKey: true,
|
altKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false);
|
||||||
|
|
||||||
const combo4: KeyCombo = {
|
const combo4: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match key + multiple modifiers key combo', () => {
|
it('should match key + multiple modifiers key combo', () => {
|
||||||
|
@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => {
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
altKey: true,
|
altKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
|
||||||
false), false);
|
false)).toBe(false);
|
||||||
|
|
||||||
const combo2: KeyCombo = {
|
const combo2: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
|
@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => {
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
altKey: true,
|
altKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||||
false), true);
|
false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||||
false), false);
|
false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false);
|
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||||
|
|
||||||
const combo3: KeyCombo = {
|
const combo3: KeyCombo = {
|
||||||
key: 'k',
|
key: 'k',
|
||||||
|
@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => {
|
||||||
altKey: true,
|
altKey: true,
|
||||||
metaKey: true,
|
metaKey: true,
|
||||||
};
|
};
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true);
|
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n',
|
expect(isKeyComboMatch(mockKeyEvent('n',
|
||||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false);
|
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||||
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false);
|
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match ctrlOrMeta key combo', () => {
|
it('should match ctrlOrMeta key combo', () => {
|
||||||
|
@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => {
|
||||||
ctrlOrCmd: true,
|
ctrlOrCmd: true,
|
||||||
};
|
};
|
||||||
// PC:
|
// PC:
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
|
||||||
// MAC:
|
// MAC:
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false);
|
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match advanced ctrlOrMeta key combo', () => {
|
it('should match advanced ctrlOrMeta key combo', () => {
|
||||||
|
@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => {
|
||||||
altKey: true,
|
altKey: true,
|
||||||
};
|
};
|
||||||
// PC:
|
// PC:
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false);
|
||||||
// MAC:
|
// MAC:
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true);
|
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true);
|
||||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false);
|
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
163
test/components/views/dialogs/ForwardDialog-test.js
Normal file
163
test/components/views/dialogs/ForwardDialog-test.js
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "../../../skinned-sdk";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {configure, mount} from "enzyme";
|
||||||
|
import Adapter from "enzyme-adapter-react-16";
|
||||||
|
import {act} from "react-dom/test-utils";
|
||||||
|
|
||||||
|
import * as TestUtils from "../../../test-utils";
|
||||||
|
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
|
||||||
|
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
|
||||||
|
|
||||||
|
configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
describe("ForwardDialog", () => {
|
||||||
|
const sourceRoom = "!111111111111111111:example.org";
|
||||||
|
const defaultMessage = TestUtils.mkMessage({
|
||||||
|
room: sourceRoom,
|
||||||
|
user: "@alice:example.org",
|
||||||
|
msg: "Hello world!",
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
|
||||||
|
|
||||||
|
const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mount(
|
||||||
|
<ForwardDialog
|
||||||
|
matrixClient={client}
|
||||||
|
event={message}
|
||||||
|
permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
|
||||||
|
onFinished={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Wait one tick for our profile data to load so the state update happens within act
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestUtils.stubClient();
|
||||||
|
DMRoomMap.makeShared();
|
||||||
|
MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a preview with us as the sender", async () => {
|
||||||
|
const wrapper = await mountForwardDialog();
|
||||||
|
|
||||||
|
const previewBody = wrapper.find(".mx_EventTile_body");
|
||||||
|
expect(previewBody.text()).toBe("Hello world!");
|
||||||
|
|
||||||
|
// We would just test SenderProfile for the user ID, but it's stubbed
|
||||||
|
const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
|
||||||
|
expect(previewAvatar.prop("title")).toBe("@bob:example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters the rooms", async () => {
|
||||||
|
const wrapper = await mountForwardDialog();
|
||||||
|
|
||||||
|
expect(wrapper.find("Entry")).toHaveLength(3);
|
||||||
|
|
||||||
|
const searchInput = wrapper.find("SearchBox input");
|
||||||
|
searchInput.instance().value = "a";
|
||||||
|
searchInput.simulate("change");
|
||||||
|
|
||||||
|
expect(wrapper.find("Entry")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tracks message sending progress across multiple rooms", async () => {
|
||||||
|
const wrapper = await mountForwardDialog();
|
||||||
|
|
||||||
|
// Make sendEvent require manual resolution so we can see the sending state
|
||||||
|
let finishSend;
|
||||||
|
let cancelSend;
|
||||||
|
MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
|
||||||
|
finishSend = resolve;
|
||||||
|
cancelSend = reject;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
|
||||||
|
expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
|
||||||
|
|
||||||
|
act(() => { firstButton.simulate("click"); });
|
||||||
|
expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
cancelSend();
|
||||||
|
// Wait one tick for the button to realize the send failed
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
});
|
||||||
|
expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
|
||||||
|
|
||||||
|
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
|
||||||
|
expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
|
||||||
|
|
||||||
|
act(() => { secondButton.simulate("click"); });
|
||||||
|
expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
finishSend();
|
||||||
|
// Wait one tick for the button to realize the send succeeded
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
});
|
||||||
|
expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render replies", async () => {
|
||||||
|
const replyMessage = TestUtils.mkEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
room: "!111111111111111111:example.org",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
event_id: "$2222222222222222222222222222222222222222222",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = await mountForwardDialog(replyMessage);
|
||||||
|
expect(wrapper.find("ReplyThread")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables buttons for rooms without send permissions", async () => {
|
||||||
|
const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
|
||||||
|
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
|
||||||
|
const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
|
||||||
|
|
||||||
|
const wrapper = await mountForwardDialog(undefined, rooms);
|
||||||
|
|
||||||
|
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
|
||||||
|
expect(firstButton.prop("disabled")).toBe(true);
|
||||||
|
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
|
||||||
|
expect(secondButton.prop("disabled")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work
|
||||||
import SpaceStore, {
|
import SpaceStore, {
|
||||||
UPDATE_INVITED_SPACES,
|
UPDATE_INVITED_SPACES,
|
||||||
UPDATE_SELECTED_SPACE,
|
UPDATE_SELECTED_SPACE,
|
||||||
UPDATE_TOP_LEVEL_SPACES
|
UPDATE_TOP_LEVEL_SPACES,
|
||||||
} from "../../src/stores/SpaceStore";
|
} from "../../src/stores/SpaceStore";
|
||||||
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
|
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
|
||||||
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
|
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
|
||||||
|
|
|
@ -219,7 +219,7 @@ export function mkMessage(opts) {
|
||||||
return mkEvent(opts);
|
return mkEvent(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkStubRoom(roomId = null) {
|
export function mkStubRoom(roomId = null, name) {
|
||||||
const stubTimeline = { getEvents: () => [] };
|
const stubTimeline = { getEvents: () => [] };
|
||||||
return {
|
return {
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
|
||||||
getPendingEvents: () => [],
|
getPendingEvents: () => [],
|
||||||
getLiveTimeline: () => stubTimeline,
|
getLiveTimeline: () => stubTimeline,
|
||||||
getUnfilteredTimelineSet: () => null,
|
getUnfilteredTimelineSet: () => null,
|
||||||
|
findEventById: () => null,
|
||||||
getAccountData: () => null,
|
getAccountData: () => null,
|
||||||
hasMembershipState: () => null,
|
hasMembershipState: () => null,
|
||||||
getVersion: () => '1',
|
getVersion: () => '1',
|
||||||
|
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
|
||||||
tags: {},
|
tags: {},
|
||||||
setBlacklistUnverifiedDevices: jest.fn(),
|
setBlacklistUnverifiedDevices: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
getDMInviter: jest.fn(),
|
getDMInviter: jest.fn(),
|
||||||
|
name,
|
||||||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
isSpaceRoom: jest.fn(() => false),
|
isSpaceRoom: jest.fn(() => false),
|
||||||
getUnreadNotificationCount: jest.fn(() => 0),
|
getUnreadNotificationCount: jest.fn(() => 0),
|
||||||
getEventReadUpTo: jest.fn(() => null),
|
getEventReadUpTo: jest.fn(() => null),
|
||||||
|
getCanonicalAlias: jest.fn(),
|
||||||
|
getAltAliases: jest.fn().mockReturnValue([]),
|
||||||
timeline: [],
|
timeline: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomId,
|
roomId,
|
||||||
getCanonicalAlias: () => roomId,
|
getCanonicalAlias: () => null,
|
||||||
getJoinedMembers: () => members,
|
getJoinedMembers: () => members,
|
||||||
getMember: (userId) => members.find(m => m.userId === userId),
|
getMember: (userId) => members.find(m => m.userId === userId),
|
||||||
currentState: {
|
currentState: {
|
||||||
|
|
|
@ -5701,8 +5701,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 "11.1.0"
|
version "11.2.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
|
Loading…
Reference in a new issue