Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17686
Conflicts: src/components/views/elements/MiniAvatarUploader.tsx src/components/views/spaces/SpaceSettingsVisibilityTab.tsx src/i18n/strings/en_EN.json src/settings/handlers/RoomSettingsHandler.ts src/stores/SpaceStore.tsx
This commit is contained in:
commit
dcb9b9b777
153 changed files with 3535 additions and 1153 deletions
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,3 +1,3 @@
|
||||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
|
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
|
||||||
|
|
||||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->
|
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
|
||||||
|
|
3
.github/workflows/develop.yml
vendored
3
.github/workflows/develop.yml
vendored
|
@ -1,5 +1,8 @@
|
||||||
name: Develop
|
name: Develop
|
||||||
on:
|
on:
|
||||||
|
# These tests won't work for non-develop branches at the moment as they
|
||||||
|
# won't pull in the right versions of other repos, so they're only enabled
|
||||||
|
# on develop.
|
||||||
push:
|
push:
|
||||||
branches: [develop]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -15,3 +15,6 @@ package-lock.json
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
.vscode/
|
||||||
|
|
6
__mocks__/FontManager.js
Normal file
6
__mocks__/FontManager.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
|
||||||
|
// our fixed test environment and it requires the installation of node-canvas.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fixupColorFonts: () => Promise.resolve(),
|
||||||
|
};
|
1
__mocks__/workerMock.js
Normal file
1
__mocks__/workerMock.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = jest.fn();
|
|
@ -126,6 +126,7 @@
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
|
"@types/css-font-loading-module": "^0.0.6",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
|
@ -186,7 +187,8 @@
|
||||||
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
|
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
|
||||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js"
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!matrix-js-sdk).+$"
|
"/node_modules/(?!matrix-js-sdk).+$"
|
||||||
|
|
|
@ -121,6 +121,7 @@
|
||||||
@import "./views/elements/_AddressTile.scss";
|
@import "./views/elements/_AddressTile.scss";
|
||||||
@import "./views/elements/_DesktopBuildsNotice.scss";
|
@import "./views/elements/_DesktopBuildsNotice.scss";
|
||||||
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
|
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
|
||||||
|
@import "./views/elements/_DialPadBackspaceButton.scss";
|
||||||
@import "./views/elements/_DirectorySearchBox.scss";
|
@import "./views/elements/_DirectorySearchBox.scss";
|
||||||
@import "./views/elements/_Dropdown.scss";
|
@import "./views/elements/_Dropdown.scss";
|
||||||
@import "./views/elements/_EditableItemList.scss";
|
@import "./views/elements/_EditableItemList.scss";
|
||||||
|
@ -263,6 +264,7 @@
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
@import "./views/voip/_DialPadModal.scss";
|
@import "./views/voip/_DialPadModal.scss";
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Travis Ralston
|
Copyright 2017 Travis Ralston
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,7 +21,6 @@ limitations under the License.
|
||||||
padding: 0 0 0 16px;
|
padding: 0 0 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -28,11 +28,93 @@ limitations under the License.
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabsOnLeft {
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabels {
|
||||||
|
width: 170px;
|
||||||
|
max-width: 170px;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabPanel {
|
||||||
|
margin-left: 240px; // 170px sidebar + 70px padding
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel_active {
|
||||||
|
background-color: $tab-label-active-bg-color;
|
||||||
|
color: $tab-label-active-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
|
||||||
|
background-color: $tab-label-active-icon-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_maskedIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_maskedIcon::before {
|
||||||
|
mask-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabsOnTop {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabels {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 52px;
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel_text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabPanel {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel_active {
|
||||||
|
color: $accent-color;
|
||||||
|
.mx_TabbedView_tabLabel_text {
|
||||||
|
color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_maskedIcon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_maskedIcon::before {
|
||||||
|
mask-size: 22px;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_TabbedView_tabLabels {
|
.mx_TabbedView_tabLabels {
|
||||||
width: 170px;
|
|
||||||
max-width: 170px;
|
|
||||||
color: $tab-label-fg-color;
|
color: $tab-label-fg-color;
|
||||||
position: fixed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TabbedView_tabLabel {
|
.mx_TabbedView_tabLabel {
|
||||||
|
@ -46,43 +128,25 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TabbedView_tabLabel_active {
|
|
||||||
background-color: $tab-label-active-bg-color;
|
|
||||||
color: $tab-label-active-fg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_TabbedView_maskedIcon {
|
.mx_TabbedView_maskedIcon {
|
||||||
margin-left: 8px;
|
|
||||||
margin-right: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TabbedView_maskedIcon::before {
|
.mx_TabbedView_maskedIcon::before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: $tab-label-icon-bg-color;
|
background-color: $icon-button-color;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
|
|
||||||
background-color: $tab-label-active-icon-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_TabbedView_tabLabel_text {
|
.mx_TabbedView_tabLabel_text {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TabbedView_tabPanel {
|
.mx_TabbedView_tabPanel {
|
||||||
margin-left: 240px; // 170px sidebar + 70px padding
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0; // firefox
|
min-height: 0; // firefox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,4 +49,8 @@ limitations under the License.
|
||||||
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
||||||
padding-left: 8px; // isolate from recording circle / play control
|
padding-left: 8px; // isolate from recording circle / play control
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_VoiceMessagePrimaryContainer_noWaveform {
|
||||||
|
max-width: 162px; // with all the padding this results in 185px wide
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_InviteDialog_transferWrapper .mx_Dialog {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_addressBar {
|
.mx_InviteDialog_addressBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -286,16 +290,41 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog {
|
.mx_InviteDialog_other {
|
||||||
// Prevent the dialog from jumping around randomly when elements change.
|
// Prevent the dialog from jumping around randomly when elements change.
|
||||||
height: 600px;
|
height: 600px;
|
||||||
padding-left: 20px; // the design wants some padding on the left
|
padding-left: 20px; // the design wants some padding on the left
|
||||||
display: flex;
|
|
||||||
|
.mx_InviteDialog_userSections {
|
||||||
|
height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_content {
|
||||||
|
height: calc(100% - 36px); // full height minus the size of the header
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_transfer {
|
||||||
|
width: 496px;
|
||||||
|
height: 466px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.mx_InviteDialog_content {
|
.mx_InviteDialog_content {
|
||||||
overflow: hidden;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
|
.mx_TabbedView {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
}
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_addressBar {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,7 +332,6 @@ limitations under the License.
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 45px 4px 0;
|
padding: 0 45px 4px 0;
|
||||||
height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
|
.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
|
||||||
|
@ -318,6 +346,74 @@ limitations under the License.
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField {
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-color: $quaternary-fg-color;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within {
|
||||||
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPadField .mx_Field_postfix {
|
||||||
|
/* Remove border separator between postfix and field content */
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPad {
|
||||||
|
width: 224px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPad .mx_DialPad {
|
||||||
|
row-gap: 16px;
|
||||||
|
column-gap: 48px;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_transferConsultConnect {
|
||||||
|
padding-top: 16px;
|
||||||
|
/* This wants a drop shadow the full width of the dialog, so relative-position it
|
||||||
|
* and make it wider, then compensate with padding
|
||||||
|
*/
|
||||||
|
position: relative;
|
||||||
|
width: 496px;
|
||||||
|
left: -24px;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
border-top: 1px solid $message-body-panel-bg-color;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_transferConsultConnect_pushRight {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_userDirectoryIcon::before {
|
||||||
|
mask-image: url('$(res)/img/voip/tab-userdirectory.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_dialPadIcon::before {
|
||||||
|
mask-image: url('$(res)/img/voip/tab-dialpad.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_multiInviterError {
|
.mx_InviteDialog_multiInviterError {
|
||||||
> h4 {
|
> h4 {
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
|
|
|
@ -72,7 +72,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_danger_outline {
|
.mx_AccessibleButton_kind_danger_outline {
|
||||||
color: $button-danger-bg-color;
|
color: $button-danger-bg-color;
|
||||||
background-color: $button-secondary-bg-color;
|
background-color: transparent;
|
||||||
border: 1px solid $button-danger-bg-color;
|
border: 1px solid $button-danger-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
res/css/views/elements/_DialPadBackspaceButton.scss
Normal file
40
res/css/views/elements/_DialPadBackspaceButton.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
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_DialPadBackspaceButton {
|
||||||
|
position: relative;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
/* force this element to appear on the DOM */
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
background-color: #8D97A5;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/delete.svg');
|
||||||
|
mask-position: 8px;
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$button-size: 32px;
|
||||||
|
$icon-size: 22px;
|
||||||
|
$button-gap: 24px;
|
||||||
|
|
||||||
.mx_ImageView {
|
.mx_ImageView {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -66,16 +70,17 @@ limitations under the License.
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: calc($button-gap - ($button-size - $icon-size));
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_button {
|
.mx_ImageView_button {
|
||||||
margin-left: 24px;
|
padding: calc(($button-size - $icon-size) / 2);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
height: 22px;
|
height: $icon-size;
|
||||||
width: 22px;
|
width: $icon-size;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
|
@ -109,11 +114,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_button_close {
|
.mx_ImageView_button_close {
|
||||||
|
padding: calc($button-size - $button-size);
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
background: #21262c; // same on all themes
|
background: #21262c; // same on all themes
|
||||||
&::before {
|
&::before {
|
||||||
width: 32px;
|
width: $button-size;
|
||||||
height: 32px;
|
height: $button-size;
|
||||||
mask-image: url('$(res)/img/image-view/close.svg');
|
mask-image: url('$(res)/img/image-view/close.svg');
|
||||||
mask-size: 40%;
|
mask-size: 40%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,10 +43,10 @@ limitations under the License.
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LinkPreviewWidget_siteName {
|
.mx_LinkPreviewWidget_siteName {
|
||||||
display: inline;
|
font-weight: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LinkPreviewWidget_description {
|
.mx_LinkPreviewWidget_description {
|
||||||
|
|
|
@ -30,8 +30,8 @@ limitations under the License.
|
||||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.mx_CallView_video {
|
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||||
width: 350px;
|
min-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_local {
|
.mx_VideoFeed_local {
|
||||||
|
|
21
res/css/views/voip/_CallPreview.scss
Normal file
21
res/css/views/voip/_CallPreview.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
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_CallPreview {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
|
@ -39,7 +39,6 @@ limitations under the License.
|
||||||
.mx_CallView_pip {
|
.mx_CallView_pip {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
margin-top: 10px;
|
|
||||||
background-color: $voipcall-plinth-color;
|
background-color: $voipcall-plinth-color;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -16,11 +16,21 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DialPad {
|
.mx_DialPad {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
row-gap: 16px;
|
||||||
|
column-gap: 0px;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
/* squeeze the dial pad buttons together horizontally */
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPad_button {
|
.mx_DialPad_button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: $dialpad-button-bg-color;
|
background-color: $dialpad-button-bg-color;
|
||||||
|
@ -29,10 +39,19 @@ limitations under the License.
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 40px;
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPad_deleteButton, .mx_DialPad_dialButton {
|
.mx_DialPad_button .mx_DialPad_buttonSubText {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPad_dialButton {
|
||||||
|
/* Always show the dial button in the center grid column */
|
||||||
|
grid-column: 2;
|
||||||
|
background-color: $accent-color;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -42,21 +61,7 @@ limitations under the License.
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: 20px;
|
mask-size: 20px;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $primary-bg-color;
|
background-color: #FFF; // on all themes
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_DialPad_deleteButton {
|
|
||||||
background-color: $notice-primary-color;
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/delete.svg');
|
|
||||||
mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_DialPad_dialButton {
|
|
||||||
background-color: $accent-color;
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,40 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_DialPadContextMenu_dialPad .mx_DialPad {
|
||||||
|
row-gap: 16px;
|
||||||
|
column-gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPadContextMenuWrapper {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_header {
|
.mx_DialPadContextMenu_header {
|
||||||
margin-top: 12px;
|
border: none;
|
||||||
margin-left: 12px;
|
margin-top: 32px;
|
||||||
margin-right: 12px;
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
|
||||||
|
/* a separator between the input line and the dial buttons */
|
||||||
|
border-bottom: 1px solid $quaternary-fg-color;
|
||||||
|
transition: border-bottom 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPadContextMenu_cancel {
|
||||||
|
float: right;
|
||||||
|
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: cover;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: $dialog-close-fg-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPadContextMenu_header:focus-within {
|
||||||
|
border-bottom: 1px solid $accent-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_title {
|
.mx_DialPadContextMenu_title {
|
||||||
|
@ -30,7 +60,6 @@ limitations under the License.
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
max-width: 150px;
|
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +67,7 @@ limitations under the License.
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 150px;
|
max-width: 185px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
padding: 8px 0px;
|
padding: 8px 0px;
|
||||||
|
@ -48,13 +77,3 @@ limitations under the License.
|
||||||
.mx_DialPadContextMenu_dialPad {
|
.mx_DialPadContextMenu_dialPad {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_horizSep {
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid $input-darker-bg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,14 +19,23 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal {
|
.mx_DialPadModal {
|
||||||
width: 192px;
|
width: 292px;
|
||||||
height: 368px;
|
height: 370px;
|
||||||
|
padding: 16px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal_header {
|
.mx_DialPadModal_header {
|
||||||
margin-top: 12px;
|
margin-top: 32px;
|
||||||
margin-left: 12px;
|
margin-left: 40px;
|
||||||
margin-right: 12px;
|
margin-right: 40px;
|
||||||
|
|
||||||
|
/* a separator between the input line and the dial buttons */
|
||||||
|
border-bottom: 1px solid $quaternary-fg-color;
|
||||||
|
transition: border-bottom 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPadModal_header:focus-within {
|
||||||
|
border-bottom: 1px solid $accent-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal_title {
|
.mx_DialPadModal_title {
|
||||||
|
@ -45,11 +54,18 @@ limitations under the License.
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background-color: $dialog-close-fg-color;
|
background-color: $dialog-close-fg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal_field {
|
.mx_DialPadModal_field {
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DialPadModal_field .mx_Field_postfix {
|
||||||
|
/* Remove border separator between postfix and field content */
|
||||||
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal_field input {
|
.mx_DialPadModal_field input {
|
||||||
|
@ -62,13 +78,3 @@ limitations under the License.
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadModal_horizSep {
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid $input-darker-bg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,8 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VideoFeed_voice {
|
.mx_VideoFeed_voice {
|
||||||
// We don't want to collide with the call controls that have 52px of height
|
|
||||||
padding-bottom: 52px;
|
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
res/img/voip/tab-dialpad.svg
Normal file
3
res/img/voip/tab-dialpad.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 19C10.9 19 10 19.9 10 21C10 22.1 10.9 23 12 23C13.1 23 14 22.1 14 21C14 19.9 13.1 19 12 19ZM6 1C4.9 1 4 1.9 4 3C4 4.1 4.9 5 6 5C7.1 5 8 4.1 8 3C8 1.9 7.1 1 6 1ZM6 7C4.9 7 4 7.9 4 9C4 10.1 4.9 11 6 11C7.1 11 8 10.1 8 9C8 7.9 7.1 7 6 7ZM6 13C4.9 13 4 13.9 4 15C4 16.1 4.9 17 6 17C7.1 17 8 16.1 8 15C8 13.9 7.1 13 6 13ZM18 5C19.1 5 20 4.1 20 3C20 1.9 19.1 1 18 1C16.9 1 16 1.9 16 3C16 4.1 16.9 5 18 5ZM12 13C10.9 13 10 13.9 10 15C10 16.1 10.9 17 12 17C13.1 17 14 16.1 14 15C14 13.9 13.1 13 12 13ZM18 13C16.9 13 16 13.9 16 15C16 16.1 16.9 17 18 17C19.1 17 20 16.1 20 15C20 13.9 19.1 13 18 13ZM18 7C16.9 7 16 7.9 16 9C16 10.1 16.9 11 18 11C19.1 11 20 10.1 20 9C20 7.9 19.1 7 18 7ZM12 7C10.9 7 10 7.9 10 9C10 10.1 10.9 11 12 11C13.1 11 14 10.1 14 9C14 7.9 13.1 7 12 7ZM12 1C10.9 1 10 1.9 10 3C10 4.1 10.9 5 12 5C13.1 5 14 4.1 14 3C14 1.9 13.1 1 12 1Z" fill="#8D97A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 979 B |
7
res/img/voip/tab-userdirectory.svg
Normal file
7
res/img/voip/tab-userdirectory.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="path-1-inside-1" fill="white">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z" fill="#8D97A5"/>
|
||||||
|
<path d="M18.1502 21.1214L18.9339 22.2814L18.1502 21.1214ZM5.4 20.8008L4.55919 21.9202H4.55919L5.4 20.8008ZM18.1197 18.3237L19.0934 19.3296L19.7717 18.6731L19.4173 17.7981L18.1197 18.3237ZM5.88028 18.3237L4.58268 17.7981L4.22829 18.6731L4.90659 19.3296L5.88028 18.3237ZM12 24.4C14.5662 24.4 16.9541 23.619 18.9339 22.2814L17.3665 19.9613C15.835 20.9959 13.9902 21.6 12 21.6V24.4ZM4.55919 21.9202C6.63176 23.477 9.21011 24.4 12 24.4V21.6C9.83723 21.6 7.84514 20.8865 6.24081 19.6814L4.55919 21.9202ZM-0.399998 12C-0.399998 16.0577 1.55052 19.6603 4.55919 21.9202L6.24081 19.6814C3.90591 17.9276 2.4 15.1399 2.4 12H-0.399998ZM12 -0.399998C5.15167 -0.399998 -0.399998 5.15167 -0.399998 12H2.4C2.4 6.69807 6.69807 2.4 12 2.4V-0.399998ZM24.4 12C24.4 5.15167 18.8483 -0.399998 12 -0.399998V2.4C17.3019 2.4 21.6 6.69807 21.6 12H24.4ZM18.9339 22.2814C22.2288 20.0554 24.4 16.2815 24.4 12H21.6C21.6 15.3124 19.9236 18.2337 17.3665 19.9613L18.9339 22.2814ZM13.9 8.975C13.9 10.2838 12.9459 11.15 12 11.15V13.95C14.6991 13.95 16.7 11.615 16.7 8.975H13.9ZM12 6.8C12.9459 6.8 13.9 7.66616 13.9 8.975H16.7C16.7 6.335 14.6991 4 12 4V6.8ZM10.1 8.975C10.1 7.66616 11.0541 6.8 12 6.8V4C9.30086 4 7.3 6.335 7.3 8.975H10.1ZM12 11.15C11.0541 11.15 10.1 10.2838 10.1 8.975H7.3C7.3 11.615 9.30086 13.95 12 13.95V11.15ZM17.146 17.3178C15.8129 18.6081 14.0004 19.4 12 19.4V22.2C14.756 22.2 17.2591 21.1051 19.0934 19.3296L17.146 17.3178ZM12 15.6C14.1797 15.6 16.0494 16.9415 16.8221 18.8493L19.4173 17.7981C18.2312 14.8697 15.359 12.8 12 12.8V15.6ZM7.17788 18.8493C7.95058 16.9415 9.8203 15.6 12 15.6V12.8C8.64102 12.8 5.7688 14.8697 4.58268 17.7981L7.17788 18.8493ZM12 19.4C9.99963 19.4 8.18709 18.6081 6.85397 17.3178L4.90659 19.3296C6.74088 21.1051 9.24402 22.2 12 22.2V19.4Z" fill="#8D97A5" mask="url(#path-1-inside-1)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -118,7 +118,7 @@ $voipcall-plinth-color: #394049;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
$dialpad-button-bg-color: #6F7882;
|
$dialpad-button-bg-color: #394049;
|
||||||
|
|
||||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $bg-color;
|
$roomlist-filter-active-bg-color: $bg-color;
|
||||||
|
|
5
src/@types/global.d.ts
vendored
5
src/@types/global.d.ts
vendored
|
@ -15,7 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||||
import * as ModernizrStatic from "modernizr";
|
// Load types for the WG CSS Font Loading APIs https://github.com/Microsoft/TypeScript/issues/13569
|
||||||
|
import "@types/css-font-loading-module";
|
||||||
|
import "@types/modernizr";
|
||||||
|
|
||||||
import ContentMessages from "../ContentMessages";
|
import ContentMessages from "../ContentMessages";
|
||||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
@ -50,7 +52,6 @@ import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
Modernizr: ModernizrStatic;
|
|
||||||
matrixChat: ReturnType<Renderer>;
|
matrixChat: ReturnType<Renderer>;
|
||||||
mxMatrixClientPeg: IMatrixClientPeg;
|
mxMatrixClientPeg: IMatrixClientPeg;
|
||||||
Olm: {
|
Olm: {
|
||||||
|
|
23
src/@types/worker-loader.d.ts
vendored
Normal file
23
src/@types/worker-loader.d.ts
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "*.worker.ts" {
|
||||||
|
class WebpackWorker extends Worker {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebpackWorker;
|
||||||
|
}
|
|
@ -248,7 +248,7 @@ export default class AddThreepid {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a phone number verification code as entered by the user and validates
|
* Takes a phone number verification code as entered by the user and validates
|
||||||
* it with the ID server, then if successful, adds the phone number.
|
* it with the identity server, then if successful, adds the phone number.
|
||||||
* @param {string} msisdnToken phone number verification code as entered by the user
|
* @param {string} msisdnToken phone number verification code as entered by the user
|
||||||
* @return {Promise} Resolves if the phone number was added. Rejects with an object
|
* @return {Promise} Resolves if the phone number was added. Rejects with an object
|
||||||
* with a "message" property which contains a human-readable message detailing why
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
|
|
60
src/BlurhashEncoder.ts
Normal file
60
src/BlurhashEncoder.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
// @ts-ignore - `.ts` is needed here to make TS happy
|
||||||
|
import BlurhashWorker from "./workers/blurhash.worker.ts";
|
||||||
|
|
||||||
|
interface IBlurhashWorkerResponse {
|
||||||
|
seq: number;
|
||||||
|
blurhash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlurhashEncoder {
|
||||||
|
private static internalInstance = new BlurhashEncoder();
|
||||||
|
|
||||||
|
public static get instance(): BlurhashEncoder {
|
||||||
|
return BlurhashEncoder.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly worker: Worker;
|
||||||
|
private seq = 0;
|
||||||
|
private pendingDeferredMap = new Map<number, IDeferred<string>>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.worker = new BlurhashWorker();
|
||||||
|
this.worker.onmessage = this.onMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage = (ev: MessageEvent<IBlurhashWorkerResponse>) => {
|
||||||
|
const { seq, blurhash } = ev.data;
|
||||||
|
const deferred = this.pendingDeferredMap.get(seq);
|
||||||
|
if (deferred) {
|
||||||
|
this.pendingDeferredMap.delete(seq);
|
||||||
|
deferred.resolve(blurhash);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public getBlurhash(imageData: ImageData): Promise<string> {
|
||||||
|
const seq = this.seq++;
|
||||||
|
const deferred = defer<string>();
|
||||||
|
this.pendingDeferredMap.set(seq, deferred);
|
||||||
|
this.worker.postMessage({ seq, imageData });
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -154,7 +154,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
private supportsPstnProtocol = null;
|
private supportsPstnProtocol = null;
|
||||||
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
|
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
|
||||||
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
||||||
private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
|
private pstnSupportCheckTimer: number;
|
||||||
// For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
|
// For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
|
||||||
private invitedRoomsAreVirtual = new Map<string, boolean>();
|
private invitedRoomsAreVirtual = new Map<string, boolean>();
|
||||||
private invitedRoomCheckInProgress = false;
|
private invitedRoomCheckInProgress = false;
|
||||||
|
@ -394,7 +394,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallListeners(call: MatrixCall) {
|
private setCallListeners(call: MatrixCall) {
|
||||||
let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
let mappedRoomId = this.roomIdForCall(call);
|
||||||
|
|
||||||
call.on(CallEvent.Error, (err: CallError) => {
|
call.on(CallEvent.Error, (err: CallError) => {
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
@ -871,6 +871,12 @@ export default class CallHandler extends EventEmitter {
|
||||||
case Action.DialNumber:
|
case Action.DialNumber:
|
||||||
this.dialNumber(payload.number);
|
this.dialNumber(payload.number);
|
||||||
break;
|
break;
|
||||||
|
case Action.TransferCallToMatrixID:
|
||||||
|
this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst);
|
||||||
|
break;
|
||||||
|
case Action.TransferCallToPhoneNumber:
|
||||||
|
this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -905,6 +911,48 @@ export default class CallHandler extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
|
||||||
|
const results = await this.pstnLookup(destination);
|
||||||
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
|
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||||
|
title: _t("Unable to transfer call"),
|
||||||
|
description: _t("There was an error looking up the phone number"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.startTransferToMatrixID(call, results[0].userid, consultFirst);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) {
|
||||||
|
if (consultFirst) {
|
||||||
|
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination);
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: call.type,
|
||||||
|
room_id: dmRoomId,
|
||||||
|
transferee: call,
|
||||||
|
});
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: dmRoomId,
|
||||||
|
should_peek: false,
|
||||||
|
joining: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await call.transfer(destination);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to transfer call", e);
|
||||||
|
Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, {
|
||||||
|
title: _t('Transfer Failed'),
|
||||||
|
description: _t('Failed to transfer call'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setActiveCallRoomId(activeCallRoomId: string) {
|
setActiveCallRoomId(activeCallRoomId: string) {
|
||||||
logger.info("Setting call in room " + activeCallRoomId + " active");
|
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { encode } from "blurhash";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
|
@ -28,7 +27,6 @@ import RoomViewStore from './stores/RoomViewStore';
|
||||||
import encrypt from "browser-encrypt-attachment";
|
import encrypt from "browser-encrypt-attachment";
|
||||||
import extractPngChunks from "png-chunks-extract";
|
import extractPngChunks from "png-chunks-extract";
|
||||||
import Spinner from "./components/views/elements/Spinner";
|
import Spinner from "./components/views/elements/Spinner";
|
||||||
|
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {
|
import {
|
||||||
|
@ -39,7 +37,8 @@ import {
|
||||||
UploadStartedPayload,
|
UploadStartedPayload,
|
||||||
} from "./dispatcher/payloads/UploadPayload";
|
} from "./dispatcher/payloads/UploadPayload";
|
||||||
import { IUpload } from "./models/IUpload";
|
import { IUpload } from "./models/IUpload";
|
||||||
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { BlurhashEncoder } from "./BlurhashEncoder";
|
||||||
|
|
||||||
const MAX_WIDTH = 800;
|
const MAX_WIDTH = 800;
|
||||||
const MAX_HEIGHT = 600;
|
const MAX_HEIGHT = 600;
|
||||||
|
@ -85,10 +84,6 @@ interface IThumbnail {
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAbortablePromise<T> extends Promise<T> {
|
|
||||||
abort(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a thumbnail for a image DOM element.
|
* Create a thumbnail for a image DOM element.
|
||||||
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
||||||
|
@ -107,55 +102,62 @@ interface IAbortablePromise<T> extends Promise<T> {
|
||||||
* @return {Promise} A promise that resolves with an object with an info key
|
* @return {Promise} A promise that resolves with an object with an info key
|
||||||
* and a thumbnail key.
|
* and a thumbnail key.
|
||||||
*/
|
*/
|
||||||
function createThumbnail(
|
async function createThumbnail(
|
||||||
element: ThumbnailableElement,
|
element: ThumbnailableElement,
|
||||||
inputWidth: number,
|
inputWidth: number,
|
||||||
inputHeight: number,
|
inputHeight: number,
|
||||||
mimeType: string,
|
mimeType: string,
|
||||||
): Promise<IThumbnail> {
|
): Promise<IThumbnail> {
|
||||||
return new Promise((resolve) => {
|
let targetWidth = inputWidth;
|
||||||
let targetWidth = inputWidth;
|
let targetHeight = inputHeight;
|
||||||
let targetHeight = inputHeight;
|
if (targetHeight > MAX_HEIGHT) {
|
||||||
if (targetHeight > MAX_HEIGHT) {
|
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||||
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
targetHeight = MAX_HEIGHT;
|
||||||
targetHeight = MAX_HEIGHT;
|
}
|
||||||
}
|
if (targetWidth > MAX_WIDTH) {
|
||||||
if (targetWidth > MAX_WIDTH) {
|
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
|
||||||
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
|
targetWidth = MAX_WIDTH;
|
||||||
targetWidth = MAX_WIDTH;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
let canvas: HTMLCanvasElement | OffscreenCanvas;
|
||||||
|
if (window.OffscreenCanvas) {
|
||||||
|
canvas = new window.OffscreenCanvas(targetWidth, targetHeight);
|
||||||
|
} else {
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
canvas.width = targetWidth;
|
canvas.width = targetWidth;
|
||||||
canvas.height = targetHeight;
|
canvas.height = targetHeight;
|
||||||
const context = canvas.getContext("2d");
|
}
|
||||||
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
|
||||||
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
const context = canvas.getContext("2d");
|
||||||
const blurhash = encode(
|
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||||
imageData.data,
|
|
||||||
imageData.width,
|
let thumbnailPromise: Promise<Blob>;
|
||||||
imageData.height,
|
|
||||||
// use 4 components on the longer dimension, if square then both
|
if (window.OffscreenCanvas) {
|
||||||
imageData.width >= imageData.height ? 4 : 3,
|
thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType });
|
||||||
imageData.height >= imageData.width ? 4 : 3,
|
} else {
|
||||||
);
|
thumbnailPromise = new Promise<Blob>(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));
|
||||||
canvas.toBlob(function(thumbnail) {
|
}
|
||||||
resolve({
|
|
||||||
info: {
|
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
thumbnail_info: {
|
// thumbnailPromise and blurhash promise are being awaited concurrently
|
||||||
w: targetWidth,
|
const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData);
|
||||||
h: targetHeight,
|
const thumbnail = await thumbnailPromise;
|
||||||
mimetype: thumbnail.type,
|
|
||||||
size: thumbnail.size,
|
return {
|
||||||
},
|
info: {
|
||||||
w: inputWidth,
|
thumbnail_info: {
|
||||||
h: inputHeight,
|
w: targetWidth,
|
||||||
[BLURHASH_FIELD]: blurhash,
|
h: targetHeight,
|
||||||
},
|
mimetype: thumbnail.type,
|
||||||
thumbnail,
|
size: thumbnail.size,
|
||||||
});
|
},
|
||||||
}, mimeType);
|
w: inputWidth,
|
||||||
});
|
h: inputHeight,
|
||||||
|
[BLURHASH_FIELD]: blurhash,
|
||||||
|
},
|
||||||
|
thumbnail,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -333,7 +335,7 @@ export function uploadFile(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
progressHandler?: any, // TODO: Types
|
progressHandler?: any, // TODO: Types
|
||||||
): Promise<{url?: string, file?: any}> { // TODO: Types
|
): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||||
// If the room is encrypted then encrypt the file before uploading it.
|
// If the room is encrypted then encrypt the file before uploading it.
|
||||||
|
@ -365,8 +367,8 @@ export function uploadFile(
|
||||||
encryptInfo.mimetype = file.type;
|
encryptInfo.mimetype = file.type;
|
||||||
}
|
}
|
||||||
return { "file": encryptInfo };
|
return { "file": encryptInfo };
|
||||||
});
|
}) as IAbortablePromise<{ file: any }>;
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
prom.abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||||
};
|
};
|
||||||
|
@ -379,8 +381,8 @@ export function uploadFile(
|
||||||
if (canceled) throw new UploadCanceledError();
|
if (canceled) throw new UploadCanceledError();
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return { url };
|
return { url };
|
||||||
});
|
}) as IAbortablePromise<{ url: string }>;
|
||||||
(promise1 as any).abort = () => {
|
promise1.abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
matrixClient.cancelUpload(basePromise);
|
matrixClient.cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
|
@ -551,10 +553,10 @@ export default class ContentMessages {
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
}) as IAbortablePromise<void>;
|
||||||
|
|
||||||
// create temporary abort handler for before the actual upload gets passed off to js-sdk
|
// create temporary abort handler for before the actual upload gets passed off to js-sdk
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
prom.abort = () => {
|
||||||
upload.canceled = true;
|
upload.canceled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -569,7 +571,7 @@ export default class ContentMessages {
|
||||||
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
|
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
|
||||||
|
|
||||||
// Focus the composer view
|
// Focus the composer view
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
|
|
||||||
function onProgress(ev) {
|
function onProgress(ev) {
|
||||||
upload.total = ev.total;
|
upload.total = ev.total;
|
||||||
|
@ -583,9 +585,7 @@ export default class ContentMessages {
|
||||||
// XXX: upload.promise must be the promise that
|
// XXX: upload.promise must be the promise that
|
||||||
// is returned by uploadFile as it has an abort()
|
// is returned by uploadFile as it has an abort()
|
||||||
// method hacked onto it.
|
// method hacked onto it.
|
||||||
upload.promise = uploadFile(
|
upload.promise = uploadFile(matrixClient, roomId, file, onProgress);
|
||||||
matrixClient, roomId, file, onProgress,
|
|
||||||
);
|
|
||||||
return upload.promise.then(function(result) {
|
return upload.promise.then(function(result) {
|
||||||
content.file = result.file;
|
content.file = result.file;
|
||||||
content.url = result.url;
|
content.url = result.url;
|
||||||
|
|
|
@ -364,8 +364,8 @@ export default class CountlyAnalytics {
|
||||||
|
|
||||||
private initTime = CountlyAnalytics.getTimestamp();
|
private initTime = CountlyAnalytics.getTimestamp();
|
||||||
private firstPage = true;
|
private firstPage = true;
|
||||||
private heartbeatIntervalId: NodeJS.Timeout;
|
private heartbeatIntervalId: number;
|
||||||
private activityIntervalId: NodeJS.Timeout;
|
private activityIntervalId: number;
|
||||||
private trackTime = true;
|
private trackTime = true;
|
||||||
private lastBeat: number;
|
private lastBeat: number;
|
||||||
private storedDuration = 0;
|
private storedDuration = 0;
|
||||||
|
|
|
@ -46,8 +46,8 @@ export class DecryptionFailureTracker {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set to an interval ID when `start` is called
|
// Set to an interval ID when `start` is called
|
||||||
public checkInterval: NodeJS.Timeout = null;
|
public checkInterval: number = null;
|
||||||
public trackInterval: NodeJS.Timeout = null;
|
public trackInterval: number = null;
|
||||||
|
|
||||||
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
||||||
static TRACK_INTERVAL_MS = 60000;
|
static TRACK_INTERVAL_MS = 60000;
|
||||||
|
|
|
@ -60,6 +60,8 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||||
|
|
||||||
|
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Return true if the given string contains emoji
|
* Return true if the given string contains emoji
|
||||||
* Uses a much, much simpler regex than emojibase's so will give false
|
* Uses a much, much simpler regex than emojibase's so will give false
|
||||||
|
@ -176,18 +178,31 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
|
let src = attribs.src;
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// we don't want to allow images with `https?` `src`s.
|
||||||
// We also drop inline images (as if they were not present at all) when the "show
|
// We also drop inline images (as if they were not present at all) when the "show
|
||||||
// images" preference is disabled. Future work might expose some UI to reveal them
|
// images" preference is disabled. Future work might expose some UI to reveal them
|
||||||
// like standalone image events have.
|
// like standalone image events have.
|
||||||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
if (!src || !SettingsStore.getValue("showImages")) {
|
||||||
return { tagName, attribs: {} };
|
return { tagName, attribs: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!src.startsWith("mxc://")) {
|
||||||
|
const match = MEDIA_API_MXC_REGEX.exec(src);
|
||||||
|
if (match) {
|
||||||
|
src = `mxc://${match[1]}/${match[2]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!src.startsWith("mxc://")) {
|
||||||
|
return { tagName, attribs: {} };
|
||||||
|
}
|
||||||
|
|
||||||
const width = Number(attribs.width) || 800;
|
const width = Number(attribs.width) || 800;
|
||||||
const height = Number(attribs.height) || 600;
|
const height = Number(attribs.height) || 600;
|
||||||
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
|
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class IdentityAuthClient {
|
||||||
await this._matrixClient.getIdentityAccount(token);
|
await this._matrixClient.getIdentityAccount(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
if (e.errcode === "M_TERMS_NOT_SIGNED") {
|
||||||
console.log("Identity Server requires new terms to be agreed to");
|
console.log("Identity server requires new terms to be agreed to");
|
||||||
await startTermsFlow([new Service(
|
await startTermsFlow([new Service(
|
||||||
SERVICE_TYPES.IS,
|
SERVICE_TYPES.IS,
|
||||||
identityServerUrl,
|
identityServerUrl,
|
||||||
|
|
|
@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix';
|
import { ICreateClientOpts, PendingEventOrdering } from 'matrix-js-sdk/src/matrix';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { IStartClientOpts, MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { MemoryStore } from 'matrix-js-sdk/src/store/memory';
|
import { MemoryStore } from 'matrix-js-sdk/src/store/memory';
|
||||||
import * as utils from 'matrix-js-sdk/src/utils';
|
import * as utils from 'matrix-js-sdk/src/utils';
|
||||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
@ -47,25 +47,8 @@ export interface IMatrixClientCreds {
|
||||||
freshLogin?: boolean;
|
freshLogin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to the js-sdk
|
|
||||||
export interface IOpts {
|
|
||||||
initialSyncLimit?: number;
|
|
||||||
pendingEventOrdering?: "detached" | "chronological";
|
|
||||||
lazyLoadMembers?: boolean;
|
|
||||||
clientWellKnownPollPeriod?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMatrixClientPeg {
|
export interface IMatrixClientPeg {
|
||||||
opts: IOpts;
|
opts: IStartClientOpts;
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the script href passed to the IndexedDB web worker
|
|
||||||
* If set, a separate web worker will be started to run the IndexedDB
|
|
||||||
* queries on.
|
|
||||||
*
|
|
||||||
* @param {string} script href to the script to be passed to the web worker
|
|
||||||
*/
|
|
||||||
setIndexedDbWorkerScript(script: string): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the server name of the user's homeserver
|
* Return the server name of the user's homeserver
|
||||||
|
@ -127,7 +110,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
// client is started in 'start'. These can be altered
|
// client is started in 'start'. These can be altered
|
||||||
// at any time up to after the 'will_start_client'
|
// at any time up to after the 'will_start_client'
|
||||||
// event is finished processing.
|
// event is finished processing.
|
||||||
public opts: IOpts = {
|
public opts: IStartClientOpts = {
|
||||||
initialSyncLimit: 20,
|
initialSyncLimit: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,10 +124,6 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setIndexedDbWorkerScript(script: string): void {
|
|
||||||
createMatrixClient.indexedDbWorkerScript = script;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(): MatrixClient {
|
public get(): MatrixClient {
|
||||||
return this.matrixClient;
|
return this.matrixClient;
|
||||||
}
|
}
|
||||||
|
@ -231,7 +210,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
|
|
||||||
const opts = utils.deepCopy(this.opts);
|
const opts = utils.deepCopy(this.opts);
|
||||||
// the react sdk doesn't work without this, so don't allow
|
// the react sdk doesn't work without this, so don't allow
|
||||||
opts.pendingEventOrdering = "detached";
|
opts.pendingEventOrdering = PendingEventOrdering.Detached;
|
||||||
opts.lazyLoadMembers = true;
|
opts.lazyLoadMembers = true;
|
||||||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,15 @@ import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
interface IMediaDevices {
|
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
||||||
audioOutput: Array<MediaDeviceInfo>;
|
export enum MediaDeviceKindEnum {
|
||||||
audioInput: Array<MediaDeviceInfo>;
|
AudioOutput = "audiooutput",
|
||||||
videoInput: Array<MediaDeviceInfo>;
|
AudioInput = "audioinput",
|
||||||
|
VideoInput = "videoinput",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IMediaDevices = Record<MediaDeviceKindEnum, Array<MediaDeviceInfo>>;
|
||||||
|
|
||||||
export enum MediaDeviceHandlerEvent {
|
export enum MediaDeviceHandlerEvent {
|
||||||
AudioOutputChanged = "audio_output_changed",
|
AudioOutputChanged = "audio_output_changed",
|
||||||
}
|
}
|
||||||
|
@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const output = {
|
||||||
|
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||||
|
[MediaDeviceKindEnum.AudioInput]: [],
|
||||||
|
[MediaDeviceKindEnum.VideoInput]: [],
|
||||||
|
};
|
||||||
|
|
||||||
const audioOutput = [];
|
devices.forEach((device) => output[device.kind].push(device));
|
||||||
const audioInput = [];
|
return output;
|
||||||
const videoInput = [];
|
|
||||||
|
|
||||||
devices.forEach((device) => {
|
|
||||||
switch (device.kind) {
|
|
||||||
case 'audiooutput': audioOutput.push(device); break;
|
|
||||||
case 'audioinput': audioInput.push(device); break;
|
|
||||||
case 'videoinput': videoInput.push(device); break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { audioOutput, audioInput, videoInput };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Unable to refresh WebRTC Devices: ', error);
|
console.warn('Unable to refresh WebRTC Devices: ', error);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
setMatrixCallVideoInput(deviceId);
|
setMatrixCallVideoInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
||||||
|
switch (kind) {
|
||||||
|
case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break;
|
||||||
|
case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break;
|
||||||
|
case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static getAudioOutput(): string {
|
public static getAudioOutput(): string {
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||||
}
|
}
|
||||||
|
|
22
src/Rooms.ts
22
src/Rooms.ts
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
|
import AliasCustomisations from './customisations/Alias';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a room object, return the alias we should use for it,
|
* Given a room object, return the alias we should use for it,
|
||||||
|
@ -28,7 +29,18 @@ import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
* @returns {string} A display alias for the given room
|
* @returns {string} A display alias for the given room
|
||||||
*/
|
*/
|
||||||
export function getDisplayAliasForRoom(room: Room): string {
|
export function getDisplayAliasForRoom(room: Room): string {
|
||||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
return getDisplayAliasForAliasSet(
|
||||||
|
room.getCanonicalAlias(), room.getAltAliases(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The various display alias getters should all feed through this one path so
|
||||||
|
// there's a single place to change the logic.
|
||||||
|
export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string {
|
||||||
|
if (AliasCustomisations.getDisplayAliasForAliasSet) {
|
||||||
|
return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases);
|
||||||
|
}
|
||||||
|
return canonicalAlias || altAliases?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
|
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
|
||||||
|
@ -72,10 +84,8 @@ export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void>
|
||||||
this room as a DM room
|
this room as a DM room
|
||||||
* @returns {object} A promise
|
* @returns {object} A promise
|
||||||
*/
|
*/
|
||||||
export function setDMRoom(roomId: string, userId: string): Promise<void> {
|
export async function setDMRoom(roomId: string, userId: string): Promise<void> {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
||||||
let dmRoomMap = {};
|
let dmRoomMap = {};
|
||||||
|
@ -104,7 +114,7 @@ export function setDMRoom(roomId: string, userId: string): Promise<void> {
|
||||||
dmRoomMap[userId] = roomList;
|
dmRoomMap[userId] = roomList;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
await MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,26 +14,42 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
IResultRoomEvents,
|
||||||
|
ISearchRequestBody,
|
||||||
|
ISearchResponse,
|
||||||
|
ISearchResult,
|
||||||
|
ISearchResults,
|
||||||
|
SearchOrderBy,
|
||||||
|
} from "matrix-js-sdk/src/@types/search";
|
||||||
|
import { IRoomEventFilter } from "matrix-js-sdk/src/filter";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { ISearchArgs } from "./indexing/BaseEventIndexManager";
|
||||||
import EventIndexPeg from "./indexing/EventIndexPeg";
|
import EventIndexPeg from "./indexing/EventIndexPeg";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||||
|
|
||||||
const SEARCH_LIMIT = 10;
|
const SEARCH_LIMIT = 10;
|
||||||
|
|
||||||
async function serverSideSearch(term, roomId = undefined) {
|
async function serverSideSearch(
|
||||||
|
term: string,
|
||||||
|
roomId: string = undefined,
|
||||||
|
): Promise<{ response: ISearchResponse, query: ISearchRequestBody }> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
const filter = {
|
const filter: IRoomEventFilter = {
|
||||||
limit: SEARCH_LIMIT,
|
limit: SEARCH_LIMIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (roomId !== undefined) filter.rooms = [roomId];
|
if (roomId !== undefined) filter.rooms = [roomId];
|
||||||
|
|
||||||
const body = {
|
const body: ISearchRequestBody = {
|
||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
search_term: term,
|
search_term: term,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
order_by: "recent",
|
order_by: SearchOrderBy.Recent,
|
||||||
event_context: {
|
event_context: {
|
||||||
before_limit: 1,
|
before_limit: 1,
|
||||||
after_limit: 1,
|
after_limit: 1,
|
||||||
|
@ -45,31 +61,26 @@ async function serverSideSearch(term, roomId = undefined) {
|
||||||
|
|
||||||
const response = await client.search({ body: body });
|
const response = await client.search({ body: body });
|
||||||
|
|
||||||
const result = {
|
return { response, query: body };
|
||||||
response: response,
|
|
||||||
query: body,
|
|
||||||
};
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function serverSideSearchProcess(term, roomId = undefined) {
|
async function serverSideSearchProcess(term: string, roomId: string = undefined): Promise<ISearchResults> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const result = await serverSideSearch(term, roomId);
|
const result = await serverSideSearch(term, roomId);
|
||||||
|
|
||||||
// The js-sdk method backPaginateRoomEventsSearch() uses _query internally
|
// The js-sdk method backPaginateRoomEventsSearch() uses _query internally
|
||||||
// so we're reusing the concept here since we wan't to delegate the
|
// so we're reusing the concept here since we want to delegate the
|
||||||
// pagination back to backPaginateRoomEventsSearch() in some cases.
|
// pagination back to backPaginateRoomEventsSearch() in some cases.
|
||||||
const searchResult = {
|
const searchResults: ISearchResults = {
|
||||||
_query: result.query,
|
_query: result.query,
|
||||||
results: [],
|
results: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return client.processRoomEventsSearch(searchResult, result.response);
|
return client.processRoomEventsSearch(searchResults, result.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareEvents(a, b) {
|
function compareEvents(a: ISearchResult, b: ISearchResult): number {
|
||||||
const aEvent = a.result;
|
const aEvent = a.result;
|
||||||
const bEvent = b.result;
|
const bEvent = b.result;
|
||||||
|
|
||||||
|
@ -79,7 +90,7 @@ function compareEvents(a, b) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function combinedSearch(searchTerm) {
|
async function combinedSearch(searchTerm: string): Promise<ISearchResults> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
// Create two promises, one for the local search, one for the
|
// Create two promises, one for the local search, one for the
|
||||||
|
@ -111,10 +122,10 @@ async function combinedSearch(searchTerm) {
|
||||||
// returns since that one can be either a server-side one, a local one or a
|
// returns since that one can be either a server-side one, a local one or a
|
||||||
// fake one to fetch the remaining cached events. See the docs for
|
// fake one to fetch the remaining cached events. See the docs for
|
||||||
// combineEvents() for an explanation why we need to cache events.
|
// combineEvents() for an explanation why we need to cache events.
|
||||||
const emptyResult = {
|
const emptyResult: ISeshatSearchResults = {
|
||||||
seshatQuery: localQuery,
|
seshatQuery: localQuery,
|
||||||
_query: serverQuery,
|
_query: serverQuery,
|
||||||
serverSideNextBatch: serverResponse.next_batch,
|
serverSideNextBatch: serverResponse.search_categories.room_events.next_batch,
|
||||||
cachedEvents: [],
|
cachedEvents: [],
|
||||||
oldestEventFrom: "server",
|
oldestEventFrom: "server",
|
||||||
results: [],
|
results: [],
|
||||||
|
@ -125,7 +136,7 @@ async function combinedSearch(searchTerm) {
|
||||||
const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events);
|
const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events);
|
||||||
|
|
||||||
// Let the client process the combined result.
|
// Let the client process the combined result.
|
||||||
const response = {
|
const response: ISearchResponse = {
|
||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: combinedResult,
|
room_events: combinedResult,
|
||||||
},
|
},
|
||||||
|
@ -139,10 +150,14 @@ async function combinedSearch(searchTerm) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function localSearch(searchTerm, roomId = undefined, processResult = true) {
|
async function localSearch(
|
||||||
|
searchTerm: string,
|
||||||
|
roomId: string = undefined,
|
||||||
|
processResult = true,
|
||||||
|
): Promise<{ response: IResultRoomEvents, query: ISearchArgs }> {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
const searchArgs = {
|
const searchArgs: ISearchArgs = {
|
||||||
search_term: searchTerm,
|
search_term: searchTerm,
|
||||||
before_limit: 1,
|
before_limit: 1,
|
||||||
after_limit: 1,
|
after_limit: 1,
|
||||||
|
@ -167,11 +182,18 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function localSearchProcess(searchTerm, roomId = undefined) {
|
export interface ISeshatSearchResults extends ISearchResults {
|
||||||
|
seshatQuery?: ISearchArgs;
|
||||||
|
cachedEvents?: ISearchResult[];
|
||||||
|
oldestEventFrom?: "local" | "server";
|
||||||
|
serverSideNextBatch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function localSearchProcess(searchTerm: string, roomId: string = undefined): Promise<ISeshatSearchResults> {
|
||||||
const emptyResult = {
|
const emptyResult = {
|
||||||
results: [],
|
results: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
} as ISeshatSearchResults;
|
||||||
|
|
||||||
if (searchTerm === "") return emptyResult;
|
if (searchTerm === "") return emptyResult;
|
||||||
|
|
||||||
|
@ -179,7 +201,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
|
||||||
|
|
||||||
emptyResult.seshatQuery = result.query;
|
emptyResult.seshatQuery = result.query;
|
||||||
|
|
||||||
const response = {
|
const response: ISearchResponse = {
|
||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: result.response,
|
room_events: result.response,
|
||||||
},
|
},
|
||||||
|
@ -192,7 +214,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
|
||||||
return processedResult;
|
return processedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function localPagination(searchResult) {
|
async function localPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
const searchArgs = searchResult.seshatQuery;
|
const searchArgs = searchResult.seshatQuery;
|
||||||
|
@ -221,10 +243,10 @@ async function localPagination(searchResult) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareOldestEvents(firstResults, secondResults) {
|
function compareOldestEvents(firstResults: ISearchResult[], secondResults: ISearchResult[]): number {
|
||||||
try {
|
try {
|
||||||
const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result;
|
const oldestFirstEvent = firstResults[firstResults.length - 1].result;
|
||||||
const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result;
|
const oldestSecondEvent = secondResults[secondResults.length - 1].result;
|
||||||
|
|
||||||
if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) {
|
if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -236,7 +258,12 @@ function compareOldestEvents(firstResults, secondResults) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineEventSources(previousSearchResult, response, a, b) {
|
function combineEventSources(
|
||||||
|
previousSearchResult: ISeshatSearchResults,
|
||||||
|
response: IResultRoomEvents,
|
||||||
|
a: ISearchResult[],
|
||||||
|
b: ISearchResult[],
|
||||||
|
): void {
|
||||||
// Merge event sources and sort the events.
|
// Merge event sources and sort the events.
|
||||||
const combinedEvents = a.concat(b).sort(compareEvents);
|
const combinedEvents = a.concat(b).sort(compareEvents);
|
||||||
// Put half of the events in the response, and cache the other half.
|
// Put half of the events in the response, and cache the other half.
|
||||||
|
@ -353,8 +380,12 @@ function combineEventSources(previousSearchResult, response, a, b) {
|
||||||
* different event sources.
|
* different event sources.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) {
|
function combineEvents(
|
||||||
const response = {};
|
previousSearchResult: ISeshatSearchResults,
|
||||||
|
localEvents: IResultRoomEvents = undefined,
|
||||||
|
serverEvents: IResultRoomEvents = undefined,
|
||||||
|
): IResultRoomEvents {
|
||||||
|
const response = {} as IResultRoomEvents;
|
||||||
|
|
||||||
const cachedEvents = previousSearchResult.cachedEvents;
|
const cachedEvents = previousSearchResult.cachedEvents;
|
||||||
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
||||||
|
@ -364,7 +395,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
// This is a first search call, combine the events from the server and
|
// This is a first search call, combine the events from the server and
|
||||||
// the local index. Note where our oldest event came from, we shall
|
// the local index. Note where our oldest event came from, we shall
|
||||||
// fetch the next batch of events from the other source.
|
// fetch the next batch of events from the other source.
|
||||||
if (compareOldestEvents(localEvents, serverEvents) < 0) {
|
if (compareOldestEvents(localEvents.results, serverEvents.results) < 0) {
|
||||||
oldestEventFrom = "local";
|
oldestEventFrom = "local";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,7 +406,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
// meaning that our oldest event was on the server.
|
// meaning that our oldest event was on the server.
|
||||||
// Change the source of the oldest event if our local event is older
|
// Change the source of the oldest event if our local event is older
|
||||||
// than the cached one.
|
// than the cached one.
|
||||||
if (compareOldestEvents(localEvents, cachedEvents) < 0) {
|
if (compareOldestEvents(localEvents.results, cachedEvents) < 0) {
|
||||||
oldestEventFrom = "local";
|
oldestEventFrom = "local";
|
||||||
}
|
}
|
||||||
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
||||||
|
@ -384,7 +415,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
// meaning that our oldest event was in the local index.
|
// meaning that our oldest event was in the local index.
|
||||||
// Change the source of the oldest event if our server event is older
|
// Change the source of the oldest event if our server event is older
|
||||||
// than the cached one.
|
// than the cached one.
|
||||||
if (compareOldestEvents(serverEvents, cachedEvents) < 0) {
|
if (compareOldestEvents(serverEvents.results, cachedEvents) < 0) {
|
||||||
oldestEventFrom = "server";
|
oldestEventFrom = "server";
|
||||||
}
|
}
|
||||||
combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents);
|
combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents);
|
||||||
|
@ -412,7 +443,11 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
* @return {object} A response object that combines the events from the
|
* @return {object} A response object that combines the events from the
|
||||||
* different event sources.
|
* different event sources.
|
||||||
*/
|
*/
|
||||||
function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) {
|
function combineResponses(
|
||||||
|
previousSearchResult: ISeshatSearchResults,
|
||||||
|
localEvents: IResultRoomEvents = undefined,
|
||||||
|
serverEvents: IResultRoomEvents = undefined,
|
||||||
|
): IResultRoomEvents {
|
||||||
// Combine our events first.
|
// Combine our events first.
|
||||||
const response = combineEvents(previousSearchResult, localEvents, serverEvents);
|
const response = combineEvents(previousSearchResult, localEvents, serverEvents);
|
||||||
|
|
||||||
|
@ -454,42 +489,51 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreEncryptionInfo(searchResultSlice = []) {
|
interface IEncryptedSeshatEvent {
|
||||||
|
curve25519Key: string;
|
||||||
|
ed25519Key: string;
|
||||||
|
algorithm: string;
|
||||||
|
forwardingCurve25519KeyChain: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEncryptionInfo(searchResultSlice: SearchResult[] = []): void {
|
||||||
for (let i = 0; i < searchResultSlice.length; i++) {
|
for (let i = 0; i < searchResultSlice.length; i++) {
|
||||||
const timeline = searchResultSlice[i].context.getTimeline();
|
const timeline = searchResultSlice[i].context.getTimeline();
|
||||||
|
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
const ev = timeline[j];
|
const mxEv = timeline[j];
|
||||||
|
const ev = mxEv.event as IEncryptedSeshatEvent;
|
||||||
|
|
||||||
if (ev.event.curve25519Key) {
|
if (ev.curve25519Key) {
|
||||||
ev.makeEncrypted(
|
mxEv.makeEncrypted(
|
||||||
"m.room.encrypted",
|
EventType.RoomMessageEncrypted,
|
||||||
{ algorithm: ev.event.algorithm },
|
{ algorithm: ev.algorithm },
|
||||||
ev.event.curve25519Key,
|
ev.curve25519Key,
|
||||||
ev.event.ed25519Key,
|
ev.ed25519Key,
|
||||||
);
|
);
|
||||||
ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
|
// @ts-ignore
|
||||||
|
mxEv.forwardingCurve25519KeyChain = ev.forwardingCurve25519KeyChain;
|
||||||
|
|
||||||
delete ev.event.curve25519Key;
|
delete ev.curve25519Key;
|
||||||
delete ev.event.ed25519Key;
|
delete ev.ed25519Key;
|
||||||
delete ev.event.algorithm;
|
delete ev.algorithm;
|
||||||
delete ev.event.forwardingCurve25519KeyChain;
|
delete ev.forwardingCurve25519KeyChain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function combinedPagination(searchResult) {
|
async function combinedPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
const searchArgs = searchResult.seshatQuery;
|
const searchArgs = searchResult.seshatQuery;
|
||||||
const oldestEventFrom = searchResult.oldestEventFrom;
|
const oldestEventFrom = searchResult.oldestEventFrom;
|
||||||
|
|
||||||
let localResult;
|
let localResult: IResultRoomEvents;
|
||||||
let serverSideResult;
|
let serverSideResult: ISearchResponse;
|
||||||
|
|
||||||
// Fetch events from the local index if we have a token for itand if it's
|
// Fetch events from the local index if we have a token for it and if it's
|
||||||
// the local indexes turn or the server has exhausted its results.
|
// the local indexes turn or the server has exhausted its results.
|
||||||
if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) {
|
if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) {
|
||||||
localResult = await eventIndex.search(searchArgs);
|
localResult = await eventIndex.search(searchArgs);
|
||||||
|
@ -502,7 +546,7 @@ async function combinedPagination(searchResult) {
|
||||||
serverSideResult = await client.search(body);
|
serverSideResult = await client.search(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverEvents;
|
let serverEvents: IResultRoomEvents;
|
||||||
|
|
||||||
if (serverSideResult) {
|
if (serverSideResult) {
|
||||||
serverEvents = serverSideResult.search_categories.room_events;
|
serverEvents = serverSideResult.search_categories.room_events;
|
||||||
|
@ -532,8 +576,8 @@ async function combinedPagination(searchResult) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventIndexSearch(term, roomId = undefined) {
|
function eventIndexSearch(term: string, roomId: string = undefined): Promise<ISearchResults> {
|
||||||
let searchPromise;
|
let searchPromise: Promise<ISearchResults>;
|
||||||
|
|
||||||
if (roomId !== undefined) {
|
if (roomId !== undefined) {
|
||||||
if (MatrixClientPeg.get().isRoomEncrypted(roomId)) {
|
if (MatrixClientPeg.get().isRoomEncrypted(roomId)) {
|
||||||
|
@ -554,7 +598,7 @@ function eventIndexSearch(term, roomId = undefined) {
|
||||||
return searchPromise;
|
return searchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventIndexSearchPagination(searchResult) {
|
function eventIndexSearchPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
const seshatQuery = searchResult.seshatQuery;
|
const seshatQuery = searchResult.seshatQuery;
|
||||||
|
@ -580,7 +624,7 @@ function eventIndexSearchPagination(searchResult) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchPagination(searchResult) {
|
export function searchPagination(searchResult: ISearchResults): Promise<ISearchResults> {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -590,7 +634,7 @@ export function searchPagination(searchResult) {
|
||||||
else return eventIndexSearchPagination(searchResult);
|
else return eventIndexSearchPagination(searchResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function eventSearch(term, roomId = undefined) {
|
export default function eventSearch(term: string, roomId: string = undefined): Promise<ISearchResults> {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
if (eventIndex === null) return serverSideSearchProcess(term, roomId);
|
if (eventIndex === null) return serverSideSearchProcess(term, roomId);
|
|
@ -447,7 +447,8 @@ function textForPowerEvent(event): () => string | null {
|
||||||
!event.getContent() || !event.getContent().users) {
|
!event.getContent() || !event.getContent().users) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const userDefault = event.getContent().users_default || 0;
|
const previousUserDefault = event.getPrevContent().users_default || 0;
|
||||||
|
const currentUserDefault = event.getContent().users_default || 0;
|
||||||
// Construct set of userIds
|
// Construct set of userIds
|
||||||
const users = [];
|
const users = [];
|
||||||
Object.keys(event.getContent().users).forEach(
|
Object.keys(event.getContent().users).forEach(
|
||||||
|
@ -463,9 +464,16 @@ function textForPowerEvent(event): () => string | null {
|
||||||
const diffs = [];
|
const diffs = [];
|
||||||
users.forEach((userId) => {
|
users.forEach((userId) => {
|
||||||
// Previous power level
|
// Previous power level
|
||||||
const from = event.getPrevContent().users[userId];
|
let from = event.getPrevContent().users[userId];
|
||||||
|
if (!Number.isInteger(from)) {
|
||||||
|
from = previousUserDefault;
|
||||||
|
}
|
||||||
// Current power level
|
// Current power level
|
||||||
const to = event.getContent().users[userId];
|
let to = event.getContent().users[userId];
|
||||||
|
if (!Number.isInteger(to)) {
|
||||||
|
to = currentUserDefault;
|
||||||
|
}
|
||||||
|
if (from === previousUserDefault && to === currentUserDefault) { return; }
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
diffs.push({ userId, from, to });
|
diffs.push({ userId, from, to });
|
||||||
}
|
}
|
||||||
|
@ -479,8 +487,8 @@ function textForPowerEvent(event): () => string | null {
|
||||||
powerLevelDiffText: diffs.map(diff =>
|
powerLevelDiffText: diffs.map(diff =>
|
||||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||||
userId: diff.userId,
|
userId: diff.userId,
|
||||||
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
|
fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
|
||||||
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
|
toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
|
||||||
}),
|
}),
|
||||||
).join(", "),
|
).join(", "),
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default class InteractiveAuthComponent extends React.Component {
|
||||||
// * emailSid {string} If email auth was performed, the sid of
|
// * emailSid {string} If email auth was performed, the sid of
|
||||||
// the auth session.
|
// the auth session.
|
||||||
// * clientSecret {string} The client secret used in auth
|
// * clientSecret {string} The client secret used in auth
|
||||||
// sessions with the ID server.
|
// sessions with the identity server.
|
||||||
onAuthFinished: PropTypes.func.isRequired,
|
onAuthFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// Inputs provided by the user to the auth process
|
// Inputs provided by the user to the auth process
|
||||||
|
|
|
@ -398,7 +398,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// refocusing during a paste event will make the
|
// refocusing during a paste event will make the
|
||||||
// paste end up in the newly focused element,
|
// paste end up in the newly focused element,
|
||||||
// so dispatch synchronously before paste happens
|
// so dispatch synchronously before paste happens
|
||||||
dis.fire(Action.FocusComposer, true);
|
dis.fire(Action.FocusSendMessageComposer, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -552,7 +552,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||||
// synchronous dispatch so we focus before key generates input
|
// synchronous dispatch so we focus before key generates input
|
||||||
dis.fire(Action.FocusComposer, true);
|
dis.fire(Action.FocusSendMessageComposer, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
// we should *not* preventDefault() here as
|
// we should *not* preventDefault() here as
|
||||||
// that would prevent typing in the now-focussed composer
|
// that would prevent typing in the now-focussed composer
|
||||||
|
|
|
@ -251,7 +251,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private pageChanging: boolean;
|
private pageChanging: boolean;
|
||||||
private tokenLogin?: boolean;
|
private tokenLogin?: boolean;
|
||||||
private accountPassword?: string;
|
private accountPassword?: string;
|
||||||
private accountPasswordTimer?: NodeJS.Timeout;
|
private accountPasswordTimer?: number;
|
||||||
private focusComposer: boolean;
|
private focusComposer: boolean;
|
||||||
private subTitleStatus: string;
|
private subTitleStatus: string;
|
||||||
private prevWindowWidth: number;
|
private prevWindowWidth: number;
|
||||||
|
@ -443,7 +443,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
CountlyAnalytics.instance.trackPageChange(durationMs);
|
CountlyAnalytics.instance.trackPageChange(durationMs);
|
||||||
}
|
}
|
||||||
if (this.focusComposer) {
|
if (this.focusComposer) {
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -561,7 +561,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'MatrixActions.accountData':
|
case 'MatrixActions.accountData':
|
||||||
// XXX: This is a collection of several hacks to solve a minor problem. We want to
|
// XXX: This is a collection of several hacks to solve a minor problem. We want to
|
||||||
// update our local state when the ID server changes, but don't want to put that in
|
// update our local state when the identity server changes, but don't want to put that in
|
||||||
// the js-sdk as we'd be then dictating how all consumers need to behave. However,
|
// the js-sdk as we'd be then dictating how all consumers need to behave. However,
|
||||||
// this component is already bloated and we probably don't want this tiny logic in
|
// this component is already bloated and we probably don't want this tiny logic in
|
||||||
// here, but there's no better place in the react-sdk for it. Additionally, we're
|
// here, but there's no better place in the react-sdk for it. Additionally, we're
|
||||||
|
@ -1427,7 +1427,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
showNotificationsToast(false);
|
showNotificationsToast(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
this.setState({
|
this.setState({
|
||||||
ready: true,
|
ready: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { IFieldType, IInstance, IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client";
|
||||||
|
import { Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
@ -25,7 +28,7 @@ import { _t } from '../../languageHandler';
|
||||||
import SdkConfig from '../../SdkConfig';
|
import SdkConfig from '../../SdkConfig';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import { ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols } from "../views/directory/NetworkDropdown";
|
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||||
import GroupStore from "../../stores/GroupStore";
|
import GroupStore from "../../stores/GroupStore";
|
||||||
|
@ -40,14 +43,17 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
||||||
import NetworkDropdown from "../views/directory/NetworkDropdown";
|
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 800;
|
const MAX_TOPIC_LENGTH = 800;
|
||||||
|
|
||||||
|
const LAST_SERVER_KEY = "mx_last_room_directory_server";
|
||||||
|
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
|
||||||
|
|
||||||
function track(action: string) {
|
function track(action: string) {
|
||||||
Analytics.trackEvent('RoomDirectory', action);
|
Analytics.trackEvent('RoomDirectory', action);
|
||||||
}
|
}
|
||||||
|
@ -57,46 +63,23 @@ interface IProps extends IDialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
publicRooms: IRoom[];
|
publicRooms: IPublicRoomsChunkRoom[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
protocolsLoading: boolean;
|
protocolsLoading: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
instanceId: string | symbol;
|
instanceId: string;
|
||||||
roomServer: string;
|
roomServer: string;
|
||||||
filterString: string;
|
filterString: string;
|
||||||
selectedCommunityId?: string;
|
selectedCommunityId?: string;
|
||||||
communityName?: string;
|
communityName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
interface IRoom {
|
|
||||||
room_id: string;
|
|
||||||
name?: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
topic?: string;
|
|
||||||
canonical_alias?: string;
|
|
||||||
aliases?: string[];
|
|
||||||
world_readable: boolean;
|
|
||||||
guest_can_join: boolean;
|
|
||||||
num_joined_members: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPublicRoomsRequest {
|
|
||||||
limit?: number;
|
|
||||||
since?: string;
|
|
||||||
server?: string;
|
|
||||||
filter?: object;
|
|
||||||
include_all_networks?: boolean;
|
|
||||||
third_party_instance_id?: string;
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomDirectory")
|
@replaceableComponent("structures.RoomDirectory")
|
||||||
export default class RoomDirectory extends React.Component<IProps, IState> {
|
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
private readonly startTime: number;
|
private readonly startTime: number;
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private nextBatch: string = null;
|
private nextBatch: string = null;
|
||||||
private filterTimeout: NodeJS.Timeout;
|
private filterTimeout: number;
|
||||||
private protocols: Protocols;
|
private protocols: Protocols;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -116,6 +99,36 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
} else if (!selectedCommunityId) {
|
} else if (!selectedCommunityId) {
|
||||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||||
this.protocols = response;
|
this.protocols = response;
|
||||||
|
const myHomeserver = MatrixClientPeg.getHomeserverName();
|
||||||
|
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY);
|
||||||
|
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY);
|
||||||
|
|
||||||
|
let roomServer = myHomeserver;
|
||||||
|
if (
|
||||||
|
SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) ||
|
||||||
|
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
|
||||||
|
) {
|
||||||
|
roomServer = lsRoomServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instanceId: string = null;
|
||||||
|
if (roomServer === myHomeserver && (
|
||||||
|
lsInstanceId === ALL_ROOMS ||
|
||||||
|
Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId))
|
||||||
|
)) {
|
||||||
|
instanceId = lsInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the room list only if validation failed and we had to change these
|
||||||
|
if (this.state.instanceId !== instanceId || this.state.roomServer !== roomServer) {
|
||||||
|
this.setState({
|
||||||
|
protocolsLoading: false,
|
||||||
|
instanceId,
|
||||||
|
roomServer,
|
||||||
|
});
|
||||||
|
this.refreshRoomList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({ protocolsLoading: false });
|
this.setState({ protocolsLoading: false });
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.warn(`error loading third party protocols: ${err}`);
|
console.warn(`error loading third party protocols: ${err}`);
|
||||||
|
@ -150,8 +163,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
publicRooms: [],
|
publicRooms: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
instanceId: undefined,
|
instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
|
||||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
roomServer: localStorage.getItem(LAST_SERVER_KEY),
|
||||||
filterString: this.props.initialText || "",
|
filterString: this.props.initialText || "",
|
||||||
selectedCommunityId,
|
selectedCommunityId,
|
||||||
communityName: null,
|
communityName: null,
|
||||||
|
@ -219,7 +232,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
// remember the next batch token when we sent the request
|
// remember the next batch token when we sent the request
|
||||||
// too. If it's changed, appending to the list will corrupt it.
|
// too. If it's changed, appending to the list will corrupt it.
|
||||||
const nextBatch = this.nextBatch;
|
const nextBatch = this.nextBatch;
|
||||||
const opts: IPublicRoomsRequest = { limit: 20 };
|
const opts: IRoomDirectoryOptions = { limit: 20 };
|
||||||
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||||
opts.server = roomServer;
|
opts.server = roomServer;
|
||||||
}
|
}
|
||||||
|
@ -292,7 +305,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
* HS admins to do this through the RoomSettings interface, but
|
* HS admins to do this through the RoomSettings interface, but
|
||||||
* this needs SPEC-417.
|
* this needs SPEC-417.
|
||||||
*/
|
*/
|
||||||
private removeFromDirectory(room: IRoom) {
|
private removeFromDirectory(room: IPublicRoomsChunkRoom) {
|
||||||
const alias = getDisplayAliasForRoom(room);
|
const alias = getDisplayAliasForRoom(room);
|
||||||
const name = room.name || alias || _t('Unnamed room');
|
const name = room.name || alias || _t('Unnamed room');
|
||||||
|
|
||||||
|
@ -312,7 +325,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
const modal = Modal.createDialog(Spinner);
|
const modal = Modal.createDialog(Spinner);
|
||||||
let step = _t('remove %(name)s from the directory.', { name: name });
|
let step = _t('remove %(name)s from the directory.', { name: name });
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, Visibility.Private).then(() => {
|
||||||
if (!alias) return;
|
if (!alias) return;
|
||||||
step = _t('delete the address.');
|
step = _t('delete the address.');
|
||||||
return MatrixClientPeg.get().deleteAlias(alias);
|
return MatrixClientPeg.get().deleteAlias(alias);
|
||||||
|
@ -334,7 +347,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: ButtonEvent) => {
|
||||||
// If room was shift-clicked, remove it from the room directory
|
// If room was shift-clicked, remove it from the room directory
|
||||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -342,7 +355,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOptionChange = (server: string, instanceId?: string | symbol) => {
|
private onOptionChange = (server: string, instanceId?: string) => {
|
||||||
// clear next batch so we don't try to load more rooms
|
// clear next batch so we don't try to load more rooms
|
||||||
this.nextBatch = null;
|
this.nextBatch = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -360,6 +373,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
// find the five gitter ones, at which point we do not want
|
// find the five gitter ones, at which point we do not want
|
||||||
// to render all those rooms when switching back to 'all networks'.
|
// to render all those rooms when switching back to 'all networks'.
|
||||||
// Easiest to just blow away the state & re-fetch.
|
// Easiest to just blow away the state & re-fetch.
|
||||||
|
|
||||||
|
// We have to be careful here so that we don't set instanceId = "undefined"
|
||||||
|
localStorage.setItem(LAST_SERVER_KEY, server);
|
||||||
|
if (instanceId) {
|
||||||
|
localStorage.setItem(LAST_INSTANCE_KEY, instanceId);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(LAST_INSTANCE_KEY);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFillRequest = (backwards: boolean) => {
|
private onFillRequest = (backwards: boolean) => {
|
||||||
|
@ -439,17 +460,17 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
|
private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||||
this.showRoom(room, null, false, true);
|
this.showRoom(room, null, false, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
|
private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||||
this.showRoom(room);
|
this.showRoom(room);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
|
private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
|
||||||
this.showRoom(room, null, true);
|
this.showRoom(room, null, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
@ -467,7 +488,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
this.showRoom(null, alias, autoJoin);
|
this.showRoom(null, alias, autoJoin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||||
this.onFinished();
|
this.onFinished();
|
||||||
const payload: ActionPayload = {
|
const payload: ActionPayload = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
|
@ -516,7 +537,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createRoomCells(room: IRoom) {
|
private createRoomCells(room: IPublicRoomsChunkRoom) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const clientRoom = client.getRoom(room.room_id);
|
const clientRoom = client.getRoom(room.room_id);
|
||||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||||
|
@ -812,6 +833,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||||
// but works with the objects we get from the public room list
|
// but works with the objects we get from the public room list
|
||||||
function getDisplayAliasForRoom(room: IRoom) {
|
function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) {
|
||||||
return room.canonical_alias || room.aliases?.[0] || "";
|
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,7 +131,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case RoomListAction.ClearSearch:
|
case RoomListAction.ClearSearch:
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
defaultDispatcher.fire(Action.FocusComposer);
|
defaultDispatcher.fire(Action.FocusSendMessageComposer);
|
||||||
break;
|
break;
|
||||||
case RoomListAction.NextRoom:
|
case RoomListAction.NextRoom:
|
||||||
case RoomListAction.PrevRoom:
|
case RoomListAction.PrevRoom:
|
||||||
|
|
|
@ -118,12 +118,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
this.setState({ isResending: false });
|
this.setState({ isResending: false });
|
||||||
});
|
});
|
||||||
this.setState({ isResending: true });
|
this.setState({ isResending: true });
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelAllClick = () => {
|
_onCancelAllClick = () => {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
||||||
|
|
|
@ -25,8 +25,8 @@ import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
|
||||||
import { EventSubscription } from "fbemitter";
|
import { EventSubscription } from "fbemitter";
|
||||||
|
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
|
||||||
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
@ -133,12 +133,7 @@ export interface IState {
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
searchScope?: SearchScope;
|
searchScope?: SearchScope;
|
||||||
searchResults?: XOR<{}, {
|
searchResults?: XOR<{}, ISearchResults>;
|
||||||
count: number;
|
|
||||||
highlights: string[];
|
|
||||||
results: SearchResult[];
|
|
||||||
next_batch: string; // eslint-disable-line camelcase
|
|
||||||
}>;
|
|
||||||
searchHighlights?: string[];
|
searchHighlights?: string[];
|
||||||
searchInProgress?: boolean;
|
searchInProgress?: boolean;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
|
@ -818,17 +813,16 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
case Action.ComposerInsert: {
|
case Action.ComposerInsert: {
|
||||||
// re-dispatch to the correct composer
|
// re-dispatch to the correct composer
|
||||||
if (this.state.editState) {
|
dis.dispatch({
|
||||||
dis.dispatch({
|
...payload,
|
||||||
...payload,
|
action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
|
||||||
action: "edit_composer_insert",
|
});
|
||||||
});
|
break;
|
||||||
} else {
|
}
|
||||||
dis.dispatch({
|
|
||||||
...payload,
|
case Action.FocusAComposer: {
|
||||||
action: "send_composer_insert",
|
// re-dispatch to the correct composer
|
||||||
});
|
dis.fire(this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1138,7 +1132,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (this.state.searchResults.next_batch) {
|
if (this.state.searchResults.next_batch) {
|
||||||
debuglog("requesting more search results");
|
debuglog("requesting more search results");
|
||||||
const searchPromise = searchPagination(this.state.searchResults);
|
const searchPromise = searchPagination(this.state.searchResults as ISearchResults);
|
||||||
return this.handleSearchResult(searchPromise);
|
return this.handleSearchResult(searchPromise);
|
||||||
} else {
|
} else {
|
||||||
debuglog("no more search results");
|
debuglog("no more search results");
|
||||||
|
@ -1246,7 +1240,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||||
);
|
);
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingFile: false,
|
draggingFile: false,
|
||||||
|
@ -1548,7 +1542,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we have to jump manually
|
// Otherwise we have to jump manually
|
||||||
this.messagePanel.jumpToLiveTimeline();
|
this.messagePanel.jumpToLiveTimeline();
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -187,7 +187,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
private fillRequestWhileRunning: boolean;
|
private fillRequestWhileRunning: boolean;
|
||||||
private scrollState: IScrollState;
|
private scrollState: IScrollState;
|
||||||
private preventShrinkingState: IPreventShrinkingState;
|
private preventShrinkingState: IPreventShrinkingState;
|
||||||
private unfillDebouncer: NodeJS.Timeout;
|
private unfillDebouncer: number;
|
||||||
private bottomGrowth: number;
|
private bottomGrowth: number;
|
||||||
private pages: number;
|
private pages: number;
|
||||||
private heightUpdateInProgress: boolean;
|
private heightUpdateInProgress: boolean;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React, { ReactNode, useMemo, useState } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { sortBy } from "lodash";
|
import { sortBy } from "lodash";
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ import { useStateToggle } from "../../hooks/useStateToggle";
|
||||||
import { getChildOrder } from "../../stores/SpaceStore";
|
import { getChildOrder } from "../../stores/SpaceStore";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { linkifyElement } from "../../HtmlUtils";
|
import { linkifyElement } from "../../HtmlUtils";
|
||||||
|
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -51,36 +53,6 @@ interface IHierarchyProps {
|
||||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
export interface ISpaceSummaryRoom {
|
|
||||||
canonical_alias?: string;
|
|
||||||
aliases: string[];
|
|
||||||
avatar_url?: string;
|
|
||||||
guest_can_join: boolean;
|
|
||||||
name?: string;
|
|
||||||
num_joined_members: number;
|
|
||||||
room_id: string;
|
|
||||||
topic?: string;
|
|
||||||
world_readable: boolean;
|
|
||||||
num_refs: number;
|
|
||||||
room_type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISpaceSummaryEvent {
|
|
||||||
room_id: string;
|
|
||||||
event_id: string;
|
|
||||||
origin_server_ts: number;
|
|
||||||
type: string;
|
|
||||||
state_key: string;
|
|
||||||
content: {
|
|
||||||
order?: string;
|
|
||||||
suggested?: boolean;
|
|
||||||
auto_join?: boolean;
|
|
||||||
via?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
interface ITileProps {
|
interface ITileProps {
|
||||||
room: ISpaceSummaryRoom;
|
room: ISpaceSummaryRoom;
|
||||||
suggested?: boolean;
|
suggested?: boolean;
|
||||||
|
@ -666,5 +638,5 @@ export default SpaceRoomDirectory;
|
||||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||||
// but works with the objects we get from the public room list
|
// but works with the objects we get from the public room list
|
||||||
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
|
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
|
||||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import * as React from "react";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import classNames from "classnames";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,9 +38,16 @@ export class Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TabLocation {
|
||||||
|
LEFT = 'left',
|
||||||
|
TOP = 'top',
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
initialTabId?: string;
|
initialTabId?: string;
|
||||||
|
tabLocation: TabLocation;
|
||||||
|
onChange?: (tabId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -62,6 +70,10 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
tabLocation: TabLocation.LEFT,
|
||||||
|
};
|
||||||
|
|
||||||
private _getActiveTabIndex() {
|
private _getActiveTabIndex() {
|
||||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||||
return this.state.activeTabIndex;
|
return this.state.activeTabIndex;
|
||||||
|
@ -75,6 +87,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
private _setActiveTab(tab: Tab) {
|
private _setActiveTab(tab: Tab) {
|
||||||
const idx = this.props.tabs.indexOf(tab);
|
const idx = this.props.tabs.indexOf(tab);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
|
if (this.props.onChange) this.props.onChange(tab.id);
|
||||||
this.setState({ activeTabIndex: idx });
|
this.setState({ activeTabIndex: idx });
|
||||||
} else {
|
} else {
|
||||||
console.error("Could not find tab " + tab.label + " in tabs");
|
console.error("Could not find tab " + tab.label + " in tabs");
|
||||||
|
@ -119,8 +132,14 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||||
|
|
||||||
|
const tabbedViewClasses = classNames({
|
||||||
|
'mx_TabbedView': true,
|
||||||
|
'mx_TabbedView_tabsOnLeft': this.props.tabLocation == TabLocation.LEFT,
|
||||||
|
'mx_TabbedView_tabsOnTop': this.props.tabLocation == TabLocation.TOP,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_TabbedView">
|
<div className={tabbedViewClasses}>
|
||||||
<div className="mx_TabbedView_tabLabels">
|
<div className="mx_TabbedView_tabLabels">
|
||||||
{labels}
|
{labels}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,15 +17,18 @@ limitations under the License.
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
import { Playback, PlaybackState } from "../../../voice/Playback";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import PlaybackWaveform from "./PlaybackWaveform";
|
|
||||||
import PlayPauseButton from "./PlayPauseButton";
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
import PlaybackClock from "./PlaybackClock";
|
import PlaybackClock from "./PlaybackClock";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { TileShape } from "../rooms/EventTile";
|
||||||
|
import PlaybackWaveform from "./PlaybackWaveform";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Playback instance to render. Cannot change during component lifecycle: create
|
// Playback instance to render. Cannot change during component lifecycle: create
|
||||||
// an all-new component instead.
|
// an all-new component instead.
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
|
|
||||||
|
tileShape?: TileShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -50,15 +53,22 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
|
||||||
this.props.playback.prepare();
|
this.props.playback.prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get isWaveformable(): boolean {
|
||||||
|
return this.props.tileShape !== TileShape.Notif
|
||||||
|
&& this.props.tileShape !== TileShape.FileGrid
|
||||||
|
&& this.props.tileShape !== TileShape.Pinned;
|
||||||
|
}
|
||||||
|
|
||||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||||
this.setState({ playbackPhase: ev });
|
this.setState({ playbackPhase: ev });
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
return <div className='mx_MediaBody mx_VoiceMessagePrimaryContainer'>
|
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
||||||
|
return <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
||||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
||||||
<PlaybackClock playback={this.props.playback} />
|
<PlaybackClock playback={this.props.playback} />
|
||||||
<PlaybackWaveform playback={this.props.playback} />
|
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ import CaptchaForm from "./CaptchaForm";
|
||||||
* one HS whilst beign a guest on another).
|
* one HS whilst beign a guest on another).
|
||||||
* loginType: the login type of the auth stage being attempted
|
* loginType: the login type of the auth stage being attempted
|
||||||
* authSessionId: session id from the server
|
* authSessionId: session id from the server
|
||||||
* clientSecret: The client secret in use for ID server auth sessions
|
* clientSecret: The client secret in use for identity server auth sessions
|
||||||
* stageParams: params from the server for the stage being attempted
|
* stageParams: params from the server for the stage being attempted
|
||||||
* errorText: error message from a previous attempt to authenticate
|
* errorText: error message from a previous attempt to authenticate
|
||||||
* submitAuthDict: a function which will be called with the new auth dict
|
* submitAuthDict: a function which will be called with the new auth dict
|
||||||
|
@ -54,8 +54,8 @@ import CaptchaForm from "./CaptchaForm";
|
||||||
* Defined keys for stages are:
|
* Defined keys for stages are:
|
||||||
* m.login.email.identity:
|
* m.login.email.identity:
|
||||||
* * emailSid: string representing the sid of the active
|
* * emailSid: string representing the sid of the active
|
||||||
* verification session from the ID server, or
|
* verification session from the identity server,
|
||||||
* null if no session is active.
|
* or null if no session is active.
|
||||||
* fail: a function which should be called with an error object if an
|
* fail: a function which should be called with an error object if an
|
||||||
* error occurred during the auth stage. This will cause the auth
|
* error occurred during the auth stage. This will cause the auth
|
||||||
* session to be failed and the process to go back to the start.
|
* session to be failed and the process to go back to the start.
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||||
onTransferClick = () => {
|
onTransferClick = () => {
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
|
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import Dialpad from '../voip/DialPad';
|
import DialPad from '../voip/DialPad';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps extends IContextMenuProps {
|
interface IProps extends IContextMenuProps {
|
||||||
|
@ -45,24 +45,29 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
this.setState({ value: this.state.value + digit });
|
this.setState({ value: this.state.value + digit });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCancelClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
onChange = (ev) => {
|
onChange = (ev) => {
|
||||||
this.setState({ value: ev.target.value });
|
this.setState({ value: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<div className="mx_DialPadContextMenu_header">
|
<div className="mx_DialPadContextMenuWrapper">
|
||||||
<div>
|
<div>
|
||||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
|
||||||
|
</div>
|
||||||
|
<div className="mx_DialPadContextMenu_header">
|
||||||
|
<Field className="mx_DialPadContextMenu_dialled"
|
||||||
|
value={this.state.value} autoFocus={true}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_DialPadContextMenu_dialPad">
|
||||||
|
<DialPad onDigitPress={this.onDigitPress} hasDial={false} />
|
||||||
</div>
|
</div>
|
||||||
<Field className="mx_DialPadContextMenu_dialled"
|
|
||||||
value={this.state.value} autoFocus={true}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_DialPadContextMenu_horizSep" />
|
|
||||||
<div className="mx_DialPadContextMenu_dialPad">
|
|
||||||
<Dialpad onDigitPress={this.onDigitPress} hasDialAndDelete={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,12 +16,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import Resend from '../../../Resend';
|
import Resend from '../../../Resend';
|
||||||
|
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import ReportEventDialog from '../dialogs/ReportEventDialog';
|
||||||
|
import ViewSource from '../../structures/ViewSource';
|
||||||
|
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
|
||||||
|
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||||
|
import ShareDialog from '../dialogs/ShareDialog';
|
||||||
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus: EventStatus): boolean {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IEventTileOps {
|
||||||
|
isWidgetHidden(): boolean;
|
||||||
|
unhideWidget(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/* the MatrixEvent associated with the context menu */
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||||
|
eventTileOps?: IEventTileOps;
|
||||||
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||||
|
collapseReplyThread?(): void;
|
||||||
|
/* callback called when the menu is dismissed */
|
||||||
|
onFinished(): void;
|
||||||
|
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
||||||
|
onCloseDialog?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
canRedact: boolean;
|
||||||
|
canPin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.MessageContextMenu")
|
@replaceableComponent("views.context_menus.MessageContextMenu")
|
||||||
export default class MessageContextMenu extends React.Component {
|
export default class MessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
/* the MatrixEvent associated with the context menu */
|
|
||||||
mxEvent: PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
|
||||||
eventTileOps: PropTypes.object,
|
|
||||||
|
|
||||||
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
|
||||||
collapseReplyThread: PropTypes.func,
|
|
||||||
|
|
||||||
/* callback called when the menu is dismissed */
|
|
||||||
onFinished: PropTypes.func,
|
|
||||||
|
|
||||||
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
|
||||||
onCloseDialog: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
canRedact: false,
|
canRedact: false,
|
||||||
canPin: false,
|
canPin: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
|
||||||
this._checkPermissions();
|
this.checkPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
|
cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkPermissions = () => {
|
private checkPermissions = (): void => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
|
|
||||||
|
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.setState({ canRedact, canPin });
|
this.setState({ canRedact, canPin });
|
||||||
};
|
};
|
||||||
|
|
||||||
_isPinned() {
|
private isPinned(): boolean {
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||||
if (!pinnedEvent) return false;
|
if (!pinnedEvent) return false;
|
||||||
|
@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component {
|
||||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
onResendReactionsClick = () => {
|
private onResendReactionsClick = (): void => {
|
||||||
for (const reaction of this._getUnsentReactions()) {
|
for (const reaction of this.getUnsentReactions()) {
|
||||||
Resend.resend(reaction);
|
Resend.resend(reaction);
|
||||||
}
|
}
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onReportEventClick = () => {
|
private onReportEventClick = (): void => {
|
||||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
|
||||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
}, 'mx_Dialog_reportEvent');
|
}, 'mx_Dialog_reportEvent');
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewSourceClick = () => {
|
private onViewSourceClick = (): void => {
|
||||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
|
||||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
}, 'mx_Dialog_viewsource');
|
}, 'mx_Dialog_viewsource');
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onRedactClick = () => {
|
private onRedactClick = (): void => {
|
||||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
|
||||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||||
onFinished: async (proceed, reason) => {
|
onFinished: async (proceed: boolean, reason?: string) => {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
try {
|
try {
|
||||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
this.props.onCloseDialog?.();
|
||||||
await cli.redactEvent(
|
await cli.redactEvent(
|
||||||
this.props.mxEvent.getRoomId(),
|
this.props.mxEvent.getRoomId(),
|
||||||
this.props.mxEvent.getId(),
|
this.props.mxEvent.getId(),
|
||||||
|
@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component {
|
||||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||||
// detached queue and we show the room status bar to allow retry
|
// detached queue and we show the room status bar to allow retry
|
||||||
if (typeof code !== "undefined") {
|
if (typeof code !== "undefined") {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
// display error message stating you couldn't delete this.
|
// display error message stating you couldn't delete this.
|
||||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
|
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onForwardClick = () => {
|
private onForwardClick = (): void => {
|
||||||
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
|
@ -167,12 +174,12 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPinClick = () => {
|
private onPinClick = (): void => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
const eventId = this.props.mxEvent.getId();
|
const eventId = this.props.mxEvent.getId();
|
||||||
|
|
||||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
|
||||||
if (pinnedIds.includes(eventId)) {
|
if (pinnedIds.includes(eventId)) {
|
||||||
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||||
} else {
|
} else {
|
||||||
|
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
private closeMenu = (): void => {
|
||||||
if (this.props.onFinished) this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnhidePreviewClick = () => {
|
private onUnhidePreviewClick = (): void => {
|
||||||
if (this.props.eventTileOps) {
|
this.props.eventTileOps?.unhideWidget();
|
||||||
this.props.eventTileOps.unhideWidget();
|
|
||||||
}
|
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuoteClick = () => {
|
private onQuoteClick = (): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert,
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
|
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPermalinkClick = (e) => {
|
private onPermalinkClick = (e: React.MouseEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
|
||||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||||
target: this.props.mxEvent,
|
target: this.props.mxEvent,
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
|
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onCollapseReplyThreadClick = () => {
|
private onCollapseReplyThreadClick = (): void => {
|
||||||
this.props.collapseReplyThread();
|
this.props.collapseReplyThread();
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
_getReactions(filter) {
|
private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
const eventId = this.props.mxEvent.getId();
|
const eventId = this.props.mxEvent.getId();
|
||||||
return room.getPendingEvents().filter(e => {
|
return room.getPendingEvents().filter(e => {
|
||||||
const relation = e.getRelation();
|
const relation = e.getRelation();
|
||||||
return relation &&
|
return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
|
||||||
relation.rel_type === "m.annotation" &&
|
|
||||||
relation.event_id === eventId &&
|
|
||||||
filter(e);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPendingReactions() {
|
private getPendingReactions(): MatrixEvent[] {
|
||||||
return this._getReactions(e => canCancel(e.status));
|
return this.getReactions(e => canCancel(e.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
_getUnsentReactions() {
|
private getUnsentReactions(): MatrixEvent[] {
|
||||||
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
|
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
|
||||||
const me = cli.getUserId();
|
const me = cli.getUserId();
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const eventStatus = mxEvent.status;
|
const eventStatus = mxEvent.status;
|
||||||
const unsentReactionsCount = this._getUnsentReactions().length;
|
const unsentReactionsCount = this.getUnsentReactions().length;
|
||||||
let resendReactionsButton;
|
|
||||||
let redactButton;
|
let resendReactionsButton: JSX.Element;
|
||||||
let forwardButton;
|
let redactButton: JSX.Element;
|
||||||
let pinButton;
|
let forwardButton: JSX.Element;
|
||||||
let unhidePreviewButton;
|
let pinButton: JSX.Element;
|
||||||
let externalURLButton;
|
let unhidePreviewButton: JSX.Element;
|
||||||
let quoteButton;
|
let externalURLButton: JSX.Element;
|
||||||
let collapseReplyThread;
|
let quoteButton: JSX.Element;
|
||||||
let redactItemList;
|
let collapseReplyThread: JSX.Element;
|
||||||
|
let redactItemList: JSX.Element;
|
||||||
|
|
||||||
// status is SENT before remote-echo, null after
|
// status is SENT before remote-echo, null after
|
||||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||||
|
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
pinButton = (
|
pinButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconPin"
|
iconClassName="mx_MessageContextMenu_iconPin"
|
||||||
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
|
label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
|
||||||
onClick={this.onPinClick}
|
onClick={this.onPinClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -333,9 +335,14 @@ export default class MessageContextMenu extends React.Component {
|
||||||
onClick={this.onPermalinkClick}
|
onClick={this.onPermalinkClick}
|
||||||
label= {_t('Share')}
|
label= {_t('Share')}
|
||||||
element="a"
|
element="a"
|
||||||
href={permalink}
|
{
|
||||||
target="_blank"
|
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||||
rel="noreferrer noopener"
|
...{
|
||||||
|
href: permalink,
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noreferrer noopener",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -350,8 +357,8 @@ export default class MessageContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridges can provide a 'external_url' to link back to the source.
|
// Bridges can provide a 'external_url' to link back to the source.
|
||||||
if (typeof (mxEvent.event.content.external_url) === "string" &&
|
if (typeof (mxEvent.getContent().external_url) === "string" &&
|
||||||
isUrlPermitted(mxEvent.event.content.external_url)
|
isUrlPermitted(mxEvent.getContent().external_url)
|
||||||
) {
|
) {
|
||||||
externalURLButton = (
|
externalURLButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
|
@ -359,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
|
||||||
onClick={this.closeMenu}
|
onClick={this.closeMenu}
|
||||||
label={ _t('Source URL') }
|
label={ _t('Source URL') }
|
||||||
element="a"
|
element="a"
|
||||||
target="_blank"
|
{
|
||||||
rel="noreferrer noopener"
|
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||||
href={mxEvent.event.content.external_url}
|
...{
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noreferrer noopener",
|
||||||
|
href: mxEvent.getContent().external_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -376,7 +388,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reportEventButton;
|
let reportEventButton: JSX.Element;
|
||||||
if (mxEvent.getSender() !== me) {
|
if (mxEvent.getSender() !== me) {
|
||||||
reportEventButton = (
|
reportEventButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
|
@ -46,7 +46,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
||||||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{_t(
|
||||||
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
|
"Your %(brand)s doesn't allow you to use an integration manager to do this. " +
|
||||||
"Please contact an admin.",
|
"Please contact an admin.",
|
||||||
{ brand },
|
{ brand },
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -32,7 +32,6 @@ import Modal from "../../../Modal";
|
||||||
import { humanizeTime } from "../../../utils/humanize";
|
import { humanizeTime } from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers,
|
canEncryptToAllUsers,
|
||||||
ensureDMExists,
|
|
||||||
findDMForUser,
|
findDMForUser,
|
||||||
privateShouldBeEncrypted,
|
privateShouldBeEncrypted,
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
|
@ -64,9 +63,14 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
|
||||||
import * as ContextMenu from "../../structures/ContextMenu";
|
import * as ContextMenu from "../../structures/ContextMenu";
|
||||||
import { toRightOf } from "../../structures/ContextMenu";
|
import { toRightOf } from "../../structures/ContextMenu";
|
||||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||||
|
import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
|
||||||
|
import Field from '../elements/Field';
|
||||||
|
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
|
||||||
|
import Dialpad from '../voip/DialPad';
|
||||||
import QuestionDialog from "./QuestionDialog";
|
import QuestionDialog from "./QuestionDialog";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -79,11 +83,19 @@ interface IRecentUser {
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
|
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
|
||||||
|
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
|
||||||
|
// be passed when creating the modal
|
||||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||||
|
|
||||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||||
|
|
||||||
|
enum TabId {
|
||||||
|
UserDirectory = 'users',
|
||||||
|
DialPad = 'dialpad',
|
||||||
|
}
|
||||||
|
|
||||||
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||||
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
// for 3PIDs/email addresses.
|
// for 3PIDs/email addresses.
|
||||||
|
@ -109,11 +121,11 @@ export abstract class Member {
|
||||||
|
|
||||||
class DirectoryMember extends Member {
|
class DirectoryMember extends Member {
|
||||||
private readonly _userId: string;
|
private readonly _userId: string;
|
||||||
private readonly displayName: string;
|
private readonly displayName?: string;
|
||||||
private readonly avatarUrl: string;
|
private readonly avatarUrl?: string;
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
|
||||||
super();
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this.displayName = userDirResult.display_name;
|
this.displayName = userDirResult.display_name;
|
||||||
|
@ -356,6 +368,8 @@ interface IInviteDialogState {
|
||||||
canUseIdentityServer: boolean;
|
canUseIdentityServer: boolean;
|
||||||
tryingIdentityServer: boolean;
|
tryingIdentityServer: boolean;
|
||||||
consultFirst: boolean;
|
consultFirst: boolean;
|
||||||
|
dialPadValue: string;
|
||||||
|
currentTabId: TabId;
|
||||||
|
|
||||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
@ -370,7 +384,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
};
|
};
|
||||||
|
|
||||||
private closeCopiedTooltip: () => void;
|
private closeCopiedTooltip: () => void;
|
||||||
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
private debounceTimer: number = null; // actually number because we're in the browser
|
||||||
private editorRef = createRef<HTMLInputElement>();
|
private editorRef = createRef<HTMLInputElement>();
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
|
|
||||||
|
@ -407,6 +421,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||||
tryingIdentityServer: false,
|
tryingIdentityServer: false,
|
||||||
consultFirst: false,
|
consultFirst: false,
|
||||||
|
dialPadValue: '',
|
||||||
|
currentTabId: TabId.UserDirectory,
|
||||||
|
|
||||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -768,44 +784,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
};
|
};
|
||||||
|
|
||||||
private transferCall = async () => {
|
private transferCall = async () => {
|
||||||
this.convertFilter();
|
if (this.state.currentTabId == TabId.UserDirectory) {
|
||||||
const targets = this.convertFilter();
|
this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targets = this.convertFilter();
|
||||||
if (targetIds.length > 1) {
|
const targetIds = targets.map(t => t.userId);
|
||||||
this.setState({
|
if (targetIds.length > 1) {
|
||||||
errorText: _t("A call can only be transferred to a single user."),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.consultFirst) {
|
|
||||||
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'place_call',
|
|
||||||
type: this.props.call.type,
|
|
||||||
room_id: dmRoomId,
|
|
||||||
transferee: this.props.call,
|
|
||||||
});
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: dmRoomId,
|
|
||||||
should_peek: false,
|
|
||||||
joining: false,
|
|
||||||
});
|
|
||||||
this.props.onFinished();
|
|
||||||
} else {
|
|
||||||
this.setState({ busy: true });
|
|
||||||
try {
|
|
||||||
await this.props.call.transfer(targetIds[0]);
|
|
||||||
this.setState({ busy: false });
|
|
||||||
this.props.onFinished();
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
errorText: _t("A call can only be transferred to a single user."),
|
||||||
errorText: _t("Failed to transfer call"),
|
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.TransferCallToMatrixID,
|
||||||
|
call: this.props.call,
|
||||||
|
destination: targetIds[0],
|
||||||
|
consultFirst: this.state.consultFirst,
|
||||||
|
} as TransferCallPayload);
|
||||||
|
} else {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.TransferCallToPhoneNumber,
|
||||||
|
call: this.props.call,
|
||||||
|
destination: this.state.dialPadValue,
|
||||||
|
consultFirst: this.state.consultFirst,
|
||||||
|
} as TransferCallPayload);
|
||||||
}
|
}
|
||||||
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onKeyDown = (e) => {
|
private onKeyDown = (e) => {
|
||||||
|
@ -827,6 +831,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onCancel = () => {
|
||||||
|
this.props.onFinished([]);
|
||||||
|
};
|
||||||
|
|
||||||
private updateSuggestions = async (term) => {
|
private updateSuggestions = async (term) => {
|
||||||
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
|
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
|
||||||
if (term !== this.state.filterText) {
|
if (term !== this.state.filterText) {
|
||||||
|
@ -962,11 +970,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
private toggleMember = (member: Member) => {
|
private toggleMember = (member: Member) => {
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
let filterText = this.state.filterText;
|
let filterText = this.state.filterText;
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
let targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
const idx = targets.indexOf(member);
|
const idx = targets.indexOf(member);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
targets.splice(idx, 1);
|
targets.splice(idx, 1);
|
||||||
} else {
|
} else {
|
||||||
|
if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) {
|
||||||
|
targets = [];
|
||||||
|
}
|
||||||
targets.push(member);
|
targets.push(member);
|
||||||
filterText = ""; // clear the filter when the user accepts a suggestion
|
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||||
}
|
}
|
||||||
|
@ -1189,6 +1200,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderEditor() {
|
private renderEditor() {
|
||||||
|
const hasPlaceholder = (
|
||||||
|
this.props.kind == KIND_CALL_TRANSFER &&
|
||||||
|
this.state.targets.length === 0 &&
|
||||||
|
this.state.filterText.length === 0
|
||||||
|
);
|
||||||
const targets = this.state.targets.map(t => (
|
const targets = this.state.targets.map(t => (
|
||||||
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||||
));
|
));
|
||||||
|
@ -1201,8 +1217,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
ref={this.editorRef}
|
ref={this.editorRef}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
placeholder={hasPlaceholder ? _t("Search") : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -1249,6 +1266,28 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onDialFormSubmit = ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.transferCall();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDialChange = ev => {
|
||||||
|
this.setState({ dialPadValue: ev.currentTarget.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDigitPress = digit => {
|
||||||
|
this.setState({ dialPadValue: this.state.dialPadValue + digit });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDeletePress = () => {
|
||||||
|
if (this.state.dialPadValue.length === 0) return;
|
||||||
|
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onTabChange = (tabId: TabId) => {
|
||||||
|
this.setState({ currentTabId: tabId });
|
||||||
|
};
|
||||||
|
|
||||||
private async onLinkClick(e) {
|
private async onLinkClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectText(e.target);
|
selectText(e.target);
|
||||||
|
@ -1278,12 +1317,16 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
let helpText;
|
let helpText;
|
||||||
let buttonText;
|
let buttonText;
|
||||||
let goButtonFn;
|
let goButtonFn;
|
||||||
|
let consultConnectSection;
|
||||||
let extraSection;
|
let extraSection;
|
||||||
let footer;
|
let footer;
|
||||||
let keySharingWarning = <span />;
|
let keySharingWarning = <span />;
|
||||||
|
|
||||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||||
|
|
||||||
|
const hasSelection = this.state.targets.length > 0
|
||||||
|
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const userId = cli.getUserId();
|
const userId = cli.getUserId();
|
||||||
if (this.props.kind === KIND_DM) {
|
if (this.props.kind === KIND_DM) {
|
||||||
|
@ -1421,23 +1464,116 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||||
title = _t("Transfer");
|
title = _t("Transfer");
|
||||||
buttonText = _t("Transfer");
|
|
||||||
goButtonFn = this.transferCall;
|
consultConnectSection = <div className="mx_InviteDialog_transferConsultConnect">
|
||||||
footer = <div>
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||||
{_t("Consult first")}
|
{_t("Consult first")}
|
||||||
</label>
|
</label>
|
||||||
|
<AccessibleButton
|
||||||
|
kind="secondary"
|
||||||
|
onClick={this.onCancel}
|
||||||
|
className='mx_InviteDialog_transferConsultConnect_pushRight'
|
||||||
|
>
|
||||||
|
{_t("Cancel")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary"
|
||||||
|
onClick={this.transferCall}
|
||||||
|
className='mx_InviteDialog_transferButton'
|
||||||
|
disabled={!hasSelection && this.state.dialPadValue === ''}
|
||||||
|
>
|
||||||
|
{_t("Transfer")}
|
||||||
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSelection = this.state.targets.length > 0
|
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton
|
||||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
kind="primary"
|
||||||
|
onClick={goButtonFn}
|
||||||
|
className='mx_InviteDialog_goButton'
|
||||||
|
disabled={this.state.busy || !hasSelection}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</AccessibleButton>;
|
||||||
|
|
||||||
|
const usersSection = <React.Fragment>
|
||||||
|
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||||
|
<div className='mx_InviteDialog_addressBar'>
|
||||||
|
{this.renderEditor()}
|
||||||
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||||
|
{goButton}
|
||||||
|
{spinner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{keySharingWarning}
|
||||||
|
{this.renderIdentityServerWarning()}
|
||||||
|
<div className='error'>{this.state.errorText}</div>
|
||||||
|
<div className='mx_InviteDialog_userSections'>
|
||||||
|
{this.renderSection('recents')}
|
||||||
|
{this.renderSection('suggestions')}
|
||||||
|
{extraSection}
|
||||||
|
</div>
|
||||||
|
{footer}
|
||||||
|
</React.Fragment>;
|
||||||
|
|
||||||
|
let dialogContent;
|
||||||
|
if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||||
|
const tabs = [];
|
||||||
|
tabs.push(new Tab(
|
||||||
|
TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection,
|
||||||
|
));
|
||||||
|
|
||||||
|
const backspaceButton = (
|
||||||
|
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only show the backspace button if the field has content
|
||||||
|
let dialPadField;
|
||||||
|
if (this.state.dialPadValue.length !== 0) {
|
||||||
|
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
|
||||||
|
value={this.state.dialPadValue}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this.onDialChange}
|
||||||
|
postfixComponent={backspaceButton}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
|
||||||
|
value={this.state.dialPadValue}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this.onDialChange}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialPadSection = <div className="mx_InviteDialog_dialPad">
|
||||||
|
<form onSubmit={this.onDialFormSubmit}>
|
||||||
|
{dialPadField}
|
||||||
|
</form>
|
||||||
|
<Dialpad hasDial={false}
|
||||||
|
onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
|
||||||
|
dialogContent = <React.Fragment>
|
||||||
|
<TabbedView tabs={tabs} initialTabId={this.state.currentTabId}
|
||||||
|
tabLocation={TabLocation.TOP} onChange={this.onTabChange}
|
||||||
|
/>
|
||||||
|
{consultConnectSection}
|
||||||
|
</React.Fragment>;
|
||||||
|
} else {
|
||||||
|
dialogContent = <React.Fragment>
|
||||||
|
{usersSection}
|
||||||
|
{consultConnectSection}
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className={classNames("mx_InviteDialog", {
|
className={classNames({
|
||||||
|
mx_InviteDialog_transfer: this.props.kind === KIND_CALL_TRANSFER,
|
||||||
|
mx_InviteDialog_other: this.props.kind !== KIND_CALL_TRANSFER,
|
||||||
mx_InviteDialog_hasFooter: !!footer,
|
mx_InviteDialog_hasFooter: !!footer,
|
||||||
})}
|
})}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
|
@ -1445,30 +1581,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className='mx_InviteDialog_content'>
|
<div className='mx_InviteDialog_content'>
|
||||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
{dialogContent}
|
||||||
<div className='mx_InviteDialog_addressBar'>
|
|
||||||
{this.renderEditor()}
|
|
||||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
onClick={goButtonFn}
|
|
||||||
className='mx_InviteDialog_goButton'
|
|
||||||
disabled={this.state.busy || !hasSelection}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</AccessibleButton>
|
|
||||||
{spinner}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{keySharingWarning}
|
|
||||||
{this.renderIdentityServerWarning()}
|
|
||||||
<div className='error'>{this.state.errorText}</div>
|
|
||||||
<div className='mx_InviteDialog_userSections'>
|
|
||||||
{this.renderSection('recents')}
|
|
||||||
{this.renderSection('suggestions')}
|
|
||||||
{extraSection}
|
|
||||||
</div>
|
|
||||||
{footer}
|
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu.js";
|
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||||
|
|
||||||
const socials = [
|
const socials = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -90,9 +90,9 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
||||||
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case SERVICE_TYPES.IS:
|
case SERVICE_TYPES.IS:
|
||||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
return <div>{_t("Identity server")}<br />({host})</div>;
|
||||||
case SERVICE_TYPES.IM:
|
case SERVICE_TYPES.IM:
|
||||||
return <div>{_t("Integration Manager")}<br />({host})</div>;
|
return <div>{_t("Integration manager")}<br />({host})</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import EncryptionPanel from "../right_panel/EncryptionPanel";
|
import EncryptionPanel from "../right_panel/EncryptionPanel";
|
||||||
import { User } from 'matrix-js-sdk';
|
import { User } from 'matrix-js-sdk/src/models/user';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
verificationRequest: VerificationRequest;
|
verificationRequest: VerificationRequest;
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
import { IProtocol } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
||||||
|
@ -41,7 +42,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
import { compare } from "../../../utils/strings";
|
import { compare } from "../../../utils/strings";
|
||||||
|
|
||||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage
|
||||||
|
export const ALL_ROOMS = "ALL_ROOMS";
|
||||||
|
|
||||||
const SETTING_NAME = "room_directory_servers";
|
const SETTING_NAME = "room_directory_servers";
|
||||||
|
|
||||||
|
@ -82,38 +84,13 @@ const validServer = withValidation<undefined, { error?: MatrixError }>({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
export interface IFieldType {
|
|
||||||
regexp: string;
|
|
||||||
placeholder: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInstance {
|
|
||||||
desc: string;
|
|
||||||
icon?: string;
|
|
||||||
fields: object;
|
|
||||||
network_id: string;
|
|
||||||
// XXX: this is undocumented but we rely on it.
|
|
||||||
// we inject a fake entry with a symbolic instance_id.
|
|
||||||
instance_id: string | symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IProtocol {
|
|
||||||
user_fields: string[];
|
|
||||||
location_fields: string[];
|
|
||||||
icon: string;
|
|
||||||
field_types: Record<string, IFieldType>;
|
|
||||||
instances: IInstance[];
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
export type Protocols = Record<string, IProtocol>;
|
export type Protocols = Record<string, IProtocol>;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
protocols: Protocols;
|
protocols: Protocols;
|
||||||
selectedServerName: string;
|
selectedServerName: string;
|
||||||
selectedInstanceId: string | symbol;
|
selectedInstanceId: string;
|
||||||
onOptionChange(server: string, instanceId?: string | symbol): void;
|
onOptionChange(server: string, instanceId?: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This dropdown sources homeservers from three places:
|
// This dropdown sources homeservers from three places:
|
||||||
|
@ -171,7 +148,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
|
|
||||||
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
||||||
if (protocolsList.length > 0) {
|
if (protocolsList.length > 0) {
|
||||||
// add a fake protocol with the ALL_ROOMS symbol
|
// add a fake protocol with ALL_ROOMS
|
||||||
protocolsList.push({
|
protocolsList.push({
|
||||||
instances: [{
|
instances: [{
|
||||||
fields: [],
|
fields: [],
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactHTML } from 'react';
|
||||||
|
|
||||||
import { Key } from '../../../Keyboard';
|
import { Key } from '../../../Keyboard';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -29,7 +29,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
|
||||||
*/
|
*/
|
||||||
interface IProps extends React.InputHTMLAttributes<Element> {
|
interface IProps extends React.InputHTMLAttributes<Element> {
|
||||||
inputRef?: React.Ref<Element>;
|
inputRef?: React.Ref<Element>;
|
||||||
element?: string;
|
element?: keyof ReactHTML;
|
||||||
// The kind of button, similar to how Bootstrap works.
|
// The kind of button, similar to how Bootstrap works.
|
||||||
// See available classes for AccessibleButton for options.
|
// See available classes for AccessibleButton for options.
|
||||||
kind?: string;
|
kind?: string;
|
||||||
|
@ -122,7 +122,7 @@ export default function AccessibleButton({
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessibleButton.defaultProps = {
|
AccessibleButton.defaultProps = {
|
||||||
element: 'div',
|
element: 'div' as keyof ReactHTML,
|
||||||
role: 'button',
|
role: 'button',
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class AppPermission extends React.Component {
|
||||||
|
|
||||||
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
|
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
|
||||||
const warning = this.state.isWrapped
|
const warning = this.state.isWrapped
|
||||||
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
|
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.",
|
||||||
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip })
|
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip })
|
||||||
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
|
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
|
||||||
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip });
|
{ widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip });
|
||||||
|
|
|
@ -238,6 +238,7 @@ export default class AppTile extends React.Component {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||||
|
dis.dispatch({ action: 'stickerpicker_close' });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Ignoring sticker message. Invalid capability');
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
}
|
}
|
||||||
|
|
31
src/components/views/elements/DialPadBackspaceButton.tsx
Normal file
31
src/components/views/elements/DialPadBackspaceButton.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// Callback for when the button is pressed
|
||||||
|
onBackspacePress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
|
||||||
|
render() {
|
||||||
|
return <div className="mx_DialPadBackspaceButtonWrapper">
|
||||||
|
<AccessibleButton className="mx_DialPadBackspaceButton" onClick={this.props.onBackspacePress} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -452,6 +452,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
<div className="mx_ImageView_panel">
|
<div className="mx_ImageView_panel">
|
||||||
{ info }
|
{ info }
|
||||||
<div className="mx_ImageView_toolbar">
|
<div className="mx_ImageView_toolbar">
|
||||||
|
{ zoomOutButton }
|
||||||
|
{ zoomInButton }
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||||
title={_t("Rotate Left")}
|
title={_t("Rotate Left")}
|
||||||
|
@ -462,8 +464,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
title={_t("Rotate Right")}
|
title={_t("Rotate Right")}
|
||||||
onClick={this.onRotateClockwiseClick}>
|
onClick={this.onRotateClockwiseClick}>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
{ zoomOutButton }
|
|
||||||
{ zoomInButton }
|
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_download"
|
className="mx_ImageView_button mx_ImageView_button_download"
|
||||||
title={_t("Download")}
|
title={_t("Download")}
|
||||||
|
|
|
@ -32,7 +32,7 @@ interface IProps {
|
||||||
hasAvatar: boolean;
|
hasAvatar: boolean;
|
||||||
noAvatarLabel?: string;
|
noAvatarLabel?: string;
|
||||||
hasAvatarLabel?: string;
|
hasAvatarLabel?: string;
|
||||||
setAvatarUrl(url: string): Promise<any>;
|
setAvatarUrl(url: string): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -32,6 +31,7 @@ import sanitizeHtml from "sanitize-html";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils";
|
import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { TileShape } from "../rooms/EventTile";
|
||||||
|
|
||||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||||
|
@ -334,7 +334,7 @@ export default class ReplyThread extends React.Component {
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
|
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -384,7 +384,7 @@ export default class ReplyThread extends React.Component {
|
||||||
{ dateSep }
|
{ dateSep }
|
||||||
<EventTile
|
<EventTile
|
||||||
mxEvent={ev}
|
mxEvent={ev}
|
||||||
tileShape="reply"
|
tileShape={TileShape.Reply}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
isRedacted={ev.isRedacted()}
|
isRedacted={ev.isRedacted()}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import EmojiPicker from "./EmojiPicker";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -93,6 +94,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
||||||
this.props.mxEvent.getRoomId(),
|
this.props.mxEvent.getRoomId(),
|
||||||
myReactions[reaction],
|
myReactions[reaction],
|
||||||
);
|
);
|
||||||
|
dis.dispatch({ action: Action.FocusAComposer });
|
||||||
// Tell the emoji picker not to bump this in the more frequently used list.
|
// Tell the emoji picker not to bump this in the more frequently used list.
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,6 +106,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
dis.dispatch({ action: "message_sent" });
|
dis.dispatch({ action: "message_sent" });
|
||||||
|
dis.dispatch({ action: Action.FocusAComposer });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -24,6 +24,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import { TileShape } from "../rooms/EventTile";
|
||||||
|
|
||||||
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
|
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||||
|
|
||||||
|
@ -306,7 +307,7 @@ export default class MFileBody extends React.Component {
|
||||||
// If the attachment is not encrypted then we check whether we
|
// If the attachment is not encrypted then we check whether we
|
||||||
// are being displayed in the room timeline or in a list of
|
// are being displayed in the room timeline or in a list of
|
||||||
// files in the right hand side of the screen.
|
// files in the right hand side of the screen.
|
||||||
if (this.props.tileShape === "file_grid") {
|
if (this.props.tileShape === TileShape.FileGrid) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
|
|
@ -25,9 +25,11 @@ import { mediaFromContent } from "../../../customisations/Media";
|
||||||
import { decryptFile } from "../../../utils/DecryptFile";
|
import { decryptFile } from "../../../utils/DecryptFile";
|
||||||
import RecordingPlayback from "../audio_messages/RecordingPlayback";
|
import RecordingPlayback from "../audio_messages/RecordingPlayback";
|
||||||
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
||||||
|
import { TileShape } from "../rooms/EventTile";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
tileShape?: TileShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -103,7 +105,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
||||||
// At this point we should have a playable state
|
// At this point we should have a playable state
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVoiceMessageBody">
|
<span className="mx_MVoiceMessageBody">
|
||||||
<RecordingPlayback playback={this.state.playback} />
|
<RecordingPlayback playback={this.state.playback} tileShape={this.props.tileShape} />
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default class MessageEvent extends React.Component {
|
||||||
onHeightChanged: PropTypes.func,
|
onHeightChanged: PropTypes.func,
|
||||||
|
|
||||||
/* the shape of the tile, used */
|
/* the shape of the tile, used */
|
||||||
tileShape: PropTypes.string,
|
tileShape: PropTypes.string, // TODO: Use TileShape enum
|
||||||
|
|
||||||
/* the maximum image height to use, if the event is an image */
|
/* the maximum image height to use, if the event is an image */
|
||||||
maxImageHeight: PropTypes.number,
|
maxImageHeight: PropTypes.number,
|
||||||
|
|
|
@ -244,7 +244,11 @@ export default class TextualBody extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private highlightCode(code: HTMLElement): void {
|
private highlightCode(code: HTMLElement): void {
|
||||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
// Auto-detect language only if enabled and only for codeblocks
|
||||||
|
if (
|
||||||
|
SettingsStore.getValue("enableSyntaxHighlightLanguageDetection") &&
|
||||||
|
code.parentElement instanceof HTMLPreElement
|
||||||
|
) {
|
||||||
highlight.highlightBlock(code);
|
highlight.highlightBlock(code);
|
||||||
} else {
|
} else {
|
||||||
// Only syntax highlight if there's a class starting with language-
|
// Only syntax highlight if there's a class starting with language-
|
||||||
|
|
|
@ -15,11 +15,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import DirectoryCustomisations from '../../../customisations/Directory';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -49,7 +51,7 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
||||||
|
|
||||||
client.setRoomDirectoryVisibility(
|
client.setRoomDirectoryVisibility(
|
||||||
this.props.roomId,
|
this.props.roomId,
|
||||||
newValue ? 'public' : 'private',
|
newValue ? Visibility.Public : Visibility.Private,
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
// Roll back the local echo on the change
|
// Roll back the local echo on the change
|
||||||
this.setState({ isRoomPublished: valueBefore });
|
this.setState({ isRoomPublished: valueBefore });
|
||||||
|
@ -66,10 +68,15 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
const enabled = (
|
||||||
|
DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
|
||||||
|
this.props.canSetCanonicalAlias
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||||
onChange={this.onRoomPublishChange}
|
onChange={this.onRoomPublishChange}
|
||||||
disabled={!this.props.canSetCanonicalAlias}
|
disabled={!enabled}
|
||||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||||
domain: client.getDomain(),
|
domain: client.getDomain(),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -55,7 +55,7 @@ interface IState {
|
||||||
export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
||||||
autocompleter: Autocompleter;
|
autocompleter: Autocompleter;
|
||||||
queryRequested: string;
|
queryRequested: string;
|
||||||
debounceCompletionsRequest: NodeJS.Timeout;
|
debounceCompletionsRequest: number;
|
||||||
private containerRef = createRef<HTMLDivElement>();
|
private containerRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -181,7 +181,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
} else {
|
} else {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: 'edit_event', event: null });
|
dis.dispatch({ action: 'edit_event', event: null });
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
@ -200,7 +200,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
private cancelEdit = (): void => {
|
private cancelEdit = (): void => {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: "edit_event", event: null });
|
dis.dispatch({ action: "edit_event", event: null });
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
private get shouldSaveStoredEditorState(): boolean {
|
private get shouldSaveStoredEditorState(): boolean {
|
||||||
|
@ -375,7 +375,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
|
|
||||||
// close the event editing and focus composer
|
// close the event editing and focus composer
|
||||||
dis.dispatch({ action: "edit_event", event: null });
|
dis.dispatch({ action: "edit_event", event: null });
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
private cancelPreviousPendingEdit(): void {
|
private cancelPreviousPendingEdit(): void {
|
||||||
|
@ -452,6 +452,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
} else if (payload.text) {
|
} else if (payload.text) {
|
||||||
this.editorRef.current?.insertPlaintext(payload.text);
|
this.editorRef.current?.insertPlaintext(payload.text);
|
||||||
}
|
}
|
||||||
|
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
|
||||||
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -194,6 +194,7 @@ export enum TileShape {
|
||||||
FileGrid = "file_grid",
|
FileGrid = "file_grid",
|
||||||
Reply = "reply",
|
Reply = "reply",
|
||||||
ReplyPreview = "reply_preview",
|
ReplyPreview = "reply_preview",
|
||||||
|
Pinned = "pinned",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -902,7 +903,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||||
// Note: we keep the `sending` state class for tests, not for our styles
|
// Note: we keep the `sending` state class for tests, not for our styles
|
||||||
mx_EventTile_sending: !isEditing && isSending,
|
mx_EventTile_sending: !isEditing && isSending,
|
||||||
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
|
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
|
||||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||||
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
|
@ -935,7 +936,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
let avatarSize;
|
let avatarSize;
|
||||||
let needsSenderProfile;
|
let needsSenderProfile;
|
||||||
|
|
||||||
if (this.props.tileShape === "notif") {
|
if (this.props.tileShape === TileShape.Notif) {
|
||||||
avatarSize = 24;
|
avatarSize = 24;
|
||||||
needsSenderProfile = true;
|
needsSenderProfile = true;
|
||||||
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
|
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
|
||||||
|
@ -949,7 +950,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
} else if (this.props.layout == Layout.IRC) {
|
} else if (this.props.layout == Layout.IRC) {
|
||||||
avatarSize = 14;
|
avatarSize = 14;
|
||||||
needsSenderProfile = true;
|
needsSenderProfile = true;
|
||||||
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
|
} else if (this.props.continuation && this.props.tileShape !== TileShape.FileGrid) {
|
||||||
// no avatar or sender profile for continuation messages
|
// no avatar or sender profile for continuation messages
|
||||||
avatarSize = 0;
|
avatarSize = 0;
|
||||||
needsSenderProfile = false;
|
needsSenderProfile = false;
|
||||||
|
@ -979,7 +980,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsSenderProfile) {
|
if (needsSenderProfile) {
|
||||||
if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
|
if (
|
||||||
|
!this.props.tileShape
|
||||||
|
|| this.props.tileShape === TileShape.Reply
|
||||||
|
|| this.props.tileShape === TileShape.ReplyPreview
|
||||||
|
) {
|
||||||
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
enableFlair={this.props.enableFlair}
|
enableFlair={this.props.enableFlair}
|
||||||
|
@ -1065,7 +1070,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
case 'notif': {
|
case TileShape.Notif: {
|
||||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return React.createElement(this.props.as || "li", {
|
return React.createElement(this.props.as || "li", {
|
||||||
"className": classes,
|
"className": classes,
|
||||||
|
@ -1093,11 +1098,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
tileShape={this.props.tileShape}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
case 'file_grid': {
|
case TileShape.FileGrid: {
|
||||||
return React.createElement(this.props.as || "li", {
|
return React.createElement(this.props.as || "li", {
|
||||||
"className": classes,
|
"className": classes,
|
||||||
"aria-live": ariaLive,
|
"aria-live": ariaLive,
|
||||||
|
@ -1128,10 +1134,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'reply':
|
case TileShape.Reply:
|
||||||
case 'reply_preview': {
|
case TileShape.ReplyPreview: {
|
||||||
let thread;
|
let thread;
|
||||||
if (this.props.tileShape === 'reply_preview') {
|
if (this.props.tileShape === TileShape.ReplyPreview) {
|
||||||
thread = ReplyThread.makeThread(
|
thread = ReplyThread.makeThread(
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
this.props.onHeightChanged,
|
this.props.onHeightChanged,
|
||||||
|
|
|
@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||||
import LinkPreviewWidget from "./LinkPreviewWidget";
|
import LinkPreviewWidget from "./LinkPreviewWidget";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
|
||||||
const INITIAL_NUM_PREVIEWS = 2;
|
const INITIAL_NUM_PREVIEWS = 2;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
links: string[]; // the URLs to be previewed
|
links: string[]; // the URLs to be previewed
|
||||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||||
onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked
|
onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
|
||||||
onHeightChanged?(): void; // called when the preview's contents has loaded
|
onHeightChanged(): void; // called when the preview's contents has loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
|
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
const [expanded, toggleExpanded] = useStateToggle();
|
const [expanded, toggleExpanded] = useStateToggle();
|
||||||
|
|
||||||
|
const ts = mxEvent.getTs();
|
||||||
|
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
|
||||||
|
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(link => {
|
||||||
|
return cli.getUrlPreview(link, ts).then(preview => [link, preview], error => {
|
||||||
|
console.error("Failed to get URL preview: " + error);
|
||||||
|
});
|
||||||
|
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
|
||||||
|
}, [links, ts], []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onHeightChanged();
|
onHeightChanged();
|
||||||
}, [onHeightChanged, expanded]);
|
}, [onHeightChanged, expanded, previews]);
|
||||||
|
|
||||||
const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS);
|
const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
|
||||||
|
|
||||||
let toggleButton;
|
let toggleButton: JSX.Element;
|
||||||
if (links.length > INITIAL_NUM_PREVIEWS) {
|
if (previews.length > INITIAL_NUM_PREVIEWS) {
|
||||||
toggleButton = <AccessibleButton onClick={toggleExpanded}>
|
toggleButton = <AccessibleButton onClick={toggleExpanded}>
|
||||||
{ expanded
|
{ expanded
|
||||||
? _t("Collapse")
|
? _t("Collapse")
|
||||||
: _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) }
|
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_LinkPreviewGroup">
|
return <div className="mx_LinkPreviewGroup">
|
||||||
{ shownLinks.map((link, i) => (
|
{ showPreviews.map(([link, preview], i) => (
|
||||||
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
|
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||||
{ i === 0 ? (
|
{ i === 0 ? (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_LinkPreviewGroup_hide"
|
className="mx_LinkPreviewGroup_hide"
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
import { linkifyElement } from '../../../HtmlUtils';
|
import { linkifyElement } from '../../../HtmlUtils';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import * as ImageUtils from "../../../ImageUtils";
|
import * as ImageUtils from "../../../ImageUtils";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -29,37 +28,15 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
link: string; // the URL being previewed
|
link: string;
|
||||||
|
preview: IPreviewUrlResponse;
|
||||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||||
onHeightChanged(): void; // called when the preview's contents has loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
preview?: IPreviewUrlResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||||
export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
private unmounted = false;
|
|
||||||
private readonly description = createRef<HTMLDivElement>();
|
private readonly description = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
preview: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
|
|
||||||
if (this.unmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({ preview }, this.props.onHeightChanged);
|
|
||||||
}, (error) => {
|
|
||||||
console.error("Failed to get URL preview: " + error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.description.current) {
|
if (this.description.current) {
|
||||||
linkifyElement(this.description.current);
|
linkifyElement(this.description.current);
|
||||||
|
@ -72,12 +49,8 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onImageClick = ev => {
|
private onImageClick = ev => {
|
||||||
const p = this.state.preview;
|
const p = this.props.preview;
|
||||||
if (ev.button != 0 || ev.metaKey) return;
|
if (ev.button != 0 || ev.metaKey) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
@ -99,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const p = this.state.preview;
|
const p = this.props.preview;
|
||||||
if (!p || Object.keys(p).length === 0) {
|
if (!p || Object.keys(p).length === 0) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
@ -139,8 +112,12 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||||
<div className="mx_LinkPreviewWidget">
|
<div className="mx_LinkPreviewWidget">
|
||||||
{ img }
|
{ img }
|
||||||
<div className="mx_LinkPreviewWidget_caption">
|
<div className="mx_LinkPreviewWidget_caption">
|
||||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
<div className="mx_LinkPreviewWidget_title">
|
||||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
<a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a>
|
||||||
|
{ p["og:site_name"] && <span className="mx_LinkPreviewWidget_siteName">
|
||||||
|
{ (" - " + p["og:site_name"]) }
|
||||||
|
</span> }
|
||||||
|
</div>
|
||||||
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
||||||
{ description }
|
{ description }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { TileShape } from "./EventTile";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -87,6 +88,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
||||||
className="mx_PinnedEventTile_body"
|
className="mx_PinnedEventTile_body"
|
||||||
maxImageHeight={150}
|
maxImageHeight={150}
|
||||||
onHeightChanged={() => {}} // we need to give this, apparently
|
onHeightChanged={() => {}} // we need to give this, apparently
|
||||||
|
tileShape={TileShape.Pinned}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -24,6 +24,7 @@ import PropTypes from "prop-types";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { TileShape } from "./EventTile";
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -90,7 +91,7 @@ export default class ReplyPreview extends React.Component {
|
||||||
<div className="mx_ReplyPreview_clear" />
|
<div className="mx_ReplyPreview_clear" />
|
||||||
<EventTile
|
<EventTile
|
||||||
alwaysShowTimestamps={true}
|
alwaysShowTimestamps={true}
|
||||||
tileShape="reply_preview"
|
tileShape={TileShape.ReplyPreview}
|
||||||
mxEvent={this.state.event}
|
mxEvent={this.state.event}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd.
|
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -21,9 +21,10 @@ import { linkifyElement } from '../../../HtmlUtils';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import { getDisplayAliasForAliasSet } from '../../../Rooms';
|
||||||
|
|
||||||
export function getDisplayAliasForRoom(room) {
|
export function getDisplayAliasForRoom(room) {
|
||||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const roomShape = PropTypes.shape({
|
export const roomShape = PropTypes.shape({
|
||||||
|
|
|
@ -497,7 +497,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'reply_to_event':
|
case 'reply_to_event':
|
||||||
case Action.FocusComposer:
|
case Action.FocusSendMessageComposer:
|
||||||
this.editorRef.current?.focus();
|
this.editorRef.current?.focus();
|
||||||
break;
|
break;
|
||||||
case "send_composer_insert":
|
case "send_composer_insert":
|
||||||
|
|
|
@ -224,7 +224,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getStickerpickerContent() {
|
_getStickerpickerContent() {
|
||||||
// Handle Integration Manager errors
|
// Handle integration manager errors
|
||||||
if (this.state._imError) {
|
if (this.state._imError) {
|
||||||
return this._errorStickerpickerContent();
|
return this._errorStickerpickerContent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import RecordingPlayback from "../audio_messages/RecordingPlayback";
|
||||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -95,7 +95,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||||
|
|
||||||
// https://github.com/matrix-org/matrix-doc/pull/3246
|
// https://github.com/matrix-org/matrix-doc/pull/3246
|
||||||
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
|
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
|
||||||
},
|
},
|
||||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||||
});
|
});
|
||||||
|
@ -135,7 +135,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
// change between this and recording, but at least we will have tried.
|
// change between this and recording, but at least we will have tried.
|
||||||
try {
|
try {
|
||||||
const devices = await MediaDeviceHandler.getDevices();
|
const devices = await MediaDeviceHandler.getDevices();
|
||||||
if (!devices?.['audioInput']?.length) {
|
if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
|
||||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||||
title: _t("No microphone found"),
|
title: _t("No microphone found"),
|
||||||
description: <>
|
description: <>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const REACHABILITY_TIMEOUT = 10000; // ms
|
||||||
async function checkIdentityServerUrl(u) {
|
async function checkIdentityServerUrl(u) {
|
||||||
const parsedUrl = url.parse(u);
|
const parsedUrl = url.parse(u);
|
||||||
|
|
||||||
if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS");
|
if (parsedUrl.protocol !== 'https:') return _t("Identity server URL must be HTTPS");
|
||||||
|
|
||||||
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
|
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
|
||||||
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
|
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
|
||||||
|
@ -53,17 +53,17 @@ async function checkIdentityServerUrl(u) {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return null;
|
return null;
|
||||||
} else if (response.status < 200 || response.status >= 300) {
|
} else if (response.status < 200 || response.status >= 300) {
|
||||||
return _t("Not a valid Identity Server (status code %(code)s)", { code: response.status });
|
return _t("Not a valid identity server (status code %(code)s)", { code: response.status });
|
||||||
} else {
|
} else {
|
||||||
return _t("Could not connect to Identity Server");
|
return _t("Could not connect to identity server");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return _t("Could not connect to Identity Server");
|
return _t("Could not connect to identity server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Whether or not the ID server is missing terms. This affects the text
|
// Whether or not the identity server is missing terms. This affects the text
|
||||||
// shown to the user.
|
// shown to the user.
|
||||||
missingTerms: boolean;
|
missingTerms: boolean;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let defaultIdServer = '';
|
let defaultIdServer = '';
|
||||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
|
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
|
||||||
// If no ID server is configured but there's one in the config, prepopulate
|
// If no identity server is configured but there's one in the config, prepopulate
|
||||||
// the field to help the user.
|
// the field to help the user.
|
||||||
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
|
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
// We react to changes in the ID server in the event the user is staring at this form
|
// We react to changes in the identity server in the event the user is staring at this form
|
||||||
// when changing their identity server on another device.
|
// when changing their identity server on another device.
|
||||||
if (payload.action !== "id_server_changed") return;
|
if (payload.action !== "id_server_changed") return;
|
||||||
|
|
||||||
|
@ -356,7 +356,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
||||||
let sectionTitle;
|
let sectionTitle;
|
||||||
let bodyText;
|
let bodyText;
|
||||||
if (idServerUrl) {
|
if (idServerUrl) {
|
||||||
sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
|
sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
|
||||||
bodyText = _t(
|
bodyText = _t(
|
||||||
"You are currently using <server></server> to discover and be discoverable by " +
|
"You are currently using <server></server> to discover and be discoverable by " +
|
||||||
"existing contacts you know. You can change your identity server below.",
|
"existing contacts you know. You can change your identity server below.",
|
||||||
|
@ -371,7 +371,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sectionTitle = _t("Identity Server");
|
sectionTitle = _t("Identity server");
|
||||||
bodyText = _t(
|
bodyText = _t(
|
||||||
"You are not currently using an identity server. " +
|
"You are not currently using an identity server. " +
|
||||||
"To discover and be discoverable by existing contacts you know, " +
|
"To discover and be discoverable by existing contacts you know, " +
|
||||||
|
|
|
@ -65,13 +65,13 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
|
||||||
if (currentManager) {
|
if (currentManager) {
|
||||||
managerName = `(${currentManager.name})`;
|
managerName = `(${currentManager.name})`;
|
||||||
bodyText = _t(
|
bodyText = _t(
|
||||||
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
|
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
|
||||||
"and sticker packs.",
|
"and sticker packs.",
|
||||||
{ serverName: currentManager.name },
|
{ serverName: currentManager.name },
|
||||||
{ b: sub => <b>{sub}</b> },
|
{ b: sub => <b>{sub}</b> },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
|
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -86,7 +86,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
{_t(
|
{_t(
|
||||||
"Integration Managers receive configuration data, and can modify widgets, " +
|
"Integration managers receive configuration data, and can modify widgets, " +
|
||||||
"send room invites, and set power levels on your behalf.",
|
"send room invites, and set power levels on your behalf.",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -280,6 +280,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
const mutedUsers = [];
|
const mutedUsers = [];
|
||||||
|
|
||||||
Object.keys(userLevels).forEach((user) => {
|
Object.keys(userLevels).forEach((user) => {
|
||||||
|
if (!Number.isInteger(userLevels[user])) { return; }
|
||||||
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
|
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
|
||||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||||
privilegedUsers.push(
|
privilegedUsers.push(
|
||||||
|
|
|
@ -441,6 +441,29 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
const state = client.getRoom(this.props.roomId).currentState;
|
const state = client.getRoom(this.props.roomId).currentState;
|
||||||
const canChangeHistory = state.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
|
const canChangeHistory = state.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: HistoryVisibility.Shared,
|
||||||
|
label: _t('Members only (since the point in time of selecting this option)'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: HistoryVisibility.Invited,
|
||||||
|
label: _t('Members only (since they were invited)'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: HistoryVisibility.Joined,
|
||||||
|
label: _t('Members only (since they joined)'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// World readable doesn't make sense for encrypted rooms
|
||||||
|
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
|
||||||
|
options.unshift({
|
||||||
|
value: HistoryVisibility.WorldReadable,
|
||||||
|
label: _t("Anyone"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -451,28 +474,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
name="historyVis"
|
name="historyVis"
|
||||||
value={history}
|
value={history}
|
||||||
onChange={this.onHistoryRadioToggle}
|
onChange={this.onHistoryRadioToggle}
|
||||||
definitions={[
|
disabled={!canChangeHistory}
|
||||||
{
|
definitions={options}
|
||||||
value: HistoryVisibility.WorldReadable,
|
|
||||||
disabled: !canChangeHistory,
|
|
||||||
label: _t("Anyone"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: HistoryVisibility.Shared,
|
|
||||||
disabled: !canChangeHistory,
|
|
||||||
label: _t('Members only (since the point in time of selecting this option)'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: HistoryVisibility.Invited,
|
|
||||||
disabled: !canChangeHistory,
|
|
||||||
label: _t('Members only (since they were invited)'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: HistoryVisibility.Joined,
|
|
||||||
disabled: !canChangeHistory,
|
|
||||||
label: _t('Members only (since they joined)'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -75,7 +75,7 @@ interface IState extends IThemeState {
|
||||||
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
|
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
|
private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
|
||||||
|
|
||||||
private themeTimer: NodeJS.Timeout;
|
private themeTimer: number;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -364,7 +364,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
onFinished={this.state.requiredPolicyInfo.resolve}
|
onFinished={this.state.requiredPolicyInfo.resolve}
|
||||||
introElement={intro}
|
introElement={intro}
|
||||||
/>
|
/>
|
||||||
{ /* has its own heading as it includes the current ID server */ }
|
{ /* has its own heading as it includes the current identity server */ }
|
||||||
<SetIdServer missingTerms={true} />
|
<SetIdServer missingTerms={true} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -387,7 +387,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
{threepidSection}
|
{threepidSection}
|
||||||
{ /* has its own heading as it includes the current ID server */ }
|
{ /* has its own heading as it includes the current identity server */ }
|
||||||
<SetIdServer />
|
<SetIdServer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -290,7 +290,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
|
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
|
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
|
||||||
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
|
{_t("Identity server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
|
||||||
<br />
|
<br />
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Access Token")}</summary><br />
|
<summary>{_t("Access Token")}</summary><br />
|
||||||
|
|
|
@ -18,41 +18,58 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
|
||||||
import Field from "../../../elements/Field";
|
import Field from "../../../elements/Field";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../../../index";
|
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
|
import ErrorDialog from '../../../dialogs/ErrorDialog';
|
||||||
|
|
||||||
|
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
|
||||||
|
// Note we're looking for a device with deviceId 'default' but adding a device
|
||||||
|
// with deviceId == the empty string: this is because Chrome gives us a device
|
||||||
|
// with deviceId 'default', so we're looking for this, not the one we are adding.
|
||||||
|
if (!devices.some((i) => i.deviceId === 'default')) {
|
||||||
|
devices.unshift({ deviceId: '', label: _t('Default Device') });
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IState extends Record<MediaDeviceKindEnum, string> {
|
||||||
|
mediaDevices: IMediaDevices;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
|
||||||
export default class VoiceUserSettingsTab extends React.Component {
|
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
||||||
constructor() {
|
constructor(props: {}) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mediaDevices: false,
|
mediaDevices: null,
|
||||||
activeAudioOutput: null,
|
[MediaDeviceKindEnum.AudioOutput]: null,
|
||||||
activeAudioInput: null,
|
[MediaDeviceKindEnum.AudioInput]: null,
|
||||||
activeVideoInput: null,
|
[MediaDeviceKindEnum.VideoInput]: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
||||||
if (canSeeDeviceLabels) {
|
if (canSeeDeviceLabels) {
|
||||||
this._refreshMediaDevices();
|
this.refreshMediaDevices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshMediaDevices = async (stream) => {
|
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mediaDevices: await MediaDeviceHandler.getDevices(),
|
mediaDevices: await MediaDeviceHandler.getDevices(),
|
||||||
activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
|
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
|
||||||
activeAudioInput: MediaDeviceHandler.getAudioInput(),
|
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
|
||||||
activeVideoInput: MediaDeviceHandler.getVideoInput(),
|
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
|
||||||
});
|
});
|
||||||
if (stream) {
|
if (stream) {
|
||||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||||
|
@ -62,7 +79,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_requestMediaPermissions = async () => {
|
private requestMediaPermissions = async (): Promise<void> => {
|
||||||
let constraints;
|
let constraints;
|
||||||
let stream;
|
let stream;
|
||||||
let error;
|
let error;
|
||||||
|
@ -86,7 +103,6 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("Failed to list userMedia devices", error);
|
console.log("Failed to list userMedia devices", error);
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
|
||||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||||
title: _t('No media permissions'),
|
title: _t('No media permissions'),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
@ -95,137 +111,93 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._refreshMediaDevices(stream);
|
this.refreshMediaDevices(stream);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_setAudioOutput = (e) => {
|
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
|
||||||
MediaDeviceHandler.instance.setAudioOutput(e.target.value);
|
MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
||||||
this.setState({
|
this.setState<null>({ [kind]: deviceId });
|
||||||
activeAudioOutput: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_setAudioInput = (e) => {
|
private changeWebRtcMethod = (p2p: boolean): void => {
|
||||||
MediaDeviceHandler.instance.setAudioInput(e.target.value);
|
|
||||||
this.setState({
|
|
||||||
activeAudioInput: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_setVideoInput = (e) => {
|
|
||||||
MediaDeviceHandler.instance.setVideoInput(e.target.value);
|
|
||||||
this.setState({
|
|
||||||
activeVideoInput: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_changeWebRtcMethod = (p2p) => {
|
|
||||||
MatrixClientPeg.get().setForceTURN(!p2p);
|
MatrixClientPeg.get().setForceTURN(!p2p);
|
||||||
};
|
};
|
||||||
|
|
||||||
_changeFallbackICEServerAllowed = (allow) => {
|
private changeFallbackICEServerAllowed = (allow: boolean): void => {
|
||||||
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
|
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderDeviceOptions(devices, category) {
|
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
|
||||||
return devices.map((d) => {
|
return devices.map((d) => {
|
||||||
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
|
||||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
const devices = this.state.mediaDevices[kind].slice(0);
|
||||||
|
if (devices.length === 0) return null;
|
||||||
|
|
||||||
|
const defaultDevice = getDefaultDevice(devices);
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
element="select"
|
||||||
|
label={label}
|
||||||
|
value={this.state[kind] || defaultDevice}
|
||||||
|
onChange={(e) => this.setDevice(e.target.value, kind)}
|
||||||
|
>
|
||||||
|
{ this.renderDeviceOptions(devices, kind) }
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
let requestButton = null;
|
let requestButton = null;
|
||||||
let speakerDropdown = null;
|
let speakerDropdown = null;
|
||||||
let microphoneDropdown = null;
|
let microphoneDropdown = null;
|
||||||
let webcamDropdown = null;
|
let webcamDropdown = null;
|
||||||
if (this.state.mediaDevices === false) {
|
if (!this.state.mediaDevices) {
|
||||||
requestButton = (
|
requestButton = (
|
||||||
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
|
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
|
||||||
<p>{_t("Missing media permissions, click the button below to request.")}</p>
|
<p>{_t("Missing media permissions, click the button below to request.")}</p>
|
||||||
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
|
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
|
||||||
{_t("Request media permissions")}
|
{_t("Request media permissions")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.mediaDevices) {
|
} else if (this.state.mediaDevices) {
|
||||||
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
|
speakerDropdown = (
|
||||||
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
|
||||||
webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
<p>{ _t('No Audio Outputs detected') }</p>
|
||||||
|
);
|
||||||
const defaultOption = {
|
microphoneDropdown = (
|
||||||
deviceId: '',
|
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
|
||||||
label: _t('Default Device'),
|
<p>{ _t('No Microphones detected') }</p>
|
||||||
};
|
);
|
||||||
const getDefaultDevice = (devices) => {
|
webcamDropdown = (
|
||||||
// Note we're looking for a device with deviceId 'default' but adding a device
|
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
|
||||||
// with deviceId == the empty string: this is because Chrome gives us a device
|
<p>{ _t('No Webcams detected') }</p>
|
||||||
// with deviceId 'default', so we're looking for this, not the one we are adding.
|
);
|
||||||
if (!devices.some((i) => i.deviceId === 'default')) {
|
|
||||||
devices.unshift(defaultOption);
|
|
||||||
return '';
|
|
||||||
} else {
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
|
|
||||||
if (audioOutputs.length > 0) {
|
|
||||||
const defaultDevice = getDefaultDevice(audioOutputs);
|
|
||||||
speakerDropdown = (
|
|
||||||
<Field element="select" label={_t("Audio Output")}
|
|
||||||
value={this.state.activeAudioOutput || defaultDevice}
|
|
||||||
onChange={this._setAudioOutput}>
|
|
||||||
{this._renderDeviceOptions(audioOutputs, 'audioOutput')}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioInputs = this.state.mediaDevices.audioInput.slice(0);
|
|
||||||
if (audioInputs.length > 0) {
|
|
||||||
const defaultDevice = getDefaultDevice(audioInputs);
|
|
||||||
microphoneDropdown = (
|
|
||||||
<Field element="select" label={_t("Microphone")}
|
|
||||||
value={this.state.activeAudioInput || defaultDevice}
|
|
||||||
onChange={this._setAudioInput}>
|
|
||||||
{this._renderDeviceOptions(audioInputs, 'audioInput')}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoInputs = this.state.mediaDevices.videoInput.slice(0);
|
|
||||||
if (videoInputs.length > 0) {
|
|
||||||
const defaultDevice = getDefaultDevice(videoInputs);
|
|
||||||
webcamDropdown = (
|
|
||||||
<Field element="select" label={_t("Camera")}
|
|
||||||
value={this.state.activeVideoInput || defaultDevice}
|
|
||||||
onChange={this._setVideoInput}>
|
|
||||||
{this._renderDeviceOptions(videoInputs, 'videoInput')}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
|
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
{requestButton}
|
{ requestButton }
|
||||||
{speakerDropdown}
|
{ speakerDropdown }
|
||||||
{microphoneDropdown}
|
{ microphoneDropdown }
|
||||||
{webcamDropdown}
|
{ webcamDropdown }
|
||||||
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name='webRtcAllowPeerToPeer'
|
name='webRtcAllowPeerToPeer'
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
onChange={this._changeWebRtcMethod}
|
onChange={this.changeWebRtcMethod}
|
||||||
/>
|
/>
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name='fallbackICEServerAllowed'
|
name='fallbackICEServerAllowed'
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
onChange={this._changeFallbackICEServerAllowed}
|
onChange={this.changeFallbackICEServerAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -39,7 +39,7 @@ enum SpaceVisibility {
|
||||||
|
|
||||||
const useLocalEcho = <T extends any>(
|
const useLocalEcho = <T extends any>(
|
||||||
currentFactory: () => T,
|
currentFactory: () => T,
|
||||||
setterFn: (value: T) => Promise<any>,
|
setterFn: (value: T) => Promise<unknown>,
|
||||||
errorFn: (error: Error) => void,
|
errorFn: (error: Error) => void,
|
||||||
): [value: T, handler: (value: T) => void] => {
|
): [value: T, handler: (value: T) => void] => {
|
||||||
const [value, setValue] = useState(currentFactory);
|
const [value, setValue] = useState(currentFactory);
|
||||||
|
|
|
@ -44,7 +44,7 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.toasts.VerificationRequestToast")
|
@replaceableComponent("views.toasts.VerificationRequestToast")
|
||||||
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
|
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
|
||||||
private intervalHandle: NodeJS.Timeout;
|
private intervalHandle: number;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createRef } from 'react';
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
@ -27,6 +27,22 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import UIStore from '../../../stores/UIStore';
|
||||||
|
import { lerp } from '../../../utils/AnimationUtils';
|
||||||
|
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||||
|
|
||||||
|
const PIP_VIEW_WIDTH = 336;
|
||||||
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
|
||||||
|
const MOVING_AMT = 0.2;
|
||||||
|
const SNAPPING_AMT = 0.05;
|
||||||
|
|
||||||
|
const PADDING = {
|
||||||
|
top: 58,
|
||||||
|
bottom: 58,
|
||||||
|
left: 76,
|
||||||
|
right: 8,
|
||||||
|
};
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -49,6 +65,10 @@ interface IState {
|
||||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||||
// they belong to
|
// they belong to
|
||||||
secondaryCall: MatrixCall;
|
secondaryCall: MatrixCall;
|
||||||
|
|
||||||
|
// Position of the CallPreview
|
||||||
|
translationX: number;
|
||||||
|
translationY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
|
@ -91,6 +111,16 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: any;
|
private roomStoreToken: any;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private settingsWatcherRef: string;
|
private settingsWatcherRef: string;
|
||||||
|
private callViewWrapper = createRef<HTMLDivElement>();
|
||||||
|
private initX = 0;
|
||||||
|
private initY = 0;
|
||||||
|
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||||
|
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
||||||
|
private moving = false;
|
||||||
|
private scheduledUpdate = new MarkedExecution(
|
||||||
|
() => this.animationCallback(),
|
||||||
|
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -105,12 +135,17 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
roomId,
|
roomId,
|
||||||
primaryCall: primaryCall,
|
primaryCall: primaryCall,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
|
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||||
|
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
|
document.addEventListener("mousemove", this.onMoving);
|
||||||
|
document.addEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.addEventListener("resize", this.snap);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
@ -118,6 +153,9 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
|
document.removeEventListener("mousemove", this.onMoving);
|
||||||
|
document.removeEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.removeEventListener("resize", this.snap);
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -125,6 +163,83 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = (payload) => {
|
private onRoomViewStoreUpdate = (payload) => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
|
@ -173,10 +288,52 @@ 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();
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
|
const style = {
|
||||||
|
transform: `translateX(${translatePixelsX})
|
||||||
|
translateY(${translatePixelsY})`,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CallView call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} />
|
<div
|
||||||
|
className="mx_CallPreview"
|
||||||
|
style={style}
|
||||||
|
ref={this.callViewWrapper}
|
||||||
|
>
|
||||||
|
<CallView
|
||||||
|
call={this.state.primaryCall}
|
||||||
|
secondaryCall={this.state.secondaryCall}
|
||||||
|
pipMode={true}
|
||||||
|
onMouseDownOnHeader={this.onStartMoving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,9 @@ interface IProps {
|
||||||
// This is sort of a proxy for a number of things but we currently have no
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
// need to control those things separately, so this is simpler.
|
// need to control those things separately, so this is simpler.
|
||||||
pipMode?: boolean;
|
pipMode?: boolean;
|
||||||
|
|
||||||
|
// Used for dragging the PiP CallView
|
||||||
|
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -698,19 +701,24 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
header = <div className="mx_CallView_header">
|
header = (
|
||||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
<div
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
className="mx_CallView_header"
|
||||||
</AccessibleButton>
|
onMouseDown={this.props.onMouseDownOnHeader}
|
||||||
<div className="mx_CallView_header_callInfo">
|
>
|
||||||
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
|
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||||
<div className="mx_CallView_header_callTypeSmall">
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
{callTypeText}
|
</AccessibleButton>
|
||||||
{secondaryCallInfo}
|
<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>
|
</div>
|
||||||
|
{headerControls}
|
||||||
</div>
|
</div>
|
||||||
{headerControls}
|
);
|
||||||
</div>;
|
|
||||||
myClassName = 'mx_CallView_pip';
|
myClassName = 'mx_CallView_pip';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,17 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
|
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
|
||||||
|
const BUTTON_LETTERS = ['', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '', '+', ''];
|
||||||
|
|
||||||
enum DialPadButtonKind {
|
enum DialPadButtonKind {
|
||||||
Digit,
|
Digit,
|
||||||
Delete,
|
|
||||||
Dial,
|
Dial,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IButtonProps {
|
interface IButtonProps {
|
||||||
kind: DialPadButtonKind;
|
kind: DialPadButtonKind;
|
||||||
digit?: string;
|
digit?: string;
|
||||||
|
digitSubtext?: string;
|
||||||
onButtonPress: (string) => void;
|
onButtonPress: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +43,10 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||||
case DialPadButtonKind.Digit:
|
case DialPadButtonKind.Digit:
|
||||||
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
|
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
|
||||||
{this.props.digit}
|
{this.props.digit}
|
||||||
|
<div className="mx_DialPad_buttonSubText">
|
||||||
|
{this.props.digitSubtext}
|
||||||
|
</div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
case DialPadButtonKind.Delete:
|
|
||||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_deleteButton"
|
|
||||||
onClick={this.onClick}
|
|
||||||
/>;
|
|
||||||
case DialPadButtonKind.Dial:
|
case DialPadButtonKind.Dial:
|
||||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
|
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onDigitPress: (string) => void;
|
onDigitPress: (string) => void;
|
||||||
hasDialAndDelete: boolean;
|
hasDial: boolean;
|
||||||
onDeletePress?: (string) => void;
|
onDeletePress?: (string) => void;
|
||||||
onDialPress?: (string) => void;
|
onDialPress?: (string) => void;
|
||||||
}
|
}
|
||||||
|
@ -65,16 +65,15 @@ export default class Dialpad extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const buttonNodes = [];
|
const buttonNodes = [];
|
||||||
|
|
||||||
for (const button of BUTTONS) {
|
for (let i = 0; i < BUTTONS.length; i++) {
|
||||||
|
const button = BUTTONS[i];
|
||||||
|
const digitSubtext = BUTTON_LETTERS[i];
|
||||||
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
|
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
|
||||||
digit={button} onButtonPress={this.props.onDigitPress}
|
digit={button} digitSubtext={digitSubtext} onButtonPress={this.props.onDigitPress}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasDialAndDelete) {
|
if (this.props.hasDial) {
|
||||||
buttonNodes.push(<DialPadButton key="del" kind={DialPadButtonKind.Delete}
|
|
||||||
onButtonPress={this.props.onDeletePress}
|
|
||||||
/>);
|
|
||||||
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
|
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
|
||||||
onButtonPress={this.props.onDialPress}
|
onButtonPress={this.props.onDialPress}
|
||||||
/>);
|
/>);
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import DialPad from './DialPad';
|
import DialPad from './DialPad';
|
||||||
|
@ -23,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
|
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (boolean) => void;
|
onFinished: (boolean) => void;
|
||||||
|
@ -74,22 +74,38 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const backspaceButton = (
|
||||||
|
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only show the backspace button if the field has content
|
||||||
|
let dialPadField;
|
||||||
|
if (this.state.value.length !== 0) {
|
||||||
|
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
|
||||||
|
value={this.state.value}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this.onChange}
|
||||||
|
postfixComponent={backspaceButton}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
|
||||||
|
value={this.state.value}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_DialPadModal">
|
return <div className="mx_DialPadModal">
|
||||||
|
<div>
|
||||||
|
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
|
||||||
|
</div>
|
||||||
<div className="mx_DialPadModal_header">
|
<div className="mx_DialPadModal_header">
|
||||||
<div>
|
|
||||||
<span className="mx_DialPadModal_title">{_t("Dial pad")}</span>
|
|
||||||
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
|
|
||||||
</div>
|
|
||||||
<form onSubmit={this.onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
<Field className="mx_DialPadModal_field" id="dialpad_number"
|
{dialPadField}
|
||||||
value={this.state.value} autoFocus={true}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadModal_horizSep" />
|
|
||||||
<div className="mx_DialPadModal_dialPad">
|
<div className="mx_DialPadModal_dialPad">
|
||||||
<DialPad hasDialAndDelete={true}
|
<DialPad hasDial={true}
|
||||||
onDigitPress={this.onDigitPress}
|
onDigitPress={this.onDigitPress}
|
||||||
onDeletePress={this.onDeletePress}
|
onDeletePress={this.onDeletePress}
|
||||||
onDialPress={this.onDialPress}
|
onDialPress={this.onDialPress}
|
||||||
|
|
31
src/customisations/Alias.ts
Normal file
31
src/customisations/Alias.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string {
|
||||||
|
// E.g. prefer one of the aliases over another
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This interface summarises all available customisation points and also marks
|
||||||
|
// them all as optional. This allows customisers to only define and export the
|
||||||
|
// customisations they need while still maintaining type safety.
|
||||||
|
export interface IAliasCustomisations {
|
||||||
|
getDisplayAliasForAliasSet?: typeof getDisplayAliasForAliasSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real customisation module will define and export one or more of the
|
||||||
|
// customisation points that make up `IAliasCustomisations`.
|
||||||
|
export default {} as IAliasCustomisations;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue