Merge branch 'develop' into gsouquet/threaded-messaging-2349
This commit is contained in:
commit
ffc7326b0c
43 changed files with 926 additions and 229 deletions
|
@ -55,6 +55,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@sentry/browser": "^6.11.0",
|
||||||
|
"@sentry/tracing": "^6.11.0",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
|
@ -62,6 +64,7 @@
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"commonmark": "^0.29.3",
|
"commonmark": "^0.29.3",
|
||||||
|
"context-filter-polyfill": "^0.2.4",
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
"diff-dom": "^4.2.2",
|
"diff-dom": "^4.2.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
@ -193,6 +196,7 @@
|
||||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
|
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
||||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
|
|
|
@ -168,7 +168,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
// it has the appearance of a text box so the controls
|
// it has the appearance of a text box so the controls
|
||||||
// appear to be part of the input
|
// appear to be part of the input
|
||||||
|
|
||||||
.mx_Dialog, .mx_MatrixChat {
|
.mx_Dialog, .mx_MatrixChat_wrapper {
|
||||||
.mx_textinput > input[type=text],
|
.mx_textinput > input[type=text],
|
||||||
.mx_textinput > input[type=search] {
|
.mx_textinput > input[type=search] {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
@import "./structures/_LeftPanelWidget.scss";
|
@import "./structures/_LeftPanelWidget.scss";
|
||||||
@import "./structures/_MainSplit.scss";
|
@import "./structures/_MainSplit.scss";
|
||||||
@import "./structures/_MatrixChat.scss";
|
@import "./structures/_MatrixChat.scss";
|
||||||
|
@import "./structures/_BackdropPanel.scss";
|
||||||
@import "./structures/_MyGroups.scss";
|
@import "./structures/_MyGroups.scss";
|
||||||
@import "./structures/_NonUrgentToastContainer.scss";
|
@import "./structures/_NonUrgentToastContainer.scss";
|
||||||
@import "./structures/_NotificationPanel.scss";
|
@import "./structures/_NotificationPanel.scss";
|
||||||
|
|
51
res/css/structures/_BackdropPanel.scss
Normal file
51
res/css/structures/_BackdropPanel.scss
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_BackdropPanel {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--lp-background-overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BackdropPanel--canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:nth-of-type(2n-1) {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
&:nth-of-type(2n) {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,10 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_GroupFilterPanel {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_GroupFilterPanel {
|
.mx_GroupFilterPanel {
|
||||||
flex: 1;
|
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
flex: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -17,15 +17,22 @@ limitations under the License.
|
||||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||||
$roomListCollapsedWidth: 68px;
|
$roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_LeftPanel,
|
||||||
|
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel {
|
.mx_LeftPanel {
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||||
min-width: 206px;
|
min-width: 206px;
|
||||||
max-width: 50%;
|
|
||||||
|
|
||||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||||
display: flex;
|
display: flex;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
|
@ -29,8 +29,6 @@ limitations under the License.
|
||||||
.mx_MatrixChat_wrapper {
|
.mx_MatrixChat_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -42,15 +40,16 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat {
|
.mx_MatrixChat {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
order: 2;
|
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-grow: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat_syncError {
|
.mx_MatrixChat_syncError {
|
||||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,18 @@ $activeBorderTransparentGap: 1px;
|
||||||
$activeBackgroundColor: $roomtile-selected-bg-color;
|
$activeBackgroundColor: $roomtile-selected-bg-color;
|
||||||
$activeBorderColor: $secondary-fg-color;
|
$activeBorderColor: $secondary-fg-color;
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_SpacePanel {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpacePanel {
|
.mx_SpacePanel {
|
||||||
flex: 0 0 auto;
|
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
// Create another flexbox so the Panel fills the container
|
// Create another flexbox so the Panel fills the container
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -95,17 +95,23 @@ limitations under the License.
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.mx_CallEvent_info_basic {
|
.mx_CallEvent_info_basic {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 10px; // To match mx_CallEvent
|
margin-left: 10px; // To match mx_CallEvent
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.mx_CallEvent_sender {
|
.mx_CallEvent_sender {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1.8rem;
|
line-height: 1.8rem;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_type {
|
.mx_CallEvent_type {
|
||||||
|
@ -142,13 +148,13 @@ limitations under the License.
|
||||||
color: $secondary-fg-color;
|
color: $secondary-fg-color;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-width: max-content;
|
||||||
|
|
||||||
.mx_CallEvent_content_button {
|
.mx_CallEvent_content_button {
|
||||||
height: 24px;
|
|
||||||
padding: 0px 12px;
|
padding: 0px 12px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 8px 0;
|
padding: 1px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -162,6 +168,8 @@ limitations under the License.
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,10 @@ $timelineImageBorderRadius: 4px;
|
||||||
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
|
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_no-image-placeholder {
|
||||||
|
background-color: $primary-bg-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_container {
|
.mx_MImageBody_thumbnail_container {
|
||||||
|
|
|
@ -36,6 +36,7 @@ limitations under the License.
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_copyButton {
|
.mx_HelpUserSettingsTab_copyButton {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
|
@ -238,9 +238,12 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme)
|
||||||
$roomlist-background-blur-amount: 60px;
|
:root {
|
||||||
$groupFilterPanel-background-blur-amount: 30px;
|
--llp-background-blur: 160px;
|
||||||
|
--lp-background-blur: 90px;
|
||||||
|
--lp-background-overlay: rgba(255, 255, 255, 0.055);
|
||||||
|
}
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
@import "../../light/css/_paths.scss";
|
@import "../../light/css/_paths.scss";
|
||||||
@import "../../light/css/_fonts.scss";
|
@import "../../light/css/_fonts.scss";
|
||||||
@import "../../light/css/_light.scss";
|
@import "../../light/css/_light.scss";
|
||||||
// important this goes before _mods,
|
|
||||||
// as $groupFilterPanel-background-blur-amount and
|
|
||||||
// $roomlist-background-blur-amount
|
|
||||||
// are overridden in _dark.scss
|
|
||||||
@import "_dark.scss";
|
@import "_dark.scss";
|
||||||
@import "../../light/css/_mods.scss";
|
@import "../../light/css/_mods.scss";
|
||||||
@import "../../../../res/css/_components.scss";
|
@import "../../../../res/css/_components.scss";
|
||||||
|
|
|
@ -361,10 +361,12 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme)
|
||||||
$roomlist-background-blur-amount: 40px;
|
:root {
|
||||||
$groupFilterPanel-background-blur-amount: 20px;
|
--llp-background-blur: 120px;
|
||||||
|
--lp-background-blur: 60px;
|
||||||
|
--lp-background-overlay: rgba(0, 0, 0, 0.055);
|
||||||
|
}
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
|
|
|
@ -4,27 +4,6 @@
|
||||||
// set the user avatar (if any) as a background so
|
// set the user avatar (if any) as a background so
|
||||||
// it can be blurred by the tag panel and room list
|
// it can be blurred by the tag panel and room list
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
|
||||||
.mx_LeftPanel {
|
|
||||||
background-image: var(--avatar-url, unset);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: left top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_GroupFilterPanel {
|
|
||||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpacePanel {
|
|
||||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
|
||||||
backdrop-filter: blur($roomlist-background-blur-amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSublist_showNButton {
|
.mx_RoomSublist_showNButton {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -464,8 +464,66 @@ export default class CallHandler extends EventEmitter {
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
});
|
});
|
||||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||||
|
this.onCallStateChanged(newState, oldState, call);
|
||||||
|
});
|
||||||
|
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
|
||||||
|
|
||||||
|
if (call.state === CallState.Ringing) {
|
||||||
|
this.pause(AudioID.Ring);
|
||||||
|
} else if (call.state === CallState.InviteSent) {
|
||||||
|
this.pause(AudioID.Ringback);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calls.set(mappedRoomId, newCall);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
|
this.setCallListeners(newCall);
|
||||||
|
this.setCallState(newCall, newCall.state);
|
||||||
|
});
|
||||||
|
call.on(CallEvent.AssertedIdentityChanged, async () => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
|
||||||
|
|
||||||
|
const newAssertedIdentity = call.getRemoteAssertedIdentity().id;
|
||||||
|
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||||
|
if (newAssertedIdentity) {
|
||||||
|
const response = await this.sipNativeLookup(newAssertedIdentity);
|
||||||
|
if (response.length && response[0].fields.lookup_success) {
|
||||||
|
newNativeAssertedIdentity = response[0].userid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
||||||
|
|
||||||
|
if (newNativeAssertedIdentity) {
|
||||||
|
this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity;
|
||||||
|
|
||||||
|
// If we don't already have a room with this user, make one. This will be slightly odd
|
||||||
|
// if they called us because we'll be inviting them, but there's not much we can do about
|
||||||
|
// this if we want the actual, native room to exist (which we do). This is why it's
|
||||||
|
// important to only obey asserted identity in trusted environments, since anyone you're
|
||||||
|
// on a call with can cause you to send a room invite to someone.
|
||||||
|
await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity);
|
||||||
|
|
||||||
|
const newMappedRoomId = this.roomIdForCall(call);
|
||||||
|
console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
|
||||||
|
if (newMappedRoomId !== mappedRoomId) {
|
||||||
|
this.removeCallForRoom(mappedRoomId);
|
||||||
|
mappedRoomId = newMappedRoomId;
|
||||||
|
console.log("Moving call to room " + mappedRoomId);
|
||||||
|
this.calls.set(mappedRoomId, call);
|
||||||
|
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCallStateChanged = (newState: CallState, oldState: CallState, call: MatrixCall): void => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
const mappedRoomId = this.roomIdForCall(call);
|
||||||
this.setCallState(call, newState);
|
this.setCallState(call, newState);
|
||||||
|
|
||||||
switch (oldState) {
|
switch (oldState) {
|
||||||
|
@ -543,60 +601,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
|
||||||
|
|
||||||
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
|
|
||||||
|
|
||||||
if (call.state === CallState.Ringing) {
|
|
||||||
this.pause(AudioID.Ring);
|
|
||||||
} else if (call.state === CallState.InviteSent) {
|
|
||||||
this.pause(AudioID.Ringback);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calls.set(mappedRoomId, newCall);
|
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
|
||||||
this.setCallListeners(newCall);
|
|
||||||
this.setCallState(newCall, newCall.state);
|
|
||||||
});
|
|
||||||
call.on(CallEvent.AssertedIdentityChanged, async () => {
|
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
|
||||||
|
|
||||||
console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
|
|
||||||
|
|
||||||
const newAssertedIdentity = call.getRemoteAssertedIdentity().id;
|
|
||||||
let newNativeAssertedIdentity = newAssertedIdentity;
|
|
||||||
if (newAssertedIdentity) {
|
|
||||||
const response = await this.sipNativeLookup(newAssertedIdentity);
|
|
||||||
if (response.length && response[0].fields.lookup_success) {
|
|
||||||
newNativeAssertedIdentity = response[0].userid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
|
||||||
|
|
||||||
if (newNativeAssertedIdentity) {
|
|
||||||
this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity;
|
|
||||||
|
|
||||||
// If we don't already have a room with this user, make one. This will be slightly odd
|
|
||||||
// if they called us because we'll be inviting them, but there's not much we can do about
|
|
||||||
// this if we want the actual, native room to exist (which we do). This is why it's
|
|
||||||
// important to only obey asserted identity in trusted environments, since anyone you're
|
|
||||||
// on a call with can cause you to send a room invite to someone.
|
|
||||||
await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity);
|
|
||||||
|
|
||||||
const newMappedRoomId = this.roomIdForCall(call);
|
|
||||||
console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
|
|
||||||
if (newMappedRoomId !== mappedRoomId) {
|
|
||||||
this.removeCallForRoom(mappedRoomId);
|
|
||||||
mappedRoomId = newMappedRoomId;
|
|
||||||
console.log("Moving call to room " + mappedRoomId);
|
|
||||||
this.calls.set(mappedRoomId, call);
|
|
||||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
|
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
|
||||||
const stats = await call.getCurrentCallStats();
|
const stats = await call.getCurrentCallStats();
|
||||||
|
@ -861,6 +866,8 @@ export default class CallHandler extends EventEmitter {
|
||||||
this.calls.set(mappedRoomId, call);
|
this.calls.set(mappedRoomId, call);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
// Explicitly handle first state change
|
||||||
|
this.onCallStateChanged(call.state, null, call);
|
||||||
|
|
||||||
// get ready to send encrypted events in the room, so if the user does answer
|
// get ready to send encrypted events in the room, so if the user does answer
|
||||||
// the call, we'll be ready to send. NB. This is the protocol-level room ID not
|
// the call, we'll be ready to send. NB. This is the protocol-level room ID not
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,12 +26,18 @@ import { _t } from './languageHandler';
|
||||||
* API on the homeserver in question with the new password.
|
* API on the homeserver in question with the new password.
|
||||||
*/
|
*/
|
||||||
export default class PasswordReset {
|
export default class PasswordReset {
|
||||||
|
private client: MatrixClient;
|
||||||
|
private clientSecret: string;
|
||||||
|
private identityServerDomain: string;
|
||||||
|
private password: string;
|
||||||
|
private sessionId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the endpoints for password resetting.
|
* Configure the endpoints for password resetting.
|
||||||
* @param {string} homeserverUrl The URL to the HS which has the account to reset.
|
* @param {string} homeserverUrl The URL to the HS which has the account to reset.
|
||||||
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
|
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
|
||||||
*/
|
*/
|
||||||
constructor(homeserverUrl, identityUrl) {
|
constructor(homeserverUrl: string, identityUrl: string) {
|
||||||
this.client = createClient({
|
this.client = createClient({
|
||||||
baseUrl: homeserverUrl,
|
baseUrl: homeserverUrl,
|
||||||
idBaseUrl: identityUrl,
|
idBaseUrl: identityUrl,
|
||||||
|
@ -47,7 +53,7 @@ export default class PasswordReset {
|
||||||
* @param {string} newPassword The new password for the account.
|
* @param {string} newPassword The new password for the account.
|
||||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||||
*/
|
*/
|
||||||
resetPassword(emailAddress, newPassword) {
|
public resetPassword(emailAddress: string, newPassword: string): Promise<IRequestTokenResponse> {
|
||||||
this.password = newPassword;
|
this.password = newPassword;
|
||||||
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
|
@ -69,7 +75,7 @@ export default class PasswordReset {
|
||||||
* with a "message" property which contains a human-readable message detailing why
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||||
*/
|
*/
|
||||||
async checkEmailLinkClicked() {
|
public async checkEmailLinkClicked(): Promise<void> {
|
||||||
const creds = {
|
const creds = {
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
165
src/components/structures/BackdropPanel.tsx
Normal file
165
src/components/structures/BackdropPanel.tsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 React, { createRef } from "react";
|
||||||
|
import "context-filter-polyfill";
|
||||||
|
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
backgroundImage?: CanvasImageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Left Panel image
|
||||||
|
lpImage?: string;
|
||||||
|
// Left-left panel image
|
||||||
|
llpImage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BackdropPanel extends React.PureComponent<IProps, IState> {
|
||||||
|
private leftLeftPanelRef = createRef<HTMLCanvasElement>();
|
||||||
|
private leftPanelRef = createRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
private sizes = {
|
||||||
|
leftLeftPanelWidth: 0,
|
||||||
|
leftPanelWidth: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
private style = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
public state: IState = {};
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
UIStore.instance.on("SpacePanel", this.onResize);
|
||||||
|
UIStore.instance.on("GroupFilterPanelContainer", this.onResize);
|
||||||
|
this.onResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
UIStore.instance.off("SpacePanel", this.onResize);
|
||||||
|
UIStore.instance.on("GroupFilterPanelContainer", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
|
if (prevProps.backgroundImage !== this.props.backgroundImage) {
|
||||||
|
this.setState({});
|
||||||
|
this.onResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = () => {
|
||||||
|
if (this.props.backgroundImage) {
|
||||||
|
const groupFilterPanelDimensions = UIStore.instance.getElementDimensions("GroupFilterPanelContainer");
|
||||||
|
const spacePanelDimensions = UIStore.instance.getElementDimensions("SpacePanel");
|
||||||
|
const roomListDimensions = UIStore.instance.getElementDimensions("LeftPanel");
|
||||||
|
this.sizes = {
|
||||||
|
leftLeftPanelWidth: spacePanelDimensions?.width ?? groupFilterPanelDimensions?.width ?? 0,
|
||||||
|
leftPanelWidth: roomListDimensions?.width ?? 0,
|
||||||
|
height: UIStore.instance.windowHeight,
|
||||||
|
};
|
||||||
|
this.refreshBackdropImage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshBackdropImage = (): void => {
|
||||||
|
const leftLeftPanelContext = this.leftLeftPanelRef.current.getContext("2d");
|
||||||
|
const leftPanelContext = this.leftPanelRef.current.getContext("2d");
|
||||||
|
const { leftLeftPanelWidth, leftPanelWidth, height } = this.sizes;
|
||||||
|
const width = leftLeftPanelWidth + leftPanelWidth;
|
||||||
|
const { backgroundImage } = this.props;
|
||||||
|
|
||||||
|
const imageWidth = (backgroundImage as ImageBitmap).width;
|
||||||
|
const imageHeight = (backgroundImage as ImageBitmap).height;
|
||||||
|
|
||||||
|
const contentRatio = imageWidth / imageHeight;
|
||||||
|
const containerRatio = width / height;
|
||||||
|
let resultHeight;
|
||||||
|
let resultWidth;
|
||||||
|
if (contentRatio > containerRatio) {
|
||||||
|
resultHeight = height;
|
||||||
|
resultWidth = height * contentRatio;
|
||||||
|
} else {
|
||||||
|
resultWidth = width;
|
||||||
|
resultHeight = width / contentRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This value has been chosen to be as close with rendering as the css-only
|
||||||
|
// backdrop-filter: blur effect was, mostly takes effect for vertical pictures.
|
||||||
|
const x = width * 0.1;
|
||||||
|
const y = (height - resultHeight) / 2;
|
||||||
|
|
||||||
|
this.leftLeftPanelRef.current.width = leftLeftPanelWidth;
|
||||||
|
this.leftLeftPanelRef.current.height = height;
|
||||||
|
this.leftPanelRef.current.width = (window.screen.width * 0.5);
|
||||||
|
this.leftPanelRef.current.height = height;
|
||||||
|
|
||||||
|
const spacesBlur = this.style.getPropertyValue('--llp-background-blur');
|
||||||
|
const roomListBlur = this.style.getPropertyValue('--lp-background-blur');
|
||||||
|
|
||||||
|
leftLeftPanelContext.filter = `blur(${spacesBlur})`;
|
||||||
|
leftPanelContext.filter = `blur(${roomListBlur})`;
|
||||||
|
leftLeftPanelContext.drawImage(
|
||||||
|
backgroundImage,
|
||||||
|
0, 0,
|
||||||
|
imageWidth, imageHeight,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
resultWidth,
|
||||||
|
resultHeight,
|
||||||
|
);
|
||||||
|
leftPanelContext.drawImage(
|
||||||
|
backgroundImage,
|
||||||
|
0, 0,
|
||||||
|
imageWidth, imageHeight,
|
||||||
|
x - leftLeftPanelWidth,
|
||||||
|
y,
|
||||||
|
resultWidth,
|
||||||
|
resultHeight,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
lpImage: this.leftPanelRef.current.toDataURL('image/jpeg', 1),
|
||||||
|
llpImage: this.leftLeftPanelRef.current.toDataURL('image/jpeg', 1),
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.props.backgroundImage) return null;
|
||||||
|
return <div className="mx_BackdropPanel">
|
||||||
|
<img
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
src={this.state.llpImage} />
|
||||||
|
<img
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
src={this.state.lpImage} />
|
||||||
|
<canvas
|
||||||
|
ref={this.leftLeftPanelRef}
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
style={{
|
||||||
|
display: this.state.lpImage ? 'none' : 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
style={{
|
||||||
|
display: this.state.lpImage ? 'none' : 'block',
|
||||||
|
}}
|
||||||
|
ref={this.leftPanelRef}
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { EventSubscription } from "fbemitter";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
||||||
|
|
||||||
|
@ -30,22 +31,43 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import UserTagTile from "../views/elements/UserTagTile";
|
import UserTagTile from "../views/elements/UserTagTile";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
|
interface IGroupFilterPanelProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
type OrderedTagsTemporaryType = Array<{}>;
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
type SelectedTagsTemporaryType = Array<{}>;
|
||||||
|
|
||||||
|
interface IGroupFilterPanelState {
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
orderedTags: OrderedTagsTemporaryType;
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
selectedTags: SelectedTagsTemporaryType;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.GroupFilterPanel")
|
@replaceableComponent("structures.GroupFilterPanel")
|
||||||
class GroupFilterPanel extends React.Component {
|
class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFilterPanelState> {
|
||||||
static contextType = MatrixClientContext;
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
state = {
|
public state = {
|
||||||
orderedTags: [],
|
orderedTags: [],
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
private ref = React.createRef<HTMLDivElement>();
|
||||||
this.unmounted = false;
|
private unmounted = false;
|
||||||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
private groupFilterOrderStoreToken?: EventSubscription;
|
||||||
this.context.on("sync", this._onClientSync);
|
|
||||||
|
|
||||||
this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
public componentDidMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.context.on("Group.myMembership", this.onGroupMyMembership);
|
||||||
|
this.context.on("sync", this.onClientSync);
|
||||||
|
|
||||||
|
this.groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -56,23 +78,25 @@ class GroupFilterPanel extends React.Component {
|
||||||
});
|
});
|
||||||
// This could be done by anything with a matrix client
|
// This could be done by anything with a matrix client
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
|
UIStore.instance.trackElementDimensions("GroupPanel", this.ref.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.removeListener("Group.myMembership", this.onGroupMyMembership);
|
||||||
this.context.removeListener("sync", this._onClientSync);
|
this.context.removeListener("sync", this.onClientSync);
|
||||||
if (this._groupFilterOrderStoreToken) {
|
if (this.groupFilterOrderStoreToken) {
|
||||||
this._groupFilterOrderStoreToken.remove();
|
this.groupFilterOrderStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
UIStore.instance.stopTrackingElementDimensions("GroupPanel");
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGroupMyMembership = () => {
|
private onGroupMyMembership = () => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClientSync = (syncState, prevState) => {
|
private onClientSync = (syncState, prevState) => {
|
||||||
// Consider the client reconnected if there is no error with syncing.
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
@ -82,18 +106,18 @@ class GroupFilterPanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClick = e => {
|
private onClick = e => {
|
||||||
// only dispatch if its not a no-op
|
// only dispatch if its not a no-op
|
||||||
if (this.state.selectedTags.length > 0) {
|
if (this.state.selectedTags.length > 0) {
|
||||||
dis.dispatch({ action: 'deselect_tags' });
|
dis.dispatch({ action: 'deselect_tags' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClearFilterClick = ev => {
|
private onClearFilterClick = ev => {
|
||||||
dis.dispatch({ action: 'deselect_tags' });
|
dis.dispatch({ action: 'deselect_tags' });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderGlobalIcon() {
|
private renderGlobalIcon() {
|
||||||
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
|
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -104,7 +128,7 @@ class GroupFilterPanel extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
|
||||||
|
@ -147,7 +171,7 @@ class GroupFilterPanel extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
return <div className={classes} onClick={this.onClearFilterClick} ref={this.ref}>
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
className="mx_GroupFilterPanel_scroller"
|
className="mx_GroupFilterPanel_scroller"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
|
@ -37,11 +37,9 @@ import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
|
||||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||||
import LeftPanelWidget from "./LeftPanelWidget";
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
|
||||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
@ -71,6 +69,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private groupFilterPanelWatcherRef: string;
|
private groupFilterPanelWatcherRef: string;
|
||||||
|
private groupFilterPanelContainer = createRef<HTMLDivElement>();
|
||||||
private bgImageWatcherRef: string;
|
private bgImageWatcherRef: string;
|
||||||
private focusedElement = null;
|
private focusedElement = null;
|
||||||
private isDoingStickyHeaders = false;
|
private isDoingStickyHeaders = false;
|
||||||
|
@ -86,17 +85,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
this.bgImageWatcherRef = SettingsStore.watchSetting(
|
|
||||||
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
|
|
||||||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") });
|
this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
|
||||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
|
if (this.groupFilterPanelContainer.current) {
|
||||||
|
const componentName = "GroupFilterPanelContainer";
|
||||||
|
UIStore.instance.trackElementDimensions(componentName, this.groupFilterPanelContainer.current);
|
||||||
|
}
|
||||||
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
@ -105,10 +106,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
||||||
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
|
||||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
|
@ -149,23 +148,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onBackgroundImageUpdate = () => {
|
|
||||||
// Note: we do this in the LeftPanel as it uses this variable most prominently.
|
|
||||||
const avatarSize = 32; // arbitrary
|
|
||||||
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
|
||||||
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
|
||||||
if (settingBgMxc) {
|
|
||||||
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatarUrlProp = `url(${avatarUrl})`;
|
|
||||||
if (!avatarUrl) {
|
|
||||||
document.body.style.removeProperty("--avatar-url");
|
|
||||||
} else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) {
|
|
||||||
document.body.style.setProperty("--avatar-url", avatarUrlProp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleStickyHeaders(list: HTMLDivElement) {
|
private handleStickyHeaders(list: HTMLDivElement) {
|
||||||
if (this.isDoingStickyHeaders) return;
|
if (this.isDoingStickyHeaders) return;
|
||||||
this.isDoingStickyHeaders = true;
|
this.isDoingStickyHeaders = true;
|
||||||
|
@ -443,7 +425,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
let leftLeftPanel;
|
let leftLeftPanel;
|
||||||
if (this.state.showGroupFilterPanel) {
|
if (this.state.showGroupFilterPanel) {
|
||||||
leftLeftPanel = (
|
leftLeftPanel = (
|
||||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
<div className="mx_LeftPanel_GroupFilterPanelContainer" ref={this.groupFilterPanelContainer}>
|
||||||
<GroupFilterPanel />
|
<GroupFilterPanel />
|
||||||
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,15 +55,19 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
import CallHandler from '../../CallHandler';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||||
|
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||||
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
import ToastContainer from './ToastContainer';
|
import ToastContainer from './ToastContainer';
|
||||||
import MyGroups from "./MyGroups";
|
import MyGroups from "./MyGroups";
|
||||||
import UserView from "./UserView";
|
import UserView from "./UserView";
|
||||||
import GroupView from "./GroupView";
|
import GroupView from "./GroupView";
|
||||||
|
import BackdropPanel from "./BackdropPanel";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -127,6 +131,7 @@ interface IState {
|
||||||
usageLimitEventTs?: number;
|
usageLimitEventTs?: number;
|
||||||
useCompactLayout: boolean;
|
useCompactLayout: boolean;
|
||||||
activeCalls: Array<MatrixCall>;
|
activeCalls: Array<MatrixCall>;
|
||||||
|
backgroundImage?: CanvasImageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,6 +147,7 @@ interface IState {
|
||||||
class LoggedInView extends React.Component<IProps, IState> {
|
class LoggedInView extends React.Component<IProps, IState> {
|
||||||
static displayName = 'LoggedInView';
|
static displayName = 'LoggedInView';
|
||||||
|
|
||||||
|
private dispatcherRef: string;
|
||||||
protected readonly _matrixClient: MatrixClient;
|
protected readonly _matrixClient: MatrixClient;
|
||||||
protected readonly _roomView: React.RefObject<any>;
|
protected readonly _roomView: React.RefObject<any>;
|
||||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||||
|
@ -156,7 +162,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||||
usageLimitDismissed: false,
|
usageLimitDismissed: false,
|
||||||
activeCalls: [],
|
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
|
@ -172,7 +178,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
this.updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
|
|
||||||
|
@ -192,25 +198,41 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.resizer = this.createResizer();
|
this.resizer = this.createResizer();
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
|
|
||||||
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
|
||||||
this.loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
|
this.refreshBackgroundImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
dis.unregister(this.dispatcherRef);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
|
||||||
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCallsChanged = () => {
|
private refreshBackgroundImage = async (): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onAction = (payload): void => {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'call_state': {
|
||||||
|
const activeCalls = CallHandler.sharedInstance().getAllActiveCalls();
|
||||||
|
if (activeCalls !== this.state.activeCalls) {
|
||||||
|
this.setState({ activeCalls });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public canResetTimelineInRoom = (roomId: string) => {
|
public canResetTimelineInRoom = (roomId: string) => {
|
||||||
if (!this._roomView.current) {
|
if (!this._roomView.current) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -601,10 +623,11 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bodyClasses = 'mx_MatrixChat';
|
const bodyClasses = classNames({
|
||||||
if (this.state.useCompactLayout) {
|
'mx_MatrixChat': true,
|
||||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
'mx_MatrixChat_useCompactLayout': this.state.useCompactLayout,
|
||||||
}
|
'mx_MatrixChat--with-avatar': this.state.backgroundImage,
|
||||||
|
});
|
||||||
|
|
||||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||||
return (
|
return (
|
||||||
|
@ -622,14 +645,17 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||||
|
<BackdropPanel
|
||||||
|
backgroundImage={this.state.backgroundImage}
|
||||||
|
/>
|
||||||
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
|
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
isMinimized={this.props.collapseLhs || false}
|
isMinimized={this.props.collapseLhs || false}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>
|
/>
|
||||||
<ResizeHandle />
|
<ResizeHandle />
|
||||||
{ pageElement }
|
|
||||||
</div>
|
</div>
|
||||||
|
{ pageElement }
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
|
|
|
@ -108,6 +108,7 @@ import SoftLogout from './auth/SoftLogout';
|
||||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||||
import { copyPlaintext } from "../../utils/strings";
|
import { copyPlaintext } from "../../utils/strings";
|
||||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
||||||
|
import { initSentry } from "../../sentry";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export enum Views {
|
export enum Views {
|
||||||
|
@ -393,6 +394,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
PosthogAnalytics.instance.updatePlatformSuperProperties();
|
PosthogAnalytics.instance.updatePlatformSuperProperties();
|
||||||
|
|
||||||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||||
|
|
||||||
|
initSentry(SdkConfig.get()["sentry"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async postLoginSetup() {
|
private async postLoginSetup() {
|
||||||
|
|
|
@ -29,11 +29,13 @@ import BaseDialog from "./BaseDialog";
|
||||||
import Field from '../elements/Field';
|
import Field from '../elements/Field';
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import { sendSentryReport } from "../../../sentry";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (success: boolean) => void;
|
onFinished: (success: boolean) => void;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -113,6 +115,8 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sendSentryReport(this.state.text, this.state.issueUrl, this.props.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDownload = async (): Promise<void> => {
|
private onDownload = async (): Promise<void> => {
|
||||||
|
@ -200,8 +204,8 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||||
{ _t(
|
{ _t(
|
||||||
"Debug logs contain application usage data including your " +
|
"Debug logs contain application usage data including your " +
|
||||||
"username, the IDs or aliases of the rooms or groups you " +
|
"username, the IDs or aliases of the rooms or groups you " +
|
||||||
"have visited and the usernames of other users. They do " +
|
"have visited, which UI elements you last interacted with, " +
|
||||||
"not contain messages.",
|
"and the usernames of other users. They do not contain messages.",
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p><b>
|
<p><b>
|
||||||
|
|
|
@ -218,6 +218,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
|
|
||||||
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
||||||
}
|
}
|
||||||
|
@ -307,7 +308,6 @@ export default class AppTile extends React.Component {
|
||||||
if (this.iframe) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
this.iframe.src = this._sgWidget.embedUrl;
|
this.iframe.src = this._sgWidget.embedUrl;
|
||||||
this.setState({});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
||||||
private onBugReport = (): void => {
|
private onBugReport = (): void => {
|
||||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
|
||||||
label: 'react-soft-crash',
|
label: 'react-soft-crash',
|
||||||
|
error: this.state.error,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,8 +94,9 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
||||||
"If you've submitted a bug via GitHub, debug logs can help " +
|
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||||
"us track down the problem. Debug logs contain application " +
|
"us track down the problem. Debug logs contain application " +
|
||||||
"usage data including your username, the IDs or aliases of " +
|
"usage data including your username, the IDs or aliases of " +
|
||||||
"the rooms or groups you have visited and the usernames of " +
|
"the rooms or groups you have visited, which UI elements you " +
|
||||||
"other users. They do not contain messages.",
|
"last interacted with, and the usernames of other users. " +
|
||||||
|
"They do not contain messages.",
|
||||||
) }</p>
|
) }</p>
|
||||||
<AccessibleButton onClick={this.onBugReport} kind='primary'>
|
<AccessibleButton onClick={this.onBugReport} kind='primary'>
|
||||||
{ _t("Submit debug logs") }
|
{ _t("Submit debug logs") }
|
||||||
|
|
|
@ -27,7 +27,7 @@ import classNames from 'classnames';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
import { formatCallTime } from "../../../DateUtils";
|
import { formatCallTime } from "../../../DateUtils";
|
||||||
|
|
||||||
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
|
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
|
|
@ -178,7 +178,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onPlaceholderClick = async () => {
|
private onPlaceholderClick = async () => {
|
||||||
const mediaHelper = this.props.mediaEventHelper;
|
const mediaHelper = this.props.mediaEventHelper;
|
||||||
if (mediaHelper.media.isEncrypted) {
|
if (mediaHelper?.media.isEncrypted) {
|
||||||
await this.decryptFile();
|
await this.decryptFile();
|
||||||
this.downloadFile(this.fileName, this.linkText);
|
this.downloadFile(this.fileName, this.linkText);
|
||||||
} else {
|
} else {
|
||||||
|
@ -192,7 +192,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
|
const isEncrypted = this.props.mediaEventHelper?.media.isEncrypted;
|
||||||
const contentUrl = this.getContentUrl();
|
const contentUrl = this.getContentUrl();
|
||||||
const fileSize = this.content.info ? this.content.info.size : null;
|
const fileSize = this.content.info ? this.content.info.size : null;
|
||||||
const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
|
const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
|
||||||
|
|
|
@ -47,6 +47,7 @@ interface IState {
|
||||||
};
|
};
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
showImage: boolean;
|
showImage: boolean;
|
||||||
|
placeholder: 'no-image' | 'blurhash';
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MImageBody")
|
@replaceableComponent("views.messages.MImageBody")
|
||||||
|
@ -68,6 +69,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
loadedImageDimensions: null,
|
loadedImageDimensions: null,
|
||||||
hover: false,
|
hover: false,
|
||||||
showImage: SettingsStore.getValue("showImages"),
|
showImage: SettingsStore.getValue("showImages"),
|
||||||
|
placeholder: 'no-image',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,6 +279,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
this.downloadImage();
|
this.downloadImage();
|
||||||
this.setState({ showImage: true });
|
this.setState({ showImage: true });
|
||||||
} // else don't download anything because we don't want to display anything.
|
} // else don't download anything because we don't want to display anything.
|
||||||
|
|
||||||
|
// Add a 150ms timer for blurhash to first appear.
|
||||||
|
if (this.media.isEncrypted) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.state.imgLoaded || !this.state.imgError) {
|
||||||
|
this.setState({
|
||||||
|
placeholder: 'blurhash',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -434,7 +447,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||||
if (blurhash) return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
|
|
||||||
|
if (blurhash) {
|
||||||
|
if (this.state.placeholder === 'no-image') {
|
||||||
|
return <div className="mx_no-image-placeholder" style={{ width: width, height: height }} />;
|
||||||
|
} else if (this.state.placeholder === 'blurhash') {
|
||||||
|
return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<InlineSpinner w={32} h={32} />
|
<InlineSpinner w={32} h={32} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -51,6 +51,7 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
|
||||||
private onBugReport = (): void => {
|
private onBugReport = (): void => {
|
||||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
|
||||||
label: 'react-soft-crash-tile',
|
label: 'react-soft-crash-tile',
|
||||||
|
error: this.state.error,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -907,13 +907,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||||
|
const eventType = this.props.mxEvent.getType() as EventType;
|
||||||
const { tileHandler, isBubbleMessage, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
|
const { tileHandler, isBubbleMessage, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
|
||||||
|
|
||||||
// This shouldn't happen: the caller should check we support this type
|
// This shouldn't happen: the caller should check we support this type
|
||||||
// before trying to instantiate us
|
// before trying to instantiate us
|
||||||
if (!tileHandler) {
|
if (!tileHandler) {
|
||||||
const { mxEvent } = this.props;
|
const { mxEvent } = this.props;
|
||||||
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
console.warn(`Event type not supported: type:${eventType} isState:${mxEvent.isState()}`);
|
||||||
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
||||||
<div className="mx_EventTile_line">
|
<div className="mx_EventTile_line">
|
||||||
{ _t('This event could not be displayed') }
|
{ _t('This event could not be displayed') }
|
||||||
|
@ -937,7 +938,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
mx_EventTile_sending: !isEditing && isSending,
|
mx_EventTile_sending: !isEditing && isSending,
|
||||||
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
|
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
|
||||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||||
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
mx_EventTile_continuation: (
|
||||||
|
(this.props.tileShape ? '' : this.props.continuation) ||
|
||||||
|
eventType === EventType.CallInvite
|
||||||
|
),
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
|
@ -985,7 +989,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
needsSenderProfile = true;
|
needsSenderProfile = true;
|
||||||
} else if (
|
} else if (
|
||||||
(this.props.continuation && this.props.tileShape !== TileShape.FileGrid) ||
|
(this.props.continuation && this.props.tileShape !== TileShape.FileGrid) ||
|
||||||
this.props.mxEvent.getType() === EventType.CallInvite
|
eventType === EventType.CallInvite
|
||||||
) {
|
) {
|
||||||
// no avatar or sender profile for continuation messages and call tiles
|
// no avatar or sender profile for continuation messages and call tiles
|
||||||
avatarSize = 0;
|
avatarSize = 0;
|
||||||
|
|
|
@ -268,7 +268,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
"If you've submitted a bug via GitHub, debug logs can help " +
|
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||||
"us track down the problem. Debug logs contain application " +
|
"us track down the problem. Debug logs contain application " +
|
||||||
"usage data including your username, the IDs or aliases of " +
|
"usage data including your username, the IDs or aliases of " +
|
||||||
"the rooms or groups you have visited and the usernames of " +
|
"the rooms or groups you have visited, which UI elements you " +
|
||||||
|
"last interacted with, and the usernames of " +
|
||||||
"other users. They do not contain messages.",
|
"other users. They do not contain messages.",
|
||||||
) }
|
) }
|
||||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||||
|
|
|
@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -43,6 +52,7 @@ import IconizedContextMenu, {
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
const useSpaces = (): [Room[], Room[], Room | null] => {
|
const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||||
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
||||||
|
@ -206,6 +216,11 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
|
||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
const ref = useRef<HTMLUListElement>();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
||||||
|
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
|
@ -280,6 +295,7 @@ const SpacePanel = () => {
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
role="tree"
|
role="tree"
|
||||||
aria-label={_t("Spaces")}
|
aria-label={_t("Spaces")}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<Droppable droppableId="top-level-spaces">
|
<Droppable droppableId="top-level-spaces">
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default class AudioFeed extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private playMedia() {
|
private async playMedia() {
|
||||||
const element = this.element.current;
|
const element = this.element.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
|
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
|
||||||
|
@ -90,7 +90,7 @@ export default class AudioFeed extends React.Component<IProps, IState> {
|
||||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||||
// them with another load() which will cancel the pending one, but since we don't call
|
// them with another load() which will cancel the pending one, but since we don't call
|
||||||
// load() explicitly, it shouldn't be a problem. - Dave
|
// load() explicitly, it shouldn't be a problem. - Dave
|
||||||
element.play();
|
await element.load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default class AudioFeedArrayForCall extends React.Component<IProps, IStat
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
feeds: [],
|
feeds: this.props.call.getRemoteFeeds(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private playMedia() {
|
private async playMedia() {
|
||||||
const element = this.element;
|
const element = this.element;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
// We play audio in AudioFeed, not here
|
// We play audio in AudioFeed, not here
|
||||||
|
@ -129,7 +129,7 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
|
||||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||||
// them with another load() which will cancel the pending one, but since we don't call
|
// them with another load() which will cancel the pending one, but since we don't call
|
||||||
// load() explicitly, it shouldn't be a problem. - Dave
|
// load() explicitly, it shouldn't be a problem. - Dave
|
||||||
element.play();
|
await element.play();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1292,7 +1292,7 @@
|
||||||
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.",
|
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.",
|
||||||
"Chat with %(brand)s Bot": "Chat with %(brand)s Bot",
|
"Chat with %(brand)s Bot": "Chat with %(brand)s Bot",
|
||||||
"Bug reporting": "Bug reporting",
|
"Bug reporting": "Bug reporting",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
|
||||||
"Submit debug logs": "Submit debug logs",
|
"Submit debug logs": "Submit debug logs",
|
||||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.",
|
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.",
|
||||||
"Help & About": "Help & About",
|
"Help & About": "Help & About",
|
||||||
|
@ -2171,7 +2171,7 @@
|
||||||
"Failed to send logs: ": "Failed to send logs: ",
|
"Failed to send logs: ": "Failed to send logs: ",
|
||||||
"Preparing to download logs": "Preparing to download logs",
|
"Preparing to download logs": "Preparing to download logs",
|
||||||
"Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Reminder: Your browser is unsupported, so your experience may be unpredictable.",
|
"Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Reminder: Your browser is unsupported, so your experience may be unpredictable.",
|
||||||
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
|
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
|
||||||
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.",
|
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.",
|
||||||
"Download logs": "Download logs",
|
"Download logs": "Download logs",
|
||||||
"GitHub issue": "GitHub issue",
|
"GitHub issue": "GitHub issue",
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
import { TimelineIndex, TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
import { IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
|
import { IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
|
||||||
|
|
||||||
|
@ -859,13 +859,27 @@ export default class EventIndex extends EventEmitter {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const paginationMethod = async (timelineWindow, timeline, room, direction, limit) => {
|
const paginationMethod = async (
|
||||||
const timelineSet = timelineWindow._timelineSet;
|
timelineWindow: TimelineWindow,
|
||||||
const token = timeline.timeline.getPaginationToken(direction);
|
timelineIndex: TimelineIndex,
|
||||||
|
room: Room,
|
||||||
|
direction: Direction,
|
||||||
|
limit: number,
|
||||||
|
) => {
|
||||||
|
const timeline = timelineIndex.timeline;
|
||||||
|
const timelineSet = timeline.getTimelineSet();
|
||||||
|
const token = timeline.getPaginationToken(direction);
|
||||||
|
|
||||||
const ret = await this.populateFileTimeline(timelineSet, timeline.timeline, room, limit, token, direction);
|
const ret = await this.populateFileTimeline(
|
||||||
|
timelineSet,
|
||||||
|
timeline,
|
||||||
|
room,
|
||||||
|
limit,
|
||||||
|
token,
|
||||||
|
direction,
|
||||||
|
);
|
||||||
|
|
||||||
timeline.pendingPaginate = null;
|
timelineIndex.pendingPaginate = null;
|
||||||
timelineWindow.extend(direction, limit);
|
timelineWindow.extend(direction, limit);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
229
src/sentry.ts
Normal file
229
src/sentry.ts
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import PlatformPeg from "./PlatformPeg";
|
||||||
|
import SdkConfig from "./SdkConfig";
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
type StorageContext = {
|
||||||
|
storageManager_persisted?: string;
|
||||||
|
storageManager_quota?: string;
|
||||||
|
storageManager_usage?: string;
|
||||||
|
storageManager_usageDetails?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserContext = {
|
||||||
|
username: string;
|
||||||
|
enabled_labs: string;
|
||||||
|
low_bandwidth: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CryptoContext = {
|
||||||
|
device_keys?: string;
|
||||||
|
cross_signing_ready?: string;
|
||||||
|
cross_signing_supported_by_hs?: string;
|
||||||
|
cross_signing_key?: string;
|
||||||
|
cross_signing_privkey_in_secret_storage?: string;
|
||||||
|
cross_signing_master_privkey_cached?: string;
|
||||||
|
cross_signing_user_signing_privkey_cached?: string;
|
||||||
|
secret_storage_ready?: string;
|
||||||
|
secret_storage_key_in_account?: string;
|
||||||
|
session_backup_key_in_secret_storage?: string;
|
||||||
|
session_backup_key_cached?: string;
|
||||||
|
session_backup_key_well_formed?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeviceContext = {
|
||||||
|
device_id: string;
|
||||||
|
mx_local_settings: string;
|
||||||
|
modernizr_missing_features?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Contexts = {
|
||||||
|
user: UserContext;
|
||||||
|
crypto: CryptoContext;
|
||||||
|
device: DeviceContext;
|
||||||
|
storage: StorageContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
async function getStorageContext(): Promise<StorageContext> {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
// add storage persistence/quota information
|
||||||
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
|
try {
|
||||||
|
result["storageManager_persisted"] = String(await navigator.storage.persisted());
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (document.hasStorageAccess) { // Safari
|
||||||
|
try {
|
||||||
|
result["storageManager_persisted"] = String(await document.hasStorageAccess());
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
if (navigator.storage && navigator.storage.estimate) {
|
||||||
|
try {
|
||||||
|
const estimate = await navigator.storage.estimate();
|
||||||
|
result["storageManager_quota"] = String(estimate.quota);
|
||||||
|
result["storageManager_usage"] = String(estimate.usage);
|
||||||
|
if (estimate.usageDetails) {
|
||||||
|
const usageDetails = [];
|
||||||
|
Object.keys(estimate.usageDetails).forEach(k => {
|
||||||
|
usageDetails.push(`${k}: ${String(estimate.usageDetails[k])}`);
|
||||||
|
});
|
||||||
|
result[`storageManager_usage`] = usageDetails.join(", ");
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserContext(client: MatrixClient): UserContext {
|
||||||
|
return {
|
||||||
|
"username": client.credentials.userId,
|
||||||
|
"enabled_labs": getEnabledLabs(),
|
||||||
|
"low_bandwidth": SettingsStore.getValue("lowBandwidth") ? "enabled" : "disabled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledLabs(): string {
|
||||||
|
const enabledLabs = SettingsStore.getFeatureSettingNames().filter(f => SettingsStore.getValue(f));
|
||||||
|
if (enabledLabs.length) {
|
||||||
|
return enabledLabs.join(", ");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> {
|
||||||
|
if (!client.isCryptoEnabled()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
||||||
|
}
|
||||||
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
const pkCache = client.getCrossSigningCacheCallbacks();
|
||||||
|
const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey();
|
||||||
|
|
||||||
|
return {
|
||||||
|
"device_keys": keys.join(', '),
|
||||||
|
"cross_signing_ready": String(await client.isCrossSigningReady()),
|
||||||
|
"cross_signing_supported_by_hs":
|
||||||
|
String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")),
|
||||||
|
"cross_signing_key": crossSigning.getId(),
|
||||||
|
"cross_signing_privkey_in_secret_storage": String(
|
||||||
|
!!(await crossSigning.isStoredInSecretStorage(secretStorage))),
|
||||||
|
"cross_signing_master_privkey_cached": String(
|
||||||
|
!!(pkCache && await pkCache.getCrossSigningKeyCache("master"))),
|
||||||
|
"cross_signing_user_signing_privkey_cached": String(
|
||||||
|
!!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"))),
|
||||||
|
"secret_storage_ready": String(await client.isSecretStorageReady()),
|
||||||
|
"secret_storage_key_in_account": String(!!(await secretStorage.hasKey())),
|
||||||
|
"session_backup_key_in_secret_storage": String(!!(await client.isKeyBackupKeyStored())),
|
||||||
|
"session_backup_key_cached": String(!!sessionBackupKeyFromCache),
|
||||||
|
"session_backup_key_well_formed": String(sessionBackupKeyFromCache instanceof Uint8Array),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeviceContext(client: MatrixClient): DeviceContext {
|
||||||
|
const result = {
|
||||||
|
"device_id": client?.deviceId,
|
||||||
|
"mx_local_settings": localStorage.getItem('mx_local_settings'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.Modernizr) {
|
||||||
|
const missingFeatures = Object.keys(window.Modernizr).filter(key => window.Modernizr[key] === false);
|
||||||
|
if (missingFeatures.length > 0) {
|
||||||
|
result["modernizr_missing_features"] = missingFeatures.join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContexts(): Promise<Contexts> {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
return {
|
||||||
|
"user": getUserContext(client),
|
||||||
|
"crypto": await getCryptoContext(client),
|
||||||
|
"device": getDeviceContext(client),
|
||||||
|
"storage": await getStorageContext(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendSentryReport(userText: string, issueUrl: string, error: Error): Promise<void> {
|
||||||
|
const sentryConfig = SdkConfig.get()["sentry"];
|
||||||
|
if (!sentryConfig) return;
|
||||||
|
|
||||||
|
const captureContext = {
|
||||||
|
"contexts": await getContexts(),
|
||||||
|
"extra": {
|
||||||
|
"user_text": userText,
|
||||||
|
"issue_url": issueUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there's no error and no issueUrl, the report will just produce non-grouped noise in Sentry, so don't
|
||||||
|
// upload it
|
||||||
|
if (error) {
|
||||||
|
Sentry.captureException(error, captureContext);
|
||||||
|
} else if (issueUrl) {
|
||||||
|
Sentry.captureMessage(`Issue: ${issueUrl}`, captureContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISentryConfig {
|
||||||
|
dsn: string;
|
||||||
|
environment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initSentry(sentryConfig: ISentryConfig): Promise<void> {
|
||||||
|
if (!sentryConfig) return;
|
||||||
|
const platform = PlatformPeg.get();
|
||||||
|
let appVersion = "unknown";
|
||||||
|
try {
|
||||||
|
appVersion = await platform.getAppVersion();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: sentryConfig.dsn,
|
||||||
|
release: `${platform.getHumanReadableName()}@${appVersion}`,
|
||||||
|
environment: sentryConfig.environment,
|
||||||
|
defaultIntegrations: false,
|
||||||
|
autoSessionTracking: false,
|
||||||
|
debug: true,
|
||||||
|
integrations: [
|
||||||
|
// specifically disable Integrations.GlobalHandlers, which hooks uncaught exceptions - we don't
|
||||||
|
// want to capture those at this stage, just explicit rageshakes
|
||||||
|
new Sentry.Integrations.InboundFilters(),
|
||||||
|
new Sentry.Integrations.FunctionToString(),
|
||||||
|
new Sentry.Integrations.Breadcrumbs(),
|
||||||
|
new Sentry.Integrations.UserAgent(),
|
||||||
|
new Sentry.Integrations.Dedupe(),
|
||||||
|
],
|
||||||
|
// Set to 1.0 which is reasonable if we're only submitting Rageshakes; will need to be set < 1.0
|
||||||
|
// if we collect more frequently.
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
}
|
|
@ -19,10 +19,12 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { throttle } from "lodash";
|
import { memoize, throttle } from "lodash";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import { mediaFromMxc } from "../customisations/Media";
|
import { mediaFromMxc } from "../customisations/Media";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { getDrawable } from "../utils/drawable";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
@ -137,6 +139,22 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||||
await this.updateState({ displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url });
|
await this.updateState({ displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async getAvatarBitmap(avatarSize = 32): Promise<CanvasImageSource> {
|
||||||
|
let avatarUrl = this.getHttpAvatarUrl(avatarSize);
|
||||||
|
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
||||||
|
if (settingBgMxc) {
|
||||||
|
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarUrl) {
|
||||||
|
return await this.buildBitmap(avatarUrl);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBitmap = memoize(getDrawable);
|
||||||
|
|
||||||
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||||
|
|
36
src/utils/drawable.ts
Normal file
36
src/utils/drawable.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an image using the best available method based on browser compatibility
|
||||||
|
* @param url the URL of the image to fetch
|
||||||
|
* @returns a canvas drawable object
|
||||||
|
*/
|
||||||
|
export async function getDrawable(url: string): Promise<CanvasImageSource> {
|
||||||
|
if ('createImageBitmap' in window) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
return await createImageBitmap(blob);
|
||||||
|
} else {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = (e) => reject(e);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
68
yarn.lock
68
yarn.lock
|
@ -1502,11 +1502,74 @@
|
||||||
tslib "^2.2.0"
|
tslib "^2.2.0"
|
||||||
webcrypto-core "^1.2.0"
|
webcrypto-core "^1.2.0"
|
||||||
|
|
||||||
|
"@sentry/browser@^6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.11.0.tgz#9e90bbc0488ebcdd1e67937d8d5b4f13c3f6dee0"
|
||||||
|
integrity sha512-Qr2QRA0t5/S9QQqxzYKvM9W8prvmiWuldfwRX4hubovXzcXLgUi4WK0/H612wSbYZ4dNAEcQbtlxFWJNN4wxdg==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/core" "6.11.0"
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
"@sentry/utils" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/core@6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.11.0.tgz#40e94043afcf6407a109be26655c77832c64e740"
|
||||||
|
integrity sha512-09TB+f3pqEq8LFahFWHO6I/4DxHo+NcS52OkbWMDqEi6oNZRD7PhPn3i14LfjsYVv3u3AESU8oxSEGbFrr2UjQ==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "6.11.0"
|
||||||
|
"@sentry/minimal" "6.11.0"
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
"@sentry/utils" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/hub@6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.11.0.tgz#ddf9ddb0577d1c8290dc02c0242d274fe84d6c16"
|
||||||
|
integrity sha512-pT9hf+ZJfVFpoZopoC+yJmFNclr4NPqPcl2cgguqCHb69DklD1NxgBNWK8D6X05qjnNFDF991U6t1mxP9HrGuw==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
"@sentry/utils" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/minimal@6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.11.0.tgz#806d5512658370e40827b3e3663061db708fff33"
|
||||||
|
integrity sha512-XkZ7qrdlGp4IM/gjGxf1Q575yIbl5RvPbg+WFeekpo16Ufvzx37Mr8c2xsZaWosISVyE6eyFpooORjUlzy8EDw==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "6.11.0"
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/tracing@^6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.11.0.tgz#9bd9287addea1ebc12c75b226f71c7713c0fac4f"
|
||||||
|
integrity sha512-9VA1/SY++WeoMQI4K6n/sYgIdRtCu9NLWqmGqu/5kbOtESYFgAt1DqSyqGCr00ZjQiC2s7tkDkTNZb38K6KytQ==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "6.11.0"
|
||||||
|
"@sentry/minimal" "6.11.0"
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
"@sentry/utils" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/types@6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.11.0.tgz#5122685478d32ddacd3a891cbcf550012df85f7c"
|
||||||
|
integrity sha512-gm5H9eZhL6bsIy/h3T+/Fzzz2vINhHhqd92CjHle3w7uXdTdFV98i2pDpErBGNTSNzbntqOMifYEB5ENtZAvcg==
|
||||||
|
|
||||||
"@sentry/types@^6.10.0":
|
"@sentry/types@^6.10.0":
|
||||||
version "6.10.0"
|
version "6.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1"
|
||||||
integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==
|
integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==
|
||||||
|
|
||||||
|
"@sentry/utils@6.11.0":
|
||||||
|
version "6.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.11.0.tgz#d1dee4faf4d9c42c54bba88d5a66fb96b902a14c"
|
||||||
|
integrity sha512-IOvyFHcnbRQxa++jO+ZUzRvFHEJ1cZjrBIQaNVc0IYF0twUOB5PTP6joTcix38ldaLeapaPZ9LGfudbvYvxkdg==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types" "6.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.8.3"
|
version "1.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
|
||||||
|
@ -2854,6 +2917,11 @@ content-type@^1.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||||
|
|
||||||
|
context-filter-polyfill@^0.2.4:
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/context-filter-polyfill/-/context-filter-polyfill-0.2.4.tgz#ecf88d3197e7c3a47e9a7ae2d5167b703945a5d4"
|
||||||
|
integrity sha512-LDZ3WiTzo6kIeJM7j8kPSgZf+gbD1cV1GaLyYO8RWvAg25cO3zUo3d2KizO0w9hAezNwz7tTbuWKpPdvLWzKqQ==
|
||||||
|
|
||||||
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue