mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 01:35:49 +03:00
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",
|
||||
"parse5": "^6.0.1",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.12.1",
|
||||
"posthog-js": "1.12.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"qrcode": "^1.4.4",
|
||||
"re-resizable": "^6.9.0",
|
||||
|
@ -125,6 +125,7 @@
|
|||
"@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",
|
||||
"@peculiar/webcrypto": "^1.1.4",
|
||||
"@sentry/types": "^6.10.0",
|
||||
"@sinonjs/fake-timers": "^7.0.2",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
|
@ -151,13 +152,14 @@
|
|||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||
"allchange": "github:matrix-org/allchange",
|
||||
"babel-jest": "^26.6.3",
|
||||
"chokidar": "^3.5.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "7.18.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-hooks": "^4.2.0",
|
||||
"glob": "^7.1.6",
|
||||
|
@ -172,6 +174,7 @@
|
|||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rrweb-snapshot": "1.1.7",
|
||||
"stylelint": "^13.9.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
|
@ -197,6 +200,7 @@
|
|||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader"
|
||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/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/_EmailAddresses.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
@import "./views/settings/_LayoutSwitcher.scss";
|
||||
@import "./views/settings/_Notifications.scss";
|
||||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
@import "./views/settings/_ProfileSettings.scss";
|
||||
|
@ -267,12 +268,14 @@
|
|||
@import "./views/spaces/_SpacePublicShare.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
@import "./views/toasts/_AnalyticsToast.scss";
|
||||
@import "./views/toasts/_IncomingCallToast.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallPreview.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
@import "./views/voip/_CallViewHeader.scss";
|
||||
@import "./views/voip/_CallViewSidebar.scss";
|
||||
@import "./views/voip/_DialPad.scss";
|
||||
@import "./views/voip/_DialPadContextMenu.scss";
|
||||
|
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
margin: 0 4px;
|
||||
grid-row: 2 / 4;
|
||||
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);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
|||
grid-row: 1 / 3;
|
||||
grid-column: 1;
|
||||
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);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -85,7 +85,7 @@ limitations under the License.
|
|||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,35 +24,33 @@ limitations under the License.
|
|||
align-items: flex-start;
|
||||
height: 500px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 8px;
|
||||
}
|
||||
.mx_desktopCapturerSourcePicker_source {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
width: 312px;
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
|
||||
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $accent-color;
|
||||
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
||||
&:hover,
|
||||
&:focus {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
padding: 5px 0;
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
|
|
|
@ -310,14 +310,12 @@ $hover-select-border: 4px;
|
|||
}
|
||||
|
||||
.mx_RoomView_timeline_rr_enabled {
|
||||
|
||||
.mx_EventTile:not([data-layout=bubble]) {
|
||||
.mx_EventTile[data-layout=group] {
|
||||
.mx_EventTile_line {
|
||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||
margin-right: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
border: 2px solid $voice-record-stop-border-color;
|
||||
border-radius: 32px;
|
||||
margin-right: 16px; // between us and the send button
|
||||
margin-right: 8px; // between us and the waveform component
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
|
@ -46,9 +46,28 @@ limitations under the License.
|
|||
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 {
|
||||
// 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-right: 12px; // isolate from stop/send button
|
||||
|
||||
|
@ -68,7 +87,7 @@ limitations under the License.
|
|||
height: 10px;
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
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 {
|
||||
& > textarea {
|
||||
font-family: inherit;
|
||||
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");
|
||||
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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: $primary-fg-color;
|
||||
|
||||
|
|
|
@ -28,28 +28,32 @@ limitations under the License.
|
|||
user-select: all;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken {
|
||||
.mx_HelpUserSettingsTab_copy {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
border: solid 1px $light-fg-color;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
width: max-content;
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
display: inherit;
|
||||
}
|
||||
.mx_HelpUserSettingsTab_copyButton {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
display: block;
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
||||
mask-image: url($copy-button-url);
|
||||
background-color: $message-action-bar-fg-color;
|
||||
margin-left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
mask-image: url($copy-button-url);
|
||||
background-color: $message-action-bar-fg-color;
|
||||
width: 20px;
|
||||
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 {
|
||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||
cursor: pointer;
|
||||
|
||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||
min-height: 150px;
|
||||
|
@ -43,84 +42,4 @@ limitations under the License.
|
|||
.mx_AppTile_persistedWrapper div {
|
||||
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 {
|
||||
width: 320px;
|
||||
padding-bottom: 8px;
|
||||
background-color: $voipcall-plinth-color;
|
||||
background-color: $toast-bg-color;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||
border-radius: 8px;
|
||||
|
||||
|
@ -75,8 +75,6 @@ limitations under the License.
|
|||
height: 100%;
|
||||
|
||||
&.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;
|
||||
display: flex;
|
||||
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 {
|
||||
position: absolute;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.mx_VideoFeed_video {
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
|
||||
&.mx_VideoFeed_voice {
|
||||
background-color: $inverted-bg-color;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.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">
|
||||
<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="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"/>
|
||||
<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"/>
|
||||
</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
|
||||
// try to use these colors when possible
|
||||
$bg-color: #15191E;
|
||||
|
@ -47,7 +50,7 @@ $inverted-bg-color: $base-color;
|
|||
$selected-color: $room-highlight-color;
|
||||
|
||||
// selected for hoverover & selected event tiles
|
||||
$event-selected-color: #21262c;
|
||||
$event-selected-color: $system-dark;
|
||||
|
||||
// used for the hairline dividers in RoomView
|
||||
$primary-hairline-color: transparent;
|
||||
|
@ -91,7 +94,7 @@ $lightbox-background-bg-color: #000;
|
|||
$lightbox-background-bg-opacity: 0.85;
|
||||
|
||||
$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-button-bg-color: #e7e7e7;
|
||||
$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;
|
||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #394049;
|
||||
$quinary-content-color: #394049;
|
||||
$toast-bg-color: $quinary-content-color;
|
||||
|
||||
// ********************
|
||||
|
||||
|
@ -175,7 +178,7 @@ $button-link-bg-color: transparent;
|
|||
$togglesw-off-color: $room-highlight-color;
|
||||
|
||||
$progressbar-fg-color: $accent-color;
|
||||
$progressbar-bg-color: #21262c;
|
||||
$progressbar-bg-color: $system-dark;
|
||||
|
||||
$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-bg-color: #394049; // "Dark Tile"
|
||||
$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-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);
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #143A34;
|
||||
$eventbubble-others-bg: #394049;
|
||||
$eventbubble-bg-hover: #433C23;
|
||||
$eventbubble-self-bg: #14322E;
|
||||
$eventbubble-others-bg: $event-selected-color;
|
||||
$eventbubble-bg-hover: #1C2026;
|
||||
$eventbubble-avatar-outline: $bg-color;
|
||||
$eventbubble-reply-color: #C1C6CD;
|
||||
|
||||
|
|
|
@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$header-divider-color: $header-panel-text-primary-color;
|
||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #394049;
|
||||
$quinary-content-color: #394049;
|
||||
$toast-bg-color: $quinary-content-color;
|
||||
|
||||
// ********************
|
||||
|
||||
|
@ -222,6 +222,13 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
$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! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||
digits in flowed text to stand out.
|
||||
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
|
||||
// try to use these colors when possible
|
||||
|
@ -178,8 +181,8 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$composer-e2e-icon-color: #91a1c0;
|
||||
$header-divider-color: #91a1c0;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #F4F6FA;
|
||||
$toast-bg-color: $system-light;
|
||||
$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-bg-color: #E3E8F0;
|
||||
$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
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
|
@ -348,9 +351,9 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
$composer-shadow-color: tranparent;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #F8FDFC;
|
||||
$eventbubble-others-bg: #F7F8F9;
|
||||
$eventbubble-bg-hover: rgb(242, 242, 242);
|
||||
$eventbubble-self-bg: #F0FBF8;
|
||||
$eventbubble-others-bg: $system-light;
|
||||
$eventbubble-bg-hover: #FAFBFD;
|
||||
$eventbubble-avatar-outline: #fff;
|
||||
$eventbubble-reply-color: #C1C6CD;
|
||||
|
||||
|
@ -390,7 +393,7 @@ $eventbubble-reply-color: #C1C6CD;
|
|||
@define-mixin mx_DialogButton_secondary {
|
||||
// flip colours for the secondary ones
|
||||
font-weight: 600;
|
||||
border: 1px solid $accent-color ! important;
|
||||
border: 1px solid $accent-color !important;
|
||||
color: $accent-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
|
||||
$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
|
||||
digits in flowed text to stand out.
|
||||
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
|
||||
// try to use these colors when possible
|
||||
|
@ -138,7 +141,7 @@ $blockquote-bar-color: #ddd;
|
|||
$blockquote-fg-color: #777;
|
||||
|
||||
$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-button-bg-color: #e7e7e7;
|
||||
$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;
|
||||
$header-divider-color: #91A1C0;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #F4F6FA;
|
||||
$toast-bg-color: $system-light;
|
||||
$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-bg-color: #E3E8F0; // "Separator"
|
||||
$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
|
||||
// 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);
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #F8FDFC;
|
||||
$eventbubble-others-bg: #F7F8F9;
|
||||
$eventbubble-bg-hover: #FEFCF5;
|
||||
$eventbubble-self-bg: #F0FBF8;
|
||||
$eventbubble-others-bg: $system-light;
|
||||
$eventbubble-bg-hover: #FAFBFD;
|
||||
$eventbubble-avatar-outline: $primary-bg-color;
|
||||
$eventbubble-reply-color: #C1C6CD;
|
||||
|
||||
|
@ -392,7 +395,7 @@ $eventbubble-reply-color: #C1C6CD;
|
|||
@define-mixin mx_DialogButton_secondary {
|
||||
// flip colours for the secondary ones
|
||||
font-weight: 600;
|
||||
border: 1px solid $accent-color ! important;
|
||||
border: 1px solid $accent-color !important;
|
||||
color: $accent-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 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");
|
||||
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 dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import { Jitsi } from "./widgets/Jitsi";
|
||||
import { WidgetType } from "./widgets/WidgetType";
|
||||
|
@ -86,6 +86,12 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
|||
import EventEmitter from 'events';
|
||||
import SdkConfig from './SdkConfig';
|
||||
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_PREFIXED = 'im.vector.protocol.pstn';
|
||||
|
@ -476,26 +482,44 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
switch (newState) {
|
||||
case CallState.Ringing:
|
||||
this.play(AudioID.Ring);
|
||||
case CallState.Ringing: {
|
||||
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;
|
||||
case CallState.InviteSent:
|
||||
}
|
||||
case CallState.InviteSent: {
|
||||
this.play(AudioID.Ringback);
|
||||
break;
|
||||
case CallState.Ended:
|
||||
{
|
||||
}
|
||||
case CallState.Ended: {
|
||||
const hangupReason = call.hangupReason;
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||
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 description;
|
||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
title = _t("User Busy");
|
||||
description = _t("The user you called is busy.");
|
||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
||||
} else {
|
||||
title = _t("Call Failed");
|
||||
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}`,
|
||||
);
|
||||
|
||||
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({
|
||||
action: 'call_state',
|
||||
room_id: mappedRoomId,
|
||||
|
@ -914,6 +951,8 @@ export default class CallHandler extends EventEmitter {
|
|||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
await this.placeCall(roomId, PlaceCallType.Voice, null);
|
||||
}
|
||||
|
||||
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
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
||||
const hasJitsi = currentJitsiWidgets.length > 0
|
||||
|| WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
|
||||
if (hasJitsi) {
|
||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is currently being placed!'),
|
||||
});
|
||||
const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
|
||||
if (jitsiWidget) {
|
||||
// If there already is a Jitsi widget pin it
|
||||
WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
|
|||
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.
|
||||
*
|
||||
|
@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
|
|||
* @param {File} imageFile The image to read and thumbnail.
|
||||
* @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";
|
||||
if (imageFile.type === "image/jpeg") {
|
||||
thumbnailType = "image/jpeg";
|
||||
}
|
||||
|
||||
let imageInfo;
|
||||
return loadImageElement(imageFile).then((r) => {
|
||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||
}).then((result) => {
|
||||
imageInfo = result.info;
|
||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
}).then((result) => {
|
||||
imageInfo.thumbnail_url = result.url;
|
||||
imageInfo.thumbnail_file = result.file;
|
||||
const imageElement = await loadImageElement(imageFile);
|
||||
|
||||
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||
const imageInfo = result.info;
|
||||
|
||||
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||
if (
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
|
|
|
@ -57,7 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
|||
|
||||
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)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||
|
||||
|
|
|
@ -146,23 +146,23 @@ export default class IdentityAuthClient {
|
|||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
|
||||
QuestionDialog, {
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"This action requires accessing the default identity server " +
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"This action requires accessing the default identity server " +
|
||||
"<server /> to validate an email address or phone number, " +
|
||||
"but the server does not have any terms of service.", {},
|
||||
{
|
||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Only continue if you trust the owner of the server.",
|
||||
) }</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
{
|
||||
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Only continue if you trust the owner of the server.",
|
||||
) }</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) {
|
||||
|
|
|
@ -163,7 +163,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
|||
modifiers: [Modifiers.SHIFT],
|
||||
key: Key.PAGE_UP,
|
||||
}],
|
||||
description: _td("Jump to oldest unread message"),
|
||||
description: _td("Jump to oldest unread message"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
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".
|
||||
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.
|
||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
||||
const resampled = 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);
|
||||
// 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 so we end up with a clamped waveform to rely upon.
|
||||
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||
}
|
||||
|
||||
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 { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||
import { clamp } from "../utils/numbers";
|
||||
import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||
|
||||
const CHANNELS = 1; // stereo isn't important
|
||||
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);
|
||||
|
||||
// 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
|
||||
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);
|
||||
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
||||
this.recorderSource.connect(this.recorderWorklet);
|
||||
|
|
|
@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
|
|||
SilencedChanged = "silenced_changed",
|
||||
}
|
||||
|
||||
const CONNECTING_STATES = [
|
||||
CallState.Connecting,
|
||||
CallState.WaitLocalMedia,
|
||||
CallState.CreateOffer,
|
||||
CallState.CreateAnswer,
|
||||
];
|
||||
|
||||
const SUPPORTED_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.Connecting,
|
||||
CallState.Ringing,
|
||||
];
|
||||
|
||||
|
@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
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 {
|
||||
const invite = this.invite;
|
||||
if (!invite) return;
|
||||
|
@ -82,6 +92,11 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
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
|
||||
*/
|
||||
|
@ -127,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
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 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
|
||||
// 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
|
||||
class MemberGrouper extends BaseGrouper {
|
||||
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(
|
||||
|
@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
|
|||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return membershipTypes.includes(ev.getType() as EventType);
|
||||
return groupedEvents.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||
|
|
|
@ -192,11 +192,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
|
||||
if (inviteSender) {
|
||||
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 className="mx_SpaceRoomView_preview_inviter_name">
|
||||
{ _t("<inviter/> invites you", {}, {
|
||||
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
||||
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||
}) }
|
||||
</div>
|
||||
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||
|
|
|
@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
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 ',
|
||||
this.props.timelineSet.room.roomId,
|
||||
'rm', this.state.readMarkerEventId,
|
||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||
' hidden:' + hiddenRR,
|
||||
);
|
||||
MatrixClientPeg.get().setRoomReadMarkers(
|
||||
this.props.timelineSet.room.roomId,
|
||||
roomId,
|
||||
this.state.readMarkerEventId,
|
||||
lastReadEvent, // Could be null, in which case no RR is sent
|
||||
{},
|
||||
{ hidden: hiddenRR },
|
||||
).catch((e) => {
|
||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||
|
|
|
@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
let containerClasses;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const { title, icon, key, component, className, props } = topToast;
|
||||
const toastClasses = classNames("mx_Toast_toast", {
|
||||
const { title, icon, key, component, className, bodyClassName, props } = topToast;
|
||||
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
|
||||
const toastClasses = classNames("mx_Toast_toast", className, {
|
||||
"mx_Toast_hasIcon": 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, {
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{ title }</h2>
|
||||
<span>{ countIndicator }</span>
|
||||
const content = React.createElement(component, toastProps);
|
||||
|
||||
let countIndicator;
|
||||
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 className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
|
||||
</div>);
|
||||
);
|
||||
|
||||
containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
|
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arrayFastResample } from "../../../utils/arrays";
|
||||
import { percentageOf } from "../../../utils/numbers";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
waveform: [],
|
||||
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
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, but typically even screaming into a
|
||||
// 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));
|
||||
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -490,9 +490,9 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
|||
forceStateEvent={true}
|
||||
onBack={this.onBack}
|
||||
inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||
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")
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onFinished: function() {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
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);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
|
|||
};
|
||||
|
||||
if (shouldLoadBackupStatus) {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
private async loadBackupStatus() {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.setState({
|
||||
|
@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onSettingsLinkClick() {
|
||||
private onSettingsLinkClick = (): void => {
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked() {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_onFinished(confirmed) {
|
||||
private onFinished = (confirmed: boolean): void => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
}
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(confirmed);
|
||||
};
|
||||
|
||||
_onSetRecoveryMethodClick() {
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
if (this.state.backupInfo) {
|
||||
// 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
|
||||
|
@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onLogoutConfirm() {
|
||||
private onLogoutConfirm = (): void => {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.shouldLoadBackupStatus) {
|
||||
|
@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons primaryButton={setupButtonCaption}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this._onLogoutConfirm}>
|
||||
<button onClick={this.onLogoutConfirm}>
|
||||
{ _t("I don't want my encrypted messages") }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<p><button onClick={this._onExportE2eKeysClicked}>
|
||||
<p><button onClick={this.onExportE2eKeysClicked}>
|
||||
{ _t("Manually export keys") }
|
||||
</button></p>
|
||||
</details>
|
||||
|
@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component {
|
|||
title={_t("You'll lose access to your encrypted messages")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={true}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{ dialogContent }
|
||||
</BaseDialog>);
|
||||
|
@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
|
|||
"Are you sure you want to 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
|
||||
this.state = {
|
||||
...urlInfo,
|
||||
roomMember,
|
||||
isWrapped: 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
|
||||
summaryMembers?: RoomMember[];
|
||||
// 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
|
||||
children: ReactNode[];
|
||||
// Called when the event list expansion is toggled
|
||||
|
|
|
@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
|||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
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';
|
||||
|
||||
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"> {
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength?: number;
|
||||
|
@ -60,6 +76,7 @@ enum TransitionType {
|
|||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
ServerAcl = "server_acl",
|
||||
ChangedPins = "pinned_messages"
|
||||
}
|
||||
|
||||
const SEP = ",";
|
||||
|
@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* `Object.keys(eventAggregates)`.
|
||||
* @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 userNames = eventAggregates[transitions];
|
||||
const nameList = this.renderNameList(userNames);
|
||||
|
@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
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.
|
||||
* @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'
|
||||
// are there only to show translators to non-English languages
|
||||
// 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 })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||
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;
|
||||
|
@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* if a transition is not recognised.
|
||||
*/
|
||||
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
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return TransitionType.InviteWithdrawal;
|
||||
}
|
||||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
||||
} else if (type === EventType.RoomServerAcl) {
|
||||
return TransitionType.ServerAcl;
|
||||
} else if (type === EventType.RoomPinnedEvents) {
|
||||
return TransitionType.ChangedPins;
|
||||
}
|
||||
|
||||
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
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
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
|
||||
if (!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);
|
||||
} else if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
if (e.getType() === 'm.room.third_party_invite') {
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
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;
|
||||
} else if (e.target) {
|
||||
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 classNames from 'classnames';
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { formatCallTime } from "../../../DateUtils";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -117,14 +117,12 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
if (state === CallState.Ended) {
|
||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||
const gotRejected = this.props.callEventGrouper.gotRejected;
|
||||
const rejectParty = this.props.callEventGrouper.rejectParty;
|
||||
|
||||
if (gotRejected) {
|
||||
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
|
||||
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
|
||||
{ _t("Call declined") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
} 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
|
||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||
// 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 (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("This call has ended") }
|
||||
{ text }
|
||||
</div>
|
||||
);
|
||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("They didn't pick up") }
|
||||
{ this.renderCallBackButton(_t("Call again")) }
|
||||
{ _t("Missed call") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -176,7 +179,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
className="mx_CallEvent_content_tooltip"
|
||||
kind={InfoTooltipKind.Warning}
|
||||
/>
|
||||
{ _t("This call has failed") }
|
||||
{ _t("Connection failed") }
|
||||
{ this.renderCallBackButton(_t("Retry")) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -190,7 +194,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
if (state === CustomCallState.Missed) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("You missed this call") }
|
||||
{ _t("Missed call") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// 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;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -152,7 +152,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
|||
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||
{ _t("If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -1381,8 +1381,8 @@ const BasicUserInfo: React.FC<{
|
|||
className="mx_UserInfo_field"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 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");
|
||||
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 SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||
["(", ")"],
|
||||
["[", "]"],
|
||||
["{", "}"],
|
||||
["<", ">"],
|
||||
]);
|
||||
|
||||
function ctrlShortcutLabel(key: string): string {
|
||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||
}
|
||||
|
@ -99,6 +106,7 @@ interface IState {
|
|||
showVisualBell?: boolean;
|
||||
autoComplete?: AutocompleteWrapperModel;
|
||||
completionIndex?: number;
|
||||
surroundWith: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||
|
@ -117,12 +125,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private readonly emoticonSettingHandle: string;
|
||||
private readonly shouldShowPillAvatarSettingHandle: string;
|
||||
private readonly surroundWithHandle: string;
|
||||
private readonly historyManager = new HistoryManager();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||
};
|
||||
|
||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||
|
@ -130,6 +140,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.configureEmoticonAutoReplace();
|
||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||
this.configureShouldShowPillAvatar);
|
||||
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||
this.surroundWithSettingChanged);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps) {
|
||||
|
@ -422,6 +434,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||
const model = this.props.model;
|
||||
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);
|
||||
switch (action) {
|
||||
case MessageComposerAction.FormatBold:
|
||||
|
@ -574,6 +608,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.setState({ showPillAvatar });
|
||||
};
|
||||
|
||||
private surroundWithSettingChanged = () => {
|
||||
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
|
||||
this.setState({ surroundWith });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||
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);
|
||||
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
||||
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
||||
SettingsStore.unwatchSetting(this.surroundWithHandle);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -684,7 +724,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||
<div
|
||||
className={classes}
|
||||
contentEditable="true"
|
||||
contentEditable={this.props.disabled ? null : true}
|
||||
tabIndex={0}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
|
|
|
@ -58,6 +58,7 @@ function ComposerAvatar(props: IComposerAvatarProps) {
|
|||
|
||||
interface ISendButtonProps {
|
||||
onClick: () => void;
|
||||
title?: string; // defaults to something generic
|
||||
}
|
||||
|
||||
function SendButton(props: ISendButtonProps) {
|
||||
|
@ -65,7 +66,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -64,9 +64,9 @@ const NewRoomIntro = () => {
|
|||
height={AVATAR_SIZE}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
if (oldInfo && oldInfo.left) {
|
||||
// start at the old height and in the old h pos
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: toPx(oldInfo.left) });
|
||||
left: toPx(oldInfo.left) });
|
||||
}
|
||||
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||
|
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{ userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
dateTime: dateString },
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||
{ displayName: this.props.member.rawDisplayName,
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -38,6 +38,8 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.rooms.ReplyTile")
|
||||
export default class ReplyTile extends React.PureComponent<IProps> {
|
||||
private anchorElement = createRef<HTMLAnchorElement>();
|
||||
|
||||
static defaultProps = {
|
||||
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
|
||||
// so that the browser can direct the user to the correct location
|
||||
// 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
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
|
@ -141,7 +147,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a href={permalink} onClick={this.onClick}>
|
||||
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||
{ sender }
|
||||
<EventTileType
|
||||
ref="tile"
|
||||
|
|
|
@ -17,10 +17,7 @@ limitations under the License.
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
RecordingState,
|
||||
VoiceRecording,
|
||||
} from "../../../audio/VoiceRecording";
|
||||
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
|
@ -34,6 +31,11 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
|||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
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 {
|
||||
room: Room;
|
||||
|
@ -42,6 +44,7 @@ interface IProps {
|
|||
interface IState {
|
||||
recorder?: VoiceRecording;
|
||||
recordingPhase?: RecordingState;
|
||||
didUploadFail?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,9 +72,19 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
|
||||
await this.state.recorder.stop();
|
||||
|
||||
let upload: IUpload;
|
||||
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.
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"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
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending/uploading voice message:", e);
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
title: _t('Upload Failed'),
|
||||
description: _t("The voice message failed to upload."),
|
||||
});
|
||||
return; // don't dispose the recording so the user can retry, maybe
|
||||
console.error("Error sending voice message:", e);
|
||||
|
||||
// Voice message should be in the timeline at this point, so let other things take care
|
||||
// of error handling. We also shouldn't need the recording anymore, so fall through to
|
||||
// disposal.
|
||||
}
|
||||
await this.disposeRecording();
|
||||
}
|
||||
|
@ -118,7 +130,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
|
||||
// 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 () => {
|
||||
|
@ -166,6 +178,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
try {
|
||||
// stop any noises which might be happening
|
||||
await PlaybackManager.instance.playOnly(null);
|
||||
|
||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||
await recorder.start();
|
||||
|
||||
|
@ -200,7 +215,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
let recordingInfo;
|
||||
let stopOrRecordBtn;
|
||||
let deleteButton;
|
||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||
const classes = classNames({
|
||||
|
@ -209,12 +224,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||
});
|
||||
|
||||
let tooltip = _t("Record a voice message");
|
||||
let tooltip = _t("Send voice message");
|
||||
if (!!this.state.recorder) {
|
||||
tooltip = _t("Stop the recording");
|
||||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
||||
stopOrRecordBtn = <AccessibleTooltipButton
|
||||
className={classes}
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
|
@ -222,22 +237,41 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopOrRecordBtn = null;
|
||||
}
|
||||
|
||||
recordingInfo = stopOrRecordBtn;
|
||||
}
|
||||
|
||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||
deleteButton = <AccessibleTooltipButton
|
||||
className='mx_VoiceRecordComposerTile_delete'
|
||||
title={_t("Delete recording")}
|
||||
title={_t("Delete")}
|
||||
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 (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopOrRecordBtn }
|
||||
{ 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, 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");
|
||||
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 { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import classNames from 'classnames';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
import LayoutSwitcher from "../../LayoutSwitcher";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -243,17 +242,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({ customThemeUrl: e.target.value });
|
||||
};
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): 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;
|
||||
}
|
||||
|
||||
private onLayoutChanged = (layout: Layout): void => {
|
||||
this.setState({ layout: layout });
|
||||
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
};
|
||||
|
||||
private onIRCLayoutChange = (enabled: boolean) => {
|
||||
|
@ -391,75 +381,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</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() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
|
@ -527,6 +448,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
render() {
|
||||
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 (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
<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 }) }
|
||||
</div>
|
||||
{ this.renderThemeSection() }
|
||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
||||
{ layoutSection }
|
||||
{ this.renderFontSection() }
|
||||
{ this.renderAdvancedSection() }
|
||||
</div>
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
|
@ -69,6 +69,18 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
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) => {
|
||||
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();
|
||||
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 { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
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() {
|
||||
|
@ -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;
|
||||
if (this.state.canUpdate) {
|
||||
updateButton = <UpdateCheckButton />;
|
||||
|
@ -275,6 +291,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<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'>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
|
||||
{ _t("olm version:") } { olmVersion }<br />
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
{ appVersion }<br />
|
||||
{ olmVersion }<br />
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyVersionClicked}
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
{ updateButton }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -308,12 +333,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
<summary>{ _t("Access Token") }</summary><br />
|
||||
<b>{ _t("Your access token gives full access to your account."
|
||||
+ " Do not share it with anyone." ) }</b>
|
||||
<div className="mx_HelpUserSettingsTab_accessToken">
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onAccessTokenCopyClick}
|
||||
className="mx_HelpUserSettingsTab_accessToken_copy"
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
</details><br />
|
||||
|
|
|
@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler";
|
|||
import PropTypes from "prop-types";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import BetaCard from "../../../beta/BetaCard";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component {
|
|||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||
this.setState({ showHiddenReadReceipts });
|
||||
});
|
||||
|
||||
this.state = {
|
||||
showHiddenReadReceipts: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -65,15 +74,22 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
|
||||
let labsSection;
|
||||
if (SdkConfig.get()['showLabsSettings']) {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
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">
|
||||
{ flags }
|
||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
||||
{ hiddenReadReceipts }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
'MessageComposerInput.surroundWith',
|
||||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -15,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import IncomingCallBox from './IncomingCallBox';
|
||||
import CallPreview from './CallPreview';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -31,7 +31,6 @@ interface IState {
|
|||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||
public render() {
|
||||
return <div className="mx_CallContainer">
|
||||
<IncomingCallBox />
|
||||
<CallPreview />
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import CallView from "./CallView";
|
||||
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 { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from '../../../stores/UIStore';
|
||||
import { lerp } from '../../../utils/AnimationUtils';
|
||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
|
||||
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,
|
||||
};
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
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
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
|
||||
// Position of the CallPreview
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
}
|
||||
|
||||
// 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 dispatcherRef: 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) {
|
||||
super(props);
|
||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
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);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
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) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
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 = () => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
|
||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
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': {
|
||||
// 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();
|
||||
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() {
|
||||
const pipMode = true;
|
||||
if (this.state.primaryCall) {
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
const translatePixelsY = this.state.translationY + "px";
|
||||
const style = {
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<PictureInPictureDragger
|
||||
className="mx_CallPreview"
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
draggable={pipMode}
|
||||
>
|
||||
<CallView
|
||||
{ ({ onStartMoving, onResize }) => <CallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={true}
|
||||
onMouseDownOnHeader={this.onStartMoving}
|
||||
onResize={this.onResize}
|
||||
/>
|
||||
</div>
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
/> }
|
||||
</PictureInPictureDragger>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return <PersistentApp />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,11 +23,16 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed from './VideoFeed';
|
||||
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 AccessibleButton from '../elements/AccessibleButton';
|
||||
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 { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
|
@ -37,26 +42,29 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
|||
import Modal from '../../../Modal';
|
||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||
import CallViewSidebar from './CallViewSidebar';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: (event: Event) => void;
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
pipMode?: boolean;
|
||||
// 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.
|
||||
// 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.
|
||||
pipMode?: boolean;
|
||||
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -75,6 +83,8 @@ interface IState {
|
|||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
const tooltipYOffset = -24;
|
||||
|
||||
function getFullScreenElement() {
|
||||
return (
|
||||
document.fullscreenElement ||
|
||||
|
@ -115,7 +125,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
private controlsHideTimer: number = null;
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
private contextMenu = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -144,6 +153,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener('keydown', this.onNativeKeyDown);
|
||||
this.showControls();
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||
this.controlsHideTimer = null;
|
||||
|
@ -389,23 +384,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
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 => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
|
@ -479,9 +457,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let vidMuteButton;
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
vidMuteButton = (
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={vidClasses}
|
||||
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
|
||||
) {
|
||||
screensharingButton = (
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={screensharingClasses}
|
||||
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
|
||||
className={sidebarButtonClasses}
|
||||
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;
|
||||
if (this.state.callState === CallState.Connected) {
|
||||
contextMenuButton = (
|
||||
<ContextMenuButton
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.showMoreMenu}
|
||||
title={_t("More")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let dialpadButton;
|
||||
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
||||
dialpadButton = (
|
||||
<ContextMenuButton
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
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,
|
||||
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}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
|
@ -568,7 +566,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
mountAsChild={true}
|
||||
mountAsChild={!this.props.pipMode}
|
||||
onFinished={this.closeContextMenu}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
|
@ -583,9 +581,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{ dialPad }
|
||||
{ contextMenu }
|
||||
{ dialpadButton }
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
title={this.state.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
{ vidMuteButton }
|
||||
<div className={micCacheClasses} />
|
||||
|
@ -593,9 +594,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{ screensharingButton }
|
||||
{ sidebarButton }
|
||||
{ contextMenuButton }
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||
onClick={this.onHangupClick}
|
||||
title={_t("Hangup")}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={tooltipYOffset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -692,7 +696,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
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',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
|
@ -712,7 +716,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
contentView =(
|
||||
contentView = (
|
||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div
|
||||
|
@ -814,83 +818,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
||||
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';
|
||||
}
|
||||
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||
|
||||
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 }
|
||||
</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", {
|
||||
mx_VideoFeed_video_mirror: (
|
||||
this.props.feed.isLocal() &&
|
||||
this.props.feed.purpose === SDPStreamMetadataPurpose.Usermedia &&
|
||||
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));
|
||||
|
||||
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 = [
|
||||
"people", // smileys
|
||||
"people", // actually people
|
||||
|
@ -72,7 +82,11 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
|||
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)) {
|
||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||
}
|
||||
|
|
|
@ -64,8 +64,6 @@
|
|||
"Unable to transfer call": "Unable to transfer call",
|
||||
"Transfer Failed": "Transfer Failed",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -752,6 +750,13 @@
|
|||
"Notifications": "Notifications",
|
||||
"Enable desktop notifications": "Enable desktop notifications",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -835,6 +840,7 @@
|
|||
"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",
|
||||
"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",
|
||||
"Use custom size": "Use custom size",
|
||||
"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 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",
|
||||
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
|
||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||
|
@ -916,6 +923,17 @@
|
|||
"sends snowfall": "sends snowfall",
|
||||
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||
"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",
|
||||
"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>",
|
||||
|
@ -931,14 +949,6 @@
|
|||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"%(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.",
|
||||
"Verified!": "Verified!",
|
||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||
|
@ -1149,6 +1159,10 @@
|
|||
"Connecting to integration manager...": "Connecting 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.",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
"Message bubbles": "Message bubbles",
|
||||
"Messages containing keywords": "Messages containing keywords",
|
||||
"Error saving notification preferences": "Error saving 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",
|
||||
"Add theme": "Add 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.",
|
||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
|
@ -1285,6 +1295,8 @@
|
|||
"Deactivate Account": "Deactivate Account",
|
||||
"Deactivate account": "Deactivate account",
|
||||
"Discovery": "Discovery",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"Olm version:": "Olm version:",
|
||||
"Legal": "Legal",
|
||||
"Credits": "Credits",
|
||||
"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",
|
||||
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
||||
"Versions": "Versions",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"olm version:": "olm version:",
|
||||
"Copy": "Copy",
|
||||
"Homeserver is": "Homeserver is",
|
||||
"Identity server is": "Identity server is",
|
||||
"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.",
|
||||
"Copy": "Copy",
|
||||
"Clear cache and reload": "Clear cache and reload",
|
||||
"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>.",
|
||||
|
@ -1547,6 +1557,7 @@
|
|||
"Send a reply…": "Send a reply…",
|
||||
"Send an encrypted message…": "Send an encrypted message…",
|
||||
"Send a message…": "Send a message…",
|
||||
"Send voice message": "Send voice message",
|
||||
"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.",
|
||||
"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",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Voice call": "Voice call",
|
||||
"Video call": "Video call",
|
||||
"Invites": "Invites",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
|
@ -1719,14 +1728,11 @@
|
|||
"Invited by %(sender)s": "Invited by %(sender)s",
|
||||
"Jump to first unread message.": "Jump to first unread message.",
|
||||
"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",
|
||||
"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",
|
||||
"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 the recording": "Stop the recording",
|
||||
"Delete recording": "Delete recording",
|
||||
"Stop recording": "Stop recording",
|
||||
"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 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",
|
||||
"Compare emoji": "Compare emoji",
|
||||
"Connected": "Connected",
|
||||
"You declined this call": "You declined this call",
|
||||
"They declined this call": "They declined this call",
|
||||
"Call declined": "Call declined",
|
||||
"Call back": "Call back",
|
||||
"Call again": "Call again",
|
||||
"This call has ended": "This call has ended",
|
||||
"They didn't pick up": "They didn't pick up",
|
||||
"Missed call": "Missed call",
|
||||
"Could not connect media": "Could not connect media",
|
||||
"Connection failed": "Connection failed",
|
||||
"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",
|
||||
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
||||
"This call has failed": "This call has failed",
|
||||
"You missed this call": "You missed this call",
|
||||
"Retry": "Retry",
|
||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
|
@ -1923,7 +1925,6 @@
|
|||
"Error processing audio message": "Error processing audio message",
|
||||
"React": "React",
|
||||
"Edit": "Edit",
|
||||
"Retry": "Retry",
|
||||
"Reply": "Reply",
|
||||
"Message Actions": "Message Actions",
|
||||
"Download %(text)s": "Download %(text)s",
|
||||
|
@ -2104,6 +2105,8 @@
|
|||
"%(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|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",
|
||||
"Custom level": "Custom level",
|
||||
"QR Code": "QR Code",
|
||||
|
|
|
@ -311,6 +311,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"feature_hidden_read_receipts": {
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td(
|
||||
"Don't send read receipts",
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
"baseFontSize": {
|
||||
displayName: _td("Font size"),
|
||||
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"),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.surroundWith": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Surround selected text when typing special characters"),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.autoReplaceEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Automatically replace plain text Emoji'),
|
||||
|
|
|
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
|
|||
key: string;
|
||||
// higher priority number will be shown on top of lower priority
|
||||
priority: number;
|
||||
title: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
component: C;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
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.
|
||||
// styleElements is a map from style name to HTMLLinkElement.
|
||||
const styleElements = Object.create(null);
|
||||
let a;
|
||||
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||
const href = a.getAttribute("href");
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||
themes.forEach(theme => {
|
||||
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||
});
|
||||
|
||||
if (!(stylesheetName in styleElements)) {
|
||||
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.
|
||||
|
||||
// @ts-ignore
|
||||
// noinspection JSConstantReassignment
|
||||
managedIframe.style = { display: "none" };
|
||||
managedIframe.style.display = "none";
|
||||
|
||||
// @ts-ignore
|
||||
// noinspection JSConstantReassignment
|
||||
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import { jsxJoin } from './ReactUtils';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
|
@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
|
|||
return items[0];
|
||||
} else if (remaining > 0) {
|
||||
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 {
|
||||
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);
|
||||
// @ts-ignore
|
||||
window.mxCallHandler = null;
|
||||
fakeCall = null;
|
||||
MatrixClientPeg.unset();
|
||||
|
||||
document.body.removeChild(audioElement);
|
||||
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([{
|
||||
userid: '@user2:example.org',
|
||||
protocol: "im.vector.protocol.sip_native",
|
||||
|
@ -179,6 +180,9 @@ describe('CallHandler', () => {
|
|||
|
||||
const viewRoomPayload = await untilDispatch('view_room');
|
||||
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 () => {
|
||||
|
|
|
@ -106,7 +106,7 @@ describe('MemberEventListSummary', function() {
|
|||
const result = wrapper.props.children;
|
||||
|
||||
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;
|
||||
|
||||
expect(result.props.children).toEqual([
|
||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||
<div className="event_tile" key="event1">Expanded membership</div>,
|
||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||
<div className="event_tile" key="event1">Expanded membership</div>,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ describe("ContentRules", function() {
|
|||
describe("parseContentRules", function() {
|
||||
it("should handle there being no keyword rules", function() {
|
||||
const rules = { 'global': { 'content': [
|
||||
USERNAME_RULE,
|
||||
USERNAME_RULE,
|
||||
] } };
|
||||
const parsed = ContentRules.parseContentRules(rules);
|
||||
expect(parsed.rules).toEqual([]);
|
||||
|
|
|
@ -78,6 +78,7 @@ export function createTestClient() {
|
|||
},
|
||||
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
||||
setAccountData: jest.fn(),
|
||||
setRoomAccountData: jest.fn(),
|
||||
sendTyping: jest.fn().mockResolvedValue({}),
|
||||
sendMessage: () => jest.fn().mockResolvedValue({}),
|
||||
getSyncState: () => "SYNCING",
|
||||
|
@ -95,6 +96,7 @@ export function createTestClient() {
|
|||
getItem: jest.fn(),
|
||||
},
|
||||
},
|
||||
pushRules: {},
|
||||
decryptEventIfNeeded: () => Promise.resolve(),
|
||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||
|
@ -129,8 +131,8 @@ export function mkEvent(opts) {
|
|||
if (opts.skey) {
|
||||
event.state_key = opts.skey;
|
||||
} 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",
|
||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||
"m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
|
||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||
event.state_key = "";
|
||||
}
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
|
|
|
@ -49,7 +49,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[[true, true], [true, false],
|
||||
[false, true], [false, false]],
|
||||
[false, true], [false, false]],
|
||||
)("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
@ -62,7 +62,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[["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) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
@ -75,7 +75,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[["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) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
@ -88,7 +88,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[["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) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
@ -101,7 +101,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[["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) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
@ -114,7 +114,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
|
|||
|
||||
it.each(
|
||||
[["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) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
|
|
|
@ -12,25 +12,14 @@
|
|||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
"jsx": "react",
|
||||
"types": [
|
||||
"node",
|
||||
"react",
|
||||
"flux",
|
||||
"react-transition-group"
|
||||
],
|
||||
"lib": [
|
||||
"es2019",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"paths": {
|
||||
"posthog-js": [
|
||||
"./src/@types/posthog.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
],
|
||||
],
|
||||
}
|
||||
|
|
335
yarn.lock
335
yarn.lock
|
@ -1324,6 +1324,107 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
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":
|
||||
version "2.0.37"
|
||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.37.tgz#700476512ab903d809f64a3040fb1b2fe6fb6d4b"
|
||||
|
@ -1352,6 +1453,11 @@
|
|||
tslib "^2.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":
|
||||
version "1.8.3"
|
||||
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"
|
||||
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:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc"
|
||||
|
@ -1882,6 +1999,11 @@ ansi-escapes@^4.2.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||
|
@ -1929,6 +2051,11 @@ argparse@^1.0.7:
|
|||
dependencies:
|
||||
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:
|
||||
version "4.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.13.1"
|
||||
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"
|
||||
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:
|
||||
version "5.0.0"
|
||||
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"
|
||||
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:
|
||||
version "4.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.14.1"
|
||||
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"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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-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:
|
||||
version "3.1.1"
|
||||
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"
|
||||
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
|
||||
|
||||
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
|
||||
version "0.3.4"
|
||||
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/45f6937539192e3820edcafc4d6d4d4187e85a6a"
|
||||
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945":
|
||||
version "0.3.5"
|
||||
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945"
|
||||
|
||||
eslint-plugin-react-hooks@^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"
|
||||
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:
|
||||
version "3.3.0"
|
||||
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-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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
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"
|
||||
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:
|
||||
version "1.1.3"
|
||||
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"
|
||||
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:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
|
@ -5462,6 +5691,13 @@ lru-cache@^6.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "9.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
node-fetch@2.6.1:
|
||||
node-fetch@2.6.1, node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
@ -6320,10 +6580,10 @@ postcss@^8.0.2:
|
|||
nanoid "^3.1.23"
|
||||
source-map-js "^0.6.2"
|
||||
|
||||
posthog-js@1.12.1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27"
|
||||
integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg==
|
||||
posthog-js@1.12.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.2.tgz#ff76e26634067e003f8af7df654d7ea0e647d946"
|
||||
integrity sha512-I0d6c+Yu2f91PFidz65AIkkqZM219EY9Z1wlbTkW5Zqfq5oXqogBMKS8BaDBOrMc46LjLX7IH67ytCcBFRo1uw==
|
||||
dependencies:
|
||||
fflate "^0.4.1"
|
||||
|
||||
|
@ -6915,6 +7175,11 @@ rimraf@^3.0.0, rimraf@^3.0.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.2.3"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "3.1.5"
|
||||
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:
|
||||
"@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:
|
||||
version "0.1.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
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"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^20.2.3:
|
||||
yargs-parser@^20.2.2, yargs-parser@^20.2.3:
|
||||
version "20.2.9"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
@ -8255,6 +8557,19 @@ yargs@^15.4.1:
|
|||
y18n "^4.0.0"
|
||||
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:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||
|
|
Loading…
Reference in a new issue