diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a445a4041..c28d72a3eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) =================================================================================================== diff --git a/package.json b/package.json index 3e3d9383c4..89084acd68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.30.0", + "version": "3.31.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 86c2efeb4a..fd9c4a14fc 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -89,7 +89,6 @@ limitations under the License. margin: 0px auto; overflow: auto; - flex: 0 0 auto; } .mx_RoomView_auxPanel_fullHeight { diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index 032cb49359..e19be82e25 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -59,3 +59,14 @@ limitations under the License. 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; + } +} diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index b9d845ea7a..1043fd08d1 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -18,7 +18,7 @@ a.mx_Pill { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - max-width: calc(100% - 1ch); + max-width: 100%; } .mx_Pill { diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 6805036e3d..46fc11956f 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -117,6 +117,16 @@ limitations under the License. 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 { background-color: transparent; // hide the download icon mask } diff --git a/res/img/element-icons/collapse-message.svg b/res/img/element-icons/collapse-message.svg new file mode 100644 index 0000000000..91b0713f43 --- /dev/null +++ b/res/img/element-icons/collapse-message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/expand-message.svg b/res/img/element-icons/expand-message.svg new file mode 100644 index 0000000000..a1c5149718 --- /dev/null +++ b/res/img/element-icons/expand-message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 8ad93fa960..d5856a5702 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,6 +49,8 @@ import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; +import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -92,6 +94,7 @@ declare global { mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; + mxActiveWidgetStore?: ActiveWidgetStore; mxOnRecaptchaLoaded?: () => void; electron?: Electron; } @@ -223,6 +226,15 @@ declare global { ) => string; 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; + // eslint-disable-next-line no-var, camelcase + var mx_rage_initStoragePromise: Promise; + // eslint-disable-next-line no-var, camelcase + var mx_rage_store: IndexedDBLogStore; } /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 57c922aeb7..3685f7b938 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise { UserActivity.sharedInstance().start(); DMRoomMap.makeShared().start(); IntegrationManagers.sharedInstance().startWatching(); - ActiveWidgetStore.start(); + ActiveWidgetStore.instance.start(); CallHandler.sharedInstance().start(); // 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(); TypingStore.sharedInstance().reset(); Presence.stop(); - ActiveWidgetStore.stop(); + ActiveWidgetStore.instance.stop(); IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); DeviceListener.sharedInstance().stop(); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index d65f8e3a10..2173230627 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -45,7 +45,7 @@ function getOrCreateContainer(): HTMLDivElement { const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); -interface IPosition { +export interface IPosition { top?: number; bottom?: number; left?: number; @@ -430,7 +430,11 @@ export type AboveLeftOf = IPosition & { // Placement method for 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?) -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 buttonRight = elementRect.right + window.pageXOffset; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index f626bb67d9..5d9d2a0b6a 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -18,7 +18,6 @@ limitations under the License. import React from 'react'; import { Room } from "matrix-js-sdk/src/models/room"; 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -59,7 +58,7 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan interface IProps { room?: Room; // if showing panels for a given room, 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; permalinkCreator?: RoomPermalinkCreator; e2eStatus?: E2EStatus; @@ -100,10 +99,10 @@ export default class RightPanel extends React.Component { // Helper function to split out the logic for getPhaseFromProps() and 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; 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 @@ -225,7 +224,7 @@ export default class RightPanel extends React.Component { // 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 // 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 // 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 diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 743928c272..15bf327a74 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -78,7 +78,6 @@ import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import UIStore from "../../stores/UIStore"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { throttle } from "lodash"; import ErrorDialog from '../views/dialogs/ErrorDialog'; @@ -158,7 +157,6 @@ export interface IState { // used by componentDidUpdate to avoid unnecessary checks atEndOfLiveTimelineInit: boolean; showTopUnreadMessagesBar: boolean; - auxPanelMaxHeight?: number; statusBarVisible: boolean; // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() @@ -565,10 +563,6 @@ export default class RoomView extends React.Component { }); window.addEventListener('beforeunload', this.onPageUnload); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("middlePanelResized", this.onResize); - } - this.onResize(); } shouldComponentUpdate(nextProps, nextState) { @@ -656,9 +650,6 @@ export default class RoomView extends React.Component { } window.removeEventListener('beforeunload', this.onPageUnload); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize); - } // Remove RoomStore listener if (this.roomStoreToken) { @@ -1619,28 +1610,6 @@ export default class RoomView extends React.Component { }; } - 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 = () => { if (this.unmounted || this.state.statusBarVisible) return; this.setState({ statusBarVisible: true }); @@ -1941,11 +1910,8 @@ export default class RoomView extends React.Component { const auxPanel = ( { aux } diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 0b686995fd..32168e8449 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -86,8 +86,8 @@ export default class UserView extends React.Component { public render(): JSX.Element { if (this.state.loading) { return ; - } else if (this.state.member?.user) { - const panel = ; + } else if (this.state.member) { + const panel = ; return ( ); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 8f5d3baa17..c7fcf32260 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -38,6 +38,7 @@ import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog'; import ErrorDialog from '../dialogs/ErrorDialog'; import ShareDialog from '../dialogs/ShareDialog'; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { IPosition, ChevronFace } from '../../structures/ContextMenu'; export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -52,7 +53,8 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps; } -interface IProps { +interface IProps extends IPosition { + chevronFace: ChevronFace; /* the MatrixEvent associated with the context menu */ mxEvent: MatrixEvent; /* an optional EventTileOps implementation that can be used to unhide preview widgets */ diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index 94ae71f1c9..a6f2f1e5c4 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -131,8 +131,13 @@ interface IProps { } const isOnlyAdmin = (room: Room): boolean => { - return !room.getJoinedMembers().some(member => { - return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100; + const userId = room.client.getUserId(); + 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; }); }; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index ea9ef71626..3f4d75df27 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -163,7 +163,7 @@ export default class AppTile extends React.Component { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { // 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); if (this.sgWidget) this.sgWidget.stop(); } @@ -198,8 +198,8 @@ export default class AppTile extends React.Component { if (this.dispatcherRef) dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container - if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { - ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); + if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) { + ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); PersistedElement.destroyElement(this.persistKey); } @@ -282,7 +282,7 @@ export default class AppTile extends React.Component { // Delete the widget from the persisted store for good measure. 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 }); } diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 1f911659e2..8d0751cc1d 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import RoomViewStore from '../../../stores/RoomViewStore'; -import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; +import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -39,13 +39,13 @@ export default class PersistentApp extends React.Component<{}, IState> { this.state = { roomId: RoomViewStore.getRoomId(), - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), }; } public componentDidMount(): void { 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); } @@ -53,7 +53,7 @@ export default class PersistentApp extends React.Component<{}, IState> { if (this.roomStoreToken) { this.roomStoreToken.remove(); } - ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); + ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); } @@ -68,23 +68,23 @@ export default class PersistentApp extends React.Component<{}, IState> { private onActiveWidgetStoreUpdate = (): void => { this.setState({ - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), }); }; private onMyMembership = async (room: Room, membership: string): Promise => { - const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); if (membership !== "join") { // we're not in the room anymore - delete if (room .roomId === persistentWidgetInRoomId) { - ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); + ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId); } } }; public render(): JSX.Element { 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); @@ -96,7 +96,7 @@ export default class PersistentApp extends React.Component<{}, IState> { if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { // get the widget data const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { - return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); + return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index 59c827d5d8..bd81218623 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import classNames from 'classnames'; + import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; @@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile"; import Pill from './Pill'; 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 { // the latest event in this chain of replies parentEv?: MatrixEvent; @@ -45,6 +53,8 @@ interface IProps { layout?: Layout; // Whether to always show a timestamp alwaysShowTimestamps?: boolean; + isQuoteExpanded?: boolean; + setQuoteExpanded: (isExpanded: boolean) => void; } interface IState { @@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component { static contextType = MatrixClientContext; private unmounted = false; private room: Room; + private blockquoteRef = React.createRef(); constructor(props, context) { super(props, context); @@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component { 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; // XXX: For newer relations (annotations, replacements, etc.), we now @@ -137,7 +148,7 @@ export default class ReplyThread extends React.Component { public static getNestedReplyText( ev: MatrixEvent, permalinkCreator: RoomPermalinkCreator, - ): { body: string, html: string } { + ): { body: string, html: string } | null { if (!ev) return null; let { body, formatted_body: html } = ev.getContent(); @@ -237,37 +248,38 @@ export default class ReplyThread extends React.Component { return replyMixin; } - public static makeThread( - parentEv: MatrixEvent, - onHeightChanged: () => void, - permalinkCreator: RoomPermalinkCreator, - ref: React.RefObject, - layout: Layout, - alwaysShowTimestamps: boolean, - ): JSX.Element { - if (!ReplyThread.getParentEventId(parentEv)) return null; - return ; + public static hasThreadReply(event: MatrixEvent) { + return Boolean(ReplyThread.getParentEventId(event)); } componentDidMount() { this.initialize(); + this.trySetExpandableQuotes(); } componentDidUpdate() { this.props.onHeightChanged(); + this.trySetExpandableQuotes(); } componentWillUnmount() { 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 { const { parentEv } = this.props; // 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 { this.initialize(); }; - private onQuoteClick = async (): Promise => { + private onQuoteClick = async (event: React.MouseEvent): Promise => { const events = [this.state.loadedEv, ...this.state.events]; let loadedEv = null; @@ -373,14 +385,26 @@ export default class ReplyThread extends React.Component { header = ; } + const { isQuoteExpanded } = this.props; const evTiles = this.state.events.map((ev) => { - return
- -
; + const classname = classNames({ + 'mx_ReplyThread': true, + [this.getReplyThreadColorClass(ev)]: true, + // We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false + '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 + 'mx_ReplyThread--collapsed': isQuoteExpanded === false, + }); + return ( +
+ this.props.setQuoteExpanded(!this.props.isQuoteExpanded)} + /> +
+ ); }); return
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.tsx similarity index 73% rename from src/components/views/messages/EditHistoryMessage.js rename to src/components/views/messages/EditHistoryMessage.tsx index 2c6a567f6b..1abed87b76 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -15,107 +15,112 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import * as HtmlUtils from '../../../HtmlUtils'; import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils'; 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 { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import classNames from 'classnames'; import RedactedBody from "./RedactedBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AccessibleButton from "../elements/AccessibleButton"; +import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog"; +import ViewSource from "../../structures/ViewSource"; function getReplacedContent(event) { const originalContent = event.getOriginalContent(); return originalContent["m.new_content"] || originalContent; } -@replaceableComponent("views.messages.EditHistoryMessage") -export default class EditHistoryMessage extends React.PureComponent { - static propTypes = { - // the message event being edited - mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, - previousEdit: PropTypes.instanceOf(MatrixEvent), - isBaseEvent: PropTypes.bool, - }; +interface IProps { + // the message event being edited + mxEvent: MatrixEvent; + previousEdit?: MatrixEvent; + isBaseEvent?: boolean; + isTwelveHour?: boolean; +} - constructor(props) { +interface IState { + canRedact: boolean; + sendStatus: EventStatus; +} + +@replaceableComponent("views.messages.EditHistoryMessage") +export default class EditHistoryMessage extends React.PureComponent { + private content = createRef(); + private pills: Element[] = []; + + constructor(props: IProps) { super(props); + const cli = MatrixClientPeg.get(); const { userId } = cli.credentials; const event = this.props.mxEvent; const room = cli.getRoom(event.getRoomId()); if (event.localRedactionEvent()) { - event.localRedactionEvent().on("status", this._onAssociatedStatusChanged); + event.localRedactionEvent().on("status", this.onAssociatedStatusChanged); } const canRedact = room.currentState.maySendRedactionForEvent(event, userId); this.state = { canRedact, sendStatus: event.getAssociatedStatus() }; - - this._content = createRef(); - this._pills = []; } - _onAssociatedStatusChanged = () => { + private onAssociatedStatusChanged = (): void => { this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() }); }; - _onRedactClick = async () => { + private onRedactClick = async (): Promise => { const event = this.props.mxEvent; const cli = MatrixClientPeg.get(); - const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog"); Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, { redact: () => cli.redactEvent(event.getRoomId(), event.getId()), }, 'mx_Dialog_confirmredact'); }; - _onViewSourceClick = () => { - const ViewSource = sdk.getComponent('structures.ViewSource'); + private onViewSourceClick = (): void => { Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); }; - pillifyLinks() { + private pillifyLinks(): void { // not present for redacted events - if (this._content.current) { - pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills); + if (this.content.current) { + pillifyLinks(this.content.current.children, this.props.mxEvent, this.pills); } } - componentDidMount() { + public componentDidMount(): void { this.pillifyLinks(); } - componentWillUnmount() { - unmountPills(this._pills); + public componentWillUnmount(): void { + unmountPills(this.pills); const event = this.props.mxEvent; if (event.localRedactionEvent()) { - event.localRedactionEvent().off("status", this._onAssociatedStatusChanged); + event.localRedactionEvent().off("status", this.onAssociatedStatusChanged); } } - componentDidUpdate() { + public componentDidUpdate(): void { this.pillifyLinks(); } - _renderActionBar() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + private renderActionBar(): JSX.Element { // hide the button when already redacted let redactButton; if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) { redactButton = ( - + { _t("Remove") } ); } const viewSourceButton = ( - + { _t("View Source") } ); @@ -128,7 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent { ); } - render() { + public render(): JSX.Element { const { mxEvent } = this.props; const content = getReplacedContent(mxEvent); let contentContainer; @@ -139,18 +144,22 @@ export default class EditHistoryMessage extends React.PureComponent { if (this.props.previousEdit) { contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content); } else { - contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true }); + contentElements = HtmlUtils.bodyToHtml( + content, + null, + { stripReplyFallback: true, returnString: false }, + ); } if (mxEvent.getContent().msgtype === "m.emote") { const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); contentContainer = ( -
*  +
{ name }  { contentElements }
); } else { - contentContainer =
{ contentElements }
; + contentContainer =
{ contentElements }
; } } @@ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent {
{ timestamp } { contentContainer } - { this._renderActionBar() } + { this.renderActionBar() }
diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.tsx similarity index 69% rename from src/components/views/messages/MKeyVerificationConclusion.js rename to src/components/views/messages/MKeyVerificationConclusion.tsx index a5f12df47d..1ce39e1157 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.tsx @@ -16,44 +16,50 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import { getNameForEventRoom, userLabelForEventRoom } - from '../../../utils/KeyVerificationStateObserver'; +import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver'; import EventTileBubble from "./EventTileBubble"; 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") -export default class MKeyVerificationConclusion extends React.Component { - constructor(props) { +export default class MKeyVerificationConclusion extends React.Component { + constructor(props: IProps) { super(props); } - componentDidMount() { + public componentDidMount(): void { const request = this.props.mxEvent.verificationRequest; 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; if (request) { - request.off("change", this._onRequestChanged); + request.off("change", this.onRequestChanged); } const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener("userTrustStatusChanged", this._onTrustChanged); + cli.removeListener("userTrustStatusChanged", this.onTrustChanged); } } - _onRequestChanged = () => { + private onRequestChanged = (): void => { this.forceUpdate(); }; - _onTrustChanged = (userId, status) => { + private onTrustChanged = (userId: string): void => { const { mxEvent } = this.props; const request = mxEvent.verificationRequest; if (!request || request.otherUserId !== userId) { @@ -62,17 +68,17 @@ export default class MKeyVerificationConclusion extends React.Component { this.forceUpdate(); }; - _shouldRender(mxEvent, request) { + public static shouldRender(mxEvent: MatrixEvent, request: VerificationRequest): boolean { // normally should not happen if (!request) { return false; } // .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; } // .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; } @@ -89,11 +95,11 @@ export default class MKeyVerificationConclusion extends React.Component { return true; } - render() { + public render(): JSX.Element { const { mxEvent } = this.props; const request = mxEvent.verificationRequest; - if (!this._shouldRender(mxEvent, request)) { + if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) { return null; } @@ -103,15 +109,18 @@ export default class MKeyVerificationConclusion extends React.Component { let title; 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) { const userId = request.cancellingUserId; if (userId === myUserId) { title = _t("You cancelled verifying %(name)s", - { name: getNameForEventRoom(request.otherUserId, mxEvent) }); + { name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) }); } else { 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; } } - -MKeyVerificationConclusion.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index f7c58e27dc..3be24d47ab 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -17,7 +17,8 @@ limitations under the License. */ 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 * as sdk from '../../../index'; @@ -35,13 +36,17 @@ import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import DownloadActionButton from "./DownloadActionButton"; +import MessageContextMenu from "../context_menus/MessageContextMenu"; +import classNames from 'classnames'; + import SettingsStore from '../../../settings/SettingsStore'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import ReplyThread from '../elements/ReplyThread'; interface IOptionsButtonProps { mxEvent: MatrixEvent; - getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here + // TODO: Types + getTile: () => any | null; getReplyThread: () => ReplyThread; permalinkCreator: RoomPermalinkCreator; onFocusChange: (menuDisplayed: boolean) => void; @@ -57,8 +62,6 @@ const OptionsButton: React.FC = let contextMenu; if (menuDisplayed) { - const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - const tile = getTile && getTile(); const replyThread = getReplyThread && getReplyThread(); @@ -90,7 +93,7 @@ const OptionsButton: React.FC = interface IReactButtonProps { mxEvent: MatrixEvent; - reactions: any; // TODO: types + reactions: Relations; onFocusChange: (menuDisplayed: boolean) => void; } @@ -127,12 +130,15 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC interface IMessageActionBarProps { mxEvent: MatrixEvent; - // The Relations model from the JS SDK for reactions to `mxEvent` - reactions?: any; // TODO: types + reactions?: Relations; + // TODO: Types + getTile: () => any | null; + getReplyThread: () => ReplyThread | undefined; permalinkCreator?: RoomPermalinkCreator; getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here getReplyThread?: () => ReplyThread; onFocusChange?: (menuDisplayed: boolean) => void; + toggleThreadExpanded: () => void; isInThreadTimeline?: boolean; } @@ -329,6 +335,20 @@ export default class MessageActionBar extends React.PureComponent); + } + // The menu button should be last, so dump it there. toolbarOpts.push( void; +} @replaceableComponent("views.messages.MjolnirBody") -export default class MjolnirBody extends React.Component { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - onMessageAllowed: PropTypes.func.isRequired, - }; - - constructor() { - super(); - } - - _onAllowClick = (e) => { +export default class MjolnirBody extends React.Component { + private onAllowClick = (e: React.MouseEvent): void => { e.preventDefault(); e.stopPropagation(); @@ -39,11 +35,11 @@ export default class MjolnirBody extends React.Component { this.props.onMessageAllowed(); }; - render() { + public render(): JSX.Element { return (
{ _t( "You have ignored this user, so their message is hidden. Show anyways.", - {}, { a: (sub) => { sub } }, + {}, { a: (sub) => { sub } }, ) }
); } diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index c2e137c97b..66200036cd 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.tsx @@ -16,13 +16,18 @@ limitations under the License. import React, { useContext } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { formatFullDate } from "../../../DateUtils"; import SettingsStore from "../../../settings/SettingsStore"; import { IBodyProps } from "./IBodyProps"; -const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { +interface IProps { + mxEvent: MatrixEvent; +} + +const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { const cli: MatrixClient = useContext(MatrixClientContext); let text = _t("Message deleted"); diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.tsx similarity index 88% rename from src/components/views/messages/RoomAvatarEvent.js rename to src/components/views/messages/RoomAvatarEvent.tsx index 9832332311..12a8c88913 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.tsx @@ -17,23 +17,24 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") -export default class RoomAvatarEvent extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - }; - - onAvatarClick = () => { +export default class RoomAvatarEvent extends React.Component { + private onAvatarClick = (): void => { const cli = MatrixClientPeg.get(); const ev = this.props.mxEvent; const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; @@ -44,7 +45,6 @@ export default class RoomAvatarEvent extends React.Component { roomName: room ? room.name : '', }); - const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, name: text, @@ -52,10 +52,9 @@ export default class RoomAvatarEvent extends React.Component { Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); }; - render() { + public render(): JSX.Element { const ev = this.props.mxEvent; 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) { return ( diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.tsx similarity index 85% rename from src/components/views/messages/RoomCreate.js rename to src/components/views/messages/RoomCreate.tsx index a0bc8daa64..c846ba5632 100644 --- a/src/components/views/messages/RoomCreate.js +++ b/src/components/views/messages/RoomCreate.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; @@ -24,15 +23,16 @@ import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import EventTileBubble from "./EventTileBubble"; 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") -export default class RoomCreate extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - }; - - _onLinkClicked = e => { +export default class RoomCreate extends React.Component { + private onLinkClicked = (e: React.MouseEvent): void => { e.preventDefault(); 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']; if (predecessor === undefined) { return
; // We should never have been instantiated in this case @@ -55,7 +55,7 @@ export default class RoomCreate extends React.Component { permalinkCreator.load(); const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']); const link = ( - + { _t("Click here to see older messages.") } ); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 83fe7f5a3d..63ff39721d 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -138,6 +138,7 @@ export default class TextualBody extends React.Component { // 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... 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; const button = document.createElement("span"); diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 4a62d6711e..7afa29624a 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import classNames from 'classnames'; import { lexicographicCompare } from 'matrix-js-sdk/src/utils'; import { Room } from 'matrix-js-sdk/src/models/room'; @@ -35,16 +34,6 @@ interface IProps { room: Room; userId: string; 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; } @@ -92,13 +81,6 @@ export default class AuxPanel extends React.Component { 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(() => { this.setState({ counters: this.computeCounters() }); }, 500, { leading: true, trailing: true }); @@ -138,7 +120,6 @@ export default class AuxPanel extends React.Component { const callView = ( ); @@ -148,7 +129,6 @@ export default class AuxPanel extends React.Component { appsDrawer = ; @@ -204,21 +184,12 @@ export default class AuxPanel extends React.Component { } } - 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 ( - + { stateViews } + { this.props.children } { appsDrawer } { callView } - { this.props.children } ); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 06be8f943e..00f4c058fd 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import SettingsStore from "../../../settings/SettingsStore"; +import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion"; const eventTileTypes = { [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 // declining to render? if (type === "m.key.verification.cancel" || type === "m.key.verification.done") { - const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion"); - if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) { + if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) { return; } } @@ -323,7 +323,7 @@ interface IState { reactions: Relations; hover: boolean; - + isQuoteExpanded?: boolean; thread?: Thread; } @@ -331,7 +331,8 @@ interface IState { export default class EventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; - private tile = React.createRef(); + // TODO: Types + private tile = React.createRef(); private replyThread = React.createRef(); public readonly ref = createRef(); @@ -889,8 +890,8 @@ export default class EventTile extends React.Component { actionBarFocused: focused, }); }; - - getTile = () => this.tile.current; + // TODO: Types + getTile: () => any | null = () => this.tile.current; getReplyThread = () => this.replyThread.current; @@ -915,6 +916,11 @@ export default class EventTile extends React.Component { }); }; + private setQuoteExpanded = (expanded: boolean) => { + this.setState({ + isQuoteExpanded: expanded, + }); + }; render() { const msgtype = this.props.mxEvent.getContent().msgtype; const eventType = this.props.mxEvent.getType() as EventType; @@ -924,6 +930,7 @@ export default class EventTile extends React.Component { isInfoMessage, isLeftAlignedBubbleMessage, } = getEventDisplayInfo(this.props.mxEvent); + const { isQuoteExpanded } = this.state; // This shouldn't happen: the caller should check we support this type // before trying to instantiate us @@ -936,6 +943,7 @@ export default class EventTile extends React.Component {
; } + const EventTileType = sdk.getComponent(tileHandler); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); @@ -1057,6 +1065,7 @@ export default class EventTile extends React.Component { getReplyThread={this.getReplyThread} onFocusChange={this.onActionBarFocusChange} isInThreadTimeline={isInThreadTimeline} + toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)} /> : undefined; const showTimestamp = this.props.mxEvent.getTs() @@ -1229,20 +1238,18 @@ export default class EventTile extends React.Component { } default: { - let thread; - // When the "showHiddenEventsInTimeline" lab is enabled, - // avoid showing replies for hidden events (events without tiles) - if (haveTileForEvent(this.props.mxEvent)) { - thread = ReplyThread.makeThread( - this.props.mxEvent, - this.props.onHeightChanged, - this.props.permalinkCreator, - this.replyThread, - this.props.layout, - this.props.alwaysShowTimestamps || this.state.hover, - ); - } - + const thread = haveTileForEvent(this.props.mxEvent) && + ReplyThread.hasThreadReply(this.props.mxEvent) ? ( + ) : null; 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 diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index c9842bdd33..eed13aff0f 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { useContext, useEffect } from "react"; 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 LinkPreviewWidget from "./LinkPreviewWidget"; @@ -40,13 +40,7 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onH const ts = mxEvent.getTs(); const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => { - return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => { - 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][]>; + return fetchPreviews(cli, links, ts); }, [links, ts], []); useEffect(() => { @@ -89,4 +83,18 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onH ; }; +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; diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index cf7d1ce945..01a9e2f18b 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -35,6 +35,7 @@ interface IProps { highlights?: string[]; highlightLink?: string; onHeightChanged?(): void; + toggleExpandedQuote?: () => void; } @replaceableComponent("views.rooms.ReplyTile") @@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent { // 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. e.preventDefault(); - dis.dispatch({ - action: 'view_room', - event_id: this.props.mxEvent.getId(), - highlighted: true, - room_id: this.props.mxEvent.getRoomId(), - }); + // Expand thread on shift key + if (this.props.toggleExpandedQuote && e.shiftKey) { + this.props.toggleExpandedQuote(); + } else { + dis.dispatch({ + action: 'view_room', + event_id: this.props.mxEvent.getId(), + highlighted: true, + room_id: this.props.mxEvent.getRoomId(), + }); + } } }; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 05c41f74bf..cc27ccf153 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -164,6 +164,20 @@ export default class SendMessageComposer extends React.Component { 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 => { // ignore any keypress while doing IME compositions if (this.editorRef.current?.isComposing(event)) { @@ -484,7 +498,12 @@ export default class SendMessageComposer extends React.Component { } 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 { @@ -492,6 +511,10 @@ export default class SendMessageComposer extends React.Component { } private restoreStoredEditorState(partCreator: PartCreator): Part[] { + if (this.props.replyInThread && !this.props.replyToEvent) { + return null; + } + const json = localStorage.getItem(this.editorStateKey); if (json) { try { diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index a5aa3e7734..b0a6f17095 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -27,9 +27,6 @@ interface IProps { // What room we should display the call for roomId: string; - // maxHeight style attribute for the video panel - maxVideoHeight?: number; - resizeNotifier: ResizeNotifier; } @@ -99,14 +96,12 @@ export default class CallViewForRoom extends React.Component { public render() { 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 (
{ 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) { return arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof (arg) === 'object') { @@ -118,7 +116,7 @@ class ConsoleLogger { * @param {boolean} keepLogs True to not delete logs after flushing. * @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 // flushes them to the caller. if (keepLogs) { @@ -131,27 +129,28 @@ class ConsoleLogger { } // A class which stores log lines in an IndexedDB instance. -class IndexedDBLogStore { - constructor(indexedDB, logger) { - this.indexedDB = indexedDB; - this.logger = logger; - this.id = "instance-" + Math.random() + Date.now(); - this.index = 0; - this.db = null; +export class IndexedDBLogStore { + private id: string; + private index = 0; + private db = null; + private flushPromise = null; + private flushAgainPromise = null; - // these promises are cleared as soon as fulfilled - this.flushPromise = null; - // set if flush() is called whilst one is ongoing - this.flushAgainPromise = null; + constructor( + private indexedDB: IDBFactory, + private logger: ConsoleLogger, + ) { + this.id = "instance-" + Math.random() + Date.now(); } /** * @return {Promise} Resolves when the store is ready. */ - connect() { + public connect(): Promise { const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { - req.onsuccess = (event) => { + req.onsuccess = (event: Event) => { + // @ts-ignore this.db = event.target.result; // Periodically flush logs to local storage / indexeddb setInterval(this.flush.bind(this), FLUSH_RATE_MS); @@ -160,6 +159,7 @@ class IndexedDBLogStore { req.onerror = (event) => { const err = ( + // @ts-ignore "Failed to open log database: " + event.target.error.name ); console.error(err); @@ -168,6 +168,7 @@ class IndexedDBLogStore { // First time: Setup the object store req.onupgradeneeded = (event) => { + // @ts-ignore const db = event.target.result; const logObjStore = db.createObjectStore("logs", { keyPath: ["id", "index"], @@ -178,7 +179,7 @@ class IndexedDBLogStore { logObjStore.createIndex("id", "id", { unique: false }); logObjStore.add( - this._generateLogEntry( + this.generateLogEntry( new Date() + " ::: Log database was created.", ), ); @@ -186,7 +187,7 @@ class IndexedDBLogStore { const lastModifiedStore = db.createObjectStore("logslastmod", { 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. */ - flush() { + public flush(): Promise { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -227,7 +228,7 @@ class IndexedDBLogStore { } // 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. - this.flushPromise = new Promise((resolve, reject) => { + this.flushPromise = new Promise((resolve, reject) => { if (!this.db) { // not connected yet or user rejected access for us to r/w to the db. reject(new Error("No connected database")); @@ -251,9 +252,9 @@ class IndexedDBLogStore { new Error("Failed to write logs: " + event.target.errorCode), ); }; - objStore.add(this._generateLogEntry(lines)); + objStore.add(this.generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); - lastModStore.put(this._generateLastModifiedTime()); + lastModStore.put(this.generateLastModifiedTime()); }).then(() => { this.flushPromise = null; }); @@ -270,12 +271,12 @@ class IndexedDBLogStore { * 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. */ - async consume() { + public async consume(): Promise<{lines: string, id: string}[]> { const db = this.db; // Returns: a string representing the concatenated logs for this ID. // Stops adding log fragments when the size exceeds maxSize - function fetchLogs(id, maxSize) { + function fetchLogs(id: string, maxSize: number): Promise { const objectStore = db.transaction("logs", "readonly").objectStore("logs"); return new Promise((resolve, reject) => { @@ -301,7 +302,7 @@ class IndexedDBLogStore { } // Returns: A sorted array of log IDs. (newest first) - function fetchLogIds() { + function fetchLogIds(): Promise { // To gather all the log IDs, query for all records in logslastmod. const o = db.transaction("logslastmod", "readonly").objectStore( "logslastmod", @@ -319,8 +320,8 @@ class IndexedDBLogStore { }); } - function deleteLogs(id) { - return new Promise((resolve, reject) => { + function deleteLogs(id: number): Promise { + return new Promise((resolve, reject) => { const txn = db.transaction( ["logs", "logslastmod"], "readwrite", ); @@ -389,7 +390,7 @@ class IndexedDBLogStore { return logs; } - _generateLogEntry(lines) { + private generateLogEntry(lines: string): {id: string, lines: string, index: number} { return { id: this.id, lines: lines, @@ -397,7 +398,7 @@ class IndexedDBLogStore { }; } - _generateLastModifiedTime() { + private generateLastModifiedTime(): {id: string, ts: number} { return { id: this.id, ts: Date.now(), @@ -415,15 +416,19 @@ class IndexedDBLogStore { * @return {Promise} Resolves to an array of whatever you returned from * resultMapper. */ -function selectQuery(store, keyRange, resultMapper) { +function selectQuery( + store: IDBIndex, keyRange: IDBKeyRange, resultMapper: (cursor: IDBCursorWithValue) => T, +): Promise { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { const results = []; query.onerror = (event) => { + // @ts-ignore reject(new Error("Query failed: " + event.target.errorCode)); }; // collect results query.onsuccess = (event) => { + // @ts-ignore const cursor = event.target.result; if (!cursor) { resolve(results); @@ -442,7 +447,7 @@ function selectQuery(store, keyRange, resultMapper) { * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ -export function init(setUpPersistence = true) { +export function init(setUpPersistence = true): Promise { if (global.mx_rage_initPromise) { return global.mx_rage_initPromise; } @@ -462,7 +467,7 @@ export function init(setUpPersistence = true) { * then this no-ops. * @return {Promise} Resolves when complete. */ -export function tryInitStorage() { +export function tryInitStorage(): Promise { if (global.mx_rage_initStoragePromise) { return global.mx_rage_initStoragePromise; } diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js deleted file mode 100644 index b270d99693..0000000000 --- a/src/stores/ActiveWidgetStore.js +++ /dev/null @@ -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; diff --git a/src/stores/ActiveWidgetStore.ts b/src/stores/ActiveWidgetStore.ts new file mode 100644 index 0000000000..ca50689188 --- /dev/null +++ b/src/stores/ActiveWidgetStore.ts @@ -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(); + + 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; diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index bd03e2065b..44c8327c04 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient { // 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 - const persistentWidgetId = ActiveWidgetStore.getPersistentWidgetId(); + const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId(); if (persistentWidgetId) { if ( - ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId && + ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId && !roomInfo.widgets.some(w => w.id === persistentWidgetId) ) { 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 { // A persistent conference widget indicates that we're participating 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)); } } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 361b02bc3e..e00c4c6c0b 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -266,7 +266,7 @@ export class StopGapWidget extends EventEmitter { WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); 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 @@ -319,7 +319,7 @@ export class StopGapWidget extends EventEmitter { if (WidgetType.JITSI.matches(this.mockWidget.type)) { 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(); this.messaging.transport.reply(ev.detail, {}); // ack } @@ -406,13 +406,13 @@ export class StopGapWidget extends EventEmitter { } 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"); return; } if (!this.started) return; WidgetMessagingStore.instance.stopMessaging(this.mockWidget); - ActiveWidgetStore.delRoomId(this.mockWidget.id); + ActiveWidgetStore.instance.delRoomId(this.mockWidget.id); if (MatrixClientPeg.get()) { MatrixClientPeg.get().off('event', this.onEvent); diff --git a/src/usercontent/index.js b/src/usercontent/index.ts similarity index 55% rename from src/usercontent/index.js rename to src/usercontent/index.ts index c03126ec80..df551e88e6 100644 --- a/src/usercontent/index.js +++ b/src/usercontent/index.ts @@ -1,5 +1,5 @@ let hasCalled = false; -function remoteRender(event) { +function remoteRender(event: MessageEvent): void { const data = event.data; // If we're handling secondary calls, start from scratch @@ -8,13 +8,14 @@ function remoteRender(event) { } 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"; - const a = document.createElement("a"); + const a: HTMLAnchorElement = document.createElement("a"); a.id = "a"; a.rel = "noreferrer noopener"; a.download = data.download; + // @ts-ignore a.style = data.style; a.style.fontFamily = "Arial, Helvetica, Sans-Serif"; 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. // Style copied from a rendered version of mx_MFileBody_download_icon - img.style = (data.imgStyle || "" + - "width: 12px; height: 12px;" + - "-webkit-mask-size: 12px;" + - "mask-size: 12px;" + - "-webkit-mask-position: center;" + - "mask-position: center;" + - "-webkit-mask-repeat: no-repeat;" + - "mask-repeat: no-repeat;" + - "display: inline-block;") + "" + - - // Always add these styles - `-webkit-mask-image: url('${data.imgSrc}');` + - `mask-image: url('${data.imgSrc}');` + - `background-color: ${a.style.color};`; + if (data.imgStyle) { + // @ts-ignore + img.style = data.imgStyle; + } else { + img.style.width = "12px"; + img.style.height = "12px"; + img.style.webkitMaskSize = "12px"; + img.style.webkitMaskPosition = "center"; + img.style.webkitMaskRepeat = "no-repeat"; + img.style.display = "inline-block"; + img.style.webkitMaskImage = `url('${data.imgSrc}')`; + img.style.backgroundColor = `${a.style.color}`; + } const body = document.body; // 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); 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.data.blob) remoteRender(e); } diff --git a/src/utils/DirectoryUtils.js b/src/utils/DirectoryUtils.ts similarity index 81% rename from src/utils/DirectoryUtils.js rename to src/utils/DirectoryUtils.ts index 577a6441f8..255ae0e3fd 100644 --- a/src/utils/DirectoryUtils.js +++ b/src/utils/DirectoryUtils.ts @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and 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 // in the supplied protocols dict -export function instanceForInstanceId(protocols, instanceId) { +export function instanceForInstanceId(protocols: Protocols, instanceId: string): IInstance { if (!instanceId) return null; for (const proto of Object.keys(protocols)) { 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 // 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; for (const proto of Object.keys(protocols)) { if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; diff --git a/src/utils/HostingLink.js b/src/utils/HostingLink.ts similarity index 91% rename from src/utils/HostingLink.js rename to src/utils/HostingLink.ts index 134e045ca2..f8c0f12c3f 100644 --- a/src/utils/HostingLink.js +++ b/src/utils/HostingLink.ts @@ -17,7 +17,7 @@ limitations under the License. import SdkConfig from '../SdkConfig'; import { MatrixClientPeg } from '../MatrixClientPeg'; -export function getHostingLink(campaign) { +export function getHostingLink(campaign: string): string { const hostingLink = SdkConfig.get().hosting_signup_link; if (!hostingLink) return null; if (!campaign) return hostingLink; @@ -27,7 +27,7 @@ export function getHostingLink(campaign) { try { const hostingUrl = new URL(hostingLink); hostingUrl.searchParams.set("utm_campaign", campaign); - return hostingUrl.format(); + return hostingUrl.toString(); } catch (e) { return hostingLink; } diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.ts similarity index 86% rename from src/utils/KeyVerificationStateObserver.js rename to src/utils/KeyVerificationStateObserver.ts index 023cdf3a75..e4fae3f3c8 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.ts @@ -17,14 +17,14 @@ limitations under the License. import { MatrixClientPeg } from '../MatrixClientPeg'; import { _t } from '../languageHandler'; -export function getNameForEventRoom(userId, roomId) { +export function getNameForEventRoom(userId: string, roomId: string): string { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); const member = room && room.getMember(userId); return member ? member.name : userId; } -export function userLabelForEventRoom(userId, roomId) { +export function userLabelForEventRoom(userId: string, roomId: string): string { const name = getNameForEventRoom(userId, roomId); if (name !== userId) { return _t("%(name)s (%(userId)s)", { name, userId }); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.ts similarity index 90% rename from src/utils/MegolmExportEncryption.js rename to src/utils/MegolmExportEncryption.ts index 8e7ee2005d..47c395bfb7 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.ts @@ -26,17 +26,17 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; * Make an Error object which has a friendlyText property which is already * 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 - * @returns {Error} + * @returns {{message: string, friendlyText: string}} */ -function friendlyError(msg, friendlyText) { - const e = new Error(msg); - e.friendlyText = friendlyText; - return e; +function friendlyError( + message: string, friendlyText: string, +): { message: string, friendlyText: string } { + return { message, friendlyText }; } -function cryptoFailMsg() { +function cryptoFailMsg(): string { 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 { const body = unpackMegolmKeyFile(data); const brand = SdkConfig.get().brand; @@ -124,7 +124,11 @@ export async function decryptMegolmKeyFile(data, password) { * key-derivation function. * @return {Promise} 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 { options = options || {}; const kdfRounds = options.kdf_rounds || 500000; @@ -196,7 +200,7 @@ export async function encryptMegolmKeyFile(data, password, options) { * @param {String} password password * @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(); let key; @@ -229,7 +233,7 @@ async function deriveKeys(salt, iterations, password) { } 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 hmacKey = keybits.slice(32); @@ -271,7 +275,7 @@ const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----'; * @param {ArrayBuffer} data input file * @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 // 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. @@ -279,6 +283,7 @@ function unpackMegolmKeyFile(data) { // look for the start line let lineStart = 0; + // eslint-disable-next-line no-constant-condition while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); if (lineEnd < 0) { @@ -297,6 +302,7 @@ function unpackMegolmKeyFile(data) { const dataStart = lineStart; // look for the end line + // eslint-disable-next-line no-constant-condition while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim(); @@ -324,7 +330,7 @@ function unpackMegolmKeyFile(data) { * @param {Uint8Array} data raw data * @return {ArrayBuffer} formatted file */ -function packMegolmKeyFile(data) { +function packMegolmKeyFile(data: Uint8Array): ArrayBuffer { // we split into lines before base64ing, because encodeBase64 doesn't deal // terribly well with large arrays. const LINE_LENGTH = (72 * 4 / 3); @@ -347,7 +353,7 @@ function packMegolmKeyFile(data) { * @param {Uint8Array} uint8Array The data to encode. * @return {string} The base64. */ -function encodeBase64(uint8Array) { +function encodeBase64(uint8Array: Uint8Array): string { // Misinterpt the Uint8Array as Latin-1. // window.btoa expects a unicode string with codepoints in the range 0-255. const latin1String = String.fromCharCode.apply(null, uint8Array); @@ -360,7 +366,7 @@ function encodeBase64(uint8Array) { * @param {string} base64 The base64 to decode. * @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. const latin1String = window.atob(base64); // Encode the string as a Uint8Array diff --git a/yarn.lock b/yarn.lock index 622d96cc0a..39c50464d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5807,8 +5807,8 @@ mathml-tag-names@^2.1.3: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "12.5.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f84905b00398072b592addfb1dae64c8f3a07fa2" + version "13.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2515d07c8fc3bf5e1afc8352e3e330cca30dde85" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0"