Merge branch 'develop' into gsouquet/threads-action-bar-19127

pull/21833/head
Germain Souquet 2021-09-28 09:34:43 +01:00
commit e52a33e93c
45 changed files with 668 additions and 468 deletions

View File

@ -1,3 +1,106 @@
Changes in [3.31.0](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0) (2021-09-27)
===================================================================================================
## ✨ Features
* Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
* Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
* Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
* Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
* Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
* Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
* Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
* Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
* Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
* Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
* Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
* Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
* Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
* Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
* Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
* hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
* Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
* Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
## 🐛 Bug Fixes
* Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
* Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
* Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
* Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
* Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
* Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
* Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
* Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
* Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
* Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
* Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
* Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
* Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
* Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
* Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
* Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
* Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
* Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
* Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
* Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
Changes in [3.31.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.2) (2021-09-22)
=============================================================================================================
## 🐛 Bug Fixes
* Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
Changes in [3.31.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.1) (2021-09-21)
=============================================================================================================
## ✨ Features
* Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
* Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
* Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
* Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
* Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
* Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
* Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
* Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
* Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
* Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
* Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
* Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
* Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
* Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
* Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
* hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
* Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
* Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
## 🐛 Bug Fixes
* Revert Firefox composer deletion hacks ([\#6844](https://github.com/matrix-org/matrix-react-sdk/pull/6844)). Fixes vector-im/element-web#19103 and vector-im/element-web#19103. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
* Fix pill deletion on Firefox 78 ([\#6832](https://github.com/matrix-org/matrix-react-sdk/pull/6832)). Fixes vector-im/element-web#19077 and vector-im/element-web#19077. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
* Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
* Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
* Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
* Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
* Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
* Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
* Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
* Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
* Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
* Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
* Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
* Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
* Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
* Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
* Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
* Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
* Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14) Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14)
=================================================================================================== ===================================================================================================

View File

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

View File

@ -89,7 +89,6 @@ limitations under the License.
margin: 0px auto; margin: 0px auto;
overflow: auto; overflow: auto;
flex: 0 0 auto;
} }
.mx_RoomView_auxPanel_fullHeight { .mx_RoomView_auxPanel_fullHeight {

View File

@ -59,3 +59,14 @@ limitations under the License.
border-left-color: $username-variant8-color; border-left-color: $username-variant8-color;
} }
} }
.mx_ReplyThread--expanded {
.mx_EventTile_body {
display: block;
overflow-y: scroll !important;
}
.mx_EventTile_collapsedCodeBlock {
// !important needed due to .mx_ReplyTile .mx_EventTile_content .mx_EventTile_pre_container > pre
display: block !important;
}
}

View File

@ -18,7 +18,7 @@ a.mx_Pill {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
max-width: calc(100% - 1ch); max-width: 100%;
} }
.mx_Pill { .mx_Pill {

View File

@ -117,6 +117,16 @@ limitations under the License.
mask-image: url('$(res)/img/download.svg'); mask-image: url('$(res)/img/download.svg');
} }
.mx_MessageActionBar_expandMessageButton::after {
mask-size: 12px;
mask-image: url('$(res)/img/element-icons/expand-message.svg');
}
.mx_MessageActionBar_collapseMessageButton::after {
mask-size: 12px;
mask-image: url('$(res)/img/element-icons/collapse-message.svg');
}
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after { .mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
background-color: transparent; // hide the download icon mask background-color: transparent; // hide the download icon mask
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#737D8C" fill-rule="evenodd" d="M.2192.234A.753.753 0 011.2815.2321l3.7243 3.7003L8.7181.2202A.753.753 0 019.7805.2185a.747.747 0 01.0017 1.0589L5.5396 5.52a.753.753 0 01-1.0624.0018L.221 1.2928A.747.747 0 01.2192.234zM9.7822 13.7663a.7529.7529 0 01-1.0623.0017l-3.7243-3.7003L1.2833 13.78a.753.753 0 01-1.0624.0018.7471.7471 0 01-.0017-1.059l4.2426-4.2426a.753.753 0 011.0624-.0017l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#17191C" fill-rule="evenodd" d="M.2192 8.494a.753.753 0 011.0623-.0018l3.7243 3.7003 3.7123-3.7123a.753.753 0 011.0624-.0017.747.747 0 01.0017 1.059L5.5396 13.78a.753.753 0 01-1.0624.0018L.221 9.5528A.747.747 0 01.2192 8.494zM9.7822 5.5063A.753.753 0 018.72 5.508L4.9956 1.8077 1.2833 5.52a.753.753 0 01-1.0624.0018.747.747 0 01-.0017-1.059L4.4618.2202A.753.753 0 015.5242.2185l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -49,6 +49,8 @@ import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore"; import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
@ -92,6 +94,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxActiveWidgetStore?: ActiveWidgetStore;
mxOnRecaptchaLoaded?: () => void; mxOnRecaptchaLoaded?: () => void;
electron?: Electron; electron?: Electron;
} }
@ -223,6 +226,15 @@ declare global {
) => string; ) => string;
isReady: () => boolean; isReady: () => boolean;
}; };
// eslint-disable-next-line no-var, camelcase
var mx_rage_logger: ConsoleLogger;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initPromise: Promise<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initStoragePromise: Promise<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_store: IndexedDBLogStore;
} }
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View File

@ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.instance.start();
CallHandler.sharedInstance().start(); CallHandler.sharedInstance().start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // Start Mjolnir even though we haven't checked the feature flag yet. Starting
@ -892,7 +892,7 @@ export function stopMatrixClient(unsetClient = true): void {
UserActivity.sharedInstance().stop(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();
ActiveWidgetStore.stop(); ActiveWidgetStore.instance.stop();
IntegrationManagers.sharedInstance().stopWatching(); IntegrationManagers.sharedInstance().stopWatching();
Mjolnir.sharedInstance().stop(); Mjolnir.sharedInstance().stop();
DeviceListener.sharedInstance().stop(); DeviceListener.sharedInstance().stop();

View File

@ -45,7 +45,7 @@ function getOrCreateContainer(): HTMLDivElement {
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
interface IPosition { export interface IPosition {
top?: number; top?: number;
bottom?: number; bottom?: number;
left?: number; left?: number;
@ -430,7 +430,11 @@ export type AboveLeftOf = IPosition & {
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect, // Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?) // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => { export const aboveLeftOf = (
elementRect: DOMRect,
chevronFace = ChevronFace.None,
vPadding = 0,
): AboveLeftOf => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset; const buttonRight = elementRect.right + window.pageXOffset;

View File

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
@ -59,7 +58,7 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set groupId?: string; // if showing panels for a given group, this is set
user?: User; // used if we know the user ahead of opening the panel member?: RoomMember; // used if we know the room member ahead of opening the panel
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
@ -100,10 +99,10 @@ export default class RightPanel extends React.Component<IProps, IState> {
// Helper function to split out the logic for getPhaseFromProps() and the constructor // Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor. // as both are called at the same time in the constructor.
private getUserForPanel() { private getUserForPanel(): RoomMember {
if (this.state && this.state.member) return this.state.member; if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member']; return this.props.member || lastParams['member'];
} }
// gets the current phase from the props and also maybe the store // gets the current phase from the props and also maybe the store
@ -225,7 +224,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
// XXX: There are three different ways of 'closing' this panel depending on what state // XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest // things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly. // of the app and is generally a bit silly.
if (this.props.user) { if (this.props.member) {
// If we have a user prop then we're displaying a user from the 'user' page type // If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch // in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviously the correct thing to do, but I'm not sure // to the home page which is not obviously the correct thing to do, but I'm not sure

View File

@ -78,7 +78,6 @@ import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
import EditorStateTransfer from "../../utils/EditorStateTransfer"; import EditorStateTransfer from "../../utils/EditorStateTransfer";
import { throttle } from "lodash"; import { throttle } from "lodash";
import ErrorDialog from '../views/dialogs/ErrorDialog'; import ErrorDialog from '../views/dialogs/ErrorDialog';
@ -158,7 +157,6 @@ export interface IState {
// used by componentDidUpdate to avoid unnecessary checks // used by componentDidUpdate to avoid unnecessary checks
atEndOfLiveTimelineInit: boolean; atEndOfLiveTimelineInit: boolean;
showTopUnreadMessagesBar: boolean; showTopUnreadMessagesBar: boolean;
auxPanelMaxHeight?: number;
statusBarVisible: boolean; statusBarVisible: boolean;
// We load this later by asking the js-sdk to suggest a version for us. // We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion() // This object is the result of Room#getRecommendedVersion()
@ -565,10 +563,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}); });
window.addEventListener('beforeunload', this.onPageUnload); window.addEventListener('beforeunload', this.onPageUnload);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
}
this.onResize();
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -656,9 +650,6 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
window.removeEventListener('beforeunload', this.onPageUnload); window.removeEventListener('beforeunload', this.onPageUnload);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
}
// Remove RoomStore listener // Remove RoomStore listener
if (this.roomStoreToken) { if (this.roomStoreToken) {
@ -1619,28 +1610,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
} }
private onResize = () => {
// It seems flexbox doesn't give us a way to constrain the auxPanel height to have
// a minimum of the height of the video element, whilst also capping it from pushing out the page
// so we have to do it via JS instead. In this implementation we cap the height by putting
// a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times.
let auxPanelMaxHeight = UIStore.instance.windowHeight -
(54 + // height of RoomHeader
36 + // height of the status area
51 + // minimum height of the message composer
120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
this.setState({ auxPanelMaxHeight });
}
};
private onStatusBarVisible = () => { private onStatusBarVisible = () => {
if (this.unmounted || this.state.statusBarVisible) return; if (this.unmounted || this.state.statusBarVisible) return;
this.setState({ statusBarVisible: true }); this.setState({ statusBarVisible: true });
@ -1941,11 +1910,8 @@ export default class RoomView extends React.Component<IProps, IState> {
const auxPanel = ( const auxPanel = (
<AuxPanel <AuxPanel
room={this.state.room} room={this.state.room}
fullHeight={false}
userId={this.context.credentials.userId} userId={this.context.credentials.userId}
maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps} showApps={this.state.showApps}
onResize={this.onResize}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
> >
{ aux } { aux }

View File

@ -86,8 +86,8 @@ export default class UserView extends React.Component<IProps, IState> {
public render(): JSX.Element { public render(): JSX.Element {
if (this.state.loading) { if (this.state.loading) {
return <Spinner />; return <Spinner />;
} else if (this.state.member?.user) { } else if (this.state.member) {
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />; const panel = <RightPanel member={this.state.member} resizeNotifier={this.props.resizeNotifier} />;
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}> return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
<HomePage /> <HomePage />
</MainSplit>); </MainSplit>);

View File

@ -38,6 +38,7 @@ import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
import ErrorDialog from '../dialogs/ErrorDialog'; import ErrorDialog from '../dialogs/ErrorDialog';
import ShareDialog from '../dialogs/ShareDialog'; import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
export function canCancel(eventStatus: EventStatus): boolean { export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -52,7 +53,8 @@ export interface IOperableEventTile {
getEventTileOps(): IEventTileOps; getEventTileOps(): IEventTileOps;
} }
interface IProps { interface IProps extends IPosition {
chevronFace: ChevronFace;
/* the MatrixEvent associated with the context menu */ /* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */ /* an optional EventTileOps implementation that can be used to unhide preview widgets */

View File

@ -131,8 +131,13 @@ interface IProps {
} }
const isOnlyAdmin = (room: Room): boolean => { const isOnlyAdmin = (room: Room): boolean => {
return !room.getJoinedMembers().some(member => { const userId = room.client.getUserId();
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100; if (room.getMember(userId).powerLevelNorm !== 100) {
return false; // user is not an admin
}
return room.getJoinedMembers().every(member => {
// return true if every other member has a lower power level (we are highest)
return member.userId === userId || member.powerLevelNorm < 100;
}); });
}; };

View File

@ -163,7 +163,7 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this.persistKey); PersistedElement.destroyElement(this.persistKey);
if (this.sgWidget) this.sgWidget.stop(); if (this.sgWidget) this.sgWidget.stop();
} }
@ -198,8 +198,8 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container // if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this.persistKey); PersistedElement.destroyElement(this.persistKey);
} }
@ -282,7 +282,7 @@ export default class AppTile extends React.Component<IProps, IState> {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this.persistKey); PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -39,13 +39,13 @@ export default class PersistentApp extends React.Component<{}, IState> {
this.state = { this.state = {
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
}; };
} }
public componentDidMount(): void { public componentDidMount(): void {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate); ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
} }
@ -53,7 +53,7 @@ export default class PersistentApp extends React.Component<{}, IState> {
if (this.roomStoreToken) { if (this.roomStoreToken) {
this.roomStoreToken.remove(); this.roomStoreToken.remove();
} }
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
} }
@ -68,23 +68,23 @@ export default class PersistentApp extends React.Component<{}, IState> {
private onActiveWidgetStoreUpdate = (): void => { private onActiveWidgetStoreUpdate = (): void => {
this.setState({ this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
}); });
}; };
private onMyMembership = async (room: Room, membership: string): Promise<void> => { private onMyMembership = async (room: Room, membership: string): Promise<void> => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") { if (membership !== "join") {
// we're not in the room anymore - delete // we're not in the room anymore - delete
if (room .roomId === persistentWidgetInRoomId) { if (room .roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
} }
} }
}; };
public render(): JSX.Element { public render(): JSX.Element {
if (this.state.persistentWidgetId) { if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
@ -96,7 +96,7 @@ export default class PersistentApp extends React.Component<{}, IState> {
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data // get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
}); });
const app = WidgetUtils.makeAppConfig( const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),

View File

@ -16,6 +16,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile";
import Pill from './Pill'; import Pill from './Pill';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
/**
* This number is based on the previous behavior - if we have message of height
* over 60px then we want to show button that will allow to expand it.
*/
const SHOW_EXPAND_QUOTE_PIXELS = 60;
interface IProps { interface IProps {
// the latest event in this chain of replies // the latest event in this chain of replies
parentEv?: MatrixEvent; parentEv?: MatrixEvent;
@ -45,6 +53,8 @@ interface IProps {
layout?: Layout; layout?: Layout;
// Whether to always show a timestamp // Whether to always show a timestamp
alwaysShowTimestamps?: boolean; alwaysShowTimestamps?: boolean;
isQuoteExpanded?: boolean;
setQuoteExpanded: (isExpanded: boolean) => void;
} }
interface IState { interface IState {
@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private unmounted = false; private unmounted = false;
private room: Room; private room: Room;
private blockquoteRef = React.createRef<HTMLElement>();
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
this.room = this.context.getRoom(this.props.parentEv.getRoomId()); this.room = this.context.getRoom(this.props.parentEv.getRoomId());
} }
public static getParentEventId(ev: MatrixEvent): string { public static getParentEventId(ev: MatrixEvent): string | undefined {
if (!ev || ev.isRedacted()) return; if (!ev || ev.isRedacted()) return;
// XXX: For newer relations (annotations, replacements, etc.), we now // XXX: For newer relations (annotations, replacements, etc.), we now
@ -137,7 +148,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
public static getNestedReplyText( public static getNestedReplyText(
ev: MatrixEvent, ev: MatrixEvent,
permalinkCreator: RoomPermalinkCreator, permalinkCreator: RoomPermalinkCreator,
): { body: string, html: string } { ): { body: string, html: string } | null {
if (!ev) return null; if (!ev) return null;
let { body, formatted_body: html } = ev.getContent(); let { body, formatted_body: html } = ev.getContent();
@ -237,37 +248,38 @@ export default class ReplyThread extends React.Component<IProps, IState> {
return replyMixin; return replyMixin;
} }
public static makeThread( public static hasThreadReply(event: MatrixEvent) {
parentEv: MatrixEvent, return Boolean(ReplyThread.getParentEventId(event));
onHeightChanged: () => void,
permalinkCreator: RoomPermalinkCreator,
ref: React.RefObject<ReplyThread>,
layout: Layout,
alwaysShowTimestamps: boolean,
): JSX.Element {
if (!ReplyThread.getParentEventId(parentEv)) return null;
return <ReplyThread
parentEv={parentEv}
onHeightChanged={onHeightChanged}
ref={ref}
permalinkCreator={permalinkCreator}
layout={layout}
alwaysShowTimestamps={alwaysShowTimestamps}
/>;
} }
componentDidMount() { componentDidMount() {
this.initialize(); this.initialize();
this.trySetExpandableQuotes();
} }
componentDidUpdate() { componentDidUpdate() {
this.props.onHeightChanged(); this.props.onHeightChanged();
this.trySetExpandableQuotes();
} }
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
} }
private trySetExpandableQuotes() {
if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
if (el) {
const code: HTMLElement | null = el.querySelector('code');
const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
if (isElipsisShown) {
this.props.setQuoteExpanded(false);
}
}
}
}
private async initialize(): Promise<void> { private async initialize(): Promise<void> {
const { parentEv } = this.props; const { parentEv } = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId // at time of making this component we checked that props.parentEv has a parentEventId
@ -321,7 +333,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
this.initialize(); this.initialize();
}; };
private onQuoteClick = async (): Promise<void> => { private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
const events = [this.state.loadedEv, ...this.state.events]; const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null; let loadedEv = null;
@ -373,14 +385,26 @@ export default class ReplyThread extends React.Component<IProps, IState> {
header = <Spinner w={16} h={16} />; header = <Spinner w={16} h={16} />;
} }
const { isQuoteExpanded } = this.props;
const evTiles = this.state.events.map((ev) => { const evTiles = this.state.events.map((ev) => {
return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}> const classname = classNames({
<ReplyTile 'mx_ReplyThread': true,
mxEvent={ev} [this.getReplyThreadColorClass(ev)]: true,
onHeightChanged={this.props.onHeightChanged} // We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
permalinkCreator={this.props.permalinkCreator} 'mx_ReplyThread--expanded': isQuoteExpanded === true,
/> // We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
</blockquote>; 'mx_ReplyThread--collapsed': isQuoteExpanded === false,
});
return (
<blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}>
<ReplyTile
mxEvent={ev}
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
toggleExpandedQuote={() => this.props.setQuoteExpanded(!this.props.isQuoteExpanded)}
/>
</blockquote>
);
}); });
return <div className="mx_ReplyThread_wrapper"> return <div className="mx_ReplyThread_wrapper">

View File

@ -15,107 +15,112 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils'; import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import { formatTime } from '../../../DateUtils'; import { formatTime } from '../../../DateUtils';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { pillifyLinks, unmountPills } from '../../../utils/pillify'; import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import classNames from 'classnames'; import classNames from 'classnames';
import RedactedBody from "./RedactedBody"; import RedactedBody from "./RedactedBody";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
function getReplacedContent(event) { function getReplacedContent(event) {
const originalContent = event.getOriginalContent(); const originalContent = event.getOriginalContent();
return originalContent["m.new_content"] || originalContent; return originalContent["m.new_content"] || originalContent;
} }
@replaceableComponent("views.messages.EditHistoryMessage") interface IProps {
export default class EditHistoryMessage extends React.PureComponent { // the message event being edited
static propTypes = { mxEvent: MatrixEvent;
// the message event being edited previousEdit?: MatrixEvent;
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, isBaseEvent?: boolean;
previousEdit: PropTypes.instanceOf(MatrixEvent), isTwelveHour?: boolean;
isBaseEvent: PropTypes.bool, }
};
constructor(props) { interface IState {
canRedact: boolean;
sendStatus: EventStatus;
}
@replaceableComponent("views.messages.EditHistoryMessage")
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
private content = createRef<HTMLDivElement>();
private pills: Element[] = [];
constructor(props: IProps) {
super(props); super(props);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const { userId } = cli.credentials; const { userId } = cli.credentials;
const event = this.props.mxEvent; const event = this.props.mxEvent;
const room = cli.getRoom(event.getRoomId()); const room = cli.getRoom(event.getRoomId());
if (event.localRedactionEvent()) { if (event.localRedactionEvent()) {
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged); event.localRedactionEvent().on("status", this.onAssociatedStatusChanged);
} }
const canRedact = room.currentState.maySendRedactionForEvent(event, userId); const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = { canRedact, sendStatus: event.getAssociatedStatus() }; this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
this._content = createRef();
this._pills = [];
} }
_onAssociatedStatusChanged = () => { private onAssociatedStatusChanged = (): void => {
this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() }); this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() });
}; };
_onRedactClick = async () => { private onRedactClick = async (): Promise<void> => {
const event = this.props.mxEvent; const event = this.props.mxEvent;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
redact: () => cli.redactEvent(event.getRoomId(), event.getId()), redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
}, 'mx_Dialog_confirmredact'); }, 'mx_Dialog_confirmredact');
}; };
_onViewSourceClick = () => { private onViewSourceClick = (): void => {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, { Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
}; };
pillifyLinks() { private pillifyLinks(): void {
// not present for redacted events // not present for redacted events
if (this._content.current) { if (this.content.current) {
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills); pillifyLinks(this.content.current.children, this.props.mxEvent, this.pills);
} }
} }
componentDidMount() { public componentDidMount(): void {
this.pillifyLinks(); this.pillifyLinks();
} }
componentWillUnmount() { public componentWillUnmount(): void {
unmountPills(this._pills); unmountPills(this.pills);
const event = this.props.mxEvent; const event = this.props.mxEvent;
if (event.localRedactionEvent()) { if (event.localRedactionEvent()) {
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged); event.localRedactionEvent().off("status", this.onAssociatedStatusChanged);
} }
} }
componentDidUpdate() { public componentDidUpdate(): void {
this.pillifyLinks(); this.pillifyLinks();
} }
_renderActionBar() { private renderActionBar(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
// hide the button when already redacted // hide the button when already redacted
let redactButton; let redactButton;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) { if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = ( redactButton = (
<AccessibleButton onClick={this._onRedactClick}> <AccessibleButton onClick={this.onRedactClick}>
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
); );
} }
const viewSourceButton = ( const viewSourceButton = (
<AccessibleButton onClick={this._onViewSourceClick}> <AccessibleButton onClick={this.onViewSourceClick}>
{ _t("View Source") } { _t("View Source") }
</AccessibleButton> </AccessibleButton>
); );
@ -128,7 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
); );
} }
render() { public render(): JSX.Element {
const { mxEvent } = this.props; const { mxEvent } = this.props;
const content = getReplacedContent(mxEvent); const content = getReplacedContent(mxEvent);
let contentContainer; let contentContainer;
@ -139,18 +144,22 @@ export default class EditHistoryMessage extends React.PureComponent {
if (this.props.previousEdit) { if (this.props.previousEdit) {
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content); contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
} else { } else {
contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true }); contentElements = HtmlUtils.bodyToHtml(
content,
null,
{ stripReplyFallback: true, returnString: false },
);
} }
if (mxEvent.getContent().msgtype === "m.emote") { if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
contentContainer = ( contentContainer = (
<div className="mx_EventTile_content" ref={this._content}>*&nbsp; <div className="mx_EventTile_content" ref={this.content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span> <span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{ contentElements } &nbsp;{ contentElements }
</div> </div>
); );
} else { } else {
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>; contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>;
} }
} }
@ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent {
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<span className="mx_MessageTimestamp">{ timestamp }</span> <span className="mx_MessageTimestamp">{ timestamp }</span>
{ contentContainer } { contentContainer }
{ this._renderActionBar() } { this.renderActionBar() }
</div> </div>
</div> </div>
</li> </li>

View File

@ -16,44 +16,50 @@ limitations under the License.
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { getNameForEventRoom, userLabelForEventRoom } import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
from '../../../utils/KeyVerificationStateObserver';
import EventTileBubble from "./EventTileBubble"; import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { EventType } from "matrix-js-sdk/src/@types/event";
interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.MKeyVerificationConclusion") @replaceableComponent("views.messages.MKeyVerificationConclusion")
export default class MKeyVerificationConclusion extends React.Component { export default class MKeyVerificationConclusion extends React.Component<IProps> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
} }
componentDidMount() { public componentDidMount(): void {
const request = this.props.mxEvent.verificationRequest; const request = this.props.mxEvent.verificationRequest;
if (request) { if (request) {
request.on("change", this._onRequestChanged); request.on("change", this.onRequestChanged);
} }
MatrixClientPeg.get().on("userTrustStatusChanged", this._onTrustChanged); MatrixClientPeg.get().on("userTrustStatusChanged", this.onTrustChanged);
} }
componentWillUnmount() { public componentWillUnmount(): void {
const request = this.props.mxEvent.verificationRequest; const request = this.props.mxEvent.verificationRequest;
if (request) { if (request) {
request.off("change", this._onRequestChanged); request.off("change", this.onRequestChanged);
} }
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("userTrustStatusChanged", this._onTrustChanged); cli.removeListener("userTrustStatusChanged", this.onTrustChanged);
} }
} }
_onRequestChanged = () => { private onRequestChanged = (): void => {
this.forceUpdate(); this.forceUpdate();
}; };
_onTrustChanged = (userId, status) => { private onTrustChanged = (userId: string): void => {
const { mxEvent } = this.props; const { mxEvent } = this.props;
const request = mxEvent.verificationRequest; const request = mxEvent.verificationRequest;
if (!request || request.otherUserId !== userId) { if (!request || request.otherUserId !== userId) {
@ -62,17 +68,17 @@ export default class MKeyVerificationConclusion extends React.Component {
this.forceUpdate(); this.forceUpdate();
}; };
_shouldRender(mxEvent, request) { public static shouldRender(mxEvent: MatrixEvent, request: VerificationRequest): boolean {
// normally should not happen // normally should not happen
if (!request) { if (!request) {
return false; return false;
} }
// .cancel event that was sent after the verification finished, ignore // .cancel event that was sent after the verification finished, ignore
if (mxEvent.getType() === "m.key.verification.cancel" && !request.cancelled) { if (mxEvent.getType() === EventType.KeyVerificationCancel && !request.cancelled) {
return false; return false;
} }
// .done event that was sent after the verification cancelled, ignore // .done event that was sent after the verification cancelled, ignore
if (mxEvent.getType() === "m.key.verification.done" && !request.done) { if (mxEvent.getType() === EventType.KeyVerificationDone && !request.done) {
return false; return false;
} }
@ -89,11 +95,11 @@ export default class MKeyVerificationConclusion extends React.Component {
return true; return true;
} }
render() { public render(): JSX.Element {
const { mxEvent } = this.props; const { mxEvent } = this.props;
const request = mxEvent.verificationRequest; const request = mxEvent.verificationRequest;
if (!this._shouldRender(mxEvent, request)) { if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) {
return null; return null;
} }
@ -103,15 +109,18 @@ export default class MKeyVerificationConclusion extends React.Component {
let title; let title;
if (request.done) { if (request.done) {
title = _t("You verified %(name)s", { name: getNameForEventRoom(request.otherUserId, mxEvent) }); title = _t(
"You verified %(name)s",
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) },
);
} else if (request.cancelled) { } else if (request.cancelled) {
const userId = request.cancellingUserId; const userId = request.cancellingUserId;
if (userId === myUserId) { if (userId === myUserId) {
title = _t("You cancelled verifying %(name)s", title = _t("You cancelled verifying %(name)s",
{ name: getNameForEventRoom(request.otherUserId, mxEvent) }); { name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) });
} else { } else {
title = _t("%(name)s cancelled verifying", title = _t("%(name)s cancelled verifying",
{ name: getNameForEventRoom(userId, mxEvent) }); { name: getNameForEventRoom(userId, mxEvent.getRoomId()) });
} }
} }
@ -129,8 +138,3 @@ export default class MKeyVerificationConclusion extends React.Component {
return null; return null;
} }
} }
MKeyVerificationConclusion.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View File

@ -17,7 +17,8 @@ limitations under the License.
*/ */
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
@ -35,13 +36,17 @@ import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import DownloadActionButton from "./DownloadActionButton"; import DownloadActionButton from "./DownloadActionButton";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import classNames from 'classnames';
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ReplyThread from '../elements/ReplyThread'; import ReplyThread from '../elements/ReplyThread';
interface IOptionsButtonProps { interface IOptionsButtonProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here // TODO: Types
getTile: () => any | null;
getReplyThread: () => ReplyThread; getReplyThread: () => ReplyThread;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
onFocusChange: (menuDisplayed: boolean) => void; onFocusChange: (menuDisplayed: boolean) => void;
@ -57,8 +62,6 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
let contextMenu; let contextMenu;
if (menuDisplayed) { if (menuDisplayed) {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const tile = getTile && getTile(); const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread(); const replyThread = getReplyThread && getReplyThread();
@ -90,7 +93,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
interface IReactButtonProps { interface IReactButtonProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
reactions: any; // TODO: types reactions: Relations;
onFocusChange: (menuDisplayed: boolean) => void; onFocusChange: (menuDisplayed: boolean) => void;
} }
@ -127,12 +130,15 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
interface IMessageActionBarProps { interface IMessageActionBarProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent` reactions?: Relations;
reactions?: any; // TODO: types // TODO: Types
getTile: () => any | null;
getReplyThread: () => ReplyThread | undefined;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
getReplyThread?: () => ReplyThread; getReplyThread?: () => ReplyThread;
onFocusChange?: (menuDisplayed: boolean) => void; onFocusChange?: (menuDisplayed: boolean) => void;
toggleThreadExpanded: () => void;
isInThreadTimeline?: boolean; isInThreadTimeline?: boolean;
} }
@ -329,6 +335,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
toolbarOpts.push(cancelSendingButton); toolbarOpts.push(cancelSendingButton);
} }
if (this.props.isQuoteExpanded !== undefined && ReplyThread.hasThreadReply(this.props.mxEvent)) {
const expandClassName = classNames({
'mx_MessageActionBar_maskButton': true,
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded,
});
toolbarOpts.push(<RovingAccessibleTooltipButton
className={expandClassName}
title={this.props.isQuoteExpanded ? _t("Collapse quotes │ ⇧+click") : _t("Expand quotes │ ⇧+click")}
onClick={this.props.toggleThreadExpanded}
key="expand"
/>);
}
// The menu button should be last, so dump it there. // The menu button should be last, so dump it there.
toolbarOpts.push(<OptionsButton toolbarOpts.push(<OptionsButton
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}

View File

@ -15,22 +15,18 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
interface IProps {
mxEvent: MatrixEvent;
onMessageAllowed: () => void;
}
@replaceableComponent("views.messages.MjolnirBody") @replaceableComponent("views.messages.MjolnirBody")
export default class MjolnirBody extends React.Component { export default class MjolnirBody extends React.Component<IProps> {
static propTypes = { private onAllowClick = (e: React.MouseEvent): void => {
mxEvent: PropTypes.object.isRequired,
onMessageAllowed: PropTypes.func.isRequired,
};
constructor() {
super();
}
_onAllowClick = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -39,11 +35,11 @@ export default class MjolnirBody extends React.Component {
this.props.onMessageAllowed(); this.props.onMessageAllowed();
}; };
render() { public render(): JSX.Element {
return ( return (
<div className='mx_MjolnirBody'><i>{ _t( <div className='mx_MjolnirBody'><i>{ _t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>", "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> }, {}, { a: (sub) => <a href="#" onClick={this.onAllowClick}>{ sub }</a> },
) }</i></div> ) }</i></div>
); );
} }

View File

@ -16,13 +16,18 @@ limitations under the License.
import React, { useContext } from "react"; import React, { useContext } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { formatFullDate } from "../../../DateUtils"; import { formatFullDate } from "../../../DateUtils";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => { interface IProps {
mxEvent: MatrixEvent;
}
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
const cli: MatrixClient = useContext(MatrixClientContext); const cli: MatrixClient = useContext(MatrixClientContext);
let text = _t("Message deleted"); let text = _t("Message deleted");

View File

@ -17,23 +17,24 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import RoomAvatar from "../avatars/RoomAvatar";
import ImageView from "../elements/ImageView";
interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.RoomAvatarEvent") @replaceableComponent("views.messages.RoomAvatarEvent")
export default class RoomAvatarEvent extends React.Component { export default class RoomAvatarEvent extends React.Component<IProps> {
static propTypes = { private onAvatarClick = (): void => {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
onAvatarClick = () => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
@ -44,7 +45,6 @@ export default class RoomAvatarEvent extends React.Component {
roomName: room ? room.name : '', roomName: room ? room.name : '',
}); });
const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: text, name: text,
@ -52,10 +52,9 @@ export default class RoomAvatarEvent extends React.Component {
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
}; };
render() { public render(): JSX.Element {
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return ( return (

View File

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
@ -24,15 +23,16 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTileBubble from "./EventTileBubble"; import EventTileBubble from "./EventTileBubble";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.RoomCreate") @replaceableComponent("views.messages.RoomCreate")
export default class RoomCreate extends React.Component { export default class RoomCreate extends React.Component<IProps> {
static propTypes = { private onLinkClicked = (e: React.MouseEvent): void => {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
_onLinkClicked = e => {
e.preventDefault(); e.preventDefault();
const predecessor = this.props.mxEvent.getContent()['predecessor']; const predecessor = this.props.mxEvent.getContent()['predecessor'];
@ -45,7 +45,7 @@ export default class RoomCreate extends React.Component {
}); });
}; };
render() { public render(): JSX.Element {
const predecessor = this.props.mxEvent.getContent()['predecessor']; const predecessor = this.props.mxEvent.getContent()['predecessor'];
if (predecessor === undefined) { if (predecessor === undefined) {
return <div />; // We should never have been instantiated in this case return <div />; // We should never have been instantiated in this case
@ -55,7 +55,7 @@ export default class RoomCreate extends React.Component {
permalinkCreator.load(); permalinkCreator.load();
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']); const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
const link = ( const link = (
<a href={predecessorPermalink} onClick={this._onLinkClicked}> <a href={predecessorPermalink} onClick={this.onLinkClicked}>
{ _t("Click here to see older messages.") } { _t("Click here to see older messages.") }
</a> </a>
); );

View File

@ -138,6 +138,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// If it's less than 30% we don't add the expansion button. // If it's less than 30% we don't add the expansion button.
// We also round the number as it sometimes can be 29.99... // We also round the number as it sometimes can be 29.99...
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100); const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
// TODO: additionally show the button if it's an expanded quoted message
if (percentageOfViewport < 30) return; if (percentageOfViewport < 30) return;
const button = document.createElement("span"); const button = document.createElement("span");

View File

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { lexicographicCompare } from 'matrix-js-sdk/src/utils'; import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
@ -35,16 +34,6 @@ interface IProps {
room: Room; room: Room;
userId: string; userId: string;
showApps: boolean; // Render apps showApps: boolean; // Render apps
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: number;
// a callback which is called when the content of the aux panel changes
// content in a way that is likely to make it change size.
onResize: () => void;
fullHeight: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
} }
@ -92,13 +81,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState); return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState);
} }
componentDidUpdate(prevProps, prevState) {
// most changes are likely to cause a resize
if (this.props.onResize) {
this.props.onResize();
}
}
private rateLimitedUpdate = throttle(() => { private rateLimitedUpdate = throttle(() => {
this.setState({ counters: this.computeCounters() }); this.setState({ counters: this.computeCounters() });
}, 500, { leading: true, trailing: true }); }, 500, { leading: true, trailing: true });
@ -138,7 +120,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
const callView = ( const callView = (
<CallViewForRoom <CallViewForRoom
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
maxVideoHeight={this.props.maxHeight}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/> />
); );
@ -148,7 +129,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
appsDrawer = <AppsDrawer appsDrawer = <AppsDrawer
room={this.props.room} room={this.props.room}
userId={this.props.userId} userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps} showApps={this.props.showApps}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/>; />;
@ -204,21 +184,12 @@ export default class AuxPanel extends React.Component<IProps, IState> {
} }
} }
const classes = classNames({
"mx_RoomView_auxPanel": true,
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
});
const style: React.CSSProperties = {};
if (!this.props.fullHeight) {
style.maxHeight = this.props.maxHeight;
}
return ( return (
<AutoHideScrollbar className={classes} style={style}> <AutoHideScrollbar className="mx_RoomView_auxPanel">
{ stateViews } { stateViews }
{ this.props.children }
{ appsDrawer } { appsDrawer }
{ callView } { callView }
{ this.props.children }
</AutoHideScrollbar> </AutoHideScrollbar>
); );
} }

View File

@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventUtils'; import { getEventDisplayInfo } from '../../../utils/EventUtils';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
const eventTileTypes = { const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent', [EventType.RoomMessage]: 'messages.MessageEvent',
@ -144,8 +145,7 @@ export function getHandlerTile(ev) {
// XXX: This is extremely a hack. Possibly these components should have an interface for // XXX: This is extremely a hack. Possibly these components should have an interface for
// declining to render? // declining to render?
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") { if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion"); if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
return; return;
} }
} }
@ -323,7 +323,7 @@ interface IState {
reactions: Relations; reactions: Relations;
hover: boolean; hover: boolean;
isQuoteExpanded?: boolean;
thread?: Thread; thread?: Thread;
} }
@ -331,7 +331,8 @@ interface IState {
export default class EventTile extends React.Component<IProps, IState> { export default class EventTile extends React.Component<IProps, IState> {
private suppressReadReceiptAnimation: boolean; private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean; private isListeningForReceipts: boolean;
private tile = React.createRef(); // TODO: Types
private tile = React.createRef<unknown>();
private replyThread = React.createRef<ReplyThread>(); private replyThread = React.createRef<ReplyThread>();
public readonly ref = createRef<HTMLElement>(); public readonly ref = createRef<HTMLElement>();
@ -889,8 +890,8 @@ export default class EventTile extends React.Component<IProps, IState> {
actionBarFocused: focused, actionBarFocused: focused,
}); });
}; };
// TODO: Types
getTile = () => this.tile.current; getTile: () => any | null = () => this.tile.current;
getReplyThread = () => this.replyThread.current; getReplyThread = () => this.replyThread.current;
@ -915,6 +916,11 @@ export default class EventTile extends React.Component<IProps, IState> {
}); });
}; };
private setQuoteExpanded = (expanded: boolean) => {
this.setState({
isQuoteExpanded: expanded,
});
};
render() { render() {
const msgtype = this.props.mxEvent.getContent().msgtype; const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType() as EventType; const eventType = this.props.mxEvent.getType() as EventType;
@ -924,6 +930,7 @@ export default class EventTile extends React.Component<IProps, IState> {
isInfoMessage, isInfoMessage,
isLeftAlignedBubbleMessage, isLeftAlignedBubbleMessage,
} = getEventDisplayInfo(this.props.mxEvent); } = getEventDisplayInfo(this.props.mxEvent);
const { isQuoteExpanded } = this.state;
// This shouldn't happen: the caller should check we support this type // This shouldn't happen: the caller should check we support this type
// before trying to instantiate us // before trying to instantiate us
@ -936,6 +943,7 @@ export default class EventTile extends React.Component<IProps, IState> {
</div> </div>
</div>; </div>;
} }
const EventTileType = sdk.getComponent(tileHandler); const EventTileType = sdk.getComponent(tileHandler);
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
@ -1057,6 +1065,7 @@ export default class EventTile extends React.Component<IProps, IState> {
getReplyThread={this.getReplyThread} getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange} onFocusChange={this.onActionBarFocusChange}
isInThreadTimeline={isInThreadTimeline} isInThreadTimeline={isInThreadTimeline}
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
/> : undefined; /> : undefined;
const showTimestamp = this.props.mxEvent.getTs() const showTimestamp = this.props.mxEvent.getTs()
@ -1229,20 +1238,18 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
default: { default: {
let thread; const thread = haveTileForEvent(this.props.mxEvent) &&
// When the "showHiddenEventsInTimeline" lab is enabled, ReplyThread.hasThreadReply(this.props.mxEvent) ? (
// avoid showing replies for hidden events (events without tiles) <ReplyThread
if (haveTileForEvent(this.props.mxEvent)) { parentEv={this.props.mxEvent}
thread = ReplyThread.makeThread( onHeightChanged={this.props.onHeightChanged}
this.props.mxEvent, ref={this.replyThread}
this.props.onHeightChanged, permalinkCreator={this.props.permalinkCreator}
this.props.permalinkCreator, layout={this.props.layout}
this.replyThread, alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
this.props.layout, isQuoteExpanded={isQuoteExpanded}
this.props.alwaysShowTimestamps || this.state.hover, setQuoteExpanded={this.setQuoteExpanded}
); />) : null;
}
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId(); const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// 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

View File

@ -16,7 +16,7 @@ limitations under the License.
import React, { useContext, useEffect } from "react"; import React, { useContext, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client"; import { IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/client";
import { useStateToggle } from "../../../hooks/useStateToggle"; import { useStateToggle } from "../../../hooks/useStateToggle";
import LinkPreviewWidget from "./LinkPreviewWidget"; import LinkPreviewWidget from "./LinkPreviewWidget";
@ -40,13 +40,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
const ts = mxEvent.getTs(); const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => { const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => { return fetchPreviews(cli, links, ts);
try {
return [link, await cli.getUrlPreview(link, ts)];
} catch (error) {
console.error("Failed to get URL preview: " + error);
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}, [links, ts], []); }, [links, ts], []);
useEffect(() => { useEffect(() => {
@ -89,4 +83,18 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
</div>; </div>;
}; };
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
Promise<[string, IPreviewUrlResponse][]> => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
const preview = await cli.getUrlPreview(link, ts);
if (preview && Object.keys(preview).length > 0) {
return [link, preview];
}
} catch (error) {
console.error("Failed to get URL preview: " + error);
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
};
export default LinkPreviewGroup; export default LinkPreviewGroup;

View File

@ -35,6 +35,7 @@ interface IProps {
highlights?: string[]; highlights?: string[];
highlightLink?: string; highlightLink?: string;
onHeightChanged?(): void; onHeightChanged?(): void;
toggleExpandedQuote?: () => void;
} }
@replaceableComponent("views.rooms.ReplyTile") @replaceableComponent("views.rooms.ReplyTile")
@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent<IProps> {
// This allows the permalink to be opened in a new tab/window or copied as // This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked. // matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault(); e.preventDefault();
dis.dispatch({ // Expand thread on shift key
action: 'view_room', if (this.props.toggleExpandedQuote && e.shiftKey) {
event_id: this.props.mxEvent.getId(), this.props.toggleExpandedQuote();
highlighted: true, } else {
room_id: this.props.mxEvent.getRoomId(), dis.dispatch({
}); action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
}
} }
}; };

View File

@ -164,6 +164,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
window.addEventListener("beforeunload", this.saveStoredEditorState); window.addEventListener("beforeunload", this.saveStoredEditorState);
} }
public componentDidUpdate(prevProps: IProps): void {
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
if (replyToEventChanged) {
this.model.reset([]);
}
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
const partCreator = new CommandPartCreator(this.props.room, this.context);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model.reset(parts);
this.editorRef.current?.focus();
}
}
private onKeyDown = (event: KeyboardEvent): void => { private onKeyDown = (event: KeyboardEvent): void => {
// ignore any keypress while doing IME compositions // ignore any keypress while doing IME compositions
if (this.editorRef.current?.isComposing(event)) { if (this.editorRef.current?.isComposing(event)) {
@ -484,7 +498,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
} }
private get editorStateKey() { private get editorStateKey() {
return `mx_cider_state_${this.props.room.roomId}`; let key = `mx_cider_state_${this.props.room.roomId}`;
const thread = this.props.replyToEvent?.getThread();
if (thread) {
key += `_${thread.id}`;
}
return key;
} }
private clearStoredEditorState(): void { private clearStoredEditorState(): void {
@ -492,6 +511,10 @@ export default class SendMessageComposer extends React.Component<IProps> {
} }
private restoreStoredEditorState(partCreator: PartCreator): Part[] { private restoreStoredEditorState(partCreator: PartCreator): Part[] {
if (this.props.replyInThread && !this.props.replyToEvent) {
return null;
}
const json = localStorage.getItem(this.editorStateKey); const json = localStorage.getItem(this.editorStateKey);
if (json) { if (json) {
try { try {

View File

@ -27,9 +27,6 @@ interface IProps {
// What room we should display the call for // What room we should display the call for
roomId: string; roomId: string;
// maxHeight style attribute for the video panel
maxVideoHeight?: number;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
} }
@ -99,14 +96,12 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
public render() { public render() {
if (!this.state.call) return null; if (!this.state.call) return null;
// We subtract 8 as it the margin-bottom of the mx_CallViewForRoom_ResizeWrapper
const maxHeight = this.props.maxVideoHeight - 8;
return ( return (
<div className="mx_CallViewForRoom"> <div className="mx_CallViewForRoom">
<Resizable <Resizable
minHeight={380} minHeight={380}
maxHeight={maxHeight} maxHeight="80vh"
enable={{ enable={{
top: false, top: false,
right: false, right: false,

View File

@ -1944,6 +1944,8 @@
"Edit": "Edit", "Edit": "Edit",
"Reply": "Reply", "Reply": "Reply",
"Thread": "Thread", "Thread": "Thread",
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
"Message Actions": "Message Actions", "Message Actions": "Message Actions",
"Download %(text)s": "Download %(text)s", "Download %(text)s": "Download %(text)s",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",

View File

@ -46,12 +46,10 @@ const FLUSH_RATE_MS = 30 * 1000;
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
// A class which monkey-patches the global console and stores log lines. // A class which monkey-patches the global console and stores log lines.
class ConsoleLogger { export class ConsoleLogger {
constructor() { private logs = "";
this.logs = "";
}
monkeyPatch(consoleObj) { public monkeyPatch(consoleObj: Console): void {
// Monkey-patch console logging // Monkey-patch console logging
const consoleFunctionsToLevels = { const consoleFunctionsToLevels = {
log: "I", log: "I",
@ -69,14 +67,14 @@ class ConsoleLogger {
}); });
} }
log(level, ...args) { private log(level: string, ...args: (Error | DOMException | object | string)[]): void {
// We don't know what locale the user may be running so use ISO strings // We don't know what locale the user may be running so use ISO strings
const ts = new Date().toISOString(); const ts = new Date().toISOString();
// Convert objects and errors to helpful things // Convert objects and errors to helpful things
args = args.map((arg) => { args = args.map((arg) => {
if (arg instanceof DOMException) { if (arg instanceof DOMException) {
return arg.message + ` (${arg.name} | ${arg.code}) ` + (arg.stack ? `\n${arg.stack}` : ''); return arg.message + ` (${arg.name} | ${arg.code})`;
} else if (arg instanceof Error) { } else if (arg instanceof Error) {
return arg.message + (arg.stack ? `\n${arg.stack}` : ''); return arg.message + (arg.stack ? `\n${arg.stack}` : '');
} else if (typeof (arg) === 'object') { } else if (typeof (arg) === 'object') {
@ -118,7 +116,7 @@ class ConsoleLogger {
* @param {boolean} keepLogs True to not delete logs after flushing. * @param {boolean} keepLogs True to not delete logs after flushing.
* @return {string} \n delimited log lines to flush. * @return {string} \n delimited log lines to flush.
*/ */
flush(keepLogs) { public flush(keepLogs?: boolean): string {
// The ConsoleLogger doesn't care how these end up on disk, it just // The ConsoleLogger doesn't care how these end up on disk, it just
// flushes them to the caller. // flushes them to the caller.
if (keepLogs) { if (keepLogs) {
@ -131,27 +129,28 @@ class ConsoleLogger {
} }
// A class which stores log lines in an IndexedDB instance. // A class which stores log lines in an IndexedDB instance.
class IndexedDBLogStore { export class IndexedDBLogStore {
constructor(indexedDB, logger) { private id: string;
this.indexedDB = indexedDB; private index = 0;
this.logger = logger; private db = null;
this.id = "instance-" + Math.random() + Date.now(); private flushPromise = null;
this.index = 0; private flushAgainPromise = null;
this.db = null;
// these promises are cleared as soon as fulfilled constructor(
this.flushPromise = null; private indexedDB: IDBFactory,
// set if flush() is called whilst one is ongoing private logger: ConsoleLogger,
this.flushAgainPromise = null; ) {
this.id = "instance-" + Math.random() + Date.now();
} }
/** /**
* @return {Promise} Resolves when the store is ready. * @return {Promise} Resolves when the store is ready.
*/ */
connect() { public connect(): Promise<void> {
const req = this.indexedDB.open("logs"); const req = this.indexedDB.open("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = (event) => { req.onsuccess = (event: Event) => {
// @ts-ignore
this.db = event.target.result; this.db = event.target.result;
// Periodically flush logs to local storage / indexeddb // Periodically flush logs to local storage / indexeddb
setInterval(this.flush.bind(this), FLUSH_RATE_MS); setInterval(this.flush.bind(this), FLUSH_RATE_MS);
@ -160,6 +159,7 @@ class IndexedDBLogStore {
req.onerror = (event) => { req.onerror = (event) => {
const err = ( const err = (
// @ts-ignore
"Failed to open log database: " + event.target.error.name "Failed to open log database: " + event.target.error.name
); );
console.error(err); console.error(err);
@ -168,6 +168,7 @@ class IndexedDBLogStore {
// First time: Setup the object store // First time: Setup the object store
req.onupgradeneeded = (event) => { req.onupgradeneeded = (event) => {
// @ts-ignore
const db = event.target.result; const db = event.target.result;
const logObjStore = db.createObjectStore("logs", { const logObjStore = db.createObjectStore("logs", {
keyPath: ["id", "index"], keyPath: ["id", "index"],
@ -178,7 +179,7 @@ class IndexedDBLogStore {
logObjStore.createIndex("id", "id", { unique: false }); logObjStore.createIndex("id", "id", { unique: false });
logObjStore.add( logObjStore.add(
this._generateLogEntry( this.generateLogEntry(
new Date() + " ::: Log database was created.", new Date() + " ::: Log database was created.",
), ),
); );
@ -186,7 +187,7 @@ class IndexedDBLogStore {
const lastModifiedStore = db.createObjectStore("logslastmod", { const lastModifiedStore = db.createObjectStore("logslastmod", {
keyPath: "id", keyPath: "id",
}); });
lastModifiedStore.add(this._generateLastModifiedTime()); lastModifiedStore.add(this.generateLastModifiedTime());
}; };
}); });
} }
@ -210,7 +211,7 @@ class IndexedDBLogStore {
* *
* @return {Promise} Resolved when the logs have been flushed. * @return {Promise} Resolved when the logs have been flushed.
*/ */
flush() { public flush(): Promise<void> {
// check if a flush() operation is ongoing // check if a flush() operation is ongoing
if (this.flushPromise) { if (this.flushPromise) {
if (this.flushAgainPromise) { if (this.flushAgainPromise) {
@ -227,7 +228,7 @@ class IndexedDBLogStore {
} }
// there is no flush promise or there was but it has finished, so do // there is no flush promise or there was but it has finished, so do
// a brand new one, destroying the chain which may have been built up. // a brand new one, destroying the chain which may have been built up.
this.flushPromise = new Promise((resolve, reject) => { this.flushPromise = new Promise<void>((resolve, reject) => {
if (!this.db) { if (!this.db) {
// not connected yet or user rejected access for us to r/w to the db. // not connected yet or user rejected access for us to r/w to the db.
reject(new Error("No connected database")); reject(new Error("No connected database"));
@ -251,9 +252,9 @@ class IndexedDBLogStore {
new Error("Failed to write logs: " + event.target.errorCode), new Error("Failed to write logs: " + event.target.errorCode),
); );
}; };
objStore.add(this._generateLogEntry(lines)); objStore.add(this.generateLogEntry(lines));
const lastModStore = txn.objectStore("logslastmod"); const lastModStore = txn.objectStore("logslastmod");
lastModStore.put(this._generateLastModifiedTime()); lastModStore.put(this.generateLastModifiedTime());
}).then(() => { }).then(() => {
this.flushPromise = null; this.flushPromise = null;
}); });
@ -270,12 +271,12 @@ class IndexedDBLogStore {
* log ID). The objects have said log ID in an "id" field and "lines" which * log ID). The objects have said log ID in an "id" field and "lines" which
* is a big string with all the new-line delimited logs. * is a big string with all the new-line delimited logs.
*/ */
async consume() { public async consume(): Promise<{lines: string, id: string}[]> {
const db = this.db; const db = this.db;
// Returns: a string representing the concatenated logs for this ID. // Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize // Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id, maxSize) { function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db.transaction("logs", "readonly").objectStore("logs"); const objectStore = db.transaction("logs", "readonly").objectStore("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -301,7 +302,7 @@ class IndexedDBLogStore {
} }
// Returns: A sorted array of log IDs. (newest first) // Returns: A sorted array of log IDs. (newest first)
function fetchLogIds() { function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod. // To gather all the log IDs, query for all records in logslastmod.
const o = db.transaction("logslastmod", "readonly").objectStore( const o = db.transaction("logslastmod", "readonly").objectStore(
"logslastmod", "logslastmod",
@ -319,8 +320,8 @@ class IndexedDBLogStore {
}); });
} }
function deleteLogs(id) { function deleteLogs(id: number): Promise<void> {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const txn = db.transaction( const txn = db.transaction(
["logs", "logslastmod"], "readwrite", ["logs", "logslastmod"], "readwrite",
); );
@ -389,7 +390,7 @@ class IndexedDBLogStore {
return logs; return logs;
} }
_generateLogEntry(lines) { private generateLogEntry(lines: string): {id: string, lines: string, index: number} {
return { return {
id: this.id, id: this.id,
lines: lines, lines: lines,
@ -397,7 +398,7 @@ class IndexedDBLogStore {
}; };
} }
_generateLastModifiedTime() { private generateLastModifiedTime(): {id: string, ts: number} {
return { return {
id: this.id, id: this.id,
ts: Date.now(), ts: Date.now(),
@ -415,15 +416,19 @@ class IndexedDBLogStore {
* @return {Promise<T[]>} Resolves to an array of whatever you returned from * @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper. * resultMapper.
*/ */
function selectQuery(store, keyRange, resultMapper) { function selectQuery<T>(
store: IDBIndex, keyRange: IDBKeyRange, resultMapper: (cursor: IDBCursorWithValue) => T,
): Promise<T[]> {
const query = store.openCursor(keyRange); const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results = []; const results = [];
query.onerror = (event) => { query.onerror = (event) => {
// @ts-ignore
reject(new Error("Query failed: " + event.target.errorCode)); reject(new Error("Query failed: " + event.target.errorCode));
}; };
// collect results // collect results
query.onsuccess = (event) => { query.onsuccess = (event) => {
// @ts-ignore
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(results); resolve(results);
@ -442,7 +447,7 @@ function selectQuery(store, keyRange, resultMapper) {
* be set up immediately for the logs. * be set up immediately for the logs.
* @return {Promise} Resolves when set up. * @return {Promise} Resolves when set up.
*/ */
export function init(setUpPersistence = true) { export function init(setUpPersistence = true): Promise<void> {
if (global.mx_rage_initPromise) { if (global.mx_rage_initPromise) {
return global.mx_rage_initPromise; return global.mx_rage_initPromise;
} }
@ -462,7 +467,7 @@ export function init(setUpPersistence = true) {
* then this no-ops. * then this no-ops.
* @return {Promise} Resolves when complete. * @return {Promise} Resolves when complete.
*/ */
export function tryInitStorage() { export function tryInitStorage(): Promise<void> {
if (global.mx_rage_initStoragePromise) { if (global.mx_rage_initStoragePromise) {
return global.mx_rage_initStoragePromise; return global.mx_rage_initStoragePromise;
} }

View File

@ -1,110 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
/**
* Stores information about the widgets active in the app right now:
* * What widget is set to remain always-on-screen, if any
* Only one widget may be 'always on screen' at any one time.
* * Negotiated capabilities for active apps
*/
class ActiveWidgetStore extends EventEmitter {
constructor() {
super();
this._persistentWidgetId = null;
// What room ID each widget is associated with (if it's a room widget)
this._roomIdByWidgetId = {};
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
this.dispatcherRef = null;
}
start() {
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
}
stop() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
this._roomIdByWidgetId = {};
}
onRoomStateEvents(ev, state) {
// XXX: This listens for state events in order to remove the active widget.
// Everything else relies on views listening for events and calling setters
// on this class which is terrible. This store should just listen for events
// and keep itself up to date.
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (ev.getType() !== 'im.vector.modular.widgets') return;
if (ev.getStateKey() === this._persistentWidgetId) {
this.destroyPersistentWidget(this._persistentWidgetId);
}
}
destroyPersistentWidget(id) {
if (id !== this._persistentWidgetId) return;
const toDeleteId = this._persistentWidgetId;
WidgetMessagingStore.instance.stopMessagingById(id);
this.setWidgetPersistence(toDeleteId, false);
this.delRoomId(toDeleteId);
}
setWidgetPersistence(widgetId, val) {
if (this._persistentWidgetId === widgetId && !val) {
this._persistentWidgetId = null;
} else if (this._persistentWidgetId !== widgetId && val) {
this._persistentWidgetId = widgetId;
}
this.emit('update');
}
getWidgetPersistence(widgetId) {
return this._persistentWidgetId === widgetId;
}
getPersistentWidgetId() {
return this._persistentWidgetId;
}
getRoomId(widgetId) {
return this._roomIdByWidgetId[widgetId];
}
setRoomId(widgetId, roomId) {
this._roomIdByWidgetId[widgetId] = roomId;
this.emit('update');
}
delRoomId(widgetId) {
delete this._roomIdByWidgetId[widgetId];
this.emit('update');
}
}
if (global.singletonActiveWidgetStore === undefined) {
global.singletonActiveWidgetStore = new ActiveWidgetStore();
}
export default global.singletonActiveWidgetStore;

View File

@ -0,0 +1,112 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { MatrixEvent } from "matrix-js-sdk";
import { MatrixClientPeg } from '../MatrixClientPeg';
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
export enum ActiveWidgetStoreEvent {
Update = "update",
}
/**
* Stores information about the widgets active in the app right now:
* * What widget is set to remain always-on-screen, if any
* Only one widget may be 'always on screen' at any one time.
* * Negotiated capabilities for active apps
*/
export default class ActiveWidgetStore extends EventEmitter {
private static internalInstance: ActiveWidgetStore;
private persistentWidgetId: string;
// What room ID each widget is associated with (if it's a room widget)
private roomIdByWidgetId = new Map<string, string>();
public static get instance(): ActiveWidgetStore {
if (!ActiveWidgetStore.internalInstance) {
ActiveWidgetStore.internalInstance = new ActiveWidgetStore();
}
return ActiveWidgetStore.internalInstance;
}
public start(): void {
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
}
public stop(): void {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
this.roomIdByWidgetId.clear();
}
private onRoomStateEvents = (ev: MatrixEvent): void => {
// XXX: This listens for state events in order to remove the active widget.
// Everything else relies on views listening for events and calling setters
// on this class which is terrible. This store should just listen for events
// and keep itself up to date.
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (ev.getType() !== 'im.vector.modular.widgets') return;
if (ev.getStateKey() === this.persistentWidgetId) {
this.destroyPersistentWidget(this.persistentWidgetId);
}
};
public destroyPersistentWidget(id: string): void {
if (id !== this.persistentWidgetId) return;
const toDeleteId = this.persistentWidgetId;
WidgetMessagingStore.instance.stopMessagingById(id);
this.setWidgetPersistence(toDeleteId, false);
this.delRoomId(toDeleteId);
}
public setWidgetPersistence(widgetId: string, val: boolean): void {
if (this.persistentWidgetId === widgetId && !val) {
this.persistentWidgetId = null;
} else if (this.persistentWidgetId !== widgetId && val) {
this.persistentWidgetId = widgetId;
}
this.emit(ActiveWidgetStoreEvent.Update);
}
public getWidgetPersistence(widgetId: string): boolean {
return this.persistentWidgetId === widgetId;
}
public getPersistentWidgetId(): string {
return this.persistentWidgetId;
}
public getRoomId(widgetId: string): string {
return this.roomIdByWidgetId.get(widgetId);
}
public setRoomId(widgetId: string, roomId: string): void {
this.roomIdByWidgetId.set(widgetId, roomId);
this.emit(ActiveWidgetStoreEvent.Update);
}
public delRoomId(widgetId: string): void {
this.roomIdByWidgetId.delete(widgetId);
this.emit(ActiveWidgetStoreEvent.Update);
}
}
window.mxActiveWidgetStore = ActiveWidgetStore.instance;

View File

@ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// If a persistent widget is active, check to see if it's just been removed. // If a persistent widget is active, check to see if it's just been removed.
// If it has, it needs to destroyed otherwise unmounting the node won't kill it // If it has, it needs to destroyed otherwise unmounting the node won't kill it
const persistentWidgetId = ActiveWidgetStore.getPersistentWidgetId(); const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId();
if (persistentWidgetId) { if (persistentWidgetId) {
if ( if (
ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId && ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId &&
!roomInfo.widgets.some(w => w.id === persistentWidgetId) !roomInfo.widgets.some(w => w.id === persistentWidgetId)
) { ) {
logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`); logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`);
ActiveWidgetStore.destroyPersistentWidget(persistentWidgetId); ActiveWidgetStore.instance.destroyPersistentWidget(persistentWidgetId);
} }
} }
@ -195,7 +195,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// A persistent conference widget indicates that we're participating // A persistent conference widget indicates that we're participating
const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id)); return widgets.some(w => ActiveWidgetStore.instance.getWidgetPersistence(w.id));
} }
} }

View File

@ -266,7 +266,7 @@ export class StopGapWidget extends EventEmitter {
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) { if (!this.appTileProps.userWidget && this.appTileProps.room) {
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId); ActiveWidgetStore.instance.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
} }
// Always attach a handler for ViewRoom, but permission check it internally // Always attach a handler for ViewRoom, but permission check it internally
@ -319,7 +319,7 @@ export class StopGapWidget extends EventEmitter {
if (WidgetType.JITSI.matches(this.mockWidget.type)) { if (WidgetType.JITSI.matches(this.mockWidget.type)) {
CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true); CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true);
} }
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); ActiveWidgetStore.instance.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
ev.preventDefault(); ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
} }
@ -406,13 +406,13 @@ export class StopGapWidget extends EventEmitter {
} }
public stop(opts = { forceDestroy: false }) { public stop(opts = { forceDestroy: false }) {
if (!opts?.forceDestroy && ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) { if (!opts?.forceDestroy && ActiveWidgetStore.instance.getPersistentWidgetId() === this.mockWidget.id) {
logger.log("Skipping destroy - persistent widget"); logger.log("Skipping destroy - persistent widget");
return; return;
} }
if (!this.started) return; if (!this.started) return;
WidgetMessagingStore.instance.stopMessaging(this.mockWidget); WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
ActiveWidgetStore.delRoomId(this.mockWidget.id); ActiveWidgetStore.instance.delRoomId(this.mockWidget.id);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().off('event', this.onEvent); MatrixClientPeg.get().off('event', this.onEvent);

View File

@ -1,5 +1,5 @@
let hasCalled = false; let hasCalled = false;
function remoteRender(event) { function remoteRender(event: MessageEvent): void {
const data = event.data; const data = event.data;
// If we're handling secondary calls, start from scratch // If we're handling secondary calls, start from scratch
@ -8,13 +8,14 @@ function remoteRender(event) {
} }
hasCalled = true; hasCalled = true;
const img = document.createElement("span"); // we'll mask it as an image const img: HTMLSpanElement = document.createElement("span"); // we'll mask it as an image
img.id = "img"; img.id = "img";
const a = document.createElement("a"); const a: HTMLAnchorElement = document.createElement("a");
a.id = "a"; a.id = "a";
a.rel = "noreferrer noopener"; a.rel = "noreferrer noopener";
a.download = data.download; a.download = data.download;
// @ts-ignore
a.style = data.style; a.style = data.style;
a.style.fontFamily = "Arial, Helvetica, Sans-Serif"; a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
a.href = window.URL.createObjectURL(data.blob); a.href = window.URL.createObjectURL(data.blob);
@ -23,24 +24,24 @@ function remoteRender(event) {
// Apply image style after so we can steal the anchor's colour. // Apply image style after so we can steal the anchor's colour.
// Style copied from a rendered version of mx_MFileBody_download_icon // Style copied from a rendered version of mx_MFileBody_download_icon
img.style = (data.imgStyle || "" + if (data.imgStyle) {
"width: 12px; height: 12px;" + // @ts-ignore
"-webkit-mask-size: 12px;" + img.style = data.imgStyle;
"mask-size: 12px;" + } else {
"-webkit-mask-position: center;" + img.style.width = "12px";
"mask-position: center;" + img.style.height = "12px";
"-webkit-mask-repeat: no-repeat;" + img.style.webkitMaskSize = "12px";
"mask-repeat: no-repeat;" + img.style.webkitMaskPosition = "center";
"display: inline-block;") + "" + img.style.webkitMaskRepeat = "no-repeat";
img.style.display = "inline-block";
// Always add these styles img.style.webkitMaskImage = `url('${data.imgSrc}')`;
`-webkit-mask-image: url('${data.imgSrc}');` + img.style.backgroundColor = `${a.style.color}`;
`mask-image: url('${data.imgSrc}');` + }
`background-color: ${a.style.color};`;
const body = document.body; const body = document.body;
// Don't display scrollbars if the link takes more than one line to display. // Don't display scrollbars if the link takes more than one line to display.
body.style = "margin: 0px; overflow: hidden"; body.style .margin = "0px";
body.style.overflow = "hidden";
body.appendChild(a); body.appendChild(a);
if (event.data.auto) { if (event.data.auto) {
@ -48,7 +49,7 @@ function remoteRender(event) {
} }
} }
window.onmessage = function(e) { window.onmessage = function(e: MessageEvent): void {
if (e.origin === window.location.origin) { if (e.origin === window.location.origin) {
if (e.data.blob) remoteRender(e); if (e.data.blob) remoteRender(e);
} }

View File

@ -14,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IInstance } from "matrix-js-sdk/src/client";
import { Protocols } from "../components/views/directory/NetworkDropdown";
// Find a protocol 'instance' with a given instance_id // Find a protocol 'instance' with a given instance_id
// in the supplied protocols dict // in the supplied protocols dict
export function instanceForInstanceId(protocols, instanceId) { export function instanceForInstanceId(protocols: Protocols, instanceId: string): IInstance {
if (!instanceId) return null; if (!instanceId) return null;
for (const proto of Object.keys(protocols)) { for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;
@ -28,7 +31,7 @@ export function instanceForInstanceId(protocols, instanceId) {
// given an instance_id, return the name of the protocol for // given an instance_id, return the name of the protocol for
// that instance ID in the supplied protocols dict // that instance ID in the supplied protocols dict
export function protocolNameForInstanceId(protocols, instanceId) { export function protocolNameForInstanceId(protocols: Protocols, instanceId: string): string {
if (!instanceId) return null; if (!instanceId) return null;
for (const proto of Object.keys(protocols)) { for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;

View File

@ -17,7 +17,7 @@ limitations under the License.
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
export function getHostingLink(campaign) { export function getHostingLink(campaign: string): string {
const hostingLink = SdkConfig.get().hosting_signup_link; const hostingLink = SdkConfig.get().hosting_signup_link;
if (!hostingLink) return null; if (!hostingLink) return null;
if (!campaign) return hostingLink; if (!campaign) return hostingLink;
@ -27,7 +27,7 @@ export function getHostingLink(campaign) {
try { try {
const hostingUrl = new URL(hostingLink); const hostingUrl = new URL(hostingLink);
hostingUrl.searchParams.set("utm_campaign", campaign); hostingUrl.searchParams.set("utm_campaign", campaign);
return hostingUrl.format(); return hostingUrl.toString();
} catch (e) { } catch (e) {
return hostingLink; return hostingLink;
} }

View File

@ -17,14 +17,14 @@ limitations under the License.
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
export function getNameForEventRoom(userId, roomId) { export function getNameForEventRoom(userId: string, roomId: string): string {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
const member = room && room.getMember(userId); const member = room && room.getMember(userId);
return member ? member.name : userId; return member ? member.name : userId;
} }
export function userLabelForEventRoom(userId, roomId) { export function userLabelForEventRoom(userId: string, roomId: string): string {
const name = getNameForEventRoom(userId, roomId); const name = getNameForEventRoom(userId, roomId);
if (name !== userId) { if (name !== userId) {
return _t("%(name)s (%(userId)s)", { name, userId }); return _t("%(name)s (%(userId)s)", { name, userId });

View File

@ -26,17 +26,17 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
* Make an Error object which has a friendlyText property which is already * Make an Error object which has a friendlyText property which is already
* translated and suitable for showing to the user. * translated and suitable for showing to the user.
* *
* @param {string} msg message for the exception * @param {string} message message for the exception
* @param {string} friendlyText * @param {string} friendlyText
* @returns {Error} * @returns {{message: string, friendlyText: string}}
*/ */
function friendlyError(msg, friendlyText) { function friendlyError(
const e = new Error(msg); message: string, friendlyText: string,
e.friendlyText = friendlyText; ): { message: string, friendlyText: string } {
return e; return { message, friendlyText };
} }
function cryptoFailMsg() { function cryptoFailMsg(): string {
return _t('Your browser does not support the required cryptography extensions'); return _t('Your browser does not support the required cryptography extensions');
} }
@ -49,7 +49,7 @@ function cryptoFailMsg() {
* *
* *
*/ */
export async function decryptMegolmKeyFile(data, password) { export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string): Promise<string> {
const body = unpackMegolmKeyFile(data); const body = unpackMegolmKeyFile(data);
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
@ -124,7 +124,11 @@ export async function decryptMegolmKeyFile(data, password) {
* key-derivation function. * key-derivation function.
* @return {Promise<ArrayBuffer>} promise for encrypted output * @return {Promise<ArrayBuffer>} promise for encrypted output
*/ */
export async function encryptMegolmKeyFile(data, password, options) { export async function encryptMegolmKeyFile(
data: string,
password: string,
options?: { kdf_rounds?: number }, // eslint-disable-line camelcase
): Promise<ArrayBuffer> {
options = options || {}; options = options || {};
const kdfRounds = options.kdf_rounds || 500000; const kdfRounds = options.kdf_rounds || 500000;
@ -196,7 +200,7 @@ export async function encryptMegolmKeyFile(data, password, options) {
* @param {String} password password * @param {String} password password
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
*/ */
async function deriveKeys(salt, iterations, password) { async function deriveKeys(salt: Uint8Array, iterations: number, password: string): Promise<[CryptoKey, CryptoKey]> {
const start = new Date(); const start = new Date();
let key; let key;
@ -229,7 +233,7 @@ async function deriveKeys(salt, iterations, password) {
} }
const now = new Date(); const now = new Date();
logger.log("E2e import/export: deriveKeys took " + (now - start) + "ms"); logger.log("E2e import/export: deriveKeys took " + (now.getTime() - start.getTime()) + "ms");
const aesKey = keybits.slice(0, 32); const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32); const hmacKey = keybits.slice(32);
@ -271,7 +275,7 @@ const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----';
* @param {ArrayBuffer} data input file * @param {ArrayBuffer} data input file
* @return {Uint8Array} unbase64ed content * @return {Uint8Array} unbase64ed content
*/ */
function unpackMegolmKeyFile(data) { function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array {
// parse the file as a great big String. This should be safe, because there // parse the file as a great big String. This should be safe, because there
// should be no non-ASCII characters, and it means that we can do string // should be no non-ASCII characters, and it means that we can do string
// comparisons to find the header and footer, and feed it into window.atob. // comparisons to find the header and footer, and feed it into window.atob.
@ -279,6 +283,7 @@ function unpackMegolmKeyFile(data) {
// look for the start line // look for the start line
let lineStart = 0; let lineStart = 0;
// eslint-disable-next-line no-constant-condition
while (1) { while (1) {
const lineEnd = fileStr.indexOf('\n', lineStart); const lineEnd = fileStr.indexOf('\n', lineStart);
if (lineEnd < 0) { if (lineEnd < 0) {
@ -297,6 +302,7 @@ function unpackMegolmKeyFile(data) {
const dataStart = lineStart; const dataStart = lineStart;
// look for the end line // look for the end line
// eslint-disable-next-line no-constant-condition
while (1) { while (1) {
const lineEnd = fileStr.indexOf('\n', lineStart); const lineEnd = fileStr.indexOf('\n', lineStart);
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim(); const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim();
@ -324,7 +330,7 @@ function unpackMegolmKeyFile(data) {
* @param {Uint8Array} data raw data * @param {Uint8Array} data raw data
* @return {ArrayBuffer} formatted file * @return {ArrayBuffer} formatted file
*/ */
function packMegolmKeyFile(data) { function packMegolmKeyFile(data: Uint8Array): ArrayBuffer {
// we split into lines before base64ing, because encodeBase64 doesn't deal // we split into lines before base64ing, because encodeBase64 doesn't deal
// terribly well with large arrays. // terribly well with large arrays.
const LINE_LENGTH = (72 * 4 / 3); const LINE_LENGTH = (72 * 4 / 3);
@ -347,7 +353,7 @@ function packMegolmKeyFile(data) {
* @param {Uint8Array} uint8Array The data to encode. * @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64. * @return {string} The base64.
*/ */
function encodeBase64(uint8Array) { function encodeBase64(uint8Array: Uint8Array): string {
// Misinterpt the Uint8Array as Latin-1. // Misinterpt the Uint8Array as Latin-1.
// window.btoa expects a unicode string with codepoints in the range 0-255. // window.btoa expects a unicode string with codepoints in the range 0-255.
const latin1String = String.fromCharCode.apply(null, uint8Array); const latin1String = String.fromCharCode.apply(null, uint8Array);
@ -360,7 +366,7 @@ function encodeBase64(uint8Array) {
* @param {string} base64 The base64 to decode. * @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data. * @return {Uint8Array} The decoded data.
*/ */
function decodeBase64(base64) { function decodeBase64(base64: string): Uint8Array {
// window.atob returns a unicode string with codepoints in the range 0-255. // window.atob returns a unicode string with codepoints in the range 0-255.
const latin1String = window.atob(base64); const latin1String = window.atob(base64);
// Encode the string as a Uint8Array // Encode the string as a Uint8Array

View File

@ -5807,8 +5807,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 "12.5.0" version "13.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f84905b00398072b592addfb1dae64c8f3a07fa2" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2515d07c8fc3bf5e1afc8352e3e330cca30dde85"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"