mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge branch 'develop' into export-conversations
						commit
						208b914cb0
					
				
							
								
								
									
										103
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										103
									
								
								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) | ||||
| =================================================================================================== | ||||
| 
 | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ Code should be committed as follows: | |||
|  * CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css | ||||
|  * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes | ||||
| 
 | ||||
| React components in matrix-react-sdk are come in two different flavours: | ||||
| React components in matrix-react-sdk come in two different flavours: | ||||
| 'structures' and 'views'.  Structures are stateful components which handle the | ||||
| more complicated business logic of the app, delegating their actual presentation | ||||
| rendering to stateless 'view' components.  For instance, the RoomView component | ||||
|  |  | |||
|  | @ -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": { | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| // autogenerated by rethemendex.sh | ||||
| @import "./_animations.scss"; | ||||
| @import "./_common.scss"; | ||||
| @import "./_font-sizes.scss"; | ||||
| @import "./_font-weights.scss"; | ||||
| @import "./structures/_AutoHideScrollbar.scss"; | ||||
| @import "./structures/_BackdropPanel.scss"; | ||||
| @import "./structures/_CompatibilityPage.scss"; | ||||
| @import "./structures/_ContextualMenu.scss"; | ||||
| @import "./structures/_CreateRoom.scss"; | ||||
|  | @ -17,7 +19,6 @@ | |||
| @import "./structures/_LeftPanelWidget.scss"; | ||||
| @import "./structures/_MainSplit.scss"; | ||||
| @import "./structures/_MatrixChat.scss"; | ||||
| @import "./structures/_BackdropPanel.scss"; | ||||
| @import "./structures/_MyGroups.scss"; | ||||
| @import "./structures/_NonUrgentToastContainer.scss"; | ||||
| @import "./structures/_NotificationPanel.scss"; | ||||
|  | @ -244,6 +245,7 @@ | |||
| @import "./views/settings/_E2eAdvancedPanel.scss"; | ||||
| @import "./views/settings/_EmailAddresses.scss"; | ||||
| @import "./views/settings/_IntegrationManager.scss"; | ||||
| @import "./views/settings/_JoinRuleSettings.scss"; | ||||
| @import "./views/settings/_LayoutSwitcher.scss"; | ||||
| @import "./views/settings/_Notifications.scss"; | ||||
| @import "./views/settings/_PhoneNumbers.scss"; | ||||
|  |  | |||
|  | @ -89,7 +89,6 @@ limitations under the License. | |||
|     margin: 0px auto; | ||||
| 
 | ||||
|     overflow: auto; | ||||
|     flex: 0 0 auto; | ||||
| } | ||||
| 
 | ||||
| .mx_RoomView_auxPanel_fullHeight { | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -732,6 +732,11 @@ $hover-select-border: 4px; | |||
|         margin-top: 0; | ||||
|         padding-bottom: 5px; | ||||
|         margin-bottom: 5px; | ||||
| 
 | ||||
|         .mx_MessageTimestamp { | ||||
|             left: auto; | ||||
|             right: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_MessageComposer_sendMessage { | ||||
|  |  | |||
|  | @ -185,16 +185,26 @@ limitations under the License. | |||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_ContextualMenu { | ||||
|     .mx_MessageComposer_button { | ||||
|         padding-left: calc(var(--size) + 6px); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_MessageComposer_button { | ||||
|     --size: 26px; | ||||
|     position: relative; | ||||
|     margin-right: 6px; | ||||
|     cursor: pointer; | ||||
|     height: var(--size); | ||||
|     line-height: var(--size); | ||||
|     width: auto; | ||||
|     padding-left: calc(var(--size) + 5px); | ||||
|     padding-left: var(--size); | ||||
|     border-radius: 100%; | ||||
|     margin-right: 6px; | ||||
| 
 | ||||
|     &:last-child { | ||||
|         margin-right: auto; | ||||
|     } | ||||
| 
 | ||||
|     &::before { | ||||
|         content: ''; | ||||
|  |  | |||
|  | @ -0,0 +1,88 @@ | |||
| /* | ||||
| Copyright 2021 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| .mx_JoinRuleSettings_upgradeRequired { | ||||
|     margin-left: 16px; | ||||
|     padding: 4px 16px; | ||||
|     border: 1px solid $accent-color; | ||||
|     border-radius: 8px; | ||||
|     color: $accent-color; | ||||
|     font-size: $font-12px; | ||||
|     line-height: $font-15px; | ||||
| } | ||||
| 
 | ||||
| .mx_JoinRuleSettings_spacesWithAccess { | ||||
|     > h4 { | ||||
|         color: $secondary-content; | ||||
|         font-weight: $font-semi-bold; | ||||
|         font-size: $font-12px; | ||||
|         line-height: $font-15px; | ||||
|         text-transform: uppercase; | ||||
|     } | ||||
| 
 | ||||
|     > span { | ||||
|         font-weight: 500; | ||||
|         font-size: $font-14px; | ||||
|         line-height: 32px; // matches height of avatar for v-align | ||||
|         color: $secondary-content; | ||||
|         display: inline-block; | ||||
| 
 | ||||
|         img.mx_RoomAvatar_isSpaceRoom, | ||||
|         .mx_RoomAvatar_isSpaceRoom img { | ||||
|             border-radius: 8px; | ||||
|         } | ||||
| 
 | ||||
|         .mx_BaseAvatar { | ||||
|             margin-right: 8px; | ||||
|         } | ||||
| 
 | ||||
|         & + span { | ||||
|             margin-left: 16px; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_JoinRuleSettings_radioButton { | ||||
|     padding-top: 16px; | ||||
|     margin-bottom: 8px; | ||||
| 
 | ||||
|     .mx_RadioButton_content { | ||||
|         margin-left: 14px; | ||||
|         font-weight: $font-semi-bold; | ||||
|         font-size: $font-15px; | ||||
|         line-height: $font-24px; | ||||
|         color: $primary-content; | ||||
|         display: block; | ||||
|     } | ||||
| 
 | ||||
|     & + span { | ||||
|         display: inline-block; | ||||
|         margin-left: 34px; | ||||
|         margin-bottom: 16px; | ||||
|         font-size: $font-15px; | ||||
|         line-height: $font-24px; | ||||
|         color: $secondary-content; | ||||
| 
 | ||||
|         & + .mx_RadioButton { | ||||
|             border-top: 1px solid $menu-border-color; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_JoinRuleSettings_linkButton { | ||||
|     padding: 0; | ||||
|     font-size: inherit; | ||||
| } | ||||
|  | @ -19,37 +19,6 @@ limitations under the License. | |||
|         padding: 0; | ||||
|         margin-bottom: 16px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_SecurityRoomSettingsTab_spacesWithAccess { | ||||
|         > h4 { | ||||
|             color: $secondary-content; | ||||
|             font-weight: $font-semi-bold; | ||||
|             font-size: $font-12px; | ||||
|             line-height: $font-15px; | ||||
|             text-transform: uppercase; | ||||
|         } | ||||
| 
 | ||||
|         > span { | ||||
|             font-weight: 500; | ||||
|             font-size: $font-14px; | ||||
|             line-height: 32px; // matches height of avatar for v-align | ||||
|             color: $secondary-content; | ||||
|             display: inline-block; | ||||
| 
 | ||||
|             img.mx_RoomAvatar_isSpaceRoom, | ||||
|             .mx_RoomAvatar_isSpaceRoom img { | ||||
|                 border-radius: 8px; | ||||
|             } | ||||
| 
 | ||||
|             .mx_BaseAvatar { | ||||
|                 margin-right: 8px; | ||||
|             } | ||||
| 
 | ||||
|             & + span { | ||||
|                 margin-left: 16px; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_SecurityRoomSettingsTab_warning { | ||||
|  | @ -68,47 +37,3 @@ limitations under the License. | |||
|     border-bottom: 1px solid $menu-border-color; | ||||
|     margin-bottom: 32px; | ||||
| } | ||||
| 
 | ||||
| .mx_SecurityRoomSettingsTab_upgradeRequired { | ||||
|     margin-left: 16px; | ||||
|     padding: 4px 16px; | ||||
|     border: 1px solid $accent-color; | ||||
|     border-radius: 8px; | ||||
|     color: $accent-color; | ||||
|     font-size: $font-12px; | ||||
|     line-height: $font-15px; | ||||
| } | ||||
| 
 | ||||
| .mx_SecurityRoomSettingsTab_joinRule { | ||||
|     .mx_RadioButton { | ||||
|         padding-top: 16px; | ||||
|         margin-bottom: 8px; | ||||
| 
 | ||||
|         .mx_RadioButton_content { | ||||
|             margin-left: 14px; | ||||
|             font-weight: $font-semi-bold; | ||||
|             font-size: $font-15px; | ||||
|             line-height: $font-24px; | ||||
|             color: $primary-content; | ||||
|             display: block; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     > span { | ||||
|         display: inline-block; | ||||
|         margin-left: 34px; | ||||
|         margin-bottom: 16px; | ||||
|         font-size: $font-15px; | ||||
|         line-height: $font-24px; | ||||
|         color: $secondary-content; | ||||
| 
 | ||||
|         & + .mx_RadioButton { | ||||
|             border-top: 1px solid $menu-border-color; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_AccessibleButton_kind_link { | ||||
|         padding: 0; | ||||
|         font-size: inherit; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| $spacePanelWidth: 71px; | ||||
| $spacePanelWidth: 68px; | ||||
| 
 | ||||
| .mx_SpaceCreateMenu_wrapper { | ||||
|     // background blur everything except SpacePanel | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#737D8C" fill-rule="evenodd" d="M.2192.234A.753.753 0 011.2815.2321l3.7243 3.7003L8.7181.2202A.753.753 0 019.7805.2185a.747.747 0 01.0017 1.0589L5.5396 5.52a.753.753 0 01-1.0624.0018L.221 1.2928A.747.747 0 01.2192.234zM9.7822 13.7663a.7529.7529 0 01-1.0623.0017l-3.7243-3.7003L1.2833 13.78a.753.753 0 01-1.0624.0018.7471.7471 0 01-.0017-1.059l4.2426-4.2426a.753.753 0 011.0624-.0017l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg> | ||||
| After Width: | Height: | Size: 543 B | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#17191C" fill-rule="evenodd" d="M.2192 8.494a.753.753 0 011.0623-.0018l3.7243 3.7003 3.7123-3.7123a.753.753 0 011.0624-.0017.747.747 0 01.0017 1.059L5.5396 13.78a.753.753 0 01-1.0624.0018L.221 9.5528A.747.747 0 01.2192 8.494zM9.7822 5.5063A.753.753 0 018.72 5.508L4.9956 1.8077 1.2833 5.52a.753.753 0 01-1.0624.0018.747.747 0 01-.0017-1.059L4.4618.2202A.753.753 0 015.5242.2185l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg> | ||||
| After Width: | Height: | Size: 538 B | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#737D8C" fill-rule="evenodd" d="M1 2.75A.75.75 0 0 1 1.75 2h.005a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75ZM1 6.75A.75.75 0 0 1 1.75 6h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 1 6.75ZM1 9.75A.75.75 0 0 1 1.75 9h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 1 9.75ZM1 13.75a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1-.75-.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd"/></svg> | ||||
| After Width: | Height: | Size: 1.2 KiB | 
|  | @ -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<void>; | ||||
|     // eslint-disable-next-line no-var, camelcase
 | ||||
|     var mx_rage_initStoragePromise: Promise<void>; | ||||
|     // eslint-disable-next-line no-var, camelcase
 | ||||
|     var mx_rage_store: IndexedDBLogStore; | ||||
| } | ||||
| 
 | ||||
| /* eslint-enable @typescript-eslint/naming-convention */ | ||||
|  |  | |||
|  | @ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> { | |||
|     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(); | ||||
|  |  | |||
|  | @ -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 <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
 | ||||
| // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
 | ||||
| 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; | ||||
|  |  | |||
|  | @ -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"; | ||||
|  | @ -54,11 +53,12 @@ import { throttle } from 'lodash'; | |||
| import SpaceStore from "../../stores/SpaceStore"; | ||||
| import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; | ||||
| import { E2EStatus } from '../../utils/ShieldUtils'; | ||||
| import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; | ||||
| 
 | ||||
| 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; | ||||
|  | @ -99,10 +99,10 @@ export default class RightPanel extends React.Component<IProps, IState> { | |||
| 
 | ||||
|     // 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
 | ||||
|  | @ -143,14 +143,14 @@ export default class RightPanel extends React.Component<IProps, IState> { | |||
|         return rps.roomPanelPhase; | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|     public componentDidMount(): void { | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|         const cli = this.context; | ||||
|         cli.on("RoomState.members", this.onRoomStateMember); | ||||
|         this.initGroupStore(this.props.groupId); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|     public componentWillUnmount(): void { | ||||
|         dis.unregister(this.dispatcherRef); | ||||
|         if (this.context) { | ||||
|             this.context.removeListener("RoomState.members", this.onRoomStateMember); | ||||
|  | @ -159,7 +159,7 @@ export default class RightPanel extends React.Component<IProps, IState> { | |||
|     } | ||||
| 
 | ||||
|     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
 | ||||
|     UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
 | ||||
|     public UNSAFE_componentWillReceiveProps(newProps: IProps): void { // eslint-disable-line
 | ||||
|         if (newProps.groupId !== this.props.groupId) { | ||||
|             this.unregisterGroupStore(); | ||||
|             this.initGroupStore(newProps.groupId); | ||||
|  | @ -196,6 +196,15 @@ export default class RightPanel extends React.Component<IProps, IState> { | |||
|     }; | ||||
| 
 | ||||
|     private onAction = (payload: ActionPayload) => { | ||||
|         const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId; | ||||
|         const isViewingThread = this.state.phase === RightPanelPhases.ThreadView; | ||||
|         if (isChangingRoom && isViewingThread) { | ||||
|             dis.dispatch<SetRightPanelPhasePayload>({ | ||||
|                 action: Action.SetRightPanelPhase, | ||||
|                 phase: RightPanelPhases.ThreadPanel, | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if (payload.action === Action.AfterRightPanelPhaseChange) { | ||||
|             this.setState({ | ||||
|                 phase: payload.phase, | ||||
|  | @ -215,7 +224,7 @@ export default class RightPanel extends React.Component<IProps, IState> { | |||
|         // XXX: There are three different ways of 'closing' this panel depending on what state
 | ||||
|         // 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
 | ||||
|  |  | |||
|  | @ -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<IProps, IState> { | |||
|         }); | ||||
| 
 | ||||
|         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<IProps, IState> { | |||
|         } | ||||
| 
 | ||||
|         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<IProps, IState> { | |||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private onResize = () => { | ||||
|         // It seems flexbox doesn't give us a way to constrain the auxPanel height to have
 | ||||
|         // a minimum of the height of the video element, whilst also capping it from pushing out the page
 | ||||
|         // so we have to do it via JS instead.  In this implementation we cap the height by putting
 | ||||
|         // a maxHeight on the underlying remote video tag.
 | ||||
| 
 | ||||
|         // header + footer + status + give us at least 120px of scrollback at all times.
 | ||||
|         let auxPanelMaxHeight = UIStore.instance.windowHeight - | ||||
|                 (54 + // height of RoomHeader
 | ||||
|                  36 + // height of the status area
 | ||||
|                  51 + // minimum height of the message composer
 | ||||
|                  120); // amount of desired scrollback
 | ||||
| 
 | ||||
|         // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
 | ||||
|         // but it's better than the video going missing entirely
 | ||||
|         if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; | ||||
| 
 | ||||
|         if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) { | ||||
|             this.setState({ auxPanelMaxHeight }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private onStatusBarVisible = () => { | ||||
|         if (this.unmounted || this.state.statusBarVisible) return; | ||||
|         this.setState({ statusBarVisible: true }); | ||||
|  | @ -1941,11 +1910,8 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
|         const auxPanel = ( | ||||
|             <AuxPanel | ||||
|                 room={this.state.room} | ||||
|                 fullHeight={false} | ||||
|                 userId={this.context.credentials.userId} | ||||
|                 maxHeight={this.state.auxPanelMaxHeight} | ||||
|                 showApps={this.state.showApps} | ||||
|                 onResize={this.onResize} | ||||
|                 resizeNotifier={this.props.resizeNotifier} | ||||
|             > | ||||
|                 { aux } | ||||
|  |  | |||
|  | @ -277,8 +277,15 @@ export default class ScrollPanel extends React.Component<IProps> { | |||
|         // fractional values (both too big and too small)
 | ||||
|         // for scrollTop happen on certain browsers/platforms
 | ||||
|         // when scrolled all the way down. E.g. Chrome 72 on debian.
 | ||||
|         // so check difference <= 1;
 | ||||
|         return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1; | ||||
|         //
 | ||||
|         // We therefore leave a bit of wiggle-room and assume we're at the
 | ||||
|         // bottom if the unscrolled area is less than one pixel high.
 | ||||
|         //
 | ||||
|         // non-standard DPI settings also seem to have effect here and can
 | ||||
|         // actually lead to scrollTop+clientHeight being *larger* than
 | ||||
|         // scrollHeight. (observed in element-desktop on Ubuntu 20.04)
 | ||||
|         //
 | ||||
|         return sn.scrollHeight - (sn.scrollTop + sn.clientHeight) <= 1; | ||||
|     }; | ||||
| 
 | ||||
|     // returns the vertical height in the given direction that can be removed from
 | ||||
|  |  | |||
|  | @ -133,15 +133,22 @@ export default class ThreadView extends React.Component<IProps, IState> { | |||
|                 { this.state.thread && ( | ||||
|                     <TimelinePanel | ||||
|                         ref={this.timelinePanelRef} | ||||
|                         manageReadReceipts={false} | ||||
|                         manageReadMarkers={false} | ||||
|                         showReadReceipts={false} // No RR support in thread's MVP
 | ||||
|                         manageReadReceipts={false} // No RR support in thread's MVP
 | ||||
|                         manageReadMarkers={false} // No RM support in thread's MVP
 | ||||
|                         sendReadReceiptOnLoad={false} // No RR support in thread's MVP
 | ||||
|                         timelineSet={this.state?.thread?.timelineSet} | ||||
|                         showUrlPreview={false} | ||||
|                         tileShape={TileShape.Notif} | ||||
|                         showUrlPreview={true} | ||||
|                         tileShape={TileShape.Thread} | ||||
|                         empty={<div>empty</div>} | ||||
|                         alwaysShowTimestamps={true} | ||||
|                         layout={Layout.Group} | ||||
|                         hideThreadedMessages={false} | ||||
|                         hidden={false} | ||||
|                         showReactions={true} | ||||
|                         className="mx_RoomView_messagePanel mx_GroupLayout" | ||||
|                         permalinkCreator={this.props.permalinkCreator} | ||||
|                         membersLoaded={true} | ||||
|                     /> | ||||
|                 ) } | ||||
|                 <MessageComposer | ||||
|  |  | |||
|  | @ -86,8 +86,8 @@ export default class UserView extends React.Component<IProps, IState> { | |||
|     public render(): JSX.Element { | ||||
|         if (this.state.loading) { | ||||
|             return <Spinner />; | ||||
|         } else if (this.state.member?.user) { | ||||
|             const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />; | ||||
|         } else if (this.state.member) { | ||||
|             const panel = <RightPanel member={this.state.member} resizeNotifier={this.props.resizeNotifier} />; | ||||
|             return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}> | ||||
|                 <HomePage /> | ||||
|             </MainSplit>); | ||||
|  |  | |||
|  | @ -34,10 +34,10 @@ import ForwardDialog from "../dialogs/ForwardDialog"; | |||
| import { Action } from "../../../dispatcher/actions"; | ||||
| import ReportEventDialog from '../dialogs/ReportEventDialog'; | ||||
| import ViewSource from '../../structures/ViewSource'; | ||||
| import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog'; | ||||
| import ErrorDialog from '../dialogs/ErrorDialog'; | ||||
| import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog'; | ||||
| 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 +52,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 */ | ||||
|  | @ -138,34 +139,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState> | |||
|     }; | ||||
| 
 | ||||
|     private onRedactClick = (): void => { | ||||
|         Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { | ||||
|             onFinished: async (proceed: boolean, reason?: string) => { | ||||
|                 if (!proceed) return; | ||||
| 
 | ||||
|                 const cli = MatrixClientPeg.get(); | ||||
|                 try { | ||||
|                     this.props.onCloseDialog?.(); | ||||
|                     await cli.redactEvent( | ||||
|                         this.props.mxEvent.getRoomId(), | ||||
|                         this.props.mxEvent.getId(), | ||||
|                         undefined, | ||||
|                         reason ? { reason } : {}, | ||||
|                     ); | ||||
|                 } catch (e) { | ||||
|                     const code = e.errcode || e.statusCode; | ||||
|                     // only show the dialog if failing for something other than a network error
 | ||||
|                     // (e.g. no errcode or statusCode) as in that case the redactions end up in the
 | ||||
|                     // detached queue and we show the room status bar to allow retry
 | ||||
|                     if (typeof code !== "undefined") { | ||||
|                         // display error message stating you couldn't delete this.
 | ||||
|                         Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { | ||||
|                             title: _t('Error'), | ||||
|                             description: _t('You cannot delete this message. (%(code)s)', { code }), | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|         }, 'mx_Dialog_confirmredact'); | ||||
|         const { mxEvent, onCloseDialog } = this.props; | ||||
|         createRedactEventDialog({ | ||||
|             mxEvent, | ||||
|             onCloseDialog, | ||||
|         }); | ||||
|         this.closeMenu(); | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,9 +14,13 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; | ||||
| import React from 'react'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { MatrixClientPeg } from '../../../MatrixClientPeg'; | ||||
| import Modal from '../../../Modal'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import ErrorDialog from './ErrorDialog'; | ||||
| import TextInputDialog from "./TextInputDialog"; | ||||
| 
 | ||||
| interface IProps { | ||||
|  | @ -42,3 +46,40 @@ export default class ConfirmRedactDialog extends React.Component<IProps> { | |||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function createRedactEventDialog({ | ||||
|     mxEvent, | ||||
|     onCloseDialog = () => {}, | ||||
| }: { | ||||
|     mxEvent: MatrixEvent; | ||||
|     onCloseDialog?: () => void; | ||||
| }) { | ||||
|     Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { | ||||
|         onFinished: async (proceed: boolean, reason?: string) => { | ||||
|             if (!proceed) return; | ||||
| 
 | ||||
|             const cli = MatrixClientPeg.get(); | ||||
|             try { | ||||
|                 onCloseDialog?.(); | ||||
|                 await cli.redactEvent( | ||||
|                     mxEvent.getRoomId(), | ||||
|                     mxEvent.getId(), | ||||
|                     undefined, | ||||
|                     reason ? { reason } : {}, | ||||
|                 ); | ||||
|             } catch (e) { | ||||
|                 const code = e.errcode || e.statusCode; | ||||
|                 // only show the dialog if failing for something other than a network error
 | ||||
|                 // (e.g. no errcode or statusCode) as in that case the redactions end up in the
 | ||||
|                 // detached queue and we show the room status bar to allow retry
 | ||||
|                 if (typeof code !== "undefined") { | ||||
|                     // display error message stating you couldn't delete this.
 | ||||
|                     Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { | ||||
|                         title: _t('Error'), | ||||
|                         description: _t('You cannot delete this message. (%(code)s)', { code }), | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|     }, 'mx_Dialog_confirmredact'); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -163,7 +163,7 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         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<IProps, IState> { | |||
|         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<IProps, IState> { | |||
| 
 | ||||
|         // 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 }); | ||||
|     } | ||||
|  |  | |||
|  | @ -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<void> => { | ||||
|         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(), | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -46,6 +54,8 @@ interface IProps { | |||
|     // Whether to always show a timestamp
 | ||||
|     alwaysShowTimestamps?: boolean; | ||||
|     forExport?: boolean; | ||||
|     isQuoteExpanded?: boolean; | ||||
|     setQuoteExpanded: (isExpanded: boolean) => void; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|  | @ -67,6 +77,7 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|     static contextType = MatrixClientContext; | ||||
|     private unmounted = false; | ||||
|     private room: Room; | ||||
|     private blockquoteRef = React.createRef<HTMLElement>(); | ||||
| 
 | ||||
|     constructor(props, context) { | ||||
|         super(props, context); | ||||
|  | @ -81,7 +92,7 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|         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
 | ||||
|  | @ -138,7 +149,7 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|     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(); | ||||
|  | @ -238,39 +249,38 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|         return replyMixin; | ||||
|     } | ||||
| 
 | ||||
|     public static makeThread( | ||||
|         parentEv: MatrixEvent, | ||||
|         onHeightChanged: () => void, | ||||
|         permalinkCreator: RoomPermalinkCreator, | ||||
|         ref: React.RefObject<ReplyThread>, | ||||
|         layout: Layout, | ||||
|         forExport: boolean, | ||||
|         alwaysShowTimestamps: boolean, | ||||
|     ): JSX.Element { | ||||
|         if (!ReplyThread.getParentEventId(parentEv)) return null; | ||||
|         return <ReplyThread | ||||
|             parentEv={parentEv} | ||||
|             forExport={forExport} | ||||
|             onHeightChanged={onHeightChanged} | ||||
|             ref={ref} | ||||
|             permalinkCreator={permalinkCreator} | ||||
|             layout={layout} | ||||
|             alwaysShowTimestamps={alwaysShowTimestamps} | ||||
|         />; | ||||
|     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<void> { | ||||
|         const { parentEv } = this.props; | ||||
|         // at time of making this component we checked that props.parentEv has a parentEventId
 | ||||
|  | @ -324,7 +334,7 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|         this.initialize(); | ||||
|     }; | ||||
| 
 | ||||
|     private onQuoteClick = async (): Promise<void> => { | ||||
|     private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => { | ||||
|         const events = [this.state.loadedEv, ...this.state.events]; | ||||
| 
 | ||||
|         let loadedEv = null; | ||||
|  | @ -387,14 +397,26 @@ export default class ReplyThread extends React.Component<IProps, IState> { | |||
|             header = <Spinner w={16} h={16} />; | ||||
|         } | ||||
| 
 | ||||
|         const { isQuoteExpanded } = this.props; | ||||
|         const evTiles = this.state.events.map((ev) => { | ||||
|             return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}> | ||||
|                 <ReplyTile | ||||
|                     mxEvent={ev} | ||||
|                     onHeightChanged={this.props.onHeightChanged} | ||||
|                     permalinkCreator={this.props.permalinkCreator} | ||||
|                 /> | ||||
|             </blockquote>; | ||||
|             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 ( | ||||
|                 <blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}> | ||||
|                     <ReplyTile | ||||
|                         mxEvent={ev} | ||||
|                         onHeightChanged={this.props.onHeightChanged} | ||||
|                         permalinkCreator={this.props.permalinkCreator} | ||||
|                         toggleExpandedQuote={() => this.props.setQuoteExpanded(!this.props.isQuoteExpanded)} | ||||
|                     /> | ||||
|                 </blockquote> | ||||
|             ); | ||||
|         }); | ||||
| 
 | ||||
|         return <div className="mx_ReplyThread_wrapper"> | ||||
|  |  | |||
|  | @ -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<IProps, IState> { | ||||
|     private content = createRef<HTMLDivElement>(); | ||||
|     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<void> => { | ||||
|         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 = ( | ||||
|                 <AccessibleButton onClick={this._onRedactClick}> | ||||
|                 <AccessibleButton onClick={this.onRedactClick}> | ||||
|                     { _t("Remove") } | ||||
|                 </AccessibleButton> | ||||
|             ); | ||||
|         } | ||||
|         const viewSourceButton = ( | ||||
|             <AccessibleButton onClick={this._onViewSourceClick}> | ||||
|             <AccessibleButton onClick={this.onViewSourceClick}> | ||||
|                 { _t("View Source") } | ||||
|             </AccessibleButton> | ||||
|         ); | ||||
|  | @ -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 = ( | ||||
|                     <div className="mx_EventTile_content" ref={this._content}>*  | ||||
|                     <div className="mx_EventTile_content" ref={this.content}>*  | ||||
|                         <span className="mx_MEmoteBody_sender">{ name }</span> | ||||
|                          { contentElements } | ||||
|                     </div> | ||||
|                 ); | ||||
|             } else { | ||||
|                 contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>; | ||||
|                 contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent { | |||
|                     <div className="mx_EventTile_line"> | ||||
|                         <span className="mx_MessageTimestamp">{ timestamp }</span> | ||||
|                         { contentContainer } | ||||
|                         { this._renderActionBar() } | ||||
|                         { this.renderActionBar() } | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </li> | ||||
|  | @ -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<IProps> { | ||||
|     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, | ||||
| }; | ||||
|  | @ -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<IOptionsButtonProps> = | |||
| 
 | ||||
|         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<IOptionsButtonProps> = | |||
| 
 | ||||
| interface IReactButtonProps { | ||||
|     mxEvent: MatrixEvent; | ||||
|     reactions: any; // TODO: types
 | ||||
|     reactions: Relations; | ||||
|     onFocusChange: (menuDisplayed: boolean) => void; | ||||
| } | ||||
| 
 | ||||
|  | @ -125,20 +128,32 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC | |||
|     </React.Fragment>; | ||||
| }; | ||||
| 
 | ||||
| export enum ActionBarRenderingContext { | ||||
|     Room, | ||||
|     Thread | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|     renderingContext?: ActionBarRenderingContext; | ||||
|     isQuoteExpanded?: boolean; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.messages.MessageActionBar") | ||||
| export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> { | ||||
|     public static contextType = RoomContext; | ||||
| 
 | ||||
|     public static defaultProps = { | ||||
|         renderingContext: ActionBarRenderingContext.Room, | ||||
|     }; | ||||
| 
 | ||||
|     public componentDidMount(): void { | ||||
|         if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { | ||||
|             this.props.mxEvent.on("Event.status", this.onSent); | ||||
|  | @ -283,7 +298,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction | |||
|                 // Like the resend button, the react and reply buttons need to appear before the edit.
 | ||||
|                 // The only catch is we do the reply button first so that we can make sure the react
 | ||||
|                 // button is the very first button without having to do length checks for `splice()`.
 | ||||
|                 if (this.context.canReply) { | ||||
|                 if (this.context.canReply && this.props.renderingContext === ActionBarRenderingContext.Room) { | ||||
|                     toolbarOpts.splice(0, 0, <> | ||||
|                         <RovingAccessibleTooltipButton | ||||
|                             className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" | ||||
|  | @ -324,6 +339,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction | |||
|                 toolbarOpts.push(cancelSendingButton); | ||||
|             } | ||||
| 
 | ||||
|             if (this.props.isQuoteExpanded !== undefined && ReplyThread.hasThreadReply(this.props.mxEvent)) { | ||||
|                 const expandClassName = classNames({ | ||||
|                     'mx_MessageActionBar_maskButton': true, | ||||
|                     'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded, | ||||
|                     'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded, | ||||
|                 }); | ||||
|                 toolbarOpts.push(<RovingAccessibleTooltipButton | ||||
|                     className={expandClassName} | ||||
|                     title={this.props.isQuoteExpanded ? _t("Collapse quotes │ ⇧+click") : _t("Expand quotes │ ⇧+click")} | ||||
|                     onClick={this.props.toggleThreadExpanded} | ||||
|                     key="expand" | ||||
|                 />); | ||||
|             } | ||||
| 
 | ||||
|             // The menu button should be last, so dump it there.
 | ||||
|             toolbarOpts.push(<OptionsButton | ||||
|                 mxEvent={this.props.mxEvent} | ||||
|  |  | |||
|  | @ -15,22 +15,18 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     mxEvent: MatrixEvent; | ||||
|     onMessageAllowed: () => 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<IProps> { | ||||
|     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 ( | ||||
|             <div className='mx_MjolnirBody'><i>{ _t( | ||||
|                 "You have ignored this user, so their message is hidden. <a>Show anyways.</a>", | ||||
|                 {}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> }, | ||||
|                 {}, { a: (sub) => <a href="#" onClick={this.onAllowClick}>{ sub }</a> }, | ||||
|             ) }</i></div> | ||||
|         ); | ||||
|     } | ||||
|  | @ -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<any, IBodyProps>(({ mxEvent, forExport }, ref) => { | ||||
| interface IProps { | ||||
|     mxEvent: MatrixEvent; | ||||
| } | ||||
| 
 | ||||
| const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => { | ||||
|     const cli: MatrixClient = useContext(MatrixClientContext); | ||||
|     let text = _t("Message deleted"); | ||||
|     const unsigned = mxEvent.getUnsigned(); | ||||
|  |  | |||
|  | @ -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<IProps> { | ||||
|     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 ( | ||||
|  | @ -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<IProps> { | ||||
|     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 <div />; // 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 = ( | ||||
|             <a href={predecessorPermalink} onClick={this._onLinkClicked}> | ||||
|             <a href={predecessorPermalink} onClick={this.onLinkClicked}> | ||||
|                 { _t("Click here to see older messages.") } | ||||
|             </a> | ||||
|         ); | ||||
|  | @ -138,6 +138,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { | |||
|         // If it's less than 30% we don't add the expansion button.
 | ||||
|         // 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"); | ||||
|  |  | |||
|  | @ -317,7 +317,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({ | |||
|     }); | ||||
| 
 | ||||
|     return <Resizable | ||||
|         size={{ height: Math.min(height, maxHeight), width: null }} | ||||
|         size={{ height: Math.min(height, maxHeight), width: undefined }} | ||||
|         minHeight={minHeight} | ||||
|         maxHeight={maxHeight} | ||||
|         onResizeStart={() => { | ||||
|  |  | |||
|  | @ -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<IProps, IState> { | |||
|         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<IProps, IState> { | |||
|         const callView = ( | ||||
|             <CallViewForRoom | ||||
|                 roomId={this.props.room.roomId} | ||||
|                 maxVideoHeight={this.props.maxHeight} | ||||
|                 resizeNotifier={this.props.resizeNotifier} | ||||
|             /> | ||||
|         ); | ||||
|  | @ -148,7 +129,6 @@ export default class AuxPanel extends React.Component<IProps, IState> { | |||
|             appsDrawer = <AppsDrawer | ||||
|                 room={this.props.room} | ||||
|                 userId={this.props.userId} | ||||
|                 maxHeight={this.props.maxHeight} | ||||
|                 showApps={this.props.showApps} | ||||
|                 resizeNotifier={this.props.resizeNotifier} | ||||
|             />; | ||||
|  | @ -204,21 +184,12 @@ export default class AuxPanel extends React.Component<IProps, IState> { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const classes = classNames({ | ||||
|             "mx_RoomView_auxPanel": true, | ||||
|             "mx_RoomView_auxPanel_fullHeight": this.props.fullHeight, | ||||
|         }); | ||||
|         const style: React.CSSProperties = {}; | ||||
|         if (!this.props.fullHeight) { | ||||
|             style.maxHeight = this.props.maxHeight; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <AutoHideScrollbar className={classes} style={style}> | ||||
|             <AutoHideScrollbar className="mx_RoomView_auxPanel"> | ||||
|                 { stateViews } | ||||
|                 { this.props.children } | ||||
|                 { appsDrawer } | ||||
|                 { callView } | ||||
|                 { this.props.children } | ||||
|             </AutoHideScrollbar> | ||||
|         ); | ||||
|     } | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog"; | |||
| import QuestionDialog from "../dialogs/QuestionDialog"; | ||||
| import { ActionPayload } from "../../../dispatcher/payloads"; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog'; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| 
 | ||||
| import { logger } from "matrix-js-sdk/src/logger"; | ||||
|  | @ -331,6 +332,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState> | |||
| 
 | ||||
|         let shouldSend = true; | ||||
| 
 | ||||
|         if (newContent?.body === '') { | ||||
|             this.cancelPreviousPendingEdit(); | ||||
|             createRedactEventDialog({ | ||||
|                 mxEvent: editedEvent, | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // If content is modified then send an updated event into the room
 | ||||
|         if (this.isContentModified(newContent)) { | ||||
|             const roomId = editedEvent.getRoomId(); | ||||
|  |  | |||
|  | @ -53,11 +53,12 @@ import SenderProfile from '../messages/SenderProfile'; | |||
| import MessageTimestamp from '../messages/MessageTimestamp'; | ||||
| import TooltipButton from '../elements/TooltipButton'; | ||||
| import ReadReceiptMarker from "./ReadReceiptMarker"; | ||||
| import MessageActionBar from "../messages/MessageActionBar"; | ||||
| import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar"; | ||||
| 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; | ||||
|         } | ||||
|     } | ||||
|  | @ -192,6 +192,7 @@ export enum TileShape { | |||
|     Notif = "notif", | ||||
|     FileGrid = "file_grid", | ||||
|     Pinned = "pinned", | ||||
|     Thread = "thread", | ||||
| } | ||||
| 
 | ||||
| interface IProps { | ||||
|  | @ -324,7 +325,7 @@ interface IState { | |||
|     reactions: Relations; | ||||
| 
 | ||||
|     hover: boolean; | ||||
| 
 | ||||
|     isQuoteExpanded?: boolean; | ||||
|     thread?: Thread; | ||||
| } | ||||
| 
 | ||||
|  | @ -332,7 +333,8 @@ interface IState { | |||
| export default class EventTile extends React.Component<IProps, IState> { | ||||
|     private suppressReadReceiptAnimation: boolean; | ||||
|     private isListeningForReceipts: boolean; | ||||
|     private tile = React.createRef(); | ||||
|     // TODO: Types
 | ||||
|     private tile = React.createRef<unknown>(); | ||||
|     private replyThread = React.createRef<ReplyThread>(); | ||||
| 
 | ||||
|     public readonly ref = createRef<HTMLElement>(); | ||||
|  | @ -894,8 +896,8 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|             actionBarFocused: focused, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     getTile = () => this.tile.current; | ||||
|     // TODO: Types
 | ||||
|     getTile: () => any | null = () => this.tile.current; | ||||
| 
 | ||||
|     getReplyThread = () => this.replyThread.current; | ||||
| 
 | ||||
|  | @ -920,6 +922,11 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private setQuoteExpanded = (expanded: boolean) => { | ||||
|         this.setState({ | ||||
|             isQuoteExpanded: expanded, | ||||
|         }); | ||||
|     }; | ||||
|     render() { | ||||
|         const msgtype = this.props.mxEvent.getContent().msgtype; | ||||
|         const eventType = this.props.mxEvent.getType() as EventType; | ||||
|  | @ -929,6 +936,7 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|             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
 | ||||
|  | @ -941,6 +949,7 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|                 </div> | ||||
|             </div>; | ||||
|         } | ||||
| 
 | ||||
|         const EventTileType = sdk.getComponent(tileHandler); | ||||
| 
 | ||||
|         const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); | ||||
|  | @ -1054,6 +1063,9 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|         } | ||||
| 
 | ||||
|         const showMessageActionBar = !isEditing && !this.props.forExport; | ||||
|         const renderingContext = this.props.tileShape === TileShape.Thread | ||||
|             ? ActionBarRenderingContext.Thread | ||||
|             : ActionBarRenderingContext.Room; | ||||
|         const actionBar = showMessageActionBar ? <MessageActionBar | ||||
|             mxEvent={this.props.mxEvent} | ||||
|             reactions={this.state.reactions} | ||||
|  | @ -1061,6 +1073,9 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|             getTile={this.getTile} | ||||
|             getReplyThread={this.getReplyThread} | ||||
|             onFocusChange={this.onActionBarFocusChange} | ||||
|             renderingContext={renderingContext} | ||||
|             isQuoteExpanded={isQuoteExpanded} | ||||
|             toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)} | ||||
|         /> : undefined; | ||||
| 
 | ||||
|         const showTimestamp = this.props.mxEvent.getTs() | ||||
|  | @ -1167,6 +1182,40 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|                     </div>, | ||||
|                 ]); | ||||
|             } | ||||
|             case TileShape.Thread: { | ||||
|                 const room = this.context.getRoom(this.props.mxEvent.getRoomId()); | ||||
|                 return React.createElement(this.props.as || "li", { | ||||
|                     "className": classes, | ||||
|                     "aria-live": ariaLive, | ||||
|                     "aria-atomic": true, | ||||
|                     "data-scroll-tokens": scrollToken, | ||||
|                 }, [ | ||||
|                     <div className="mx_EventTile_roomName" key="mx_EventTile_roomName"> | ||||
|                         <RoomAvatar room={room} width={28} height={28} /> | ||||
|                         <a href={permalink} onClick={this.onPermalinkClicked}> | ||||
|                             { room ? room.name : '' } | ||||
|                         </a> | ||||
|                     </div>, | ||||
|                     <div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails"> | ||||
|                         { avatar } | ||||
|                         <a href={permalink} onClick={this.onPermalinkClicked}> | ||||
|                             { sender } | ||||
|                             { timestamp } | ||||
|                         </a> | ||||
|                     </div>, | ||||
|                     <div className="mx_EventTile_line" key="mx_EventTile_line"> | ||||
|                         <EventTileType ref={this.tile} | ||||
|                             mxEvent={this.props.mxEvent} | ||||
|                             highlights={this.props.highlights} | ||||
|                             highlightLink={this.props.highlightLink} | ||||
|                             showUrlPreview={this.props.showUrlPreview} | ||||
|                             onHeightChanged={this.props.onHeightChanged} | ||||
|                             tileShape={this.props.tileShape} | ||||
|                         /> | ||||
|                         { actionBar } | ||||
|                     </div>, | ||||
|                 ]); | ||||
|             } | ||||
|             case TileShape.FileGrid: { | ||||
|                 return React.createElement(this.props.as || "li", { | ||||
|                     "className": classes, | ||||
|  | @ -1199,21 +1248,19 @@ export default class EventTile extends React.Component<IProps, IState> { | |||
|             } | ||||
| 
 | ||||
|             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.forExport, | ||||
|                         this.props.alwaysShowTimestamps || this.state.hover, | ||||
|                     ); | ||||
|                 } | ||||
| 
 | ||||
|                 const thread = haveTileForEvent(this.props.mxEvent) && | ||||
|                     ReplyThread.hasThreadReply(this.props.mxEvent) ? ( | ||||
|                         <ReplyThread | ||||
|                             parentEv={this.props.mxEvent} | ||||
|                             onHeightChanged={this.props.onHeightChanged} | ||||
|                             ref={this.replyThread} | ||||
|                             forExport={this.props.forExport} | ||||
|                             permalinkCreator={this.props.permalinkCreator} | ||||
|                             layout={this.props.layout} | ||||
|                             alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover} | ||||
|                             isQuoteExpanded={isQuoteExpanded} | ||||
|                             setQuoteExpanded={this.setQuoteExpanded} | ||||
|                         />) : 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
 | ||||
|  |  | |||
|  | @ -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<IProps> = ({ 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<IProps> = ({ links, mxEvent, onCancelClick, onH | |||
|     </div>; | ||||
| }; | ||||
| 
 | ||||
| const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): | ||||
|         Promise<[string, IPreviewUrlResponse][]> => { | ||||
|     return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => { | ||||
|         try { | ||||
|             const preview = await cli.getUrlPreview(link, ts); | ||||
|             if (preview && Object.keys(preview).length > 0) { | ||||
|                 return [link, preview]; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error("Failed to get URL preview: " + error); | ||||
|         } | ||||
|     })).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>; | ||||
| }; | ||||
| 
 | ||||
| export default LinkPreviewGroup; | ||||
|  |  | |||
|  | @ -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<IProps> { | |||
|             // 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(), | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -164,6 +164,20 @@ export default class SendMessageComposer extends React.Component<IProps> { | |||
|         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<IProps> { | |||
|     } | ||||
| 
 | ||||
|     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<IProps> { | |||
|     } | ||||
| 
 | ||||
|     private restoreStoredEditorState(partCreator: PartCreator): Part[] { | ||||
|         if (this.props.replyInThread && !this.props.replyToEvent) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const json = localStorage.getItem(this.editorStateKey); | ||||
|         if (json) { | ||||
|             try { | ||||
|  |  | |||
|  | @ -97,7 +97,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet | |||
|     if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) { | ||||
|         let upgradeRequiredPill; | ||||
|         if (preferredRestrictionVersion) { | ||||
|             upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired"> | ||||
|             upgradeRequiredPill = <span className="mx_JoinRuleSettings_upgradeRequired"> | ||||
|                 { _t("Upgrade required") } | ||||
|             </span>; | ||||
|         } | ||||
|  | @ -159,13 +159,14 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet | |||
|                             disabled={disabled} | ||||
|                             onClick={onEditRestrictedClick} | ||||
|                             kind="link" | ||||
|                             className="mx_JoinRuleSettings_linkButton" | ||||
|                         > | ||||
|                             { sub } | ||||
|                         </AccessibleButton>, | ||||
|                     }) } | ||||
|                 </span> | ||||
| 
 | ||||
|                 <div className="mx_SecurityRoomSettingsTab_spacesWithAccess"> | ||||
|                 <div className="mx_JoinRuleSettings_spacesWithAccess"> | ||||
|                     <h4>{ _t("Spaces with access") }</h4> | ||||
|                     { shownSpaces.map(room => { | ||||
|                         return <span key={room.roomId}> | ||||
|  | @ -286,6 +287,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet | |||
|             onChange={onChange} | ||||
|             definitions={definitions} | ||||
|             disabled={disabled} | ||||
|             className="mx_JoinRuleSettings_radioButton" | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
|  |  | |||
|  | @ -480,7 +480,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> { | |||
|             return masterSwitch; | ||||
|         } | ||||
| 
 | ||||
|         const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email) | ||||
|         const emailSwitches = (this.state.threepids || []).filter(t => t.medium === ThreepidMedium.Email) | ||||
|             .map(e => <LabelledToggleSwitch | ||||
|                 key={e.address} | ||||
|                 value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)} | ||||
|  |  | |||
|  | @ -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<IProps, IState> { | |||
| 
 | ||||
|     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 ( | ||||
|             <div className="mx_CallViewForRoom"> | ||||
|                 <Resizable | ||||
|                     minHeight={380} | ||||
|                     maxHeight={maxHeight} | ||||
|                     maxHeight="80vh" | ||||
|                     enable={{ | ||||
|                         top: false, | ||||
|                         right: false, | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t | |||
|     const firstPart = model.parts[0]; | ||||
|     // part type will be "plain" while editing,
 | ||||
|     // and "command" while composing a message.
 | ||||
|     let text = firstPart && firstPart.text; | ||||
|     let text = firstPart?.text || ''; | ||||
|     if (!caseSensitive) { | ||||
|         prefix = prefix.toLowerCase(); | ||||
|         text = text.toLowerCase(); | ||||
|  |  | |||
|  | @ -792,16 +792,6 @@ | |||
|     "The person who invited you already left the room.": "The person who invited you already left the room.", | ||||
|     "The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.", | ||||
|     "Failed to join room": "Failed to join room", | ||||
|     "New in the Spaces beta": "New in the Spaces beta", | ||||
|     "Help people in spaces to find and join private rooms": "Help people in spaces to find and join private rooms", | ||||
|     "Learn more": "Learn more", | ||||
|     "Help space members find private rooms": "Help space members find private rooms", | ||||
|     "To help space members find and join a private room, go to that room's Security & Privacy settings.": "To help space members find and join a private room, go to that room's Security & Privacy settings.", | ||||
|     "General": "General", | ||||
|     "Security & Privacy": "Security & Privacy", | ||||
|     "Roles & Permissions": "Roles & Permissions", | ||||
|     "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.", | ||||
|     "Skip": "Skip", | ||||
|     "You joined the call": "You joined the call", | ||||
|     "%(senderName)s joined the call": "%(senderName)s joined the call", | ||||
|     "Call in progress": "Call in progress", | ||||
|  | @ -1075,6 +1065,7 @@ | |||
|     "Invite people": "Invite people", | ||||
|     "Invite with email or username": "Invite with email or username", | ||||
|     "Failed to save space settings.": "Failed to save space settings.", | ||||
|     "General": "General", | ||||
|     "Edit settings relating to your space.": "Edit settings relating to your space.", | ||||
|     "Saving...": "Saving...", | ||||
|     "Save Changes": "Save Changes", | ||||
|  | @ -1484,6 +1475,7 @@ | |||
|     "Muted Users": "Muted Users", | ||||
|     "Banned users": "Banned users", | ||||
|     "Send %(eventType)s events": "Send %(eventType)s events", | ||||
|     "Roles & Permissions": "Roles & Permissions", | ||||
|     "Permissions": "Permissions", | ||||
|     "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", | ||||
|     "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", | ||||
|  | @ -1506,6 +1498,7 @@ | |||
|     "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.", | ||||
|     "People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.", | ||||
|     "Who can read history?": "Who can read history?", | ||||
|     "Security & Privacy": "Security & Privacy", | ||||
|     "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", | ||||
|     "Encrypted": "Encrypted", | ||||
|     "Access": "Access", | ||||
|  | @ -1964,6 +1957,8 @@ | |||
|     "Edit": "Edit", | ||||
|     "Reply": "Reply", | ||||
|     "Thread": "Thread", | ||||
|     "Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click", | ||||
|     "Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click", | ||||
|     "Message Actions": "Message Actions", | ||||
|     "Download %(text)s": "Download %(text)s", | ||||
|     "Error decrypting attachment": "Error decrypting attachment", | ||||
|  | @ -2228,6 +2223,7 @@ | |||
|     "People you know on %(brand)s": "People you know on %(brand)s", | ||||
|     "Hide": "Hide", | ||||
|     "Show": "Show", | ||||
|     "Skip": "Skip", | ||||
|     "Send %(count)s invites|other": "Send %(count)s invites", | ||||
|     "Send %(count)s invites|one": "Send %(count)s invite", | ||||
|     "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", | ||||
|  | @ -2566,6 +2562,7 @@ | |||
|     "We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.", | ||||
|     "Other homeserver": "Other homeserver", | ||||
|     "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.", | ||||
|     "Learn more": "Learn more", | ||||
|     "About homeservers": "About homeservers", | ||||
|     "Reset event store?": "Reset event store?", | ||||
|     "You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store", | ||||
|  |  | |||
|  | @ -46,12 +46,10 @@ const FLUSH_RATE_MS = 30 * 1000; | |||
| const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
 | ||||
| 
 | ||||
| // A class which monkey-patches the global console and stores log lines.
 | ||||
| class ConsoleLogger { | ||||
|     constructor() { | ||||
|         this.logs = ""; | ||||
|     } | ||||
| export class ConsoleLogger { | ||||
|     private logs = ""; | ||||
| 
 | ||||
|     monkeyPatch(consoleObj) { | ||||
|     public monkeyPatch(consoleObj: Console): void { | ||||
|         // Monkey-patch console logging
 | ||||
|         const consoleFunctionsToLevels = { | ||||
|             log: "I", | ||||
|  | @ -69,14 +67,14 @@ class ConsoleLogger { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     log(level, ...args) { | ||||
|     private log(level: string, ...args: (Error | DOMException | object | string)[]): void { | ||||
|         // We don't know what locale the user may be running so use ISO strings
 | ||||
|         const ts = new Date().toISOString(); | ||||
| 
 | ||||
|         // Convert objects and errors to helpful things
 | ||||
|         args = args.map((arg) => { | ||||
|             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<void> { | ||||
|         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<void> { | ||||
|         // 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<void>((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<string> { | ||||
|             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<string[]> { | ||||
|             // 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<void> { | ||||
|             return new Promise<void>((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<T[]>} Resolves to an array of whatever you returned from | ||||
|  * resultMapper. | ||||
|  */ | ||||
| function selectQuery(store, keyRange, resultMapper) { | ||||
| function selectQuery<T>( | ||||
|     store: IDBIndex, keyRange: IDBKeyRange, resultMapper: (cursor: IDBCursorWithValue) => T, | ||||
| ): Promise<T[]> { | ||||
|     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<void> { | ||||
|     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<void> { | ||||
|     if (global.mx_rage_initStoragePromise) { | ||||
|         return global.mx_rage_initStoragePromise; | ||||
|     } | ||||
|  | @ -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; | ||||
|  | @ -0,0 +1,112 @@ | |||
| /* | ||||
| Copyright 2018 New Vector Ltd | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import EventEmitter from 'events'; | ||||
| import { MatrixEvent } from "matrix-js-sdk"; | ||||
| 
 | ||||
| import { MatrixClientPeg } from '../MatrixClientPeg'; | ||||
| import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore"; | ||||
| 
 | ||||
| export enum ActiveWidgetStoreEvent { | ||||
|     Update = "update", | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Stores information about the widgets active in the app right now: | ||||
|  *  * What widget is set to remain always-on-screen, if any | ||||
|  *    Only one widget may be 'always on screen' at any one time. | ||||
|  *  * Negotiated capabilities for active apps | ||||
|  */ | ||||
| export default class ActiveWidgetStore extends EventEmitter { | ||||
|     private static internalInstance: ActiveWidgetStore; | ||||
|     private persistentWidgetId: string; | ||||
|     // What room ID each widget is associated with (if it's a room widget)
 | ||||
|     private roomIdByWidgetId = new Map<string, string>(); | ||||
| 
 | ||||
|     public static get instance(): ActiveWidgetStore { | ||||
|         if (!ActiveWidgetStore.internalInstance) { | ||||
|             ActiveWidgetStore.internalInstance = new ActiveWidgetStore(); | ||||
|         } | ||||
|         return ActiveWidgetStore.internalInstance; | ||||
|     } | ||||
| 
 | ||||
|     public start(): void { | ||||
|         MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); | ||||
|     } | ||||
| 
 | ||||
|     public stop(): void { | ||||
|         if (MatrixClientPeg.get()) { | ||||
|             MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); | ||||
|         } | ||||
|         this.roomIdByWidgetId.clear(); | ||||
|     } | ||||
| 
 | ||||
|     private onRoomStateEvents = (ev: MatrixEvent): void => { | ||||
|         // XXX: This listens for state events in order to remove the active widget.
 | ||||
|         // Everything else relies on views listening for events and calling setters
 | ||||
|         // on this class which is terrible. This store should just listen for events
 | ||||
|         // and keep itself up to date.
 | ||||
|         // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
 | ||||
|         if (ev.getType() !== 'im.vector.modular.widgets') return; | ||||
| 
 | ||||
|         if (ev.getStateKey() === this.persistentWidgetId) { | ||||
|             this.destroyPersistentWidget(this.persistentWidgetId); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public destroyPersistentWidget(id: string): void { | ||||
|         if (id !== this.persistentWidgetId) return; | ||||
|         const toDeleteId = this.persistentWidgetId; | ||||
| 
 | ||||
|         WidgetMessagingStore.instance.stopMessagingById(id); | ||||
| 
 | ||||
|         this.setWidgetPersistence(toDeleteId, false); | ||||
|         this.delRoomId(toDeleteId); | ||||
|     } | ||||
| 
 | ||||
|     public setWidgetPersistence(widgetId: string, val: boolean): void { | ||||
|         if (this.persistentWidgetId === widgetId && !val) { | ||||
|             this.persistentWidgetId = null; | ||||
|         } else if (this.persistentWidgetId !== widgetId && val) { | ||||
|             this.persistentWidgetId = widgetId; | ||||
|         } | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     } | ||||
| 
 | ||||
|     public getWidgetPersistence(widgetId: string): boolean { | ||||
|         return this.persistentWidgetId === widgetId; | ||||
|     } | ||||
| 
 | ||||
|     public getPersistentWidgetId(): string { | ||||
|         return this.persistentWidgetId; | ||||
|     } | ||||
| 
 | ||||
|     public getRoomId(widgetId: string): string { | ||||
|         return this.roomIdByWidgetId.get(widgetId); | ||||
|     } | ||||
| 
 | ||||
|     public setRoomId(widgetId: string, roomId: string): void { | ||||
|         this.roomIdByWidgetId.set(widgetId, roomId); | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     } | ||||
| 
 | ||||
|     public delRoomId(widgetId: string): void { | ||||
|         this.roomIdByWidgetId.delete(widgetId); | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| window.mxActiveWidgetStore = ActiveWidgetStore.instance; | ||||
|  | @ -14,13 +14,11 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from "react"; | ||||
| import { ListIteratee, Many, sortBy, throttle } from "lodash"; | ||||
| import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; | ||||
| import { JoinRule } from "matrix-js-sdk/src/@types/partials"; | ||||
| import { IRoomCapability } from "matrix-js-sdk/src/client"; | ||||
| 
 | ||||
| import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; | ||||
|  | @ -41,12 +39,6 @@ import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays"; | |||
| import { objectDiff } from "../utils/objects"; | ||||
| import { reorderLexicographically } from "../utils/stringOrderField"; | ||||
| import { TAG_ORDER } from "../components/views/rooms/RoomList"; | ||||
| import { shouldShowSpaceSettings } from "../utils/space"; | ||||
| import ToastStore from "./ToastStore"; | ||||
| import { _t } from "../languageHandler"; | ||||
| import GenericToast from "../components/views/toasts/GenericToast"; | ||||
| import Modal from "../Modal"; | ||||
| import InfoDialog from "../components/views/dialogs/InfoDialog"; | ||||
| import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; | ||||
| 
 | ||||
| type SpaceKey = string | symbol; | ||||
|  | @ -233,65 +225,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|             window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY); | ||||
|         } | ||||
| 
 | ||||
|         // New in Spaces beta toast for Restricted Join Rule
 | ||||
|         const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen"; | ||||
|         if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) && | ||||
|             space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey) | ||||
|             && this.restrictedJoinRuleSupport?.preferred | ||||
|         ) { | ||||
|             const toastKey = "restrictedjoinrule"; | ||||
|             ToastStore.sharedInstance().addOrReplaceToast({ | ||||
|                 key: toastKey, | ||||
|                 title: _t("New in the Spaces beta"), | ||||
|                 props: { | ||||
|                     description: _t("Help people in spaces to find and join private rooms"), | ||||
|                     acceptLabel: _t("Learn more"), | ||||
|                     onAccept: () => { | ||||
|                         localStorage.setItem(lsKey, "true"); | ||||
|                         ToastStore.sharedInstance().dismissToast(toastKey); | ||||
| 
 | ||||
|                         Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, { | ||||
|                             title: _t("Help space members find private rooms"), | ||||
|                             description: <> | ||||
|                                 <p>{ _t("To help space members find and join a private room, " + | ||||
|                                     "go to that room's Security & Privacy settings.") }</p> | ||||
| 
 | ||||
|                                 { /* Reuses classes from TabbedView for simplicity, non-interactive */ } | ||||
|                                 <div className="mx_TabbedView_tabsOnLeft" style={{ width: "190px", position: "relative" }}> | ||||
|                                     <div className="mx_TabbedView_tabLabel"> | ||||
|                                         <span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon" /> | ||||
|                                         <span className="mx_TabbedView_tabLabel_text">{ _t("General") }</span> | ||||
|                                     </div> | ||||
|                                     <div className="mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"> | ||||
|                                         <span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon" /> | ||||
|                                         <span className="mx_TabbedView_tabLabel_text">{ _t("Security & Privacy") }</span> | ||||
|                                     </div> | ||||
|                                     <div className="mx_TabbedView_tabLabel"> | ||||
|                                         <span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon" /> | ||||
|                                         <span className="mx_TabbedView_tabLabel_text">{ _t("Roles & Permissions") }</span> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
| 
 | ||||
|                                 <p>{ _t("This makes it easy for rooms to stay private to a space, " + | ||||
|                                     "while letting people in the space find and join them. " + | ||||
|                                     "All new rooms in a space will have this option available.") }</p> | ||||
|                             </>, | ||||
|                             button: _t("OK"), | ||||
|                             hasCloseButton: false, | ||||
|                             fixedWidth: true, | ||||
|                         }); | ||||
|                     }, | ||||
|                     rejectLabel: _t("Skip"), | ||||
|                     onReject: () => { | ||||
|                         localStorage.setItem(lsKey, "true"); | ||||
|                         ToastStore.sharedInstance().dismissToast(toastKey); | ||||
|                     }, | ||||
|                 }, | ||||
|                 component: GenericToast, | ||||
|                 priority: 35, | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if (space) { | ||||
|             this.loadSuggestedRooms(space); | ||||
|         } | ||||
|  | @ -595,7 +528,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|             // Update NotificationStates
 | ||||
|             this.getNotificationState(s).setRooms(visibleRooms.filter(room => { | ||||
|                 if (!roomIds.has(room.roomId)) return false; | ||||
|                 if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false; | ||||
| 
 | ||||
|                 if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { | ||||
|                     return s === HOME_SPACE; | ||||
|  | @ -818,7 +751,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|     } | ||||
| 
 | ||||
|     protected async onAction(payload: ActionPayload) { | ||||
|         if (!spacesEnabled) return; | ||||
|         if (!spacesEnabled || !this.matrixClient) return; | ||||
|         switch (payload.action) { | ||||
|             case "view_room": { | ||||
|                 // Don't auto-switch rooms when reacting to a context-switch
 | ||||
|  | @ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|         // If a persistent widget is active, check to see if it's just been removed.
 | ||||
|         // If 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<IState> { | |||
| 
 | ||||
|         // 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)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, <IWidgetApiRequestEmptyData>{}); // 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); | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
|  | @ -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; | ||||
|  | @ -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; | ||||
|     } | ||||
|  | @ -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 }); | ||||
|  | @ -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<string> { | ||||
|     const body = unpackMegolmKeyFile(data); | ||||
|     const brand = SdkConfig.get().brand; | ||||
| 
 | ||||
|  | @ -124,7 +124,11 @@ export async function decryptMegolmKeyFile(data, password) { | |||
|  *    key-derivation function. | ||||
|  * @return {Promise<ArrayBuffer>} promise for encrypted output | ||||
|  */ | ||||
| export async function encryptMegolmKeyFile(data, password, options) { | ||||
| export async function encryptMegolmKeyFile( | ||||
|     data: string, | ||||
|     password: string, | ||||
|     options?: { kdf_rounds?: number }, // eslint-disable-line camelcase
 | ||||
| ): Promise<ArrayBuffer> { | ||||
|     options = options || {}; | ||||
|     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
 | ||||
|  | @ -5863,8 +5863,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" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Jaiwanth
						Jaiwanth