You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-04 11:51:45 +03:00 
			
		
		
		
	Merge branch 'develop' into gsouquet/threads-action-bar-19127
This commit is contained in:
		
							
								
								
									
										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)
 | 
			
		||||
===================================================================================================
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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": {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								res/img/element-icons/collapse-message.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								res/img/element-icons/collapse-message.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#737D8C" fill-rule="evenodd" d="M.2192.234A.753.753 0 011.2815.2321l3.7243 3.7003L8.7181.2202A.753.753 0 019.7805.2185a.747.747 0 01.0017 1.0589L5.5396 5.52a.753.753 0 01-1.0624.0018L.221 1.2928A.747.747 0 01.2192.234zM9.7822 13.7663a.7529.7529 0 01-1.0623.0017l-3.7243-3.7003L1.2833 13.78a.753.753 0 01-1.0624.0018.7471.7471 0 01-.0017-1.059l4.2426-4.2426a.753.753 0 011.0624-.0017l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 543 B  | 
							
								
								
									
										1
									
								
								res/img/element-icons/expand-message.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								res/img/element-icons/expand-message.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#17191C" fill-rule="evenodd" d="M.2192 8.494a.753.753 0 011.0623-.0018l3.7243 3.7003 3.7123-3.7123a.753.753 0 011.0624-.0017.747.747 0 01.0017 1.059L5.5396 13.78a.753.753 0 01-1.0624.0018L.221 9.5528A.747.747 0 01.2192 8.494zM9.7822 5.5063A.753.753 0 018.72 5.508L4.9956 1.8077 1.2833 5.52a.753.753 0 01-1.0624.0018.747.747 0 01-.0017-1.059L4.4618.2202A.753.753 0 015.5242.2185l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 538 B  | 
							
								
								
									
										12
									
								
								src/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -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";
 | 
			
		||||
@@ -59,7 +58,7 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
 | 
			
		||||
interface IProps {
 | 
			
		||||
    room?: Room; // if showing panels for a given room, this is set
 | 
			
		||||
    groupId?: string; // if showing panels for a given group, this is set
 | 
			
		||||
    user?: User; // used if we know the user ahead of opening the panel
 | 
			
		||||
    member?: RoomMember; // used if we know the room member ahead of opening the panel
 | 
			
		||||
    resizeNotifier: ResizeNotifier;
 | 
			
		||||
    permalinkCreator?: RoomPermalinkCreator;
 | 
			
		||||
    e2eStatus?: E2EStatus;
 | 
			
		||||
@@ -100,10 +99,10 @@ export default class RightPanel extends React.Component<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
 | 
			
		||||
@@ -225,7 +224,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
 | 
			
		||||
        // XXX: There are three different ways of 'closing' this panel depending on what state
 | 
			
		||||
        // 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 }
 | 
			
		||||
 
 | 
			
		||||
@@ -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>);
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
 | 
			
		||||
import ErrorDialog from '../dialogs/ErrorDialog';
 | 
			
		||||
import ShareDialog from '../dialogs/ShareDialog';
 | 
			
		||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
 | 
			
		||||
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
 | 
			
		||||
 | 
			
		||||
export function canCancel(eventStatus: EventStatus): boolean {
 | 
			
		||||
    return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
 | 
			
		||||
@@ -52,7 +53,8 @@ export interface IOperableEventTile {
 | 
			
		||||
    getEventTileOps(): IEventTileOps;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
interface IProps extends IPosition {
 | 
			
		||||
    chevronFace: ChevronFace;
 | 
			
		||||
    /* the MatrixEvent associated with the context menu */
 | 
			
		||||
    mxEvent: MatrixEvent;
 | 
			
		||||
    /* an optional EventTileOps implementation that can be used to unhide preview widgets */
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -45,6 +53,8 @@ interface IProps {
 | 
			
		||||
    layout?: Layout;
 | 
			
		||||
    // Whether to always show a timestamp
 | 
			
		||||
    alwaysShowTimestamps?: boolean;
 | 
			
		||||
    isQuoteExpanded?: boolean;
 | 
			
		||||
    setQuoteExpanded: (isExpanded: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
@@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
 | 
			
		||||
    static contextType = MatrixClientContext;
 | 
			
		||||
    private unmounted = false;
 | 
			
		||||
    private room: Room;
 | 
			
		||||
    private blockquoteRef = React.createRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
@@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
 | 
			
		||||
        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static getParentEventId(ev: MatrixEvent): string {
 | 
			
		||||
    public static getParentEventId(ev: MatrixEvent): string | undefined {
 | 
			
		||||
        if (!ev || ev.isRedacted()) return;
 | 
			
		||||
 | 
			
		||||
        // XXX: For newer relations (annotations, replacements, etc.), we now
 | 
			
		||||
@@ -137,7 +148,7 @@ export default class ReplyThread extends React.Component<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();
 | 
			
		||||
@@ -237,37 +248,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,
 | 
			
		||||
        alwaysShowTimestamps: boolean,
 | 
			
		||||
    ): JSX.Element {
 | 
			
		||||
        if (!ReplyThread.getParentEventId(parentEv)) return null;
 | 
			
		||||
        return <ReplyThread
 | 
			
		||||
            parentEv={parentEv}
 | 
			
		||||
            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
 | 
			
		||||
@@ -321,7 +333,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;
 | 
			
		||||
@@ -373,14 +385,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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -127,12 +130,15 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
 | 
			
		||||
 | 
			
		||||
interface IMessageActionBarProps {
 | 
			
		||||
    mxEvent: MatrixEvent;
 | 
			
		||||
    // The Relations model from the JS SDK for reactions to `mxEvent`
 | 
			
		||||
    reactions?: any;  // TODO: types
 | 
			
		||||
    reactions?: Relations;
 | 
			
		||||
    // TODO: Types
 | 
			
		||||
    getTile: () => any | null;
 | 
			
		||||
    getReplyThread: () => ReplyThread | undefined;
 | 
			
		||||
    permalinkCreator?: RoomPermalinkCreator;
 | 
			
		||||
    getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
 | 
			
		||||
    getReplyThread?: () => ReplyThread;
 | 
			
		||||
    onFocusChange?: (menuDisplayed: boolean) => void;
 | 
			
		||||
    toggleThreadExpanded: () => void;
 | 
			
		||||
    isInThreadTimeline?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -329,6 +335,20 @@ export default class MessageActionBar extends React.PureComponent<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 }, ref) => {
 | 
			
		||||
interface IProps {
 | 
			
		||||
    mxEvent: MatrixEvent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
 | 
			
		||||
    const cli: MatrixClient = useContext(MatrixClientContext);
 | 
			
		||||
 | 
			
		||||
    let text = _t("Message deleted");
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow';
 | 
			
		||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
 | 
			
		||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
 | 
			
		||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
 | 
			
		||||
 | 
			
		||||
const eventTileTypes = {
 | 
			
		||||
    [EventType.RoomMessage]: 'messages.MessageEvent',
 | 
			
		||||
@@ -144,8 +145,7 @@ export function getHandlerTile(ev) {
 | 
			
		||||
    // XXX: This is extremely a hack. Possibly these components should have an interface for
 | 
			
		||||
    // declining to render?
 | 
			
		||||
    if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
 | 
			
		||||
        const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
 | 
			
		||||
        if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
 | 
			
		||||
        if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -323,7 +323,7 @@ interface IState {
 | 
			
		||||
    reactions: Relations;
 | 
			
		||||
 | 
			
		||||
    hover: boolean;
 | 
			
		||||
 | 
			
		||||
    isQuoteExpanded?: boolean;
 | 
			
		||||
    thread?: Thread;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -331,7 +331,8 @@ interface IState {
 | 
			
		||||
export default class EventTile extends React.Component<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>();
 | 
			
		||||
@@ -889,8 +890,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;
 | 
			
		||||
 | 
			
		||||
@@ -915,6 +916,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;
 | 
			
		||||
@@ -924,6 +930,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
 | 
			
		||||
@@ -936,6 +943,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);
 | 
			
		||||
@@ -1057,6 +1065,7 @@ export default class EventTile extends React.Component<IProps, IState> {
 | 
			
		||||
            getReplyThread={this.getReplyThread}
 | 
			
		||||
            onFocusChange={this.onActionBarFocusChange}
 | 
			
		||||
            isInThreadTimeline={isInThreadTimeline}
 | 
			
		||||
            toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
 | 
			
		||||
        /> : undefined;
 | 
			
		||||
 | 
			
		||||
        const showTimestamp = this.props.mxEvent.getTs()
 | 
			
		||||
@@ -1229,20 +1238,18 @@ export default class EventTile extends React.Component<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.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}
 | 
			
		||||
                            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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -1944,6 +1944,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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
							
								
								
									
										112
									
								
								src/stores/ActiveWidgetStore.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/stores/ActiveWidgetStore.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2018 New Vector Ltd
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'events';
 | 
			
		||||
import { MatrixEvent } from "matrix-js-sdk";
 | 
			
		||||
 | 
			
		||||
import { MatrixClientPeg } from '../MatrixClientPeg';
 | 
			
		||||
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
 | 
			
		||||
 | 
			
		||||
export enum ActiveWidgetStoreEvent {
 | 
			
		||||
    Update = "update",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Stores information about the widgets active in the app right now:
 | 
			
		||||
 *  * What widget is set to remain always-on-screen, if any
 | 
			
		||||
 *    Only one widget may be 'always on screen' at any one time.
 | 
			
		||||
 *  * Negotiated capabilities for active apps
 | 
			
		||||
 */
 | 
			
		||||
export default class ActiveWidgetStore extends EventEmitter {
 | 
			
		||||
    private static internalInstance: ActiveWidgetStore;
 | 
			
		||||
    private persistentWidgetId: string;
 | 
			
		||||
    // What room ID each widget is associated with (if it's a room widget)
 | 
			
		||||
    private roomIdByWidgetId = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
    public static get instance(): ActiveWidgetStore {
 | 
			
		||||
        if (!ActiveWidgetStore.internalInstance) {
 | 
			
		||||
            ActiveWidgetStore.internalInstance = new ActiveWidgetStore();
 | 
			
		||||
        }
 | 
			
		||||
        return ActiveWidgetStore.internalInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public start(): void {
 | 
			
		||||
        MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public stop(): void {
 | 
			
		||||
        if (MatrixClientPeg.get()) {
 | 
			
		||||
            MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
 | 
			
		||||
        }
 | 
			
		||||
        this.roomIdByWidgetId.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onRoomStateEvents = (ev: MatrixEvent): void => {
 | 
			
		||||
        // XXX: This listens for state events in order to remove the active widget.
 | 
			
		||||
        // Everything else relies on views listening for events and calling setters
 | 
			
		||||
        // on this class which is terrible. This store should just listen for events
 | 
			
		||||
        // and keep itself up to date.
 | 
			
		||||
        // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
 | 
			
		||||
        if (ev.getType() !== 'im.vector.modular.widgets') return;
 | 
			
		||||
 | 
			
		||||
        if (ev.getStateKey() === this.persistentWidgetId) {
 | 
			
		||||
            this.destroyPersistentWidget(this.persistentWidgetId);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public destroyPersistentWidget(id: string): void {
 | 
			
		||||
        if (id !== this.persistentWidgetId) return;
 | 
			
		||||
        const toDeleteId = this.persistentWidgetId;
 | 
			
		||||
 | 
			
		||||
        WidgetMessagingStore.instance.stopMessagingById(id);
 | 
			
		||||
 | 
			
		||||
        this.setWidgetPersistence(toDeleteId, false);
 | 
			
		||||
        this.delRoomId(toDeleteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public setWidgetPersistence(widgetId: string, val: boolean): void {
 | 
			
		||||
        if (this.persistentWidgetId === widgetId && !val) {
 | 
			
		||||
            this.persistentWidgetId = null;
 | 
			
		||||
        } else if (this.persistentWidgetId !== widgetId && val) {
 | 
			
		||||
            this.persistentWidgetId = widgetId;
 | 
			
		||||
        }
 | 
			
		||||
        this.emit(ActiveWidgetStoreEvent.Update);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public getWidgetPersistence(widgetId: string): boolean {
 | 
			
		||||
        return this.persistentWidgetId === widgetId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public getPersistentWidgetId(): string {
 | 
			
		||||
        return this.persistentWidgetId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public getRoomId(widgetId: string): string {
 | 
			
		||||
        return this.roomIdByWidgetId.get(widgetId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public setRoomId(widgetId: string, roomId: string): void {
 | 
			
		||||
        this.roomIdByWidgetId.set(widgetId, roomId);
 | 
			
		||||
        this.emit(ActiveWidgetStoreEvent.Update);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public delRoomId(widgetId: string): void {
 | 
			
		||||
        this.roomIdByWidgetId.delete(widgetId);
 | 
			
		||||
        this.emit(ActiveWidgetStoreEvent.Update);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.mxActiveWidgetStore = ActiveWidgetStore.instance;
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -5807,8 +5807,8 @@ mathml-tag-names@^2.1.3:
 | 
			
		||||
  integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 | 
			
		||||
 | 
			
		||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
 | 
			
		||||
  version "12.5.0"
 | 
			
		||||
  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f84905b00398072b592addfb1dae64c8f3a07fa2"
 | 
			
		||||
  version "13.0.0"
 | 
			
		||||
  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2515d07c8fc3bf5e1afc8352e3e330cca30dde85"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.12.5"
 | 
			
		||||
    another-json "^0.2.0"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user