Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into export-conversations
This commit is contained in:
commit
45ce352d02
86 changed files with 2202 additions and 1969 deletions
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @matrix-org/element-web
|
1
.node-version
Normal file
1
.node-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
14
|
|
@ -88,7 +88,7 @@
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"parse5": "^6.0.1",
|
"parse5": "^6.0.1",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.12.1",
|
"posthog-js": "1.12.2",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
|
@ -125,6 +125,7 @@
|
||||||
"@babel/traverse": "^7.12.12",
|
"@babel/traverse": "^7.12.12",
|
||||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||||
"@peculiar/webcrypto": "^1.1.4",
|
"@peculiar/webcrypto": "^1.1.4",
|
||||||
|
"@sentry/types": "^6.10.0",
|
||||||
"@sinonjs/fake-timers": "^7.0.2",
|
"@sinonjs/fake-timers": "^7.0.2",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
|
@ -151,13 +152,14 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||||
"@typescript-eslint/parser": "^4.17.0",
|
"@typescript-eslint/parser": "^4.17.0",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
|
"allchange": "github:matrix-org/allchange",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"eslint": "7.18.0",
|
"eslint": "7.18.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
|
@ -172,6 +174,7 @@
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
"rrweb-snapshot": "1.1.7",
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.9.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
|
@ -197,6 +200,7 @@
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
"^!!raw-loader!.*": "jest-raw-loader"
|
"^!!raw-loader!.*": "jest-raw-loader"
|
||||||
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!matrix-js-sdk).+$"
|
"/node_modules/(?!matrix-js-sdk).+$"
|
||||||
|
|
4
release_config.yaml
Normal file
4
release_config.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
subprojects:
|
||||||
|
matrix-js-sdk:
|
||||||
|
includeByDefault: false
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||||
@import "./views/settings/_EmailAddresses.scss";
|
@import "./views/settings/_EmailAddresses.scss";
|
||||||
@import "./views/settings/_IntegrationManager.scss";
|
@import "./views/settings/_IntegrationManager.scss";
|
||||||
|
@import "./views/settings/_LayoutSwitcher.scss";
|
||||||
@import "./views/settings/_Notifications.scss";
|
@import "./views/settings/_Notifications.scss";
|
||||||
@import "./views/settings/_PhoneNumbers.scss";
|
@import "./views/settings/_PhoneNumbers.scss";
|
||||||
@import "./views/settings/_ProfileSettings.scss";
|
@import "./views/settings/_ProfileSettings.scss";
|
||||||
|
@ -267,12 +268,14 @@
|
||||||
@import "./views/spaces/_SpacePublicShare.scss";
|
@import "./views/spaces/_SpacePublicShare.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
@import "./views/toasts/_AnalyticsToast.scss";
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
|
@import "./views/toasts/_IncomingCallToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallPreview.scss";
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
|
@import "./views/voip/_CallViewHeader.scss";
|
||||||
@import "./views/voip/_CallViewSidebar.scss";
|
@import "./views/voip/_CallViewSidebar.scss";
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
|
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
background-color: $dark-panel-bg-color;
|
background-color: $toast-bg-color;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $dark-panel-bg-color;
|
background-color: $toast-bg-color;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -85,7 +85,7 @@ limitations under the License.
|
||||||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,35 +24,33 @@ limitations under the License.
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_desktopCapturerSourcePicker_source {
|
.mx_desktopCapturerSourcePicker_source {
|
||||||
display: flex;
|
width: 50%;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
margin: 8px;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
width: 312px;
|
border-width: 2px;
|
||||||
border-width: 2px;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
border-style: solid;
|
||||||
border-style: solid;
|
border-color: transparent;
|
||||||
border-color: transparent;
|
|
||||||
|
|
||||||
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $accent-color;
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_desktopCapturerSourcePicker_source_name {
|
||||||
|
margin: 0 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_desktopCapturerSourcePicker_source_name {
|
|
||||||
margin: 0 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 312px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,7 +271,7 @@ limitations under the License.
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
|
|
|
@ -310,14 +310,12 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled {
|
.mx_RoomView_timeline_rr_enabled {
|
||||||
|
.mx_EventTile[data-layout=group] {
|
||||||
.mx_EventTile:not([data-layout=bubble]) {
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
margin-right: 110px;
|
margin-right: 110px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 2px solid $voice-record-stop-border-color;
|
border: 2px solid $voice-record-stop-border-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
margin-right: 16px; // between us and the send button
|
margin-right: 8px; // between us and the waveform component
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -46,9 +46,28 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadingState {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_failedState {
|
||||||
|
margin-right: 21px;
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadState_badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
||||||
// Note: remaining class properties are in the PlayerContainer CSS.
|
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||||
|
|
||||||
|
// fixed height to reduce layout jumps with the play button appearing
|
||||||
|
// https://github.com/vector-im/element-web/issues/18431
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
margin: 6px; // force the composer area to put a gutter around us
|
margin: 6px; // force the composer area to put a gutter around us
|
||||||
margin-right: 12px; // isolate from stop/send button
|
margin-right: 12px; // isolate from stop/send button
|
||||||
|
|
||||||
|
@ -68,7 +87,7 @@ limitations under the License.
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px; // 12px from the left edge for container padding
|
left: 12px; // 12px from the left edge for container padding
|
||||||
top: 18px; // vertically center (middle align with clock)
|
top: 17px; // vertically center (middle align with clock)
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
91
res/css/views/settings/_LayoutSwitcher.scss
Normal file
91
res/css/views/settings/_LayoutSwitcher.scss
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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_LayoutSwitcher {
|
||||||
|
.mx_LayoutSwitcher_RadioButtons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
> .mx_LayoutSwitcher_RadioButton {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
border: 1px solid $appearance-tab-border-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption,
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LayoutSwitcher_RadioButton_preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_content {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LayoutSwitcher_RadioButton_selected {
|
||||||
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
border-top: 1px solid $appearance-tab-border-color;
|
||||||
|
|
||||||
|
> input + div {
|
||||||
|
border-color: rgba($muted-fg-color, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_checked {
|
||||||
|
background-color: rgba($accent-color, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0;
|
||||||
|
&[data-layout=bubble] {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
&[data-layout=irc] {
|
||||||
|
> a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_line {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ProfileSettings_controls_topic {
|
.mx_ProfileSettings_controls_topic {
|
||||||
& > textarea {
|
& > textarea {
|
||||||
|
font-family: inherit;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -155,79 +155,6 @@ limitations under the License.
|
||||||
margin-left: calc($font-16px + 10px);
|
margin-left: calc($font-16px + 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
color: $primary-fg-color;
|
|
||||||
|
|
||||||
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 300px;
|
|
||||||
|
|
||||||
border: 1px solid $appearance-tab-border-color;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption,
|
|
||||||
.mx_MessageActionBar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Layout_RadioButton_preview {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
|
|
||||||
border-color: $accent-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton {
|
|
||||||
border-top: 1px solid $appearance-tab-border-color;
|
|
||||||
|
|
||||||
> input + div {
|
|
||||||
border-color: rgba($muted-fg-color, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton_checked {
|
|
||||||
background-color: rgba($accent-color, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile {
|
|
||||||
margin: 0;
|
|
||||||
&[data-layout=bubble] {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
&[data-layout=irc] {
|
|
||||||
> a {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mx_EventTile_line {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Advanced {
|
.mx_AppearanceUserSettingsTab_Advanced {
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
|
|
@ -28,28 +28,32 @@ limitations under the License.
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken {
|
.mx_HelpUserSettingsTab_copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: solid 1px $light-fg-color;
|
border: solid 1px $light-fg-color;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
width: max-content;
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken_copy {
|
.mx_HelpUserSettingsTab_copyButton {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
width: 20px;
|
||||||
margin-left: 20px;
|
height: 20px;
|
||||||
display: inherit;
|
cursor: pointer;
|
||||||
}
|
margin-left: 20px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
&::before {
|
||||||
mask-image: url($copy-button-url);
|
content: "";
|
||||||
background-color: $message-action-bar-fg-color;
|
|
||||||
margin-left: 5px;
|
mask-image: url($copy-button-url);
|
||||||
width: 20px;
|
background-color: $message-action-bar-fg-color;
|
||||||
height: 20px;
|
width: 20px;
|
||||||
background-repeat: no-repeat;
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
149
res/css/views/toasts/_IncomingCallToast.scss
Normal file
149
res/css/views/toasts/_IncomingCallToast.scss
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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_IncomingCallToast {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
pointer-events: initial; // restore pointer events so the user can accept/decline
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.mx_CallEvent_caller {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_type {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mx_CallEvent_type_icon {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_IncomingCallToast_content_voice {
|
||||||
|
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
||||||
|
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_IncomingCallToast_content_video {
|
||||||
|
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
||||||
|
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_buttons {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_button {
|
||||||
|
height: 24px;
|
||||||
|
padding: 0px 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
background-color: $button-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_IncomingCallToast_button_accept span::before {
|
||||||
|
mask-size: 13px;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_IncomingCallToast_button_decline span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||||
|
mask-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_iconButton {
|
||||||
|
display: flex;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_silence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/silence.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallToast_unSilence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CallPreview {
|
.mx_CallPreview {
|
||||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
@ -43,84 +42,4 @@ limitations under the License.
|
||||||
.mx_AppTile_persistedWrapper div {
|
.mx_AppTile_persistedWrapper div {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IncomingCallBox {
|
|
||||||
min-width: 250px;
|
|
||||||
background-color: $voipcall-plinth-color;
|
|
||||||
padding: 8px;
|
|
||||||
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
pointer-events: initial; // restore pointer events so the user can accept/decline
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_IncomingCallBox_CallerInfo {
|
|
||||||
display: flex;
|
|
||||||
direction: row;
|
|
||||||
|
|
||||||
img, .mx_BaseAvatar_initial {
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, p {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
font-size: $font-14px;
|
|
||||||
line-height: $font-16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_IncomingCallBox_buttons {
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
> .mx_IncomingCallBox_spacer {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 0;
|
|
||||||
font-size: $font-15px;
|
|
||||||
line-height: $font-24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_IncomingCallBox_iconButton {
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background-color: $icon-button-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_IncomingCallBox_silence::before {
|
|
||||||
mask-image: url('$(res)/img/voip/silence.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_IncomingCallBox_unSilence::before {
|
|
||||||
mask-image: url('$(res)/img/voip/un-silence.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
||||||
.mx_CallView_pip {
|
.mx_CallView_pip {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background-color: $voipcall-plinth-color;
|
background-color: $toast-bg-color;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
@ -75,8 +75,6 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
// We don't want to collide with the call controls that have 52px of height
|
|
||||||
margin-bottom: 52px;
|
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -201,119 +199,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: left;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callType {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_secondaryCallInfo {
|
|
||||||
&::before {
|
|
||||||
content: '·';
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_controls {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_fullscreen {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_expand {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callInfo {
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_roomName {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_secondaryCall_roomName {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeSmall {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeIcon {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_voice::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_video::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls {
|
.mx_CallView_callControls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
129
res/css/views/voip/_CallViewHeader.scss
Normal file
129
res/css/views/voip/_CallViewHeader.scss
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_CallViewHeader {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callType {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_secondaryCallInfo {
|
||||||
|
&::before {
|
||||||
|
content: '·';
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_fullscreen {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_expand {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callInfo {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_roomName {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_secondaryCall_roomName {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeSmall {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeIcon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_voice::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_video::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,8 +40,6 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
<path d="m11.068 2c-0.32021 4.772e-4 -0.66852 0.17244-0.96484 0.46875-2.5464 2.5435-5.0905 5.0892-7.6348 7.6348-0.79016 0.7902-0.69302 1.9462 1.1641 1.9707 1.855 0.02447 3.4407-0.56671 3.8281-0.69141l2.4355 3.1445c-0.83503 1.9462-0.86902 4.062-0.058594 5.7949 0.47213 1.0095 1.79 1.0049 2.5781 0.2168l3.2773-3.2773 2.8223 2.8223c1.491 1.491 3.2644 2.0696 3.4512 1.8828s-0.39181-1.9602-1.8828-3.4512l-2.8223-2.8223 3.2773-3.2773c0.788-0.788 0.79075-2.106-0.21875-2.5781-1.733-0.81044-3.8468-0.77643-5.793 0.058594l-3.1445-2.4355c0.1247-0.38742 0.71588-1.9731 0.69141-3.8281-0.015311-1.1607-0.47217-1.6336-1.0059-1.6328z" fill="#737d8c"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 744 B |
|
@ -1,3 +1,6 @@
|
||||||
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
|
$system-dark: #21262C;
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
$bg-color: #15191E;
|
$bg-color: #15191E;
|
||||||
|
@ -47,7 +50,7 @@ $inverted-bg-color: $base-color;
|
||||||
$selected-color: $room-highlight-color;
|
$selected-color: $room-highlight-color;
|
||||||
|
|
||||||
// selected for hoverover & selected event tiles
|
// selected for hoverover & selected event tiles
|
||||||
$event-selected-color: #21262c;
|
$event-selected-color: $system-dark;
|
||||||
|
|
||||||
// used for the hairline dividers in RoomView
|
// used for the hairline dividers in RoomView
|
||||||
$primary-hairline-color: transparent;
|
$primary-hairline-color: transparent;
|
||||||
|
@ -91,7 +94,7 @@ $lightbox-background-bg-color: #000;
|
||||||
$lightbox-background-bg-opacity: 0.85;
|
$lightbox-background-bg-opacity: 0.85;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: #21262c;
|
$settings-profile-placeholder-bg-color: $system-dark;
|
||||||
$settings-profile-overlay-placeholder-fg-color: #454545;
|
$settings-profile-overlay-placeholder-fg-color: #454545;
|
||||||
$settings-profile-button-bg-color: #e7e7e7;
|
$settings-profile-button-bg-color: #e7e7e7;
|
||||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||||
|
@ -112,8 +115,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$quinary-content-color: #394049;
|
||||||
$voipcall-plinth-color: #394049;
|
$toast-bg-color: $quinary-content-color;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -175,7 +178,7 @@ $button-link-bg-color: transparent;
|
||||||
$togglesw-off-color: $room-highlight-color;
|
$togglesw-off-color: $room-highlight-color;
|
||||||
|
|
||||||
$progressbar-fg-color: $accent-color;
|
$progressbar-fg-color: $accent-color;
|
||||||
$progressbar-bg-color: #21262c;
|
$progressbar-bg-color: $system-dark;
|
||||||
|
|
||||||
$visual-bell-bg-color: #800;
|
$visual-bell-bg-color: #800;
|
||||||
|
|
||||||
|
@ -210,7 +213,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #21262C; // "System Dark"
|
$message-body-panel-icon-bg-color: $system-dark; // "System Dark"
|
||||||
|
|
||||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
|
@ -228,9 +231,9 @@ $groupFilterPanel-background-blur-amount: 30px;
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
$eventbubble-self-bg: #143A34;
|
$eventbubble-self-bg: #14322E;
|
||||||
$eventbubble-others-bg: #394049;
|
$eventbubble-others-bg: $event-selected-color;
|
||||||
$eventbubble-bg-hover: #433C23;
|
$eventbubble-bg-hover: #1C2026;
|
||||||
$eventbubble-avatar-outline: $bg-color;
|
$eventbubble-avatar-outline: $bg-color;
|
||||||
$eventbubble-reply-color: #C1C6CD;
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
|
|
|
@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$quinary-content-color: #394049;
|
||||||
$voipcall-plinth-color: #394049;
|
$toast-bg-color: $quinary-content-color;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -222,6 +222,13 @@ $appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #14322E;
|
||||||
|
$eventbubble-others-bg: $event-selected-color;
|
||||||
|
$eventbubble-bg-hover: #1C2026;
|
||||||
|
$eventbubble-avatar-outline: $bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji';
|
$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
|
$system-light: #F4F6FA;
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
|
@ -178,8 +181,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$composer-e2e-icon-color: #91a1c0;
|
$composer-e2e-icon-color: #91a1c0;
|
||||||
$header-divider-color: #91a1c0;
|
$header-divider-color: #91a1c0;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$toast-bg-color: $system-light;
|
||||||
$voipcall-plinth-color: #F4F6FA;
|
$voipcall-plinth-color: $system-light;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -331,7 +334,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0;
|
$message-body-panel-bg-color: #E3E8F0;
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #F4F6FA;
|
$message-body-panel-icon-bg-color: $system-light;
|
||||||
|
|
||||||
// See non-legacy _light for variable information
|
// See non-legacy _light for variable information
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
|
@ -348,9 +351,9 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
$eventbubble-self-bg: #F8FDFC;
|
$eventbubble-self-bg: #F0FBF8;
|
||||||
$eventbubble-others-bg: #F7F8F9;
|
$eventbubble-others-bg: $system-light;
|
||||||
$eventbubble-bg-hover: rgb(242, 242, 242);
|
$eventbubble-bg-hover: #FAFBFD;
|
||||||
$eventbubble-avatar-outline: #fff;
|
$eventbubble-avatar-outline: #fff;
|
||||||
$eventbubble-reply-color: #C1C6CD;
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
|
@ -390,7 +393,7 @@ $eventbubble-reply-color: #C1C6CD;
|
||||||
@define-mixin mx_DialogButton_secondary {
|
@define-mixin mx_DialogButton_secondary {
|
||||||
// flip colours for the secondary ones
|
// flip colours for the secondary ones
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 1px solid $accent-color ! important;
|
border: 1px solid $accent-color !important;
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
background-color: $button-secondary-bg-color;
|
background-color: $button-secondary-bg-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,3 +140,10 @@ $event-highlight-bg-color: var(--timeline-highlights-color);
|
||||||
//
|
//
|
||||||
// redirect some variables away from their hardcoded values in the light theme
|
// redirect some variables away from their hardcoded values in the light theme
|
||||||
$settings-grey-fg-color: $primary-fg-color;
|
$settings-grey-fg-color: $primary-fg-color;
|
||||||
|
|
||||||
|
// --eventbubble colors
|
||||||
|
$eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg);
|
||||||
|
$eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg);
|
||||||
|
$eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover);
|
||||||
|
$eventbubble-avatar-outline: var(--eventbubble-avatar-outline, $eventbubble-avatar-outline);
|
||||||
|
$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-color);
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji';
|
$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
|
$system-light: #F4F6FA;
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
|
@ -138,7 +141,7 @@ $blockquote-bar-color: #ddd;
|
||||||
$blockquote-fg-color: #777;
|
$blockquote-fg-color: #777;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: #f4f6fa;
|
$settings-profile-placeholder-bg-color: $system-light;
|
||||||
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
||||||
$settings-profile-button-bg-color: #e7e7e7;
|
$settings-profile-button-bg-color: #e7e7e7;
|
||||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||||
|
@ -167,8 +170,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$composer-e2e-icon-color: #91A1C0;
|
$composer-e2e-icon-color: #91A1C0;
|
||||||
$header-divider-color: #91A1C0;
|
$header-divider-color: #91A1C0;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$toast-bg-color: $system-light;
|
||||||
$voipcall-plinth-color: #F4F6FA;
|
$voipcall-plinth-color: $system-light;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -327,7 +330,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #F4F6FA;
|
$message-body-panel-icon-bg-color: $system-light;
|
||||||
|
|
||||||
// These two don't change between themes. They are the $warning-color, but we don't
|
// These two don't change between themes. They are the $warning-color, but we don't
|
||||||
// want custom themes to affect them by accident.
|
// want custom themes to affect them by accident.
|
||||||
|
@ -350,9 +353,9 @@ $groupFilterPanel-background-blur-amount: 20px;
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
$eventbubble-self-bg: #F8FDFC;
|
$eventbubble-self-bg: #F0FBF8;
|
||||||
$eventbubble-others-bg: #F7F8F9;
|
$eventbubble-others-bg: $system-light;
|
||||||
$eventbubble-bg-hover: #FEFCF5;
|
$eventbubble-bg-hover: #FAFBFD;
|
||||||
$eventbubble-avatar-outline: $primary-bg-color;
|
$eventbubble-avatar-outline: $primary-bg-color;
|
||||||
$eventbubble-reply-color: #C1C6CD;
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
|
@ -392,7 +395,7 @@ $eventbubble-reply-color: #C1C6CD;
|
||||||
@define-mixin mx_DialogButton_secondary {
|
@define-mixin mx_DialogButton_secondary {
|
||||||
// flip colours for the secondary ones
|
// flip colours for the secondary ones
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 1px solid $accent-color ! important;
|
border: 1px solid $accent-color !important;
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
background-color: $button-secondary-bg-color;
|
background-color: $button-secondary-bg-color;
|
||||||
}
|
}
|
||||||
|
|
748
src/@types/posthog.d.ts
vendored
748
src/@types/posthog.d.ts
vendored
|
@ -1,748 +0,0 @@
|
||||||
// A clone of the type definitions from posthog-js, stripped of references to transitive
|
|
||||||
// dependencies which we don't actually use, so that we don't need to install them.
|
|
||||||
//
|
|
||||||
// Original file lives in node_modules/posthog/dist/module.d.ts
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/member-delimiter-style */
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
// Type definitions for exported methods
|
|
||||||
|
|
||||||
declare class posthog {
|
|
||||||
/**
|
|
||||||
* This function initializes a new instance of the PostHog capturing object.
|
|
||||||
* All new instances are added to the main posthog object as sub properties (such as
|
|
||||||
* posthog.library_name) and also returned by this function. To define a
|
|
||||||
* second instance on the page, you would call:
|
|
||||||
*
|
|
||||||
* posthog.init('new token', { your: 'config' }, 'library_name');
|
|
||||||
*
|
|
||||||
* and use it like so:
|
|
||||||
*
|
|
||||||
* posthog.library_name.capture(...);
|
|
||||||
*
|
|
||||||
* @param {String} token Your PostHog API token
|
|
||||||
* @param {Object} [config] A dictionary of config options to override. <a href="https://github.com/posthog/posthog-js/blob/6e0e873/src/posthog-core.js#L57-L91">See a list of default config options</a>.
|
|
||||||
* @param {String} [name] The name for the new posthog instance that you want created
|
|
||||||
*/
|
|
||||||
static init(token: string, config?: posthog.Config, name?: string): posthog
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears super properties and generates a new random distinct_id for this instance.
|
|
||||||
* Useful for clearing data when a user logs out.
|
|
||||||
*/
|
|
||||||
static reset(reset_device_id?: boolean): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture an event. This is the most important and
|
|
||||||
* frequently used PostHog function.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* // capture an event named 'Registered'
|
|
||||||
* posthog.capture('Registered', {'Gender': 'Male', 'Age': 21});
|
|
||||||
*
|
|
||||||
* // capture an event using navigator.sendBeacon
|
|
||||||
* posthog.capture('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'});
|
|
||||||
*
|
|
||||||
* @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
|
|
||||||
* @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
|
|
||||||
* @param {Object} [options] Optional configuration for this capture request.
|
|
||||||
* @param {String} [options.transport] Transport method for network request ('XHR' or 'sendBeacon').
|
|
||||||
*/
|
|
||||||
static capture(
|
|
||||||
event_name: string,
|
|
||||||
properties?: posthog.Properties,
|
|
||||||
options?: { transport: 'XHR' | 'sendBeacon' }
|
|
||||||
): posthog.CaptureResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture a page view event, which is currently ignored by the server.
|
|
||||||
* This function is called by default on page load unless the
|
|
||||||
* capture_pageview configuration variable is false.
|
|
||||||
*
|
|
||||||
* @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
static capture_pageview(page?: string): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a set of super properties, which are included with all
|
|
||||||
* events. This will overwrite previous super property values.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* // register 'Gender' as a super property
|
|
||||||
* posthog.register({'Gender': 'Female'});
|
|
||||||
*
|
|
||||||
* // register several super properties when a user signs up
|
|
||||||
* posthog.register({
|
|
||||||
* 'Email': 'jdoe@example.com',
|
|
||||||
* 'Account Type': 'Free'
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* @param {Object} properties An associative array of properties to store about the user
|
|
||||||
* @param {Number} [days] How many days since the user's last visit to store the super properties
|
|
||||||
*/
|
|
||||||
static register(properties: posthog.Properties, days?: number): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a set of super properties only once. This will not
|
|
||||||
* overwrite previous super property values, unlike register().
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* // register a super property for the first time only
|
|
||||||
* posthog.register_once({
|
|
||||||
* 'First Login Date': new Date().toISOString()
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* ### Notes:
|
|
||||||
*
|
|
||||||
* If default_value is specified, current super properties
|
|
||||||
* with that value will be overwritten.
|
|
||||||
*
|
|
||||||
* @param {Object} properties An associative array of properties to store about the user
|
|
||||||
* @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
|
|
||||||
* @param {Number} [days] How many days since the users last visit to store the super properties
|
|
||||||
*/
|
|
||||||
static register_once(properties: posthog.Properties, default_value?: posthog.Property, days?: number): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a super property stored with the current user.
|
|
||||||
*
|
|
||||||
* @param {String} property The name of the super property to remove
|
|
||||||
*/
|
|
||||||
static unregister(property: string): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identify a user with a unique ID instead of a PostHog
|
|
||||||
* randomly generated distinct_id. If the method is never called,
|
|
||||||
* then unique visitors will be identified by a UUID generated
|
|
||||||
* the first time they visit the site.
|
|
||||||
*
|
|
||||||
* If user properties are passed, they are also sent to posthog.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.identify('[user unique id]')
|
|
||||||
* posthog.identify('[user unique id]', { email: 'john@example.com' })
|
|
||||||
* posthog.identify('[user unique id]', {}, { referral_code: '12345' })
|
|
||||||
*
|
|
||||||
* ### Notes:
|
|
||||||
*
|
|
||||||
* You can call this function to overwrite a previously set
|
|
||||||
* unique ID for the current user. PostHog cannot translate
|
|
||||||
* between IDs at this time, so when you change a user's ID
|
|
||||||
* they will appear to be a new user.
|
|
||||||
*
|
|
||||||
* When used alone, posthog.identify will change the user's
|
|
||||||
* distinct_id to the unique ID provided. When used in tandem
|
|
||||||
* with posthog.alias, it will allow you to identify based on
|
|
||||||
* unique ID and map that back to the original, anonymous
|
|
||||||
* distinct_id given to the user upon her first arrival to your
|
|
||||||
* site (thus connecting anonymous pre-signup activity to
|
|
||||||
* post-signup activity). Though the two work together, do not
|
|
||||||
* call identify() at the same time as alias(). Calling the two
|
|
||||||
* at the same time can cause a race condition, so it is best
|
|
||||||
* practice to call identify on the original, anonymous ID
|
|
||||||
* right after you've aliased it.
|
|
||||||
*
|
|
||||||
* @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used.
|
|
||||||
* @param {Object} [userProperties] Optional: An associative array of properties to store about the user
|
|
||||||
* @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value.
|
|
||||||
*/
|
|
||||||
static identify(
|
|
||||||
unique_id?: string,
|
|
||||||
userPropertiesToSet?: posthog.Properties,
|
|
||||||
userPropertiesToSetOnce?: posthog.Properties
|
|
||||||
): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an alias, which PostHog will use to link two distinct_ids going forward (not retroactively).
|
|
||||||
* Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the
|
|
||||||
* following is a valid scenario:
|
|
||||||
*
|
|
||||||
* posthog.alias('new_id', 'existing_id');
|
|
||||||
* ...
|
|
||||||
* posthog.alias('newer_id', 'new_id');
|
|
||||||
*
|
|
||||||
* If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
|
|
||||||
*
|
|
||||||
* ### Notes:
|
|
||||||
*
|
|
||||||
* The best practice is to call alias() when a unique ID is first created for a user
|
|
||||||
* (e.g., when a user first registers for an account and provides an email address).
|
|
||||||
* alias() should never be called more than once for a given user, except to
|
|
||||||
* chain a newer ID to a previously new ID, as described above.
|
|
||||||
*
|
|
||||||
* @param {String} alias A unique identifier that you want to use for this user in the future.
|
|
||||||
* @param {String} [original] The current identifier being used for this user.
|
|
||||||
*/
|
|
||||||
static alias(alias: string, original?: string): posthog.CaptureResult | number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the configuration of a posthog library instance.
|
|
||||||
*
|
|
||||||
* The default config is:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* // HTTP method for capturing requests
|
|
||||||
* api_method: 'POST'
|
|
||||||
*
|
|
||||||
* // transport for sending requests ('XHR' or 'sendBeacon')
|
|
||||||
* // NB: sendBeacon should only be used for scenarios such as
|
|
||||||
* // page unload where a "best-effort" attempt to send is
|
|
||||||
* // acceptable; the sendBeacon API does not support callbacks
|
|
||||||
* // or any way to know the result of the request. PostHog
|
|
||||||
* // capturing via sendBeacon will not support any event-
|
|
||||||
* // batching or retry mechanisms.
|
|
||||||
* api_transport: 'XHR'
|
|
||||||
*
|
|
||||||
* // Automatically capture clicks, form submissions and change events
|
|
||||||
* autocapture: true
|
|
||||||
*
|
|
||||||
* // Capture rage clicks (beta) - useful for session recording
|
|
||||||
* rageclick: false
|
|
||||||
*
|
|
||||||
* // super properties cookie expiration (in days)
|
|
||||||
* cookie_expiration: 365
|
|
||||||
*
|
|
||||||
* // super properties span subdomains
|
|
||||||
* cross_subdomain_cookie: true
|
|
||||||
*
|
|
||||||
* // debug mode
|
|
||||||
* debug: false
|
|
||||||
*
|
|
||||||
* // if this is true, the posthog cookie or localStorage entry
|
|
||||||
* // will be deleted, and no user persistence will take place
|
|
||||||
* disable_persistence: false
|
|
||||||
*
|
|
||||||
* // if this is true, PostHog will automatically determine
|
|
||||||
* // City, Region and Country data using the IP address of
|
|
||||||
* //the client
|
|
||||||
* ip: true
|
|
||||||
*
|
|
||||||
* // opt users out of capturing by this PostHog instance by default
|
|
||||||
* opt_out_capturing_by_default: false
|
|
||||||
*
|
|
||||||
* // opt users out of browser data storage by this PostHog instance by default
|
|
||||||
* opt_out_persistence_by_default: false
|
|
||||||
*
|
|
||||||
* // persistence mechanism used by opt-in/opt-out methods - cookie
|
|
||||||
* // or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* opt_out_capturing_persistence_type: 'localStorage'
|
|
||||||
*
|
|
||||||
* // customize the name of cookie/localStorage set by opt-in/opt-out methods
|
|
||||||
* opt_out_capturing_cookie_prefix: null
|
|
||||||
*
|
|
||||||
* // type of persistent store for super properties (cookie/
|
|
||||||
* // localStorage) if set to 'localStorage', any existing
|
|
||||||
* // posthog cookie value with the same persistence_name
|
|
||||||
* // will be transferred to localStorage and deleted
|
|
||||||
* persistence: 'cookie'
|
|
||||||
*
|
|
||||||
* // name for super properties persistent store
|
|
||||||
* persistence_name: ''
|
|
||||||
*
|
|
||||||
* // names of properties/superproperties which should never
|
|
||||||
* // be sent with capture() calls
|
|
||||||
* property_blacklist: []
|
|
||||||
*
|
|
||||||
* // if this is true, posthog cookies will be marked as
|
|
||||||
* // secure, meaning they will only be transmitted over https
|
|
||||||
* secure_cookie: false
|
|
||||||
*
|
|
||||||
* // should we capture a page view on page load
|
|
||||||
* capture_pageview: true
|
|
||||||
*
|
|
||||||
* // if you set upgrade to be true, the library will check for
|
|
||||||
* // a cookie from our old js library and import super
|
|
||||||
* // properties from it, then the old cookie is deleted
|
|
||||||
* // The upgrade config option only works in the initialization,
|
|
||||||
* // so make sure you set it when you create the library.
|
|
||||||
* upgrade: false
|
|
||||||
*
|
|
||||||
* // extra HTTP request headers to set for each API request, in
|
|
||||||
* // the format {'Header-Name': value}
|
|
||||||
* xhr_headers: {}
|
|
||||||
*
|
|
||||||
* // protocol for fetching in-app message resources, e.g.
|
|
||||||
* // 'https://' or 'http://'; defaults to '//' (which defers to the
|
|
||||||
* // current page's protocol)
|
|
||||||
* inapp_protocol: '//'
|
|
||||||
*
|
|
||||||
* // whether to open in-app message link in new tab/window
|
|
||||||
* inapp_link_new_window: false
|
|
||||||
*
|
|
||||||
* // a set of rrweb config options that PostHog users can configure
|
|
||||||
* // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
|
|
||||||
* session_recording: {
|
|
||||||
* blockClass: 'ph-no-capture',
|
|
||||||
* blockSelector: null,
|
|
||||||
* ignoreClass: 'ph-ignore-input',
|
|
||||||
* maskAllInputs: false,
|
|
||||||
* maskInputOptions: {},
|
|
||||||
* maskInputFn: null,
|
|
||||||
* slimDOMOptions: {},
|
|
||||||
* collectFonts: false
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // prevent autocapture from capturing any attribute names on elements
|
|
||||||
* mask_all_element_attributes: false
|
|
||||||
*
|
|
||||||
* // prevent autocapture from capturing textContent on all elements
|
|
||||||
* mask_all_text: false
|
|
||||||
*
|
|
||||||
* // will disable requests to the /decide endpoint (please review documentation for details)
|
|
||||||
* // autocapture, feature flags, compression and session recording will be disabled when set to `true`
|
|
||||||
* advanced_disable_decide: false
|
|
||||||
*
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param {Object} config A dictionary of new configuration values to update
|
|
||||||
*/
|
|
||||||
static set_config(config: posthog.Config): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns the current config object for the library.
|
|
||||||
*/
|
|
||||||
static get_config<T extends keyof posthog.Config>(prop_name: T): posthog.Config[T]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of the super property named property_name. If no such
|
|
||||||
* property is set, get_property() will return the undefined value.
|
|
||||||
*
|
|
||||||
* ### Notes:
|
|
||||||
*
|
|
||||||
* get_property() can only be called after the PostHog library has finished loading.
|
|
||||||
* init() has a loaded function available to handle this automatically. For example:
|
|
||||||
*
|
|
||||||
* // grab value for 'user_id' after the posthog library has loaded
|
|
||||||
* posthog.init('YOUR PROJECT TOKEN', {
|
|
||||||
* loaded: function(posthog) {
|
|
||||||
* user_id = posthog.get_property('user_id');
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* @param {String} property_name The name of the super property you want to retrieve
|
|
||||||
*/
|
|
||||||
static get_property(property_name: string): posthog.Property | undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current distinct id of the user. This is either the id automatically
|
|
||||||
* generated by the library or the id that has been passed by a call to identify().
|
|
||||||
*
|
|
||||||
* ### Notes:
|
|
||||||
*
|
|
||||||
* get_distinct_id() can only be called after the PostHog library has finished loading.
|
|
||||||
* init() has a loaded function available to handle this automatically. For example:
|
|
||||||
*
|
|
||||||
* // set distinct_id after the posthog library has loaded
|
|
||||||
* posthog.init('YOUR PROJECT TOKEN', {
|
|
||||||
* loaded: function(posthog) {
|
|
||||||
* distinct_id = posthog.get_distinct_id();
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
static get_distinct_id(): string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opt the user out of data capturing and cookies/localstorage for this PostHog instance
|
|
||||||
*
|
|
||||||
* ### Usage
|
|
||||||
*
|
|
||||||
* // opt user out
|
|
||||||
* posthog.opt_out_capturing();
|
|
||||||
*
|
|
||||||
* // opt user out with different cookie configuration from PostHog instance
|
|
||||||
* posthog.opt_out_capturing({
|
|
||||||
* cookie_expiration: 30,
|
|
||||||
* secure_cookie: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* @param {Object} [options] A dictionary of config options to override
|
|
||||||
* @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence
|
|
||||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
|
||||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
|
||||||
*/
|
|
||||||
static opt_out_capturing(options?: posthog.OptInOutCapturingOptions): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opt the user in to data capturing and cookies/localstorage for this PostHog instance
|
|
||||||
*
|
|
||||||
* ### Usage
|
|
||||||
*
|
|
||||||
* // opt user in
|
|
||||||
* posthog.opt_in_capturing();
|
|
||||||
*
|
|
||||||
* // opt user in with specific event name, properties, cookie configuration
|
|
||||||
* posthog.opt_in_capturing({
|
|
||||||
* capture_event_name: 'User opted in',
|
|
||||||
* capture_event_properties: {
|
|
||||||
* 'Email': 'jdoe@example.com'
|
|
||||||
* },
|
|
||||||
* cookie_expiration: 30,
|
|
||||||
* secure_cookie: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* @param {Object} [options] A dictionary of config options to override
|
|
||||||
* @param {function} [options.capture] Function used for capturing a PostHog event to record the opt-in action (default is this PostHog instance's capture method)
|
|
||||||
* @param {string} [options.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action
|
|
||||||
* @param {Object} [options.capture_properties] Set of properties to be captured along with the opt-in action
|
|
||||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
|
||||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
|
||||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
|
||||||
*/
|
|
||||||
static opt_in_capturing(options?: posthog.OptInOutCapturingOptions): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the user has opted out of data capturing and cookies/localstorage for this PostHog instance
|
|
||||||
*
|
|
||||||
* ### Usage
|
|
||||||
*
|
|
||||||
* const has_opted_out = posthog.has_opted_out_capturing();
|
|
||||||
* // use has_opted_out value
|
|
||||||
*
|
|
||||||
* @param {Object} [options] A dictionary of config options to override
|
|
||||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
|
||||||
* @returns {boolean} current opt-out status
|
|
||||||
*/
|
|
||||||
static has_opted_out_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the user has opted in to data capturing and cookies/localstorage for this PostHog instance
|
|
||||||
*
|
|
||||||
* ### Usage
|
|
||||||
*
|
|
||||||
* const has_opted_in = posthog.has_opted_in_capturing();
|
|
||||||
* // use has_opted_in value
|
|
||||||
*
|
|
||||||
* @param {Object} [options] A dictionary of config options to override
|
|
||||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
|
||||||
* @returns {boolean} current opt-in status
|
|
||||||
*/
|
|
||||||
static has_opted_in_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the user's opt in/out status of data capturing and cookies/localstorage for this PostHog instance
|
|
||||||
*
|
|
||||||
* ### Usage
|
|
||||||
*
|
|
||||||
* // clear user's opt-in/out status
|
|
||||||
* posthog.clear_opt_in_out_capturing();
|
|
||||||
*
|
|
||||||
* // clear user's opt-in/out status with specific cookie configuration - should match
|
|
||||||
* // configuration used when opt_in_capturing/opt_out_capturing methods were called.
|
|
||||||
* posthog.clear_opt_in_out_capturing({
|
|
||||||
* cookie_expiration: 30,
|
|
||||||
* secure_cookie: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* @param {Object} [options] A dictionary of config options to override
|
|
||||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
|
||||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
|
||||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
|
||||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
|
||||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
|
||||||
*/
|
|
||||||
static clear_opt_in_out_capturing(options?: posthog.ClearOptInOutCapturingOptions): void
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See if feature flag is enabled for user.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
|
||||||
*
|
|
||||||
* @param {Object|String} prop Key of the feature flag.
|
|
||||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
|
||||||
*/
|
|
||||||
static isFeatureEnabled(key: string, options?: posthog.isFeatureEnabledOptions): boolean
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See if feature flags are available.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
|
||||||
*
|
|
||||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
|
||||||
*/
|
|
||||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Reload all feature flags for the user.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.reloadFeatureFlags()
|
|
||||||
*/
|
|
||||||
static reloadFeatureFlags(): void
|
|
||||||
|
|
||||||
static toString(): string
|
|
||||||
|
|
||||||
/* Will log all capture requests to the Javascript console, including event properties for easy debugging */
|
|
||||||
static debug(): void
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Starts session recording and updates disable_session_recording to false.
|
|
||||||
* Used for manual session recording management. By default, session recording is enabled and
|
|
||||||
* starts automatically.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.startSessionRecording()
|
|
||||||
*/
|
|
||||||
static startSessionRecording(): void
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Stops session recording and updates disable_session_recording to true.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.stopSessionRecording()
|
|
||||||
*/
|
|
||||||
static stopSessionRecording(): void
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if session recording is currently running.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* const isSessionRecordingOn = posthog.sessionRecordingStarted()
|
|
||||||
*/
|
|
||||||
static sessionRecordingStarted(): boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace posthog {
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
type Property = any;
|
|
||||||
type Properties = Record<string, Property>;
|
|
||||||
type CaptureResult = { event: string; properties: Properties } | undefined;
|
|
||||||
type CaptureCallback = (response: any, data: any) => void;
|
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
interface Config {
|
|
||||||
api_host?: string
|
|
||||||
api_method?: string
|
|
||||||
api_transport?: string
|
|
||||||
autocapture?: boolean
|
|
||||||
rageclick?: boolean
|
|
||||||
cdn?: string
|
|
||||||
cross_subdomain_cookie?: boolean
|
|
||||||
persistence?: 'localStorage' | 'cookie' | 'memory'
|
|
||||||
persistence_name?: string
|
|
||||||
cookie_name?: string
|
|
||||||
loaded?: (posthog_instance: typeof posthog) => void
|
|
||||||
store_google?: boolean
|
|
||||||
save_referrer?: boolean
|
|
||||||
test?: boolean
|
|
||||||
verbose?: boolean
|
|
||||||
img?: boolean
|
|
||||||
capture_pageview?: boolean
|
|
||||||
debug?: boolean
|
|
||||||
cookie_expiration?: number
|
|
||||||
upgrade?: boolean
|
|
||||||
disable_session_recording?: boolean
|
|
||||||
disable_persistence?: boolean
|
|
||||||
disable_cookie?: boolean
|
|
||||||
secure_cookie?: boolean
|
|
||||||
ip?: boolean
|
|
||||||
opt_out_capturing_by_default?: boolean
|
|
||||||
opt_out_persistence_by_default?: boolean
|
|
||||||
opt_out_capturing_persistence_type?: 'localStorage' | 'cookie'
|
|
||||||
opt_out_capturing_cookie_prefix?: string | null
|
|
||||||
respect_dnt?: boolean
|
|
||||||
property_blacklist?: string[]
|
|
||||||
xhr_headers?: { [header_name: string]: string }
|
|
||||||
inapp_protocol?: string
|
|
||||||
inapp_link_new_window?: boolean
|
|
||||||
request_batching?: boolean
|
|
||||||
sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties
|
|
||||||
properties_string_max_length?: number
|
|
||||||
mask_all_element_attributes?: boolean
|
|
||||||
mask_all_text?: boolean
|
|
||||||
advanced_disable_decide?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OptInOutCapturingOptions {
|
|
||||||
clear_persistence: boolean
|
|
||||||
persistence_type: string
|
|
||||||
cookie_prefix: string
|
|
||||||
cookie_expiration: number
|
|
||||||
cross_subdomain_cookie: boolean
|
|
||||||
secure_cookie: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HasOptedInOutCapturingOptions {
|
|
||||||
persistence_type: string
|
|
||||||
cookie_prefix: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClearOptInOutCapturingOptions {
|
|
||||||
enable_persistence: boolean
|
|
||||||
persistence_type: string
|
|
||||||
cookie_prefix: string
|
|
||||||
cookie_expiration: number
|
|
||||||
cross_subdomain_cookie: boolean
|
|
||||||
secure_cookie: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface isFeatureEnabledOptions {
|
|
||||||
send_event: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class persistence {
|
|
||||||
static properties(): posthog.Properties
|
|
||||||
|
|
||||||
static load(): void
|
|
||||||
|
|
||||||
static save(): void
|
|
||||||
|
|
||||||
static remove(): void
|
|
||||||
|
|
||||||
static clear(): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {*=} default_value
|
|
||||||
* @param {number=} days
|
|
||||||
*/
|
|
||||||
static register_once(props: Properties, default_value?: Property, days?: number): boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {number=} days
|
|
||||||
*/
|
|
||||||
static register(props: posthog.Properties, days?: number): boolean
|
|
||||||
|
|
||||||
static unregister(prop: string): void
|
|
||||||
|
|
||||||
static update_campaign_params(): void
|
|
||||||
|
|
||||||
static update_search_keyword(referrer: string): void
|
|
||||||
|
|
||||||
static update_referrer_info(referrer: string): void
|
|
||||||
|
|
||||||
static get_referrer_info(): posthog.Properties
|
|
||||||
|
|
||||||
static safe_merge(props: posthog.Properties): posthog.Properties
|
|
||||||
|
|
||||||
static update_config(config: posthog.Config): void
|
|
||||||
|
|
||||||
static set_disabled(disabled: boolean): void
|
|
||||||
|
|
||||||
static set_cross_subdomain(cross_subdomain: boolean): void
|
|
||||||
|
|
||||||
static get_cross_subdomain(): boolean
|
|
||||||
|
|
||||||
static set_secure(secure: boolean): void
|
|
||||||
|
|
||||||
static set_event_timer(event_name: string, timestamp: Date): void
|
|
||||||
|
|
||||||
static remove_event_timer(event_name: string): Date | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class people {
|
|
||||||
/*
|
|
||||||
* Set properties on a user record.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.people.set('gender', 'm');
|
|
||||||
*
|
|
||||||
* // or set multiple properties at once
|
|
||||||
* posthog.people.set({
|
|
||||||
* 'Company': 'Acme',
|
|
||||||
* 'Plan': 'Premium',
|
|
||||||
* 'Upgrade date': new Date()
|
|
||||||
* });
|
|
||||||
* // properties can be strings, integers, dates, or lists
|
|
||||||
*
|
|
||||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
|
||||||
* @param {*} [to] A value to set on the given property name
|
|
||||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
|
||||||
*/
|
|
||||||
static set(
|
|
||||||
prop: posthog.Properties | string,
|
|
||||||
to?: posthog.Property,
|
|
||||||
callback?: posthog.CaptureCallback
|
|
||||||
): posthog.Properties
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set properties on a user record, only if they do not yet exist.
|
|
||||||
* This will not overwrite previous people property values, unlike
|
|
||||||
* people.set().
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.people.set_once('First Login Date', new Date());
|
|
||||||
*
|
|
||||||
* // or set multiple properties at once
|
|
||||||
* posthog.people.set_once({
|
|
||||||
* 'First Login Date': new Date(),
|
|
||||||
* 'Starting Plan': 'Premium'
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // properties can be strings, integers or dates
|
|
||||||
*
|
|
||||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
|
||||||
* @param {*} [to] A value to set on the given property name
|
|
||||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
|
||||||
*/
|
|
||||||
static set_once(
|
|
||||||
prop: posthog.Properties | string,
|
|
||||||
to?: posthog.Property,
|
|
||||||
callback?: posthog.CaptureCallback
|
|
||||||
): posthog.Properties
|
|
||||||
|
|
||||||
static toString(): string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class featureFlags {
|
|
||||||
static getFlags(): string[]
|
|
||||||
|
|
||||||
static reloadFeatureFlags(): void
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See if feature flag is enabled for user.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
|
||||||
*
|
|
||||||
* @param {Object|String} prop Key of the feature flag.
|
|
||||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
|
||||||
*/
|
|
||||||
static isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See if feature flags are available.
|
|
||||||
*
|
|
||||||
* ### Usage:
|
|
||||||
*
|
|
||||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
|
||||||
*
|
|
||||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
|
||||||
*/
|
|
||||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class feature_flags extends featureFlags {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PostHog = typeof posthog;
|
|
||||||
|
|
||||||
export default posthog;
|
|
|
@ -1,7 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -60,7 +61,6 @@ import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
|
@ -86,6 +86,12 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||||
|
import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||||
|
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
|
||||||
|
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||||
|
import ToastStore from './stores/ToastStore';
|
||||||
|
import IncomingCallToast from "./toasts/IncomingCallToast";
|
||||||
|
|
||||||
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
export const PROTOCOL_PSTN = 'm.protocol.pstn';
|
||||||
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
|
||||||
|
@ -476,26 +482,44 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing:
|
case CallState.Ringing: {
|
||||||
this.play(AudioID.Ring);
|
const incomingCallPushRule = (
|
||||||
|
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
|
||||||
|
);
|
||||||
|
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||||
|
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
|
||||||
|
action.set_tweak === TweakName.Sound &&
|
||||||
|
action.value === "ring"
|
||||||
|
));
|
||||||
|
|
||||||
|
if (pushRuleEnabled && tweakSetToRing) {
|
||||||
|
this.play(AudioID.Ring);
|
||||||
|
} else {
|
||||||
|
this.silenceCall(call.callId);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CallState.InviteSent:
|
}
|
||||||
|
case CallState.InviteSent: {
|
||||||
this.play(AudioID.Ringback);
|
this.play(AudioID.Ringback);
|
||||||
break;
|
break;
|
||||||
case CallState.Ended:
|
}
|
||||||
{
|
case CallState.Ended: {
|
||||||
const hangupReason = call.hangupReason;
|
const hangupReason = call.hangupReason;
|
||||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||||
this.play(AudioID.Busy);
|
this.play(AudioID.Busy);
|
||||||
|
|
||||||
|
// Don't show a modal when we got rejected/the call was hung up
|
||||||
|
if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
let description;
|
let description;
|
||||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||||
title = _t("User Busy");
|
title = _t("User Busy");
|
||||||
description = _t("The user you called is busy.");
|
description = _t("The user you called is busy.");
|
||||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
} else {
|
||||||
title = _t("Call Failed");
|
title = _t("Call Failed");
|
||||||
description = _t("The call could not be established");
|
description = _t("The call could not be established");
|
||||||
}
|
}
|
||||||
|
@ -624,6 +648,19 @@ export default class CallHandler extends EventEmitter {
|
||||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toastKey = getIncomingCallToastKey(call.callId);
|
||||||
|
if (status === CallState.Ringing) {
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key: toastKey,
|
||||||
|
priority: 100,
|
||||||
|
component: IncomingCallToast,
|
||||||
|
bodyClassName: "mx_IncomingCallToast",
|
||||||
|
props: { call },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||||
|
}
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'call_state',
|
action: 'call_state',
|
||||||
room_id: mappedRoomId,
|
room_id: mappedRoomId,
|
||||||
|
@ -914,6 +951,8 @@ export default class CallHandler extends EventEmitter {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.placeCall(roomId, PlaceCallType.Voice, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
|
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
|
||||||
|
@ -993,14 +1032,10 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
// prevent double clicking the call button
|
// prevent double clicking the call button
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
|
||||||
const hasJitsi = currentJitsiWidgets.length > 0
|
if (jitsiWidget) {
|
||||||
|| WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
|
// If there already is a Jitsi widget pin it
|
||||||
if (hasJitsi) {
|
WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top);
|
||||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
|
||||||
title: _t('Call in Progress'),
|
|
||||||
description: _t('A call is currently being placed!'),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
|
||||||
return { width, height, img };
|
return { width, height, img };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimum size for image files before we generate a thumbnail for them.
|
||||||
|
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
|
||||||
|
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
|
||||||
|
// We don't apply these thresholds to video thumbnails as a poster image is always useful
|
||||||
|
// and videos tend to be much larger.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
||||||
*
|
*
|
||||||
|
@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
|
||||||
* @param {File} imageFile The image to read and thumbnail.
|
* @param {File} imageFile The image to read and thumbnail.
|
||||||
* @return {Promise} A promise that resolves with the attachment info.
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
*/
|
*/
|
||||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) {
|
||||||
let thumbnailType = "image/png";
|
let thumbnailType = "image/png";
|
||||||
if (imageFile.type === "image/jpeg") {
|
if (imageFile.type === "image/jpeg") {
|
||||||
thumbnailType = "image/jpeg";
|
thumbnailType = "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageInfo;
|
const imageElement = await loadImageElement(imageFile);
|
||||||
return loadImageElement(imageFile).then((r) => {
|
|
||||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||||
}).then((result) => {
|
const imageInfo = result.info;
|
||||||
imageInfo = result.info;
|
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||||
}).then((result) => {
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||||
imageInfo.thumbnail_url = result.url;
|
if (
|
||||||
imageInfo.thumbnail_file = result.file;
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
|
||||||
|
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
|
||||||
|
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
||||||
|
) {
|
||||||
|
delete imageInfo["thumbnail_info"];
|
||||||
return imageInfo;
|
return imageInfo;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
|
||||||
|
imageInfo["thumbnail_url"] = uploadResult.url;
|
||||||
|
imageInfo["thumbnail_file"] = uploadResult.file;
|
||||||
|
return imageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -123,6 +123,19 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCallTime(delta: Date): string {
|
||||||
|
const hours = delta.getUTCHours();
|
||||||
|
const minutes = delta.getUTCMinutes();
|
||||||
|
const seconds = delta.getUTCSeconds();
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
if (hours) output += `${hours}h `;
|
||||||
|
if (minutes || output) output += `${minutes}m `;
|
||||||
|
if (seconds || output) output += `${seconds}s`;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||||
if (!nextEventDate || !prevEventDate) {
|
if (!nextEventDate || !prevEventDate) {
|
||||||
|
|
|
@ -57,7 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
||||||
|
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
|
export const PERMITTED_URL_SCHEMES = [
|
||||||
|
"bitcoin",
|
||||||
|
"ftp",
|
||||||
|
"geo",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"im",
|
||||||
|
"irc",
|
||||||
|
"ircs",
|
||||||
|
"magnet",
|
||||||
|
"mailto",
|
||||||
|
"matrix",
|
||||||
|
"mms",
|
||||||
|
"news",
|
||||||
|
"nntp",
|
||||||
|
"openpgp4fpr",
|
||||||
|
"sip",
|
||||||
|
"sftp",
|
||||||
|
"sms",
|
||||||
|
"smsto",
|
||||||
|
"ssh",
|
||||||
|
"tel",
|
||||||
|
"urn",
|
||||||
|
"webcal",
|
||||||
|
"wtai",
|
||||||
|
"xmpp",
|
||||||
|
];
|
||||||
|
|
||||||
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||||
|
|
||||||
|
|
|
@ -146,23 +146,23 @@ export default class IdentityAuthClient {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
||||||
QuestionDialog, {
|
QuestionDialog, {
|
||||||
title: _t("Identity server has no terms of service"),
|
title: _t("Identity server has no terms of service"),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t(
|
<p>{ _t(
|
||||||
"This action requires accessing the default identity server " +
|
"This action requires accessing the default identity server " +
|
||||||
"<server /> to validate an email address or phone number, " +
|
"<server /> to validate an email address or phone number, " +
|
||||||
"but the server does not have any terms of service.", {},
|
"but the server does not have any terms of service.", {},
|
||||||
{
|
{
|
||||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||||
},
|
},
|
||||||
) }</p>
|
) }</p>
|
||||||
<p>{ _t(
|
<p>{ _t(
|
||||||
"Only continue if you trust the owner of the server.",
|
"Only continue if you trust the owner of the server.",
|
||||||
) }</p>
|
) }</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
button: _t("Trust"),
|
button: _t("Trust"),
|
||||||
});
|
});
|
||||||
const [confirmed] = await finished;
|
const [confirmed] = await finished;
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|
|
@ -163,7 +163,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
||||||
modifiers: [Modifiers.SHIFT],
|
modifiers: [Modifiers.SHIFT],
|
||||||
key: Key.PAGE_UP,
|
key: Key.PAGE_UP,
|
||||||
}],
|
}],
|
||||||
description: _td("Jump to oldest unread message"),
|
description: _td("Jump to oldest unread message"),
|
||||||
}, {
|
}, {
|
||||||
keybinds: [{
|
keybinds: [{
|
||||||
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
|
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
|
||||||
|
|
|
@ -38,17 +38,9 @@ function makePlaybackWaveform(input: number[]): number[] {
|
||||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||||
const noiseWaveform = input.map(v => Math.abs(v));
|
const noiseWaveform = input.map(v => Math.abs(v));
|
||||||
|
|
||||||
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
|
||||||
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||||
|
|
||||||
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
|
||||||
// waveform. Most speech happens below the 0.5 mark.
|
|
||||||
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
|
||||||
|
|
||||||
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
|
||||||
// sensible. This is what we return to keep our contract of "values between zero and one".
|
|
||||||
return arrayRescale(filtered, 0, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playback extends EventEmitter implements IDestroyable {
|
export class Playback extends EventEmitter implements IDestroyable {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { IEncryptedFile } from "matrix-js-sdk/src/@types/event";
|
||||||
import { uploadFile } from "../ContentMessages";
|
import { uploadFile } from "../ContentMessages";
|
||||||
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||||
import { clamp } from "../utils/numbers";
|
import { clamp } from "../utils/numbers";
|
||||||
|
import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||||
|
|
||||||
const CHANNELS = 1; // stereo isn't important
|
const CHANNELS = 1; // stereo isn't important
|
||||||
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
||||||
|
@ -113,16 +114,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
});
|
});
|
||||||
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
||||||
|
|
||||||
// Set up our worklet. We use this for timing information and waveform analysis: the
|
|
||||||
// web audio API prefers this be done async to avoid holding the main thread with math.
|
|
||||||
const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
|
|
||||||
if (!mxRecorderWorkletPath) {
|
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
|
||||||
throw new Error("Unable to create recorder: no worklet script registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect our inputs and outputs
|
// Connect our inputs and outputs
|
||||||
if (this.recorderContext.audioWorklet) {
|
if (this.recorderContext.audioWorklet) {
|
||||||
|
// Set up our worklet. We use this for timing information and waveform analysis: the
|
||||||
|
// web audio API prefers this be done async to avoid holding the main thread with math.
|
||||||
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
||||||
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
||||||
this.recorderSource.connect(this.recorderWorklet);
|
this.recorderSource.connect(this.recorderWorklet);
|
||||||
|
|
|
@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
|
||||||
SilencedChanged = "silenced_changed",
|
SilencedChanged = "silenced_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONNECTING_STATES = [
|
||||||
|
CallState.Connecting,
|
||||||
|
CallState.WaitLocalMedia,
|
||||||
|
CallState.CreateOffer,
|
||||||
|
CallState.CreateAnswer,
|
||||||
|
];
|
||||||
|
|
||||||
const SUPPORTED_STATES = [
|
const SUPPORTED_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
CallState.Connecting,
|
|
||||||
CallState.Ringing,
|
CallState.Ringing,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get selectAnswer(): MatrixEvent {
|
||||||
|
return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
|
||||||
|
}
|
||||||
|
|
||||||
public get isVoice(): boolean {
|
public get isVoice(): boolean {
|
||||||
const invite = this.invite;
|
const invite = this.invite;
|
||||||
if (!invite) return;
|
if (!invite) return;
|
||||||
|
@ -82,6 +92,11 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
return Boolean(this.reject);
|
return Boolean(this.reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get duration(): Date {
|
||||||
|
if (!this.hangup || !this.selectAnswer) return;
|
||||||
|
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there are only events from the other side - we missed the call
|
* Returns true if there are only events from the other side - we missed the call
|
||||||
*/
|
*/
|
||||||
|
@ -127,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setState = () => {
|
private setState = () => {
|
||||||
if (SUPPORTED_STATES.includes(this.call?.state)) {
|
if (CONNECTING_STATES.includes(this.call?.state)) {
|
||||||
|
this.state = CallState.Connecting;
|
||||||
|
} else if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||||
this.state = this.call.state;
|
this.state = this.call.state;
|
||||||
} else {
|
} else {
|
||||||
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
||||||
|
|
|
@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
|
const groupedEvents = [
|
||||||
|
EventType.RoomMember,
|
||||||
|
EventType.RoomThirdPartyInvite,
|
||||||
|
EventType.RoomServerAcl,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
];
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
|
@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
class MemberGrouper extends BaseGrouper {
|
class MemberGrouper extends BaseGrouper {
|
||||||
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
||||||
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
|
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
|
||||||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return membershipTypes.includes(ev.getType() as EventType);
|
return groupedEvents.includes(ev.getType() as EventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||||
|
|
|
@ -192,11 +192,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
|
|
||||||
if (inviteSender) {
|
if (inviteSender) {
|
||||||
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||||
<MemberAvatar member={inviter} width={32} height={32} />
|
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_SpaceRoomView_preview_inviter_name">
|
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||||
{ _t("<inviter/> invites you", {}, {
|
{ _t("<inviter/> invites you", {}, {
|
||||||
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||||
|
|
|
@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||||
|
|
||||||
|
const roomId = this.props.timelineSet.room.roomId;
|
||||||
|
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
|
||||||
|
|
||||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||||
this.props.timelineSet.room.roomId,
|
this.props.timelineSet.room.roomId,
|
||||||
'rm', this.state.readMarkerEventId,
|
'rm', this.state.readMarkerEventId,
|
||||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||||
|
' hidden:' + hiddenRR,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get().setRoomReadMarkers(
|
MatrixClientPeg.get().setRoomReadMarkers(
|
||||||
this.props.timelineSet.room.roomId,
|
roomId,
|
||||||
this.state.readMarkerEventId,
|
this.state.readMarkerEventId,
|
||||||
lastReadEvent, // Could be null, in which case no RR is sent
|
lastReadEvent, // Could be null, in which case no RR is sent
|
||||||
{},
|
{ hidden: hiddenRR },
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||||
|
|
|
@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
let containerClasses;
|
let containerClasses;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const { title, icon, key, component, className, props } = topToast;
|
const { title, icon, key, component, className, bodyClassName, props } = topToast;
|
||||||
const toastClasses = classNames("mx_Toast_toast", {
|
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
|
||||||
|
const toastClasses = classNames("mx_Toast_toast", className, {
|
||||||
"mx_Toast_hasIcon": icon,
|
"mx_Toast_hasIcon": icon,
|
||||||
[`mx_Toast_icon_${icon}`]: icon,
|
[`mx_Toast_icon_${icon}`]: icon,
|
||||||
}, className);
|
});
|
||||||
|
|
||||||
let countIndicator;
|
|
||||||
if (isStacked || this.state.countSeen > 0) {
|
|
||||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastProps = Object.assign({}, props, {
|
const toastProps = Object.assign({}, props, {
|
||||||
key,
|
key,
|
||||||
toastKey: key,
|
toastKey: key,
|
||||||
});
|
});
|
||||||
toast = (<div className={toastClasses}>
|
const content = React.createElement(component, toastProps);
|
||||||
<div className="mx_Toast_title">
|
|
||||||
<h2>{ title }</h2>
|
let countIndicator;
|
||||||
<span>{ countIndicator }</span>
|
if (title && isStacked || this.state.countSeen > 0) {
|
||||||
|
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleElement;
|
||||||
|
if (title) {
|
||||||
|
titleElement = (
|
||||||
|
<div className="mx_Toast_title">
|
||||||
|
<h2>{ title }</h2>
|
||||||
|
<span>{ countIndicator }</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast = (
|
||||||
|
<div className={toastClasses}>
|
||||||
|
{ titleElement }
|
||||||
|
<div className={bodyClasses}>{ content }</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
|
);
|
||||||
</div>);
|
|
||||||
|
|
||||||
containerClasses = classNames("mx_ToastContainer", {
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
"mx_ToastContainer_stacked": isStacked,
|
"mx_ToastContainer_stacked": isStacked,
|
||||||
|
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { arrayFastResample } from "../../../utils/arrays";
|
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||||
import { percentageOf } from "../../../utils/numbers";
|
|
||||||
import Waveform from "./Waveform";
|
import Waveform from "./Waveform";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
|
|
||||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
waveform: [],
|
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||||
// The incoming data is between zero and one, but typically even screaming into a
|
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
|
||||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
|
||||||
// user.
|
|
||||||
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
|
|
||||||
this.scheduledUpdate.mark();
|
this.scheduledUpdate.mark();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -490,9 +490,9 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
||||||
forceStateEvent={true}
|
forceStateEvent={true}
|
||||||
onBack={this.onBack}
|
onBack={this.onBack}
|
||||||
inputs={{
|
inputs={{
|
||||||
eventType: this.state.event.getType(),
|
eventType: this.state.event.getType(),
|
||||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||||
stateKey: this.state.event.getStateKey(),
|
stateKey: this.state.event.getStateKey(),
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
|
||||||
export default function KeySignatureUploadFailedDialog({
|
export default function KeySignatureUploadFailedDialog({
|
||||||
failures,
|
failures,
|
||||||
source,
|
source,
|
||||||
continuation,
|
continuation,
|
||||||
onFinished,
|
onFinished,
|
||||||
}) {
|
}) {
|
||||||
const RETRIES = 2;
|
const RETRIES = 2;
|
||||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
shouldLoadBackupStatus: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
backupInfo: IKeyBackupInfo;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.LogoutDialog")
|
@replaceableComponent("views.dialogs.LogoutDialog")
|
||||||
export default class LogoutDialog extends React.Component {
|
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||||
defaultProps = {
|
static defaultProps = {
|
||||||
onFinished: function() {},
|
onFinished: function() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
|
|
||||||
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
|
|
||||||
this._onFinished = this._onFinished.bind(this);
|
|
||||||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
|
||||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||||
|
@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldLoadBackupStatus) {
|
if (shouldLoadBackupStatus) {
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadBackupStatus() {
|
private async loadBackupStatus() {
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSettingsLinkClick() {
|
private onSettingsLinkClick = (): void => {
|
||||||
// close dialog
|
// close dialog
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onExportE2eKeysClicked() {
|
private onExportE2eKeysClicked = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||||
{
|
{
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onFinished(confirmed) {
|
private onFinished = (confirmed: boolean): void => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
dis.dispatch({ action: 'logout' });
|
dis.dispatch({ action: 'logout' });
|
||||||
}
|
}
|
||||||
// close dialog
|
// close dialog
|
||||||
this.props.onFinished();
|
this.props.onFinished(confirmed);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onSetRecoveryMethodClick() {
|
private onSetRecoveryMethodClick = (): void => {
|
||||||
if (this.state.backupInfo) {
|
if (this.state.backupInfo) {
|
||||||
// A key backup exists for this account, but the creating device is not
|
// A key backup exists for this account, but the creating device is not
|
||||||
// verified, so restore the backup which will give us the keys from it and
|
// verified, so restore the backup which will give us the keys from it and
|
||||||
|
@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// close dialog
|
// close dialog
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
_onLogoutConfirm() {
|
private onLogoutConfirm = (): void => {
|
||||||
dis.dispatch({ action: 'logout' });
|
dis.dispatch({ action: 'logout' });
|
||||||
|
|
||||||
// close dialog
|
// close dialog
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.shouldLoadBackupStatus) {
|
if (this.state.shouldLoadBackupStatus) {
|
||||||
|
@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={setupButtonCaption}
|
<DialogButtons primaryButton={setupButtonCaption}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||||
focus={true}
|
focus={true}
|
||||||
>
|
>
|
||||||
<button onClick={this._onLogoutConfirm}>
|
<button onClick={this.onLogoutConfirm}>
|
||||||
{ _t("I don't want my encrypted messages") }
|
{ _t("I don't want my encrypted messages") }
|
||||||
</button>
|
</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
<details>
|
<details>
|
||||||
<summary>{ _t("Advanced") }</summary>
|
<summary>{ _t("Advanced") }</summary>
|
||||||
<p><button onClick={this._onExportE2eKeysClicked}>
|
<p><button onClick={this.onExportE2eKeysClicked}>
|
||||||
{ _t("Manually export keys") }
|
{ _t("Manually export keys") }
|
||||||
</button></p>
|
</button></p>
|
||||||
</details>
|
</details>
|
||||||
|
@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component {
|
||||||
title={_t("You'll lose access to your encrypted messages")}
|
title={_t("You'll lose access to your encrypted messages")}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this._onFinished}
|
onFinished={this.onFinished}
|
||||||
>
|
>
|
||||||
{ dialogContent }
|
{ dialogContent }
|
||||||
</BaseDialog>);
|
</BaseDialog>);
|
||||||
|
@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
|
||||||
"Are you sure you want to sign out?",
|
"Are you sure you want to sign out?",
|
||||||
)}
|
)}
|
||||||
button={_t("Sign out")}
|
button={_t("Sign out")}
|
||||||
onFinished={this._onFinished}
|
onFinished={this.onFinished}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -62,10 +62,10 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Set all this into the initial state
|
// Set all this into the initial state
|
||||||
this.state = {
|
this.state = {
|
||||||
...urlInfo,
|
|
||||||
roomMember,
|
|
||||||
isWrapped: null,
|
|
||||||
widgetDomain: null,
|
widgetDomain: null,
|
||||||
|
isWrapped: null,
|
||||||
|
roomMember,
|
||||||
|
...urlInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ interface IProps {
|
||||||
// The list of room members for which to show avatars next to the summary
|
// The list of room members for which to show avatars next to the summary
|
||||||
summaryMembers?: RoomMember[];
|
summaryMembers?: RoomMember[];
|
||||||
// The text to show as the summary of this event list
|
// The text to show as the summary of this event list
|
||||||
summaryText?: string;
|
summaryText?: string | JSX.Element;
|
||||||
// An array of EventTiles to render when expanded
|
// An array of EventTiles to render when expanded
|
||||||
children: ReactNode[];
|
children: ReactNode[];
|
||||||
// Called when the event list expansion is toggled
|
// Called when the event list expansion is toggled
|
||||||
|
|
|
@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||||
import EventListSummary from "./EventListSummary";
|
import EventListSummary from "./EventListSummary";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||||
|
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||||
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
|
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import { Layout } from '../../../settings/Layout';
|
import { Layout } from '../../../settings/Layout';
|
||||||
|
|
||||||
|
const onPinnedMessagesClick = (): void => {
|
||||||
|
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.PinnedMessages,
|
||||||
|
allowClose: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
||||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||||
summaryLength?: number;
|
summaryLength?: number;
|
||||||
|
@ -60,6 +76,7 @@ enum TransitionType {
|
||||||
ChangedAvatar = "changed_avatar",
|
ChangedAvatar = "changed_avatar",
|
||||||
NoChange = "no_change",
|
NoChange = "no_change",
|
||||||
ServerAcl = "server_acl",
|
ServerAcl = "server_acl",
|
||||||
|
ChangedPins = "pinned_messages"
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEP = ",";
|
const SEP = ",";
|
||||||
|
@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* `Object.keys(eventAggregates)`.
|
* `Object.keys(eventAggregates)`.
|
||||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||||
*/
|
*/
|
||||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
private generateSummary(
|
||||||
|
eventAggregates: Record<string, string[]>,
|
||||||
|
orderedTransitionSequences: string[],
|
||||||
|
): string | JSX.Element {
|
||||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||||
const userNames = eventAggregates[transitions];
|
const userNames = eventAggregates[transitions];
|
||||||
const nameList = this.renderNameList(userNames);
|
const nameList = this.renderNameList(userNames);
|
||||||
|
@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return summaries.join(", ");
|
return jsxJoin(summaries, ", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||||
* @returns {string} the written Human Readable equivalent of the transition.
|
* @returns {string} the written Human Readable equivalent of the transition.
|
||||||
*/
|
*/
|
||||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
private static getDescriptionForTransition(
|
||||||
|
t: TransitionType,
|
||||||
|
userCount: number,
|
||||||
|
repeats: number,
|
||||||
|
): string | JSX.Element {
|
||||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||||
// are there only to show translators to non-English languages
|
// are there only to show translators to non-English languages
|
||||||
// that the verb is conjugated to plural or singular Subject.
|
// that the verb is conjugated to plural or singular Subject.
|
||||||
|
@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
{ severalUsers: "", count: repeats })
|
{ severalUsers: "", count: repeats })
|
||||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||||
break;
|
break;
|
||||||
|
case "pinned_messages":
|
||||||
|
res = (userCount > 1)
|
||||||
|
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
|
{ severalUsers: "", count: repeats },
|
||||||
|
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
|
||||||
|
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
|
{ oneUser: "", count: repeats },
|
||||||
|
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* if a transition is not recognised.
|
* if a transition is not recognised.
|
||||||
*/
|
*/
|
||||||
private static getTransition(e: IUserEvents): TransitionType {
|
private static getTransition(e: IUserEvents): TransitionType {
|
||||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
const type = e.mxEvent.getType();
|
||||||
|
|
||||||
|
if (type === EventType.RoomThirdPartyInvite) {
|
||||||
// Handle 3pid invites the same as invites so they get bundled together
|
// Handle 3pid invites the same as invites so they get bundled together
|
||||||
if (!isValid3pidInvite(e.mxEvent)) {
|
if (!isValid3pidInvite(e.mxEvent)) {
|
||||||
return TransitionType.InviteWithdrawal;
|
return TransitionType.InviteWithdrawal;
|
||||||
}
|
}
|
||||||
return TransitionType.Invited;
|
return TransitionType.Invited;
|
||||||
}
|
} else if (type === EventType.RoomServerAcl) {
|
||||||
|
|
||||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
|
||||||
return TransitionType.ServerAcl;
|
return TransitionType.ServerAcl;
|
||||||
|
} else if (type === EventType.RoomPinnedEvents) {
|
||||||
|
return TransitionType.ChangedPins;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (e.mxEvent.getContent().membership) {
|
switch (e.mxEvent.getContent().membership) {
|
||||||
|
@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
// Object mapping user IDs to an array of IUserEvents
|
// Object mapping user IDs to an array of IUserEvents
|
||||||
const userEvents: Record<string, IUserEvents[]> = {};
|
const userEvents: Record<string, IUserEvents[]> = {};
|
||||||
eventsToRender.forEach((e, index) => {
|
eventsToRender.forEach((e, index) => {
|
||||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
const type = e.getType();
|
||||||
|
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
|
||||||
// Initialise a user's events
|
// Initialise a user's events
|
||||||
if (!userEvents[userId]) {
|
if (!userEvents[userId]) {
|
||||||
userEvents[userId] = [];
|
userEvents[userId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.getType() === 'm.room.server_acl') {
|
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||||
latestUserAvatarMember.set(userId, e.sender);
|
latestUserAvatarMember.set(userId, e.sender);
|
||||||
} else if (e.target) {
|
} else if (e.target) {
|
||||||
latestUserAvatarMember.set(userId, e.target);
|
latestUserAvatarMember.set(userId, e.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayName = userId;
|
let displayName = userId;
|
||||||
if (e.getType() === 'm.room.third_party_invite') {
|
if (type === EventType.RoomThirdPartyInvite) {
|
||||||
displayName = e.getContent().display_name;
|
displayName = e.getContent().display_name;
|
||||||
} else if (e.getType() === 'm.room.server_acl') {
|
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||||
displayName = e.sender.name;
|
displayName = e.sender.name;
|
||||||
} else if (e.target) {
|
} else if (e.target) {
|
||||||
displayName = e.target.name;
|
displayName = e.target.name;
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { formatCallTime } from "../../../DateUtils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -117,14 +117,12 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
if (state === CallState.Ended) {
|
if (state === CallState.Ended) {
|
||||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||||
const gotRejected = this.props.callEventGrouper.gotRejected;
|
const gotRejected = this.props.callEventGrouper.gotRejected;
|
||||||
const rejectParty = this.props.callEventGrouper.rejectParty;
|
|
||||||
|
|
||||||
if (gotRejected) {
|
if (gotRejected) {
|
||||||
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
|
{ _t("Call declined") }
|
||||||
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
|
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
|
||||||
|
@ -134,16 +132,21 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
// Also, if we don't have a reason
|
// Also, if we don't have a reason
|
||||||
|
const duration = this.props.callEventGrouper.duration;
|
||||||
|
let text = _t("Call ended");
|
||||||
|
if (duration) {
|
||||||
|
text += " • " + formatCallTime(duration);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("This call has ended") }
|
{ text }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("They didn't pick up") }
|
{ _t("Missed call") }
|
||||||
{ this.renderCallBackButton(_t("Call again")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -176,7 +179,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
className="mx_CallEvent_content_tooltip"
|
className="mx_CallEvent_content_tooltip"
|
||||||
kind={InfoTooltipKind.Warning}
|
kind={InfoTooltipKind.Warning}
|
||||||
/>
|
/>
|
||||||
{ _t("This call has failed") }
|
{ _t("Connection failed") }
|
||||||
|
{ this.renderCallBackButton(_t("Retry")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +194,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
if (state === CustomCallState.Missed) {
|
if (state === CustomCallState.Missed) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("You missed this call") }
|
{ _t("Missed call") }
|
||||||
{ this.renderCallBackButton(_t("Call back")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||||
// Calculate how many percent does the pre element take up.
|
// Calculate how many percent does the pre element take up.
|
||||||
// If it's less than 30% we don't add the expansion button.
|
// If it's less than 30% we don't add the expansion button.
|
||||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
// We also round the number as it sometimes can be 29.99...
|
||||||
|
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||||
if (percentageOfViewport < 30) return;
|
if (percentageOfViewport < 30) return;
|
||||||
|
|
||||||
const button = document.createElement("span");
|
const button = document.createElement("span");
|
||||||
|
|
|
@ -152,7 +152,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||||
<h2>{ _t("Nothing pinned, yet") }</h2>
|
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||||
{ _t("If you have permissions, open the menu on any message and select " +
|
{ _t("If you have permissions, open the menu on any message and select " +
|
||||||
"<b>Pin</b> to stick them here.", {}, {
|
"<b>Pin</b> to stick them here.", {}, {
|
||||||
b: sub => <b>{ sub }</b>,
|
b: sub => <b>{ sub }</b>,
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -1381,8 +1381,8 @@ const BasicUserInfo: React.FC<{
|
||||||
className="mx_UserInfo_field"
|
className="mx_UserInfo_field"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.Security,
|
initialTabId: UserTab.Security,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -55,6 +54,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||||
|
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"],
|
||||||
|
["{", "}"],
|
||||||
|
["<", ">"],
|
||||||
|
]);
|
||||||
|
|
||||||
function ctrlShortcutLabel(key: string): string {
|
function ctrlShortcutLabel(key: string): string {
|
||||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +106,7 @@ interface IState {
|
||||||
showVisualBell?: boolean;
|
showVisualBell?: boolean;
|
||||||
autoComplete?: AutocompleteWrapperModel;
|
autoComplete?: AutocompleteWrapperModel;
|
||||||
completionIndex?: number;
|
completionIndex?: number;
|
||||||
|
surroundWith: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||||
|
@ -117,12 +125,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
private readonly emoticonSettingHandle: string;
|
private readonly emoticonSettingHandle: string;
|
||||||
private readonly shouldShowPillAvatarSettingHandle: string;
|
private readonly shouldShowPillAvatarSettingHandle: string;
|
||||||
|
private readonly surroundWithHandle: string;
|
||||||
private readonly historyManager = new HistoryManager();
|
private readonly historyManager = new HistoryManager();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||||
|
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||||
|
@ -130,6 +140,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.configureEmoticonAutoReplace();
|
this.configureEmoticonAutoReplace();
|
||||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||||
this.configureShouldShowPillAvatar);
|
this.configureShouldShowPillAvatar);
|
||||||
|
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||||
|
this.surroundWithSettingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps) {
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
|
@ -422,6 +434,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||||
const model = this.props.model;
|
const model = this.props.model;
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
|
if (this.state.surroundWith && document.getSelection().type != "Caret") {
|
||||||
|
// This surrounds the selected text with a character. This is
|
||||||
|
// intentionally left out of the keybinding manager as the keybinds
|
||||||
|
// here shouldn't be changeable
|
||||||
|
|
||||||
|
const selectionRange = getRangeForSelection(
|
||||||
|
this.editorRef.current,
|
||||||
|
this.props.model,
|
||||||
|
document.getSelection(),
|
||||||
|
);
|
||||||
|
// trim the range as we want it to exclude leading/trailing spaces
|
||||||
|
selectionRange.trim();
|
||||||
|
|
||||||
|
if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) {
|
||||||
|
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||||
|
this.modifiedFlag = true;
|
||||||
|
toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key));
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MessageComposerAction.FormatBold:
|
case MessageComposerAction.FormatBold:
|
||||||
|
@ -574,6 +608,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ showPillAvatar });
|
this.setState({ showPillAvatar });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private surroundWithSettingChanged = () => {
|
||||||
|
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
|
||||||
|
this.setState({ surroundWith });
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
@ -581,6 +620,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
||||||
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
||||||
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
||||||
|
SettingsStore.unwatchSetting(this.surroundWithHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -684,7 +724,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||||
<div
|
<div
|
||||||
className={classes}
|
className={classes}
|
||||||
contentEditable="true"
|
contentEditable={this.props.disabled ? null : true}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
|
|
|
@ -58,6 +58,7 @@ function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
|
||||||
interface ISendButtonProps {
|
interface ISendButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
title?: string; // defaults to something generic
|
||||||
}
|
}
|
||||||
|
|
||||||
function SendButton(props: ISendButtonProps) {
|
function SendButton(props: ISendButtonProps) {
|
||||||
|
@ -65,7 +66,7 @@ function SendButton(props: ISendButtonProps) {
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_MessageComposer_sendMessage"
|
className="mx_MessageComposer_sendMessage"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
title={_t('Send message')}
|
title={props.title ?? _t('Send message')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -401,7 +402,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
<SendButton
|
||||||
|
key="controls_send"
|
||||||
|
onClick={this.sendMessage}
|
||||||
|
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (this.state.tombstone) {
|
} else if (this.state.tombstone) {
|
||||||
|
|
|
@ -64,9 +64,9 @@ const NewRoomIntro = () => {
|
||||||
height={AVATAR_SIZE}
|
height={AVATAR_SIZE}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||||
action: Action.ViewUser,
|
action: Action.ViewUser,
|
||||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||||
member: member || { userId: dmPartner } as User,
|
member: member || { userId: dmPartner } as User,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
if (oldInfo && oldInfo.left) {
|
if (oldInfo && oldInfo.left) {
|
||||||
// start at the old height and in the old h pos
|
// start at the old height and in the old h pos
|
||||||
startStyles.push({ top: startTopOffset+"px",
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
left: toPx(oldInfo.left) });
|
left: toPx(oldInfo.left) });
|
||||||
}
|
}
|
||||||
|
|
||||||
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||||
|
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
title = _t(
|
title = _t(
|
||||||
"Seen by %(userName)s at %(dateTime)s",
|
"Seen by %(userName)s at %(dateTime)s",
|
||||||
{ userName: this.props.fallbackUserId,
|
{ userName: this.props.fallbackUserId,
|
||||||
dateTime: dateString },
|
dateTime: dateString },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
title = _t(
|
title = _t(
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||||
{ displayName: this.props.member.rawDisplayName,
|
{ displayName: this.props.member.rawDisplayName,
|
||||||
userName: this.props.fallbackUserId,
|
userName: this.props.fallbackUserId,
|
||||||
dateTime: dateString },
|
dateTime: dateString },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -38,6 +38,8 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ReplyTile")
|
@replaceableComponent("views.rooms.ReplyTile")
|
||||||
export default class ReplyTile extends React.PureComponent<IProps> {
|
export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
|
private anchorElement = createRef<HTMLAnchorElement>();
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onHeightChanged: () => {},
|
onHeightChanged: () => {},
|
||||||
};
|
};
|
||||||
|
@ -71,7 +73,11 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
// Following a link within a reply should not dispatch the `view_room` action
|
// Following a link within a reply should not dispatch the `view_room` action
|
||||||
// so that the browser can direct the user to the correct location
|
// so that the browser can direct the user to the correct location
|
||||||
// The exception being the link wrapping the reply
|
// The exception being the link wrapping the reply
|
||||||
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
|
if (
|
||||||
|
clickTarget.tagName.toLowerCase() !== "a" ||
|
||||||
|
clickTarget.closest("a") === null ||
|
||||||
|
clickTarget === this.anchorElement.current
|
||||||
|
) {
|
||||||
// This allows the permalink to be opened in a new tab/window or copied as
|
// This allows the permalink to be opened in a new tab/window or copied as
|
||||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -141,7 +147,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<a href={permalink} onClick={this.onClick}>
|
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||||
{ sender }
|
{ sender }
|
||||||
<EventTileType
|
<EventTileType
|
||||||
ref="tile"
|
ref="tile"
|
||||||
|
|
|
@ -17,10 +17,7 @@ limitations under the License.
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import {
|
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
RecordingState,
|
|
||||||
VoiceRecording,
|
|
||||||
} from "../../../audio/VoiceRecording";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -34,6 +31,11 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||||
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
|
import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -42,6 +44,7 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
recorder?: VoiceRecording;
|
recorder?: VoiceRecording;
|
||||||
recordingPhase?: RecordingState;
|
recordingPhase?: RecordingState;
|
||||||
|
didUploadFail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,9 +72,19 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
|
|
||||||
await this.state.recorder.stop();
|
await this.state.recorder.stop();
|
||||||
|
|
||||||
|
let upload: IUpload;
|
||||||
try {
|
try {
|
||||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error uploading voice message:", e);
|
||||||
|
|
||||||
|
// Flag error and move on. The recording phase will be reset by the upload function.
|
||||||
|
this.setState({ didUploadFail: true });
|
||||||
|
|
||||||
|
return; // don't dispose the recording: the user has a chance to re-upload
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
||||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||||
"body": "Voice message",
|
"body": "Voice message",
|
||||||
|
@ -104,12 +117,11 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error sending/uploading voice message:", e);
|
console.error("Error sending voice message:", e);
|
||||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
|
||||||
title: _t('Upload Failed'),
|
// Voice message should be in the timeline at this point, so let other things take care
|
||||||
description: _t("The voice message failed to upload."),
|
// of error handling. We also shouldn't need the recording anymore, so fall through to
|
||||||
});
|
// disposal.
|
||||||
return; // don't dispose the recording so the user can retry, maybe
|
|
||||||
}
|
}
|
||||||
await this.disposeRecording();
|
await this.disposeRecording();
|
||||||
}
|
}
|
||||||
|
@ -118,7 +130,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
await VoiceRecordingStore.instance.disposeRecording();
|
await VoiceRecordingStore.instance.disposeRecording();
|
||||||
|
|
||||||
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
||||||
this.setState({ recorder: null, recordingPhase: null });
|
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCancel = async () => {
|
private onCancel = async () => {
|
||||||
|
@ -166,6 +178,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// stop any noises which might be happening
|
||||||
|
await PlaybackManager.instance.playOnly(null);
|
||||||
|
|
||||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||||
await recorder.start();
|
await recorder.start();
|
||||||
|
|
||||||
|
@ -200,7 +215,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
let recordingInfo;
|
let stopOrRecordBtn;
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -209,12 +224,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tooltip = _t("Record a voice message");
|
let tooltip = _t("Send voice message");
|
||||||
if (!!this.state.recorder) {
|
if (!!this.state.recorder) {
|
||||||
tooltip = _t("Stop the recording");
|
tooltip = _t("Stop recording");
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
stopOrRecordBtn = <AccessibleTooltipButton
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onRecordStartEndClick}
|
onClick={this.onRecordStartEndClick}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
|
@ -222,22 +237,41 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||||
stopOrRecordBtn = null;
|
stopOrRecordBtn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingInfo = stopOrRecordBtn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||||
deleteButton = <AccessibleTooltipButton
|
deleteButton = <AccessibleTooltipButton
|
||||||
className='mx_VoiceRecordComposerTile_delete'
|
className='mx_VoiceRecordComposerTile_delete'
|
||||||
title={_t("Delete recording")}
|
title={_t("Delete")}
|
||||||
onClick={this.onCancel}
|
onClick={this.onCancel}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadIndicator;
|
||||||
|
if (this.state.recordingPhase === RecordingState.Uploading) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
|
||||||
|
<InlineSpinner w={16} h={16} />
|
||||||
|
</span>;
|
||||||
|
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
|
||||||
|
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
|
||||||
|
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
|
||||||
|
<NotificationBadge
|
||||||
|
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='text-warning'>{ _t("Failed to send") }</span>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||||
|
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||||
|
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||||
return (<>
|
return (<>
|
||||||
|
{ uploadIndicator }
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
|
{ stopOrRecordBtn }
|
||||||
{ this.renderWaveformArea() }
|
{ this.renderWaveformArea() }
|
||||||
{ recordingInfo }
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import EventTilePreview from "../elements/EventTilePreview";
|
||||||
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { Layout } from "../../../settings/Layout";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
messagePreviewText: string;
|
||||||
|
onLayoutChanged?: (layout: Layout) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
layout: Layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const layout = e.target.value as Layout;
|
||||||
|
|
||||||
|
this.setState({ layout: layout });
|
||||||
|
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||||
|
this.props.onLayoutChanged(layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||||
|
});
|
||||||
|
const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.Group,
|
||||||
|
});
|
||||||
|
const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab_section mx_LayoutSwitcher">
|
||||||
|
<span className="mx_SettingsTab_subheading">
|
||||||
|
{ _t("Message layout") }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="mx_LayoutSwitcher_RadioButtons">
|
||||||
|
<label className={ircClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.IRC}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.IRC}
|
||||||
|
checked={this.state.layout === Layout.IRC}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("IRC") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
<label className={groupClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.Group}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.Group}
|
||||||
|
checked={this.state.layout == Layout.Group}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Modern") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
<label className={bubbleClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.Bubble}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.Bubble}
|
||||||
|
checked={this.state.layout == Layout.Bubble}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Message bubbles") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -37,10 +37,9 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { Layout } from "../../../../../settings/Layout";
|
import { Layout } from "../../../../../settings/Layout";
|
||||||
import classNames from 'classnames';
|
|
||||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import { compare } from "../../../../../utils/strings";
|
import { compare } from "../../../../../utils/strings";
|
||||||
|
import LayoutSwitcher from "../../LayoutSwitcher";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -243,17 +242,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
this.setState({ customThemeUrl: e.target.value });
|
this.setState({ customThemeUrl: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onLayoutChanged = (layout: Layout): void => {
|
||||||
let layout;
|
|
||||||
switch (e.target.value) {
|
|
||||||
case "irc": layout = Layout.IRC; break;
|
|
||||||
case "group": layout = Layout.Group; break;
|
|
||||||
case "bubble": layout = Layout.Bubble; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ layout: layout });
|
this.setState({ layout: layout });
|
||||||
|
|
||||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onIRCLayoutChange = (enabled: boolean) => {
|
private onIRCLayoutChange = (enabled: boolean) => {
|
||||||
|
@ -391,75 +381,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLayoutSection = () => {
|
|
||||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
|
||||||
|
|
||||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.IRC}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="irc"
|
|
||||||
checked={this.state.layout === Layout.IRC}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("IRC") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.Group}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="group"
|
|
||||||
checked={this.state.layout == Layout.Group}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("Modern") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.Bubble}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="bubble"
|
|
||||||
checked={this.state.layout == Layout.Bubble}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("Message bubbles") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderAdvancedSection() {
|
private renderAdvancedSection() {
|
||||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||||
|
|
||||||
|
@ -527,6 +448,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
||||||
|
let layoutSection;
|
||||||
|
if (SettingsStore.getValue("feature_new_layout_switcher")) {
|
||||||
|
layoutSection = (
|
||||||
|
<LayoutSwitcher
|
||||||
|
userId={this.state.userId}
|
||||||
|
displayName={this.state.displayName}
|
||||||
|
avatarUrl={this.state.avatarUrl}
|
||||||
|
messagePreviewText={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
onLayoutChanged={this.onLayoutChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
||||||
|
@ -534,7 +468,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||||
</div>
|
</div>
|
||||||
{ this.renderThemeSection() }
|
{ this.renderThemeSection() }
|
||||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
{ layoutSection }
|
||||||
{ this.renderFontSection() }
|
{ this.renderFontSection() }
|
||||||
{ this.renderAdvancedSection() }
|
{ this.renderAdvancedSection() }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
|
||||||
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import createRoom from "../../../../../createRoom";
|
import createRoom from "../../../../../createRoom";
|
||||||
|
@ -69,6 +69,18 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getVersionInfo(): { appVersion: string, olmVersion: string } {
|
||||||
|
const brand = SdkConfig.get().brand;
|
||||||
|
const appVersion = this.state.appVersion || 'unknown';
|
||||||
|
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||||
|
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||||
|
|
||||||
|
return {
|
||||||
|
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
|
||||||
|
olmVersion: `${_t("Olm version:")} ${olmVersion}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private onClearCacheAndReload = (e) => {
|
private onClearCacheAndReload = (e) => {
|
||||||
if (!PlatformPeg.get()) return;
|
if (!PlatformPeg.get()) return;
|
||||||
|
|
||||||
|
@ -173,17 +185,26 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccessTokenCopyClick = async (e) => {
|
private async copy(text: string, e: ButtonEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = e.target; // copy target before we go async and React throws it away
|
const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away
|
||||||
|
|
||||||
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
const successful = await copyPlaintext(text);
|
||||||
const buttonRect = target.getBoundingClientRect();
|
const buttonRect = target.getBoundingClientRect();
|
||||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
...toRightOf(buttonRect, 2),
|
...toRightOf(buttonRect, 2),
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
});
|
||||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAccessTokenCopyClick = (e: ButtonEvent) => {
|
||||||
|
this.copy(MatrixClientPeg.get().getAccessToken(), e);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCopyVersionClicked = (e: ButtonEvent) => {
|
||||||
|
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||||
|
this.copy(`${appVersion}\n${olmVersion}`, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -232,11 +253,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appVersion = this.state.appVersion || 'unknown';
|
|
||||||
|
|
||||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
|
||||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
|
||||||
|
|
||||||
let updateButton = null;
|
let updateButton = null;
|
||||||
if (this.state.canUpdate) {
|
if (this.state.canUpdate) {
|
||||||
updateButton = <UpdateCheckButton />;
|
updateButton = <UpdateCheckButton />;
|
||||||
|
@ -275,6 +291,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
||||||
|
@ -291,8 +309,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
|
<div className="mx_HelpUserSettingsTab_copy">
|
||||||
{ _t("olm version:") } { olmVersion }<br />
|
{ appVersion }<br />
|
||||||
|
{ olmVersion }<br />
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
title={_t("Copy")}
|
||||||
|
onClick={this.onCopyVersionClicked}
|
||||||
|
className="mx_HelpUserSettingsTab_copyButton"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{ updateButton }
|
{ updateButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -308,12 +333,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<summary>{ _t("Access Token") }</summary><br />
|
<summary>{ _t("Access Token") }</summary><br />
|
||||||
<b>{ _t("Your access token gives full access to your account."
|
<b>{ _t("Your access token gives full access to your account."
|
||||||
+ " Do not share it with anyone." ) }</b>
|
+ " Do not share it with anyone." ) }</b>
|
||||||
<div className="mx_HelpUserSettingsTab_accessToken">
|
<div className="mx_HelpUserSettingsTab_copy">
|
||||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
title={_t("Copy")}
|
title={_t("Copy")}
|
||||||
onClick={this.onAccessTokenCopyClick}
|
onClick={this.onAccessTokenCopyClick}
|
||||||
className="mx_HelpUserSettingsTab_accessToken_copy"
|
className="mx_HelpUserSettingsTab_copyButton"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</details><br />
|
</details><br />
|
||||||
|
|
|
@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import * as sdk from "../../../../../index";
|
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import BetaCard from "../../../beta/BetaCard";
|
import BetaCard from "../../../beta/BetaCard";
|
||||||
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
|
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||||
|
|
||||||
export class LabsSettingToggle extends React.Component {
|
export class LabsSettingToggle extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component {
|
||||||
export default class LabsUserSettingsTab extends React.Component {
|
export default class LabsUserSettingsTab extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||||
|
this.setState({ showHiddenReadReceipts });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showHiddenReadReceipts: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -65,15 +74,22 @@ export default class LabsUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
let labsSection;
|
let labsSection;
|
||||||
if (SdkConfig.get()['showLabsSettings']) {
|
if (SdkConfig.get()['showLabsSettings']) {
|
||||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
|
||||||
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
||||||
|
|
||||||
|
let hiddenReadReceipts;
|
||||||
|
if (this.state.showHiddenReadReceipts) {
|
||||||
|
hiddenReadReceipts = (
|
||||||
|
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.ACCOUNT} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
labsSection = <div className="mx_SettingsTab_section">
|
labsSection = <div className="mx_SettingsTab_section">
|
||||||
{ flags }
|
{ flags }
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
||||||
|
{ hiddenReadReceipts }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
||||||
'MessageComposerInput.suggestEmoji',
|
'MessageComposerInput.suggestEmoji',
|
||||||
'sendTypingNotifications',
|
'sendTypingNotifications',
|
||||||
'MessageComposerInput.ctrlEnterToSend',
|
'MessageComposerInput.ctrlEnterToSend',
|
||||||
|
'MessageComposerInput.surroundWith',
|
||||||
'MessageComposerInput.showStickersButton',
|
'MessageComposerInput.showStickersButton',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IncomingCallBox from './IncomingCallBox';
|
|
||||||
import CallPreview from './CallPreview';
|
import CallPreview from './CallPreview';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ interface IState {
|
||||||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_CallContainer">
|
return <div className="mx_CallContainer">
|
||||||
<IncomingCallBox />
|
|
||||||
<CallPreview />
|
<CallPreview />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
@ -27,23 +27,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from '../../../stores/UIStore';
|
|
||||||
import { lerp } from '../../../utils/AnimationUtils';
|
|
||||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
|
||||||
import { EventSubscription } from 'fbemitter';
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||||
const PIP_VIEW_WIDTH = 336;
|
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
|
||||||
|
|
||||||
const MOVING_AMT = 0.2;
|
|
||||||
const SNAPPING_AMT = 0.05;
|
|
||||||
|
|
||||||
const PADDING = {
|
|
||||||
top: 58,
|
|
||||||
bottom: 58,
|
|
||||||
left: 76,
|
|
||||||
right: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -66,10 +51,6 @@ interface IState {
|
||||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||||
// they belong to
|
// they belong to
|
||||||
secondaryCall: MatrixCall;
|
secondaryCall: MatrixCall;
|
||||||
|
|
||||||
// Position of the CallPreview
|
|
||||||
translationX: number;
|
|
||||||
translationY: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
|
@ -112,16 +93,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: EventSubscription;
|
private roomStoreToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private settingsWatcherRef: string;
|
private settingsWatcherRef: string;
|
||||||
private callViewWrapper = createRef<HTMLDivElement>();
|
|
||||||
private initX = 0;
|
|
||||||
private initY = 0;
|
|
||||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
|
||||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
|
||||||
private moving = false;
|
|
||||||
private scheduledUpdate = new MarkedExecution(
|
|
||||||
() => this.animationCallback(),
|
|
||||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
roomId,
|
roomId,
|
||||||
primaryCall: primaryCall,
|
primaryCall: primaryCall,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
|
||||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
document.addEventListener("mousemove", this.onMoving);
|
|
||||||
document.addEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
document.removeEventListener("mousemove", this.onMoving);
|
|
||||||
document.removeEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize = (): void => {
|
|
||||||
this.snap(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
private animationCallback = () => {
|
|
||||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
|
||||||
// the desiredTranslation and translation, quit the animationCallback
|
|
||||||
// loop. If that is the case, it means the PiP has snapped into its
|
|
||||||
// position and there is nothing to do. Not doing this would cause an
|
|
||||||
// infinite loop
|
|
||||||
if (
|
|
||||||
!this.moving &&
|
|
||||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
|
||||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
|
||||||
) return;
|
|
||||||
|
|
||||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
|
||||||
this.setState({
|
|
||||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
|
||||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
|
||||||
});
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
|
||||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
|
||||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
|
||||||
|
|
||||||
// Avoid overflow on the x axis
|
|
||||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
|
||||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
|
||||||
} else if (inTranslationX <= 0) {
|
|
||||||
this.desiredTranslationX = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = inTranslationX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid overflow on the y axis
|
|
||||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
|
||||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
|
||||||
} else if (inTranslationY <= 0) {
|
|
||||||
this.desiredTranslationY = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationY = inTranslationY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private snap(animate?: boolean): void {
|
|
||||||
const translationX = this.desiredTranslationX;
|
|
||||||
const translationY = this.desiredTranslationY;
|
|
||||||
// We subtract the PiP size from the window size in order to calculate
|
|
||||||
// the position to snap to from the PiP center and not its top-left
|
|
||||||
// corner
|
|
||||||
const windowWidth = (
|
|
||||||
UIStore.instance.windowWidth -
|
|
||||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
|
||||||
);
|
|
||||||
const windowHeight = (
|
|
||||||
UIStore.instance.windowHeight -
|
|
||||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
// We start animating here because we want the PiP to move when we're
|
|
||||||
// resizing the window
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
translationX: this.desiredTranslationX,
|
|
||||||
translationY: this.desiredTranslationY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = () => {
|
private onRoomViewStoreUpdate = () => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// listen for call state changes to prod the render method, which
|
|
||||||
// may hide the global CallView if the call it is tracking is dead
|
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
|
// listen for call state changes to prod the render method, which
|
||||||
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
|
|
||||||
this.updateCalls();
|
this.updateCalls();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -300,57 +176,26 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStartMoving = (event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.moving = true;
|
|
||||||
this.initX = event.pageX - this.desiredTranslationX;
|
|
||||||
this.initY = event.pageY - this.desiredTranslationY;
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
|
||||||
if (!this.moving) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onEndMoving = () => {
|
|
||||||
this.moving = false;
|
|
||||||
this.snap(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const pipMode = true;
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
const translatePixelsX = this.state.translationX + "px";
|
|
||||||
const translatePixelsY = this.state.translationY + "px";
|
|
||||||
const style = {
|
|
||||||
transform: `translateX(${translatePixelsX})
|
|
||||||
translateY(${translatePixelsY})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PictureInPictureDragger
|
||||||
className="mx_CallPreview"
|
className="mx_CallPreview"
|
||||||
style={style}
|
draggable={pipMode}
|
||||||
ref={this.callViewWrapper}
|
|
||||||
>
|
>
|
||||||
<CallView
|
{ ({ onStartMoving, onResize }) => <CallView
|
||||||
|
onMouseDownOnHeader={onStartMoving}
|
||||||
call={this.state.primaryCall}
|
call={this.state.primaryCall}
|
||||||
secondaryCall={this.state.secondaryCall}
|
secondaryCall={this.state.secondaryCall}
|
||||||
pipMode={true}
|
pipMode={pipMode}
|
||||||
onMouseDownOnHeader={this.onStartMoving}
|
onResize={onResize}
|
||||||
onResize={this.onResize}
|
/> }
|
||||||
/>
|
</PictureInPictureDragger>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PersistentApp />;
|
return <PersistentApp />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,16 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import VideoFeed from './VideoFeed';
|
import VideoFeed from './VideoFeed';
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
||||||
import { alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton } from '../../structures/ContextMenu';
|
import {
|
||||||
|
alwaysAboveLeftOf,
|
||||||
|
alwaysAboveRightOf,
|
||||||
|
ChevronFace,
|
||||||
|
ContextMenuTooltipButton,
|
||||||
|
} from '../../structures/ContextMenu';
|
||||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||||
|
@ -37,26 +42,29 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
import CallViewSidebar from './CallViewSidebar';
|
import CallViewSidebar from './CallViewSidebar';
|
||||||
|
import CallViewHeader from './CallView/CallViewHeader';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { Alignment } from "../elements/Tooltip";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The call for us to display
|
// The call for us to display
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
|
|
||||||
// Another ongoing call to display information about
|
// Another ongoing call to display information about
|
||||||
secondaryCall?: MatrixCall;
|
secondaryCall?: MatrixCall;
|
||||||
|
|
||||||
// a callback which is called when the content in the CallView changes
|
// a callback which is called when the content in the CallView changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: any;
|
onResize?: (event: Event) => void;
|
||||||
|
|
||||||
// Whether this call view is for picture-in-picture mode
|
// Whether this call view is for picture-in-picture mode
|
||||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
// This is sort of a proxy for a number of things but we currently have no
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
// need to control those things separately, so this is simpler.
|
// need to control those things separately, so this is simpler.
|
||||||
pipMode?: boolean;
|
pipMode?: boolean;
|
||||||
|
|
||||||
// Used for dragging the PiP CallView
|
// Used for dragging the PiP CallView
|
||||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -75,6 +83,8 @@ interface IState {
|
||||||
sidebarShown: boolean;
|
sidebarShown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tooltipYOffset = -24;
|
||||||
|
|
||||||
function getFullScreenElement() {
|
function getFullScreenElement() {
|
||||||
return (
|
return (
|
||||||
document.fullscreenElement ||
|
document.fullscreenElement ||
|
||||||
|
@ -115,7 +125,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
private controlsHideTimer: number = null;
|
private controlsHideTimer: number = null;
|
||||||
private dialpadButton = createRef<HTMLDivElement>();
|
private dialpadButton = createRef<HTMLDivElement>();
|
||||||
private contextMenuButton = createRef<HTMLDivElement>();
|
private contextMenuButton = createRef<HTMLDivElement>();
|
||||||
private contextMenu = createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -144,6 +153,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
document.addEventListener('keydown', this.onNativeKeyDown);
|
document.addEventListener('keydown', this.onNativeKeyDown);
|
||||||
|
this.showControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -231,21 +241,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFullscreenClick = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'video_fullscreen',
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onExpandClick = () => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onControlsHideTimer = () => {
|
private onControlsHideTimer = () => {
|
||||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||||
this.controlsHideTimer = null;
|
this.controlsHideTimer = null;
|
||||||
|
@ -389,23 +384,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.setState({ hoveringControls: false });
|
this.setState({ hoveringControls: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSecondaryRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallResumeClick = (): void => {
|
private onCallResumeClick = (): void => {
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||||
|
@ -479,9 +457,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let vidMuteButton;
|
let vidMuteButton;
|
||||||
if (this.props.call.type === CallType.Video) {
|
if (this.props.call.type === CallType.Video) {
|
||||||
vidMuteButton = (
|
vidMuteButton = (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={vidClasses}
|
className={vidClasses}
|
||||||
onClick={this.onVidMuteClick}
|
onClick={this.onVidMuteClick}
|
||||||
|
title={this.state.vidMuted ? _t("Start the camera") : _t("Stop the camera")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -496,9 +477,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.props.call.state === CallState.Connected
|
this.props.call.state === CallState.Connected
|
||||||
) {
|
) {
|
||||||
screensharingButton = (
|
screensharingButton = (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={screensharingClasses}
|
className={screensharingClasses}
|
||||||
onClick={this.onScreenshareClick}
|
onClick={this.onScreenshareClick}
|
||||||
|
title={this.state.screensharing
|
||||||
|
? _t("Stop sharing your screen")
|
||||||
|
: _t("Start sharing your screen")
|
||||||
|
}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -518,6 +505,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={sidebarButtonClasses}
|
className={sidebarButtonClasses}
|
||||||
onClick={this.onToggleSidebar}
|
onClick={this.onToggleSidebar}
|
||||||
|
aria-label={this.state.sidebarShown ? _t("Hide sidebar") : _t("Show sidebar")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -526,22 +514,28 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let contextMenuButton;
|
let contextMenuButton;
|
||||||
if (this.state.callState === CallState.Connected) {
|
if (this.state.callState === CallState.Connected) {
|
||||||
contextMenuButton = (
|
contextMenuButton = (
|
||||||
<ContextMenuButton
|
<ContextMenuTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||||
onClick={this.onMoreClick}
|
onClick={this.onMoreClick}
|
||||||
inputRef={this.contextMenuButton}
|
inputRef={this.contextMenuButton}
|
||||||
isExpanded={this.state.showMoreMenu}
|
isExpanded={this.state.showMoreMenu}
|
||||||
|
title={_t("More")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let dialpadButton;
|
let dialpadButton;
|
||||||
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
||||||
dialpadButton = (
|
dialpadButton = (
|
||||||
<ContextMenuButton
|
<ContextMenuTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||||
inputRef={this.dialpadButton}
|
inputRef={this.dialpadButton}
|
||||||
onClick={this.onDialpadClick}
|
onClick={this.onDialpadClick}
|
||||||
isExpanded={this.state.showDialpad}
|
isExpanded={this.state.showDialpad}
|
||||||
|
title={_t("Dialpad")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -554,7 +548,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
ChevronFace.None,
|
ChevronFace.None,
|
||||||
CONTEXT_MENU_VPADDING,
|
CONTEXT_MENU_VPADDING,
|
||||||
)}
|
)}
|
||||||
mountAsChild={true}
|
// We mount the context menus as a as a child typically in order to include the
|
||||||
|
// context menus when fullscreening the call content.
|
||||||
|
// However, this does not work as well when the call is embedded in a
|
||||||
|
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
|
||||||
|
mountAsChild={!this.props.pipMode}
|
||||||
onFinished={this.closeDialpad}
|
onFinished={this.closeDialpad}
|
||||||
call={this.props.call}
|
call={this.props.call}
|
||||||
/>;
|
/>;
|
||||||
|
@ -568,7 +566,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
ChevronFace.None,
|
ChevronFace.None,
|
||||||
CONTEXT_MENU_VPADDING,
|
CONTEXT_MENU_VPADDING,
|
||||||
)}
|
)}
|
||||||
mountAsChild={true}
|
mountAsChild={!this.props.pipMode}
|
||||||
onFinished={this.closeContextMenu}
|
onFinished={this.closeContextMenu}
|
||||||
call={this.props.call}
|
call={this.props.call}
|
||||||
/>;
|
/>;
|
||||||
|
@ -583,9 +581,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{ dialPad }
|
{ dialPad }
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
{ dialpadButton }
|
{ dialpadButton }
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={micClasses}
|
className={micClasses}
|
||||||
onClick={this.onMicMuteClick}
|
onClick={this.onMicMuteClick}
|
||||||
|
title={this.state.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
{ vidMuteButton }
|
{ vidMuteButton }
|
||||||
<div className={micCacheClasses} />
|
<div className={micCacheClasses} />
|
||||||
|
@ -593,9 +594,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{ screensharingButton }
|
{ screensharingButton }
|
||||||
{ sidebarButton }
|
{ sidebarButton }
|
||||||
{ contextMenuButton }
|
{ contextMenuButton }
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||||
onClick={this.onHangupClick}
|
onClick={this.onHangupClick}
|
||||||
|
title={_t("Hangup")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -692,7 +696,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let onHoldBackground = null;
|
let onHoldBackground = null;
|
||||||
const backgroundStyle: CSSProperties = {};
|
const backgroundStyle: CSSProperties = {};
|
||||||
const backgroundAvatarUrl = avatarUrlForMember(
|
const backgroundAvatarUrl = avatarUrlForMember(
|
||||||
// is it worth getting the size of the div to pass here?
|
// is it worth getting the size of the div to pass here?
|
||||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||||
);
|
);
|
||||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||||
|
@ -712,7 +716,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
mx_CallView_voice_hold: isOnHold,
|
mx_CallView_voice_hold: isOnHold,
|
||||||
});
|
});
|
||||||
|
|
||||||
contentView =(
|
contentView = (
|
||||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
<div className="mx_CallView_voice_avatarsContainer">
|
<div className="mx_CallView_voice_avatarsContainer">
|
||||||
<div
|
<div
|
||||||
|
@ -814,83 +818,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||||
let myClassName;
|
|
||||||
|
|
||||||
let fullScreenButton;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
fullScreenButton = (
|
|
||||||
<div
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
|
||||||
onClick={this.onFullscreenClick}
|
|
||||||
title={_t("Fill Screen")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let expandButton;
|
|
||||||
if (this.props.pipMode) {
|
|
||||||
expandButton = <div
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
|
||||||
onClick={this.onExpandClick}
|
|
||||||
title={_t("Return to call")}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerControls = <div className="mx_CallView_header_controls">
|
|
||||||
{ fullScreenButton }
|
|
||||||
{ expandButton }
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
|
|
||||||
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
|
|
||||||
"mx_CallView_header_callTypeIcon_video": isVideoCall,
|
|
||||||
});
|
|
||||||
|
|
||||||
let header: React.ReactNode;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
header = <div className="mx_CallView_header">
|
|
||||||
<div className={callTypeIconClassName} />
|
|
||||||
<span className="mx_CallView_header_callType">{ callTypeText }</span>
|
|
||||||
{ headerControls }
|
|
||||||
</div>;
|
|
||||||
myClassName = 'mx_CallView_large';
|
|
||||||
} else {
|
|
||||||
let secondaryCallInfo;
|
|
||||||
if (this.props.secondaryCall) {
|
|
||||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
|
||||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
|
||||||
<span className="mx_CallView_secondaryCall_roomName">
|
|
||||||
{ _t("%(name)s on hold", { name: secCallRoom.name }) }
|
|
||||||
</span>
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
header = (
|
|
||||||
<div
|
|
||||||
className="mx_CallView_header"
|
|
||||||
onMouseDown={this.props.onMouseDownOnHeader}
|
|
||||||
>
|
|
||||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_CallView_header_callInfo">
|
|
||||||
<div className="mx_CallView_header_roomName">{ callRoom.name }</div>
|
|
||||||
<div className="mx_CallView_header_callTypeSmall">
|
|
||||||
{ callTypeText }
|
|
||||||
{ secondaryCallInfo }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{ headerControls }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
myClassName = 'mx_CallView_pip';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"mx_CallView " + myClassName}>
|
return <div className={"mx_CallView " + myClassName}>
|
||||||
{ header }
|
<CallViewHeader
|
||||||
|
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||||
|
pipMode={this.props.pipMode}
|
||||||
|
type={this.props.call.type}
|
||||||
|
callRooms={[callRoom, secCallRoom]}
|
||||||
|
/>
|
||||||
{ contentView }
|
{ contentView }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
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 { CallType } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import React from 'react';
|
||||||
|
import { _t, _td } from '../../../../languageHandler';
|
||||||
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
|
import dis from '../../../../dispatcher/dispatcher';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
|
const callTypeTranslationByType: Record<CallType, string> = {
|
||||||
|
[CallType.Video]: _td("Video Call"),
|
||||||
|
[CallType.Voice]: _td("Voice Call"),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CallViewHeaderProps {
|
||||||
|
pipMode: boolean;
|
||||||
|
type: CallType;
|
||||||
|
callRooms?: Room[];
|
||||||
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoomAvatarClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullscreenClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'video_fullscreen',
|
||||||
|
fullscreen: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpandClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, type, roomId }) => {
|
||||||
|
return <div className="mx_CallViewHeader_controls">
|
||||||
|
{ !pipMode && <AccessibleTooltipButton
|
||||||
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||||
|
onClick={onFullscreenClick}
|
||||||
|
title={_t("Fill Screen")}
|
||||||
|
/> }
|
||||||
|
{ pipMode && <AccessibleTooltipButton
|
||||||
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||||
|
onClick={() => onExpandClick(roomId)}
|
||||||
|
title={_t("Return to call")}
|
||||||
|
/> }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||||
|
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||||
|
<AccessibleButton element='span' onClick={() => onRoomAvatarClick(callRoom.roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||||
|
<span className="mx_CallView_secondaryCall_roomName">
|
||||||
|
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||||
|
</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallTypeIcon: React.FC<{ type: CallType }> = ({ type }) => {
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_CallViewHeader_callTypeIcon': true,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_video': type === CallType.Video,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_voice': type === CallType.Voice,
|
||||||
|
});
|
||||||
|
return <div className={classes} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
|
type,
|
||||||
|
pipMode = false,
|
||||||
|
callRooms = [],
|
||||||
|
onPipMouseDown,
|
||||||
|
}) => {
|
||||||
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
|
const callTypeText = _t(callTypeTranslationByType[type]);
|
||||||
|
const callRoomName = callRoom.name;
|
||||||
|
const { roomId } = callRoom;
|
||||||
|
|
||||||
|
if (!pipMode) {
|
||||||
|
return <div className="mx_CallViewHeader">
|
||||||
|
<CallTypeIcon type={type} />
|
||||||
|
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mx_CallViewHeader"
|
||||||
|
onMouseDown={onPipMouseDown}
|
||||||
|
>
|
||||||
|
<AccessibleButton onClick={() => onRoomAvatarClick(roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
|
</AccessibleButton>
|
||||||
|
<div className="mx_CallViewHeader_callInfo">
|
||||||
|
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||||
|
<div className="mx_CallViewHeader_callTypeSmall">
|
||||||
|
{ callTypeText }
|
||||||
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CallViewHeader;
|
|
@ -1,176 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019, 2020 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 React from 'react';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
|
||||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
incomingCall: any;
|
|
||||||
silenced: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.voip.IncomingCallBox")
|
|
||||||
export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|
||||||
private dispatcherRef: string;
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
this.state = {
|
|
||||||
incomingCall: null,
|
|
||||||
silenced: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
|
||||||
};
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
|
||||||
switch (payload.action) {
|
|
||||||
case 'call_state': {
|
|
||||||
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
|
||||||
if (call && call.state === CallState.Ringing) {
|
|
||||||
this.setState({
|
|
||||||
incomingCall: call,
|
|
||||||
silenced: false, // Reset silenced state for new call
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
incomingCall: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSilencedCallsChanged = () => {
|
|
||||||
const callId = this.state.incomingCall?.callId;
|
|
||||||
if (!callId) return;
|
|
||||||
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAnswerClick: React.MouseEventHandler = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'answer',
|
|
||||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRejectClick: React.MouseEventHandler = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'reject',
|
|
||||||
room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSilenceClick: React.MouseEventHandler = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const callId = this.state.incomingCall.callId;
|
|
||||||
this.state.silenced ?
|
|
||||||
CallHandler.sharedInstance().unSilenceCall(callId):
|
|
||||||
CallHandler.sharedInstance().silenceCall(callId);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (!this.state.incomingCall) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let room = null;
|
|
||||||
if (this.state.incomingCall) {
|
|
||||||
room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall));
|
|
||||||
}
|
|
||||||
|
|
||||||
const caller = room ? room.name : _t("Unknown caller");
|
|
||||||
|
|
||||||
let incomingCallText = null;
|
|
||||||
if (this.state.incomingCall) {
|
|
||||||
if (this.state.incomingCall.type === "voice") {
|
|
||||||
incomingCallText = _t("Incoming voice call");
|
|
||||||
} else if (this.state.incomingCall.type === "video") {
|
|
||||||
incomingCallText = _t("Incoming video call");
|
|
||||||
} else {
|
|
||||||
incomingCallText = _t("Incoming call");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const silenceClass = classNames({
|
|
||||||
"mx_IncomingCallBox_iconButton": true,
|
|
||||||
"mx_IncomingCallBox_unSilence": this.state.silenced,
|
|
||||||
"mx_IncomingCallBox_silence": !this.state.silenced,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className="mx_IncomingCallBox">
|
|
||||||
<div className="mx_IncomingCallBox_CallerInfo">
|
|
||||||
<RoomAvatar
|
|
||||||
room={room}
|
|
||||||
height={32}
|
|
||||||
width={32}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h1>{ caller }</h1>
|
|
||||||
<p>{ incomingCallText }</p>
|
|
||||||
</div>
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={silenceClass}
|
|
||||||
onClick={this.onSilenceClick}
|
|
||||||
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_IncomingCallBox_buttons">
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_IncomingCallBox_decline"
|
|
||||||
onClick={this.onRejectClick}
|
|
||||||
kind="danger"
|
|
||||||
>
|
|
||||||
{ _t("Decline") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_IncomingCallBox_spacer" />
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_IncomingCallBox_accept"
|
|
||||||
onClick={this.onAnswerClick}
|
|
||||||
kind="primary"
|
|
||||||
>
|
|
||||||
{ _t("Accept") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
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 UIStore from '../../../stores/UIStore';
|
||||||
|
import { lerp } from '../../../utils/AnimationUtils';
|
||||||
|
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||||
|
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||||
|
|
||||||
|
const PIP_VIEW_WIDTH = 336;
|
||||||
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
|
||||||
|
const MOVING_AMT = 0.2;
|
||||||
|
const SNAPPING_AMT = 0.1;
|
||||||
|
|
||||||
|
const PADDING = {
|
||||||
|
top: 58,
|
||||||
|
bottom: 58,
|
||||||
|
left: 76,
|
||||||
|
right: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IChildrenOptions {
|
||||||
|
onStartMoving: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
onResize: (event: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children: ({ onStartMoving, onResize }: IChildrenOptions) => React.ReactNode;
|
||||||
|
draggable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Position of the PictureInPictureDragger
|
||||||
|
translationX: number;
|
||||||
|
translationY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PictureInPictureDragger shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||||
|
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.voip.PictureInPictureDragger")
|
||||||
|
export default class PictureInPictureDragger extends React.Component<IProps, IState> {
|
||||||
|
private callViewWrapper = createRef<HTMLDivElement>();
|
||||||
|
private initX = 0;
|
||||||
|
private initY = 0;
|
||||||
|
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||||
|
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
|
||||||
|
private moving = false;
|
||||||
|
private scheduledUpdate = new MarkedExecution(
|
||||||
|
() => this.animationCallback(),
|
||||||
|
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||||
|
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
document.addEventListener("mousemove", this.onMoving);
|
||||||
|
document.addEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousemove", this.onMoving);
|
||||||
|
document.removeEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private animationCallback = () => {
|
||||||
|
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||||
|
// the desiredTranslation and translation, quit the animationCallback
|
||||||
|
// loop. If that is the case, it means the PiP has snapped into its
|
||||||
|
// position and there is nothing to do. Not doing this would cause an
|
||||||
|
// infinite loop
|
||||||
|
if (
|
||||||
|
!this.moving &&
|
||||||
|
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||||
|
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||||
|
this.setState({
|
||||||
|
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||||
|
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||||
|
});
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||||
|
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||||
|
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||||
|
|
||||||
|
// Avoid overflow on the x axis
|
||||||
|
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||||
|
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||||
|
} else if (inTranslationX <= 0) {
|
||||||
|
this.desiredTranslationX = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = inTranslationX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid overflow on the y axis
|
||||||
|
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||||
|
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||||
|
} else if (inTranslationY <= 0) {
|
||||||
|
this.desiredTranslationY = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationY = inTranslationY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = (): void => {
|
||||||
|
this.snap(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
private snap = (animate = false) => {
|
||||||
|
const translationX = this.desiredTranslationX;
|
||||||
|
const translationY = this.desiredTranslationY;
|
||||||
|
// We subtract the PiP size from the window size in order to calculate
|
||||||
|
// the position to snap to from the PiP center and not its top-left
|
||||||
|
// corner
|
||||||
|
const windowWidth = (
|
||||||
|
UIStore.instance.windowWidth -
|
||||||
|
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||||
|
);
|
||||||
|
const windowHeight = (
|
||||||
|
UIStore.instance.windowHeight -
|
||||||
|
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
translationX: this.desiredTranslationX,
|
||||||
|
translationY: this.desiredTranslationY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onStartMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.moving = true;
|
||||||
|
this.initX = event.pageX - this.desiredTranslationX;
|
||||||
|
this.initY = event.pageY - this.desiredTranslationY;
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
if (!this.moving) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEndMoving = () => {
|
||||||
|
this.moving = false;
|
||||||
|
this.snap(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
|
const style = {
|
||||||
|
transform: `translateX(${translatePixelsX})
|
||||||
|
translateY(${translatePixelsY})`,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.props.className}
|
||||||
|
style={this.props.draggable ? style : undefined}
|
||||||
|
ref={this.callViewWrapper}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{ this.props.children({
|
||||||
|
onStartMoving: this.onStartMoving,
|
||||||
|
onResize: this.onResize,
|
||||||
|
}) }
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -207,6 +207,7 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
|
||||||
const videoClasses = classnames("mx_VideoFeed_video", {
|
const videoClasses = classnames("mx_VideoFeed_video", {
|
||||||
mx_VideoFeed_video_mirror: (
|
mx_VideoFeed_video_mirror: (
|
||||||
this.props.feed.isLocal() &&
|
this.props.feed.isLocal() &&
|
||||||
|
this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
|
||||||
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
16
src/emoji.ts
16
src/emoji.ts
|
@ -35,6 +35,16 @@ export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
|
||||||
|
|
||||||
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
||||||
|
|
||||||
|
const isRegionalIndicator = (x: string): boolean => {
|
||||||
|
// First verify that the string is a single character. We use Array.from
|
||||||
|
// to make sure we count by characters, not UTF-8 code units.
|
||||||
|
return Array.from(x).length === 1 &&
|
||||||
|
// Next verify that the character is within the code point range for
|
||||||
|
// regional indicators.
|
||||||
|
// http://unicode.org/charts/PDF/Unicode-6.0/U60-1F100.pdf
|
||||||
|
x >= '\u{1f1e6}' && x <= '\u{1f1ff}';
|
||||||
|
};
|
||||||
|
|
||||||
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
|
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
|
||||||
"people", // smileys
|
"people", // smileys
|
||||||
"people", // actually people
|
"people", // actually people
|
||||||
|
@ -72,7 +82,11 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
||||||
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
// We manually include regional indicators in the symbols group, since
|
||||||
|
// Emojibase intentionally leaves them uncategorized
|
||||||
|
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group] ??
|
||||||
|
(isRegionalIndicator(emoji.unicode) ? "symbols" : null);
|
||||||
|
|
||||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,6 @@
|
||||||
"Unable to transfer call": "Unable to transfer call",
|
"Unable to transfer call": "Unable to transfer call",
|
||||||
"Transfer Failed": "Transfer Failed",
|
"Transfer Failed": "Transfer Failed",
|
||||||
"Failed to transfer call": "Failed to transfer call",
|
"Failed to transfer call": "Failed to transfer call",
|
||||||
"Call in Progress": "Call in Progress",
|
|
||||||
"A call is currently being placed!": "A call is currently being placed!",
|
|
||||||
"Permission Required": "Permission Required",
|
"Permission Required": "Permission Required",
|
||||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||||
"End conference": "End conference",
|
"End conference": "End conference",
|
||||||
|
@ -752,6 +750,13 @@
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
"Enable desktop notifications": "Enable desktop notifications",
|
"Enable desktop notifications": "Enable desktop notifications",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
|
"Unknown caller": "Unknown caller",
|
||||||
|
"Voice call": "Voice call",
|
||||||
|
"Video call": "Video call",
|
||||||
|
"Decline": "Decline",
|
||||||
|
"Accept": "Accept",
|
||||||
|
"Sound on": "Sound on",
|
||||||
|
"Silence call": "Silence call",
|
||||||
"Use app for a better experience": "Use app for a better experience",
|
"Use app for a better experience": "Use app for a better experience",
|
||||||
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.",
|
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.",
|
||||||
"Use app": "Use app",
|
"Use app": "Use app",
|
||||||
|
@ -835,6 +840,7 @@
|
||||||
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||||
|
"Don't send read receipts": "Don't send read receipts",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||||
|
@ -860,6 +866,7 @@
|
||||||
"Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline",
|
"Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline",
|
||||||
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
||||||
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
||||||
|
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
|
||||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||||
"Mirror local video feed": "Mirror local video feed",
|
"Mirror local video feed": "Mirror local video feed",
|
||||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||||
|
@ -916,6 +923,17 @@
|
||||||
"sends snowfall": "sends snowfall",
|
"sends snowfall": "sends snowfall",
|
||||||
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||||
"sends space invaders": "sends space invaders",
|
"sends space invaders": "sends space invaders",
|
||||||
|
"Start the camera": "Start the camera",
|
||||||
|
"Stop the camera": "Stop the camera",
|
||||||
|
"Stop sharing your screen": "Stop sharing your screen",
|
||||||
|
"Start sharing your screen": "Start sharing your screen",
|
||||||
|
"Hide sidebar": "Hide sidebar",
|
||||||
|
"Show sidebar": "Show sidebar",
|
||||||
|
"More": "More",
|
||||||
|
"Dialpad": "Dialpad",
|
||||||
|
"Unmute the microphone": "Unmute the microphone",
|
||||||
|
"Mute the microphone": "Mute the microphone",
|
||||||
|
"Hangup": "Hangup",
|
||||||
"unknown person": "unknown person",
|
"unknown person": "unknown person",
|
||||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||||
|
@ -931,14 +949,6 @@
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
"Return to call": "Return to call",
|
"Return to call": "Return to call",
|
||||||
"%(name)s on hold": "%(name)s on hold",
|
"%(name)s on hold": "%(name)s on hold",
|
||||||
"Unknown caller": "Unknown caller",
|
|
||||||
"Incoming voice call": "Incoming voice call",
|
|
||||||
"Incoming video call": "Incoming video call",
|
|
||||||
"Incoming call": "Incoming call",
|
|
||||||
"Sound on": "Sound on",
|
|
||||||
"Silence call": "Silence call",
|
|
||||||
"Decline": "Decline",
|
|
||||||
"Accept": "Accept",
|
|
||||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
@ -1149,6 +1159,10 @@
|
||||||
"Connecting to integration manager...": "Connecting to integration manager...",
|
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||||
|
"Message layout": "Message layout",
|
||||||
|
"IRC": "IRC",
|
||||||
|
"Modern": "Modern",
|
||||||
|
"Message bubbles": "Message bubbles",
|
||||||
"Messages containing keywords": "Messages containing keywords",
|
"Messages containing keywords": "Messages containing keywords",
|
||||||
"Error saving notification preferences": "Error saving notification preferences",
|
"Error saving notification preferences": "Error saving notification preferences",
|
||||||
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||||
|
@ -1261,10 +1275,6 @@
|
||||||
"Custom theme URL": "Custom theme URL",
|
"Custom theme URL": "Custom theme URL",
|
||||||
"Add theme": "Add theme",
|
"Add theme": "Add theme",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"Message layout": "Message layout",
|
|
||||||
"IRC": "IRC",
|
|
||||||
"Modern": "Modern",
|
|
||||||
"Message bubbles": "Message bubbles",
|
|
||||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||||
"Customise your appearance": "Customise your appearance",
|
"Customise your appearance": "Customise your appearance",
|
||||||
|
@ -1285,6 +1295,8 @@
|
||||||
"Deactivate Account": "Deactivate Account",
|
"Deactivate Account": "Deactivate Account",
|
||||||
"Deactivate account": "Deactivate account",
|
"Deactivate account": "Deactivate account",
|
||||||
"Discovery": "Discovery",
|
"Discovery": "Discovery",
|
||||||
|
"%(brand)s version:": "%(brand)s version:",
|
||||||
|
"Olm version:": "Olm version:",
|
||||||
"Legal": "Legal",
|
"Legal": "Legal",
|
||||||
"Credits": "Credits",
|
"Credits": "Credits",
|
||||||
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
||||||
|
@ -1298,13 +1310,11 @@
|
||||||
"FAQ": "FAQ",
|
"FAQ": "FAQ",
|
||||||
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
||||||
"Versions": "Versions",
|
"Versions": "Versions",
|
||||||
"%(brand)s version:": "%(brand)s version:",
|
"Copy": "Copy",
|
||||||
"olm version:": "olm version:",
|
|
||||||
"Homeserver is": "Homeserver is",
|
"Homeserver is": "Homeserver is",
|
||||||
"Identity server is": "Identity server is",
|
"Identity server is": "Identity server is",
|
||||||
"Access Token": "Access Token",
|
"Access Token": "Access Token",
|
||||||
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
||||||
"Copy": "Copy",
|
|
||||||
"Clear cache and reload": "Clear cache and reload",
|
"Clear cache and reload": "Clear cache and reload",
|
||||||
"Labs": "Labs",
|
"Labs": "Labs",
|
||||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
|
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
|
||||||
|
@ -1547,6 +1557,7 @@
|
||||||
"Send a reply…": "Send a reply…",
|
"Send a reply…": "Send a reply…",
|
||||||
"Send an encrypted message…": "Send an encrypted message…",
|
"Send an encrypted message…": "Send an encrypted message…",
|
||||||
"Send a message…": "Send a message…",
|
"Send a message…": "Send a message…",
|
||||||
|
"Send voice message": "Send voice message",
|
||||||
"The conversation continues here.": "The conversation continues here.",
|
"The conversation continues here.": "The conversation continues here.",
|
||||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||||
|
@ -1601,8 +1612,6 @@
|
||||||
"Hide Widgets": "Hide Widgets",
|
"Hide Widgets": "Hide Widgets",
|
||||||
"Show Widgets": "Show Widgets",
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Voice call": "Voice call",
|
|
||||||
"Video call": "Video call",
|
|
||||||
"Invites": "Invites",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
"People": "People",
|
"People": "People",
|
||||||
|
@ -1719,14 +1728,11 @@
|
||||||
"Invited by %(sender)s": "Invited by %(sender)s",
|
"Invited by %(sender)s": "Invited by %(sender)s",
|
||||||
"Jump to first unread message.": "Jump to first unread message.",
|
"Jump to first unread message.": "Jump to first unread message.",
|
||||||
"Mark all as read": "Mark all as read",
|
"Mark all as read": "Mark all as read",
|
||||||
"The voice message failed to upload.": "The voice message failed to upload.",
|
|
||||||
"Unable to access your microphone": "Unable to access your microphone",
|
"Unable to access your microphone": "Unable to access your microphone",
|
||||||
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
||||||
"No microphone found": "No microphone found",
|
"No microphone found": "No microphone found",
|
||||||
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
||||||
"Record a voice message": "Record a voice message",
|
"Stop recording": "Stop recording",
|
||||||
"Stop the recording": "Stop the recording",
|
|
||||||
"Delete recording": "Delete recording",
|
|
||||||
"Error updating main address": "Error updating main address",
|
"Error updating main address": "Error updating main address",
|
||||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
||||||
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",
|
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",
|
||||||
|
@ -1888,19 +1894,15 @@
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"Compare emoji": "Compare emoji",
|
||||||
"Connected": "Connected",
|
"Connected": "Connected",
|
||||||
"You declined this call": "You declined this call",
|
"Call declined": "Call declined",
|
||||||
"They declined this call": "They declined this call",
|
|
||||||
"Call back": "Call back",
|
"Call back": "Call back",
|
||||||
"Call again": "Call again",
|
"Missed call": "Missed call",
|
||||||
"This call has ended": "This call has ended",
|
|
||||||
"They didn't pick up": "They didn't pick up",
|
|
||||||
"Could not connect media": "Could not connect media",
|
"Could not connect media": "Could not connect media",
|
||||||
"Connection failed": "Connection failed",
|
"Connection failed": "Connection failed",
|
||||||
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||||
"An unknown error occurred": "An unknown error occurred",
|
"An unknown error occurred": "An unknown error occurred",
|
||||||
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
||||||
"This call has failed": "This call has failed",
|
"Retry": "Retry",
|
||||||
"You missed this call": "You missed this call",
|
|
||||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
|
@ -1923,7 +1925,6 @@
|
||||||
"Error processing audio message": "Error processing audio message",
|
"Error processing audio message": "Error processing audio message",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Retry": "Retry",
|
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
"Download %(text)s": "Download %(text)s",
|
"Download %(text)s": "Download %(text)s",
|
||||||
|
@ -2104,6 +2105,8 @@
|
||||||
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
|
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
|
||||||
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
|
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
|
||||||
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
|
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
|
||||||
|
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
|
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
"Power level": "Power level",
|
"Power level": "Power level",
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
"QR Code": "QR Code",
|
"QR Code": "QR Code",
|
||||||
|
|
|
@ -311,6 +311,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
"feature_hidden_read_receipts": {
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
displayName: _td(
|
||||||
|
"Don't send read receipts",
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"baseFontSize": {
|
"baseFontSize": {
|
||||||
displayName: _td("Font size"),
|
displayName: _td("Font size"),
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
@ -449,6 +456,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
|
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"MessageComposerInput.surroundWith": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td("Surround selected text when typing special characters"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.autoReplaceEmoji": {
|
"MessageComposerInput.autoReplaceEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Automatically replace plain text Emoji'),
|
displayName: _td('Automatically replace plain text Emoji'),
|
||||||
|
|
|
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
|
||||||
key: string;
|
key: string;
|
||||||
// higher priority number will be shown on top of lower priority
|
// higher priority number will be shown on top of lower priority
|
||||||
priority: number;
|
priority: number;
|
||||||
title: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: C;
|
component: C;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
src/theme.js
13
src/theme.js
|
@ -171,15 +171,10 @@ export async function setTheme(theme) {
|
||||||
// look for the stylesheet elements.
|
// look for the stylesheet elements.
|
||||||
// styleElements is a map from style name to HTMLLinkElement.
|
// styleElements is a map from style name to HTMLLinkElement.
|
||||||
const styleElements = Object.create(null);
|
const styleElements = Object.create(null);
|
||||||
let a;
|
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||||
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
themes.forEach(theme => {
|
||||||
const href = a.getAttribute("href");
|
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||||
// shouldn't we be using the 'title' tag rather than the href?
|
});
|
||||||
const match = href && href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
|
||||||
if (match) {
|
|
||||||
styleElements[match[1]] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(stylesheetName in styleElements)) {
|
if (!(stylesheetName in styleElements)) {
|
||||||
throw new Error("Unknown theme " + stylesheetName);
|
throw new Error("Unknown theme " + stylesheetName);
|
||||||
|
|
140
src/toasts/IncomingCallToast.tsx
Normal file
140
src/toasts/IncomingCallToast.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 from 'react';
|
||||||
|
import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { replaceableComponent } from '../utils/replaceableComponent';
|
||||||
|
import CallHandler, { CallHandlerEvent } from '../CallHandler';
|
||||||
|
import dis from '../dispatcher/dispatcher';
|
||||||
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||||
|
import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton';
|
||||||
|
import AccessibleButton from '../components/views/elements/AccessibleButton';
|
||||||
|
|
||||||
|
export const getIncomingCallToastKey = (callId: string) => `call_${callId}`;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
call: MatrixCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
silenced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.voip.IncomingCallToast")
|
||||||
|
export default class IncomingCallToast extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount = (): void => {
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||||
|
};
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSilencedCallsChanged = (): void => {
|
||||||
|
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAnswerClick = (e: React.MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'answer',
|
||||||
|
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRejectClick= (e: React.MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'reject',
|
||||||
|
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSilenceClick = (e: React.MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const callId = this.props.call.callId;
|
||||||
|
this.state.silenced ?
|
||||||
|
CallHandler.sharedInstance().unSilenceCall(callId) :
|
||||||
|
CallHandler.sharedInstance().silenceCall(callId);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const call = this.props.call;
|
||||||
|
const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call));
|
||||||
|
const isVoice = call.type === CallType.Voice;
|
||||||
|
|
||||||
|
const contentClass = classNames("mx_IncomingCallToast_content", {
|
||||||
|
"mx_IncomingCallToast_content_voice": isVoice,
|
||||||
|
"mx_IncomingCallToast_content_video": !isVoice,
|
||||||
|
});
|
||||||
|
const silenceClass = classNames("mx_IncomingCallToast_iconButton", {
|
||||||
|
"mx_IncomingCallToast_unSilence": this.state.silenced,
|
||||||
|
"mx_IncomingCallToast_silence": !this.state.silenced,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<RoomAvatar
|
||||||
|
room={room}
|
||||||
|
height={32}
|
||||||
|
width={32}
|
||||||
|
/>
|
||||||
|
<div className={contentClass}>
|
||||||
|
<span className="mx_CallEvent_caller">
|
||||||
|
{ room ? room.name : _t("Unknown caller") }
|
||||||
|
</span>
|
||||||
|
<div className="mx_CallEvent_type">
|
||||||
|
<div className="mx_CallEvent_type_icon" />
|
||||||
|
{ isVoice ? _t("Voice call") : _t("Video call") }
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallToast_buttons">
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_decline"
|
||||||
|
onClick={this.onRejectClick}
|
||||||
|
kind="danger"
|
||||||
|
>
|
||||||
|
<span> { _t("Decline") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_accept"
|
||||||
|
onClick={this.onAnswerClick}
|
||||||
|
kind="primary"
|
||||||
|
>
|
||||||
|
<span> { _t("Accept") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={silenceClass}
|
||||||
|
onClick={this.onSilenceClick}
|
||||||
|
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
|
||||||
|
/>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,9 +43,8 @@ function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise
|
||||||
|
|
||||||
// Dev note: the reassignment warnings are entirely incorrect here.
|
// Dev note: the reassignment warnings are entirely incorrect here.
|
||||||
|
|
||||||
// @ts-ignore
|
managedIframe.style.display = "none";
|
||||||
// noinspection JSConstantReassignment
|
|
||||||
managedIframe.style = { display: "none" };
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// noinspection JSConstantReassignment
|
// noinspection JSConstantReassignment
|
||||||
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
|
import { jsxJoin } from './ReactUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* formats numbers to fit into ~3 characters, suitable for badge counts
|
* formats numbers to fit into ~3 characters, suitable for badge counts
|
||||||
|
@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string {
|
||||||
* @returns {string} a string constructed by joining `items` with a comma
|
* @returns {string} a string constructed by joining `items` with a comma
|
||||||
* between each item, but with the last item appended as " and [lastItem]".
|
* between each item, but with the last item appended as " and [lastItem]".
|
||||||
*/
|
*/
|
||||||
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
|
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
|
||||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||||
items.length - itemLimit, 0,
|
items.length - itemLimit, 0,
|
||||||
);
|
);
|
||||||
|
@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
|
||||||
return items[0];
|
return items[0];
|
||||||
} else if (remaining > 0) {
|
} else if (remaining > 0) {
|
||||||
items = items.slice(0, itemLimit);
|
items = items.slice(0, itemLimit);
|
||||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
|
||||||
} else {
|
} else {
|
||||||
const lastItem = items.pop();
|
const lastItem = items.pop();
|
||||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
src/utils/ReactUtils.tsx
Normal file
33
src/utils/ReactUtils.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <span>hello world</span>
|
||||||
|
* @param array the array of element to join
|
||||||
|
* @param joiner the string/JSX.Element to join with
|
||||||
|
* @returns the joined array
|
||||||
|
*/
|
||||||
|
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
|
||||||
|
const newArray = [];
|
||||||
|
array.forEach((element, index) => {
|
||||||
|
newArray.push(element, (index === array.length - 1) ? null : joiner);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span>{ newArray }</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -156,13 +156,14 @@ describe('CallHandler', () => {
|
||||||
DMRoomMap.setShared(null);
|
DMRoomMap.setShared(null);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.mxCallHandler = null;
|
window.mxCallHandler = null;
|
||||||
|
fakeCall = null;
|
||||||
MatrixClientPeg.unset();
|
MatrixClientPeg.unset();
|
||||||
|
|
||||||
document.body.removeChild(audioElement);
|
document.body.removeChild(audioElement);
|
||||||
SdkConfig.unset();
|
SdkConfig.unset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should look up the correct user and open the room when a phone number is dialled', async () => {
|
it('should look up the correct user and start a call in the room when a phone number is dialled', async () => {
|
||||||
MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{
|
MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{
|
||||||
userid: '@user2:example.org',
|
userid: '@user2:example.org',
|
||||||
protocol: "im.vector.protocol.sip_native",
|
protocol: "im.vector.protocol.sip_native",
|
||||||
|
@ -179,6 +180,9 @@ describe('CallHandler', () => {
|
||||||
|
|
||||||
const viewRoomPayload = await untilDispatch('view_room');
|
const viewRoomPayload = await untilDispatch('view_room');
|
||||||
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
|
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
|
||||||
|
|
||||||
|
// Check that a call was started
|
||||||
|
expect(fakeCall.roomId).toEqual(MAPPED_ROOM_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move calls between rooms when remote asserted identity changes', async () => {
|
it('should move calls between rooms when remote asserted identity changes', async () => {
|
||||||
|
|
|
@ -106,7 +106,7 @@ describe('MemberEventListSummary', function() {
|
||||||
const result = wrapper.props.children;
|
const result = wrapper.props.children;
|
||||||
|
|
||||||
expect(result.props.children).toEqual([
|
expect(result.props.children).toEqual([
|
||||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,8 +129,8 @@ describe('MemberEventListSummary', function() {
|
||||||
const result = wrapper.props.children;
|
const result = wrapper.props.children;
|
||||||
|
|
||||||
expect(result.props.children).toEqual([
|
expect(result.props.children).toEqual([
|
||||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
<div className="event_tile" key="event1">Expanded membership</div>,
|
<div className="event_tile" key="event1">Expanded membership</div>,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe("ContentRules", function() {
|
||||||
describe("parseContentRules", function() {
|
describe("parseContentRules", function() {
|
||||||
it("should handle there being no keyword rules", function() {
|
it("should handle there being no keyword rules", function() {
|
||||||
const rules = { 'global': { 'content': [
|
const rules = { 'global': { 'content': [
|
||||||
USERNAME_RULE,
|
USERNAME_RULE,
|
||||||
] } };
|
] } };
|
||||||
const parsed = ContentRules.parseContentRules(rules);
|
const parsed = ContentRules.parseContentRules(rules);
|
||||||
expect(parsed.rules).toEqual([]);
|
expect(parsed.rules).toEqual([]);
|
||||||
|
|
|
@ -78,6 +78,7 @@ export function createTestClient() {
|
||||||
},
|
},
|
||||||
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
||||||
setAccountData: jest.fn(),
|
setAccountData: jest.fn(),
|
||||||
|
setRoomAccountData: jest.fn(),
|
||||||
sendTyping: jest.fn().mockResolvedValue({}),
|
sendTyping: jest.fn().mockResolvedValue({}),
|
||||||
sendMessage: () => jest.fn().mockResolvedValue({}),
|
sendMessage: () => jest.fn().mockResolvedValue({}),
|
||||||
getSyncState: () => "SYNCING",
|
getSyncState: () => "SYNCING",
|
||||||
|
@ -95,6 +96,7 @@ export function createTestClient() {
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pushRules: {},
|
||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
|
@ -129,8 +131,8 @@ export function mkEvent(opts) {
|
||||||
if (opts.skey) {
|
if (opts.skey) {
|
||||||
event.state_key = opts.skey;
|
event.state_key = opts.skey;
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||||
"m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
|
"m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
|
||||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||||
event.state_key = "";
|
event.state_key = "";
|
||||||
}
|
}
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
|
|
@ -49,7 +49,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[[true, true], [true, false],
|
[[true, true], [true, false],
|
||||||
[false, true], [false, false]],
|
[false, true], [false, false]],
|
||||||
)("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
|
)("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
@ -62,7 +62,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[["verified", true, true], ["verified", true, false],
|
[["verified", true, true], ["verified", true, false],
|
||||||
["verified", false, true], ["warning", false, false]],
|
["verified", false, true], ["warning", false, false]],
|
||||||
)("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
)("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
@ -75,7 +75,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[["normal", true, true], ["normal", true, false],
|
[["normal", true, true], ["normal", true, false],
|
||||||
["normal", false, true], ["warning", false, false]],
|
["normal", false, true], ["warning", false, false]],
|
||||||
)("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
)("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
@ -88,7 +88,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[["verified", true, true], ["verified", true, false],
|
[["verified", true, true], ["verified", true, false],
|
||||||
["warning", false, true], ["warning", false, false]],
|
["warning", false, true], ["warning", false, false]],
|
||||||
)("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
)("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
@ -101,7 +101,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[["verified", true, true], ["verified", true, false],
|
[["verified", true, true], ["verified", true, false],
|
||||||
["verified", false, true], ["verified", false, false]],
|
["verified", false, true], ["verified", false, false]],
|
||||||
)("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
)("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
@ -114,7 +114,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
[["normal", true, true], ["normal", true, false],
|
[["normal", true, true], ["normal", true, false],
|
||||||
["normal", false, true], ["normal", false, false]],
|
["normal", false, true], ["normal", false, false]],
|
||||||
)("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
)("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||||
const client = mkClient(trusted);
|
const client = mkClient(trusted);
|
||||||
const room = {
|
const room = {
|
||||||
|
|
|
@ -12,25 +12,14 @@
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"react",
|
|
||||||
"flux",
|
|
||||||
"react-transition-group"
|
|
||||||
],
|
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2019",
|
"es2019",
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable"
|
"dom.iterable"
|
||||||
],
|
],
|
||||||
"paths": {
|
|
||||||
"posthog-js": [
|
|
||||||
"./src/@types/posthog.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
"./src/**/*.tsx"
|
"./src/**/*.tsx"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
335
yarn.lock
335
yarn.lock
|
@ -1324,6 +1324,107 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@octokit/auth-token@^2.4.4":
|
||||||
|
version "2.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
|
||||||
|
integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/types" "^6.0.3"
|
||||||
|
|
||||||
|
"@octokit/core@^3.5.0":
|
||||||
|
version "3.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
|
||||||
|
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/auth-token" "^2.4.4"
|
||||||
|
"@octokit/graphql" "^4.5.8"
|
||||||
|
"@octokit/request" "^5.6.0"
|
||||||
|
"@octokit/request-error" "^2.0.5"
|
||||||
|
"@octokit/types" "^6.0.3"
|
||||||
|
before-after-hook "^2.2.0"
|
||||||
|
universal-user-agent "^6.0.0"
|
||||||
|
|
||||||
|
"@octokit/endpoint@^6.0.1":
|
||||||
|
version "6.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
|
||||||
|
integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/types" "^6.0.3"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
universal-user-agent "^6.0.0"
|
||||||
|
|
||||||
|
"@octokit/graphql@^4.5.8":
|
||||||
|
version "4.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed"
|
||||||
|
integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/request" "^5.6.0"
|
||||||
|
"@octokit/types" "^6.0.3"
|
||||||
|
universal-user-agent "^6.0.0"
|
||||||
|
|
||||||
|
"@octokit/openapi-types@^9.3.0":
|
||||||
|
version "9.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e"
|
||||||
|
integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw==
|
||||||
|
|
||||||
|
"@octokit/plugin-paginate-rest@^2.6.2":
|
||||||
|
version "2.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d"
|
||||||
|
integrity sha512-/vjcb0w6ggVRtsb8OJBcRR9oEm+fpdo8RJk45khaWw/W0c8rlB2TLCLyZt/knmC17NkX7T9XdyQeEY7OHLSV1g==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/types" "^6.23.0"
|
||||||
|
|
||||||
|
"@octokit/plugin-request-log@^1.0.2":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
|
||||||
|
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
|
||||||
|
|
||||||
|
"@octokit/plugin-rest-endpoint-methods@5.6.0":
|
||||||
|
version "5.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.6.0.tgz#c28833b88d0f07bf94093405d02d43d73c7de99b"
|
||||||
|
integrity sha512-2G7lIPwjG9XnTlNhe/TRnpI8yS9K2l68W4RP/ki3wqw2+sVeTK8hItPxkqEI30VeH0UwnzpuksMU/yHxiVVctw==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/types" "^6.23.0"
|
||||||
|
deprecation "^2.3.1"
|
||||||
|
|
||||||
|
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
|
||||||
|
integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/types" "^6.0.3"
|
||||||
|
deprecation "^2.0.0"
|
||||||
|
once "^1.4.0"
|
||||||
|
|
||||||
|
"@octokit/request@^5.6.0":
|
||||||
|
version "5.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672"
|
||||||
|
integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/endpoint" "^6.0.1"
|
||||||
|
"@octokit/request-error" "^2.1.0"
|
||||||
|
"@octokit/types" "^6.16.1"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
node-fetch "^2.6.1"
|
||||||
|
universal-user-agent "^6.0.0"
|
||||||
|
|
||||||
|
"@octokit/rest@^18.6.7":
|
||||||
|
version "18.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.8.0.tgz#ba24f7ba554f015a7ae2b7cc2aecef5386ddfea5"
|
||||||
|
integrity sha512-lsuNRhgzGnLMn/NmQTNCit/6jplFWiTUlPXhqN0zCMLwf2/9pseHzsnTW+Cjlp4bLMEJJNPa5JOzSLbSCOahKw==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/core" "^3.5.0"
|
||||||
|
"@octokit/plugin-paginate-rest" "^2.6.2"
|
||||||
|
"@octokit/plugin-request-log" "^1.0.2"
|
||||||
|
"@octokit/plugin-rest-endpoint-methods" "5.6.0"
|
||||||
|
|
||||||
|
"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.23.0":
|
||||||
|
version "6.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.23.0.tgz#b39f242b20036e89fa8f34f7962b4e9b7ff8f65b"
|
||||||
|
integrity sha512-eG3clC31GSS7K3oBK6C6o7wyXPrkP+mu++eus8CSZdpRytJ5PNszYxudOQ0spWZQ3S9KAtoTG6v1WK5prJcJrA==
|
||||||
|
dependencies:
|
||||||
|
"@octokit/openapi-types" "^9.3.0"
|
||||||
|
|
||||||
"@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.0.32":
|
"@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.0.32":
|
||||||
version "2.0.37"
|
version "2.0.37"
|
||||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.37.tgz#700476512ab903d809f64a3040fb1b2fe6fb6d4b"
|
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.37.tgz#700476512ab903d809f64a3040fb1b2fe6fb6d4b"
|
||||||
|
@ -1352,6 +1453,11 @@
|
||||||
tslib "^2.2.0"
|
tslib "^2.2.0"
|
||||||
webcrypto-core "^1.2.0"
|
webcrypto-core "^1.2.0"
|
||||||
|
|
||||||
|
"@sentry/types@^6.10.0":
|
||||||
|
version "6.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1"
|
||||||
|
integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==
|
||||||
|
|
||||||
"@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"
|
||||||
|
@ -1865,6 +1971,17 @@ ajv@^8.0.1:
|
||||||
require-from-string "^2.0.2"
|
require-from-string "^2.0.2"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
|
"allchange@github:matrix-org/allchange":
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab"
|
||||||
|
dependencies:
|
||||||
|
"@octokit/rest" "^18.6.7"
|
||||||
|
cli-color "^2.0.0"
|
||||||
|
js-yaml "^4.1.0"
|
||||||
|
loglevel "^1.7.1"
|
||||||
|
semver "^7.3.5"
|
||||||
|
yargs "^17.0.1"
|
||||||
|
|
||||||
another-json@^0.2.0:
|
another-json@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc"
|
resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc"
|
||||||
|
@ -1882,6 +1999,11 @@ ansi-escapes@^4.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.21.3"
|
type-fest "^0.21.3"
|
||||||
|
|
||||||
|
ansi-regex@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||||
|
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
|
||||||
|
|
||||||
ansi-regex@^4.1.0:
|
ansi-regex@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||||
|
@ -1929,6 +2051,11 @@ argparse@^1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js "~1.0.2"
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
|
argparse@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
arr-diff@^4.0.0:
|
arr-diff@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||||
|
@ -2226,6 +2353,11 @@ big.js@^5.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||||
|
|
||||||
|
before-after-hook@^2.2.0:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
|
||||||
|
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
|
||||||
|
|
||||||
binary-extensions@^1.0.0:
|
binary-extensions@^1.0.0:
|
||||||
version "1.13.1"
|
version "1.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
||||||
|
@ -2526,6 +2658,18 @@ classnames@*, classnames@^2.2.6:
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
|
||||||
|
cli-color@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.0.tgz#11ecfb58a79278cf6035a60c54e338f9d837897c"
|
||||||
|
integrity sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^2.1.1"
|
||||||
|
d "^1.0.1"
|
||||||
|
es5-ext "^0.10.51"
|
||||||
|
es6-iterator "^2.0.3"
|
||||||
|
memoizee "^0.4.14"
|
||||||
|
timers-ext "^0.1.7"
|
||||||
|
|
||||||
cliui@^5.0.0:
|
cliui@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
|
||||||
|
@ -2544,6 +2688,15 @@ cliui@^6.0.0:
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
wrap-ansi "^6.2.0"
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
|
cliui@^7.0.2:
|
||||||
|
version "7.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||||
|
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wrap-ansi "^7.0.0"
|
||||||
|
|
||||||
clone-deep@^4.0.1:
|
clone-deep@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||||
|
@ -2808,6 +2961,14 @@ csstype@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
|
||||||
integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
|
integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
|
||||||
|
|
||||||
|
d@1, d@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
|
||||||
|
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
|
||||||
|
dependencies:
|
||||||
|
es5-ext "^0.10.50"
|
||||||
|
type "^1.0.1"
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -2915,6 +3076,11 @@ delayed-stream@~1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||||
|
|
||||||
|
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
|
||||||
|
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
|
||||||
|
|
||||||
detect-newline@^3.0.0:
|
detect-newline@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||||
|
@ -3216,6 +3382,42 @@ es-to-primitive@^1.2.1:
|
||||||
is-date-object "^1.0.1"
|
is-date-object "^1.0.1"
|
||||||
is-symbol "^1.0.2"
|
is-symbol "^1.0.2"
|
||||||
|
|
||||||
|
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
|
||||||
|
version "0.10.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
|
||||||
|
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
|
||||||
|
dependencies:
|
||||||
|
es6-iterator "~2.0.3"
|
||||||
|
es6-symbol "~3.1.3"
|
||||||
|
next-tick "~1.0.0"
|
||||||
|
|
||||||
|
es6-iterator@^2.0.3, es6-iterator@~2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
|
||||||
|
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
|
||||||
|
dependencies:
|
||||||
|
d "1"
|
||||||
|
es5-ext "^0.10.35"
|
||||||
|
es6-symbol "^3.1.1"
|
||||||
|
|
||||||
|
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
|
||||||
|
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
|
||||||
|
dependencies:
|
||||||
|
d "^1.0.1"
|
||||||
|
ext "^1.1.2"
|
||||||
|
|
||||||
|
es6-weak-map@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
|
||||||
|
integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
|
||||||
|
dependencies:
|
||||||
|
d "1"
|
||||||
|
es5-ext "^0.10.46"
|
||||||
|
es6-iterator "^2.0.3"
|
||||||
|
es6-symbol "^3.1.1"
|
||||||
|
|
||||||
escalade@^3.1.1:
|
escalade@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||||
|
@ -3258,9 +3460,9 @@ eslint-config-google@^0.14.0:
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
|
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
|
||||||
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
|
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
|
||||||
|
|
||||||
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
|
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945":
|
||||||
version "0.3.4"
|
version "0.3.5"
|
||||||
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/45f6937539192e3820edcafc4d6d4d4187e85a6a"
|
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945"
|
||||||
|
|
||||||
eslint-plugin-react-hooks@^4.2.0:
|
eslint-plugin-react-hooks@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
|
@ -3408,6 +3610,14 @@ esutils@^2.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||||
|
|
||||||
|
event-emitter@^0.3.5:
|
||||||
|
version "0.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
|
||||||
|
integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
|
||||||
|
dependencies:
|
||||||
|
d "1"
|
||||||
|
es5-ext "~0.10.14"
|
||||||
|
|
||||||
events@^3.2.0:
|
events@^3.2.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||||
|
@ -3503,6 +3713,13 @@ expect@^26.6.2:
|
||||||
jest-message-util "^26.6.2"
|
jest-message-util "^26.6.2"
|
||||||
jest-regex-util "^26.0.0"
|
jest-regex-util "^26.0.0"
|
||||||
|
|
||||||
|
ext@^1.1.2:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
|
||||||
|
integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
|
||||||
|
dependencies:
|
||||||
|
type "^2.0.0"
|
||||||
|
|
||||||
extend-shallow@^2.0.1:
|
extend-shallow@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
||||||
|
@ -3813,7 +4030,7 @@ gensync@^1.0.0-beta.2:
|
||||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||||
|
|
||||||
get-caller-file@^2.0.1:
|
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
@ -4537,6 +4754,11 @@ is-potential-custom-element-name@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||||
|
|
||||||
|
is-promise@^2.2.2:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||||
|
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
|
||||||
|
|
||||||
is-regex@^1.0.3, is-regex@^1.0.5, is-regex@^1.1.2, is-regex@^1.1.3:
|
is-regex@^1.0.3, is-regex@^1.0.5, is-regex@^1.1.2, is-regex@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
|
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
|
||||||
|
@ -5162,6 +5384,13 @@ js-yaml@^3.13.1:
|
||||||
argparse "^1.0.7"
|
argparse "^1.0.7"
|
||||||
esprima "^4.0.0"
|
esprima "^4.0.0"
|
||||||
|
|
||||||
|
js-yaml@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||||
|
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||||
|
dependencies:
|
||||||
|
argparse "^2.0.1"
|
||||||
|
|
||||||
jsbn@~0.1.0:
|
jsbn@~0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||||
|
@ -5462,6 +5691,13 @@ lru-cache@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
lru-queue@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
|
||||||
|
integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
|
||||||
|
dependencies:
|
||||||
|
es5-ext "~0.10.2"
|
||||||
|
|
||||||
make-dir@^2.0.0, make-dir@^2.1.0:
|
make-dir@^2.0.0, make-dir@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||||
|
@ -5594,6 +5830,20 @@ memoize-one@^5.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
|
|
||||||
|
memoizee@^0.4.14:
|
||||||
|
version "0.4.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
|
||||||
|
integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==
|
||||||
|
dependencies:
|
||||||
|
d "^1.0.1"
|
||||||
|
es5-ext "^0.10.53"
|
||||||
|
es6-weak-map "^2.0.3"
|
||||||
|
event-emitter "^0.3.5"
|
||||||
|
is-promise "^2.2.2"
|
||||||
|
lru-queue "^0.1.0"
|
||||||
|
next-tick "^1.1.0"
|
||||||
|
timers-ext "^0.1.7"
|
||||||
|
|
||||||
meow@^9.0.0:
|
meow@^9.0.0:
|
||||||
version "9.0.0"
|
version "9.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
|
resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
|
||||||
|
@ -5767,12 +6017,22 @@ nearley@^2.7.10:
|
||||||
railroad-diagrams "^1.0.0"
|
railroad-diagrams "^1.0.0"
|
||||||
randexp "0.4.6"
|
randexp "0.4.6"
|
||||||
|
|
||||||
|
next-tick@1, next-tick@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
|
||||||
|
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
|
||||||
|
|
||||||
|
next-tick@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||||
|
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
||||||
|
|
||||||
nice-try@^1.0.4:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||||
|
|
||||||
node-fetch@2.6.1:
|
node-fetch@2.6.1, node-fetch@^2.6.1:
|
||||||
version "2.6.1"
|
version "2.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||||
|
@ -6320,10 +6580,10 @@ postcss@^8.0.2:
|
||||||
nanoid "^3.1.23"
|
nanoid "^3.1.23"
|
||||||
source-map-js "^0.6.2"
|
source-map-js "^0.6.2"
|
||||||
|
|
||||||
posthog-js@1.12.1:
|
posthog-js@1.12.2:
|
||||||
version "1.12.1"
|
version "1.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27"
|
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.2.tgz#ff76e26634067e003f8af7df654d7ea0e647d946"
|
||||||
integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg==
|
integrity sha512-I0d6c+Yu2f91PFidz65AIkkqZM219EY9Z1wlbTkW5Zqfq5oXqogBMKS8BaDBOrMc46LjLX7IH67ytCcBFRo1uw==
|
||||||
dependencies:
|
dependencies:
|
||||||
fflate "^0.4.1"
|
fflate "^0.4.1"
|
||||||
|
|
||||||
|
@ -6915,6 +7175,11 @@ rimraf@^3.0.0, rimraf@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
rrweb-snapshot@1.1.7:
|
||||||
|
version "1.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653"
|
||||||
|
integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg==
|
||||||
|
|
||||||
rst-selector-parser@^2.2.3:
|
rst-selector-parser@^2.2.3:
|
||||||
version "2.2.3"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
|
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
|
||||||
|
@ -7598,6 +7863,14 @@ throat@^5.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
||||||
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
||||||
|
|
||||||
|
timers-ext@^0.1.7:
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
|
||||||
|
integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
|
||||||
|
dependencies:
|
||||||
|
es5-ext "~0.10.46"
|
||||||
|
next-tick "1"
|
||||||
|
|
||||||
tiny-invariant@^1.0.6:
|
tiny-invariant@^1.0.6:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
|
@ -7757,6 +8030,16 @@ type-fest@^0.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
|
type@^1.0.1:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
|
||||||
|
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
|
||||||
|
|
||||||
|
type@^2.0.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
|
||||||
|
integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
|
||||||
|
|
||||||
typedarray-to-buffer@^3.1.5:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
|
@ -7853,6 +8136,11 @@ unist-util-stringify-position@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/unist" "^2.0.2"
|
"@types/unist" "^2.0.2"
|
||||||
|
|
||||||
|
universal-user-agent@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
|
||||||
|
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
|
||||||
|
|
||||||
universalify@^0.1.2:
|
universalify@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
|
@ -8156,6 +8444,15 @@ wrap-ansi@^6.2.0:
|
||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrappy@1:
|
wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
@ -8191,6 +8488,11 @@ y18n@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||||
|
|
||||||
|
y18n@^5.0.5:
|
||||||
|
version "5.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
|
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||||
|
|
||||||
yallist@^4.0.0:
|
yallist@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
|
@ -8217,7 +8519,7 @@ yargs-parser@^18.1.2:
|
||||||
camelcase "^5.0.0"
|
camelcase "^5.0.0"
|
||||||
decamelize "^1.2.0"
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
yargs-parser@^20.2.3:
|
yargs-parser@^20.2.2, yargs-parser@^20.2.3:
|
||||||
version "20.2.9"
|
version "20.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||||
|
@ -8255,6 +8557,19 @@ yargs@^15.4.1:
|
||||||
y18n "^4.0.0"
|
y18n "^4.0.0"
|
||||||
yargs-parser "^18.1.2"
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
|
yargs@^17.0.1:
|
||||||
|
version "17.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
|
||||||
|
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
|
||||||
|
dependencies:
|
||||||
|
cliui "^7.0.2"
|
||||||
|
escalade "^3.1.1"
|
||||||
|
get-caller-file "^2.0.5"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
string-width "^4.2.0"
|
||||||
|
y18n "^5.0.5"
|
||||||
|
yargs-parser "^20.2.2"
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue