Merge remote-tracking branch 'upstream/develop' into feature/narrow-voip-tiles/18398
This commit is contained in:
commit
39bb253d1f
142 changed files with 5796 additions and 1956 deletions
31
.github/workflows/layered-build.yaml
vendored
Normal file
31
.github/workflows/layered-build.yaml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
name: Layered Preview Build
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [develop]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build
|
||||||
|
run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: previewbuild
|
||||||
|
path: element-web/webapp
|
||||||
|
# We'll only use this in a triggered job, then we're done with it
|
||||||
|
retention-days: 1
|
||||||
|
- uses: actions/github-script@v3.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||||
|
- name: Upload PR Info
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: pr.json
|
||||||
|
path: pr.json
|
||||||
|
# We'll only use this in a triggered job, then we're done with it
|
||||||
|
retention-days: 1
|
||||||
|
|
80
.github/workflows/netflify.yaml
vendored
Normal file
80
.github/workflows/netflify.yaml
vendored
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
name: Upload Preview Build to Netlify
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Layered Preview Build"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
# There's a 'download artifact' action but it hasn't been updated for the
|
||||||
|
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
||||||
|
# so instead we get this mess:
|
||||||
|
- name: 'Download artifact'
|
||||||
|
uses: actions/github-script@v3.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
var artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "previewbuild"
|
||||||
|
})[0];
|
||||||
|
var download = await github.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "pr.json"
|
||||||
|
})[0];
|
||||||
|
var download = await github.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: prInfoArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
||||||
|
- name: Extract Artifacts
|
||||||
|
run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
||||||
|
- name: 'Read PR Info'
|
||||||
|
id: readctx
|
||||||
|
uses: actions/github-script@v3.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
var fs = require('fs');
|
||||||
|
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
||||||
|
console.log(`::set-output name=prnumber::${pr.number}`);
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
id: netlify
|
||||||
|
uses: nwtgck/actions-netlify@v1.2
|
||||||
|
with:
|
||||||
|
publish-dir: webapp
|
||||||
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
|
# These don't work because we're in workflow_run
|
||||||
|
enable-pull-request-comment: false
|
||||||
|
enable-commit-comment: false
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
timeout-minutes: 1
|
||||||
|
- name: Edit PR Description
|
||||||
|
uses: velas/pr-description@v1.0.1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||||
|
description-message: |
|
||||||
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
|
⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts.
|
||||||
|
|
12
.github/workflows/preview_changelog.yaml
vendored
Normal file
12
.github/workflows/preview_changelog.yaml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
name: Preview Changelog
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [ opened, edited, labeled ]
|
||||||
|
jobs:
|
||||||
|
changelog:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Preview Changelog
|
||||||
|
uses: matrix-org/allchange@main
|
||||||
|
with:
|
||||||
|
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -1,4 +1,4 @@
|
||||||
Contributing code to The React SDK
|
Contributing code to The React SDK
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst
|
matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.md
|
||||||
|
|
|
@ -193,7 +193,8 @@
|
||||||
"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"
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!matrix-js-sdk).+$"
|
"/node_modules/(?!matrix-js-sdk).+$"
|
||||||
|
|
55
res/css/_animations.scss
Normal file
55
res/css/_animations.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Transition Group animations are prefixed with 'mx_rtg--' so that we
|
||||||
|
* know they should not be used anywhere outside of React Transition Groups.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_rtg--fade-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.mx_rtg--fade-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
.mx_rtg--fade-exit {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.mx_rtg--fade-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes mx--anim-pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
@keyframes mx--anim-pulse {
|
||||||
|
// Override all keyframes in reduced-motion
|
||||||
|
}
|
||||||
|
.mx_rtg--fade-enter-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.mx_rtg--fade-exit-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
@import "./_font-sizes.scss";
|
@import "./_font-sizes.scss";
|
||||||
@import "./_font-weights.scss";
|
@import "./_font-weights.scss";
|
||||||
|
@import "./_animations.scss";
|
||||||
|
|
||||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||||
|
@import "./views/dialogs/_CreateSpaceFromCommunityDialog.scss";
|
||||||
@import "./views/dialogs/_CreateSubspaceDialog.scss";
|
@import "./views/dialogs/_CreateSubspaceDialog.scss";
|
||||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
|
@ -240,6 +241,7 @@
|
||||||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||||
@import "./views/settings/_EmailAddresses.scss";
|
@import "./views/settings/_EmailAddresses.scss";
|
||||||
@import "./views/settings/_IntegrationManager.scss";
|
@import "./views/settings/_IntegrationManager.scss";
|
||||||
|
@import "./views/settings/_LayoutSwitcher.scss";
|
||||||
@import "./views/settings/_Notifications.scss";
|
@import "./views/settings/_Notifications.scss";
|
||||||
@import "./views/settings/_PhoneNumbers.scss";
|
@import "./views/settings/_PhoneNumbers.scss";
|
||||||
@import "./views/settings/_ProfileSettings.scss";
|
@import "./views/settings/_ProfileSettings.scss";
|
||||||
|
@ -269,10 +271,12 @@
|
||||||
@import "./views/toasts/_IncomingCallToast.scss";
|
@import "./views/toasts/_IncomingCallToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
|
@import "./views/voip/CallView/_CallViewButtons.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallPreview.scss";
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
|
@import "./views/voip/_CallViewHeader.scss";
|
||||||
@import "./views/voip/_CallViewSidebar.scss";
|
@import "./views/voip/_CallViewSidebar.scss";
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
|
|
|
@ -368,6 +368,65 @@ limitations under the License.
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_GroupView_spaceUpgradePrompt {
|
||||||
|
padding: 16px 50px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 632px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p, h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: $font-24px;
|
||||||
|
width: 20px;
|
||||||
|
left: 18px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupView_spaceUpgradePrompt_close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $input-darker-bg-color;
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 8px;
|
||||||
|
mask-image: url('$(res)/img/image-view/close.svg');
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) {
|
.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
|
|
@ -269,7 +269,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover, &:focus-within {
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
|
@ -278,6 +278,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.mx_SpaceRoomDirectory_roomTileWrapper {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomDirectory_roomTile,
|
.mx_SpaceRoomDirectory_roomTile,
|
||||||
.mx_SpaceRoomDirectory_subspace_children {
|
.mx_SpaceRoomDirectory_subspace_children {
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -180,6 +180,18 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_preview_migratedCommunity {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_preview_inviter {
|
.mx_SpaceRoomView_preview_inviter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -342,7 +354,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
|
|
||||||
.mx_SpaceFeedbackPrompt {
|
.mx_SpaceFeedbackPrompt {
|
||||||
padding: 7px; // 8px - 1px border
|
padding: 7px; // 8px - 1px border
|
||||||
border: 1px solid $menu-border-color;
|
border: 1px solid rgba($primary-fg-color, .1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
margin: 0 0 -40px auto; // collapse its own height to not push other components down
|
margin: 0 0 -40px auto; // collapse its own height to not push other components down
|
||||||
|
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
background-color: $toast-bg-color;
|
background-color: $system;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $toast-bg-color;
|
background-color: $system;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -85,7 +85,7 @@ limitations under the License.
|
||||||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/hide.svg');
|
mask-image: url('$(res)/img/element-icons/hide.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TagTileContextMenu_createSpace::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/message/fwd.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_TagTileContextMenu_separator {
|
.mx_TagTileContextMenu_separator {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
187
res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss
Normal file
187
res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
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_CreateSpaceFromCommunityDialog_wrapper {
|
||||||
|
.mx_Dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog {
|
||||||
|
width: 480px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_content {
|
||||||
|
> p {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CreateSpaceFromCommunityDialog_flairNotice {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceBasicSettings {
|
||||||
|
> p {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field_textarea {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_JoinRuleDropdown .mx_Dropdown_menu {
|
||||||
|
width: auto !important; // override fixed width
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_nonPublicSpacer {
|
||||||
|
height: 63px; // balance the height of the missing room alias field to prevent modal bouncing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_footer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
.mx_ProgressBar {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_progressText {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_error {
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_errorHeading {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_errorCaption {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
display: inline-block;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary {
|
||||||
|
padding: 8px 36px;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_retryButton {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog {
|
||||||
|
.mx_InfoDialog {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid $accent-color;
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
margin: 12px auto 32px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: $accent-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
|
||||||
|
mask-size: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,18 +24,15 @@ limitations under the License.
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_desktopCapturerSourcePicker_source {
|
.mx_desktopCapturerSourcePicker_source {
|
||||||
|
width: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
width: 312px;
|
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
@ -53,6 +50,7 @@ limitations under the License.
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 312px;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ limitations under the License.
|
||||||
.mx_Field input,
|
.mx_Field input,
|
||||||
.mx_Field select,
|
.mx_Field select,
|
||||||
.mx_Field textarea {
|
.mx_Field textarea {
|
||||||
|
font-family: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -16,6 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
$timelineImageBorderRadius: 4px;
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
|
.mx_MImageBody_thumbnail--blurhash {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
@ -23,8 +29,11 @@ $timelineImageBorderRadius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
> div > canvas {
|
.mx_Blurhash > canvas {
|
||||||
|
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,14 @@ limitations under the License.
|
||||||
font-size: $font-10-4px;
|
font-size: $font-10-4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.mx_UserPill {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.mx_RoomPill {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_BasicMessageComposer_input_disabled {
|
&.mx_BasicMessageComposer_input_disabled {
|
||||||
|
|
|
@ -271,7 +271,7 @@ limitations under the License.
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
|
|
|
@ -489,6 +489,10 @@ $hover-select-border: 4px;
|
||||||
// https://github.com/vector-im/vector-web/issues/754
|
// https://github.com/vector-im/vector-web/issues/754
|
||||||
overflow-x: overlay;
|
overflow-x: overlay;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 2px solid $voice-record-stop-border-color;
|
border: 2px solid $voice-record-stop-border-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
margin-right: 16px; // between us and the send button
|
margin-right: 8px; // between us and the waveform component
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -46,9 +46,28 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadingState {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_failedState {
|
||||||
|
margin-right: 21px;
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadState_badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
||||||
// Note: remaining class properties are in the PlayerContainer CSS.
|
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||||
|
|
||||||
|
// fixed height to reduce layout jumps with the play button appearing
|
||||||
|
// https://github.com/vector-im/element-web/issues/18431
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
margin: 6px; // force the composer area to put a gutter around us
|
margin: 6px; // force the composer area to put a gutter around us
|
||||||
margin-right: 12px; // isolate from stop/send button
|
margin-right: 12px; // isolate from stop/send button
|
||||||
|
|
||||||
|
@ -68,7 +87,7 @@ limitations under the License.
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px; // 12px from the left edge for container padding
|
left: 12px; // 12px from the left edge for container padding
|
||||||
top: 18px; // vertically center (middle align with clock)
|
top: 17px; // vertically center (middle align with clock)
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
91
res/css/views/settings/_LayoutSwitcher.scss
Normal file
91
res/css/views/settings/_LayoutSwitcher.scss
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_LayoutSwitcher {
|
||||||
|
.mx_LayoutSwitcher_RadioButtons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
> .mx_LayoutSwitcher_RadioButton {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
border: 1px solid $appearance-tab-border-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption,
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LayoutSwitcher_RadioButton_preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_content {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LayoutSwitcher_RadioButton_selected {
|
||||||
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
border-top: 1px solid $appearance-tab-border-color;
|
||||||
|
|
||||||
|
> input + div {
|
||||||
|
border-color: rgba($muted-fg-color, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_checked {
|
||||||
|
background-color: rgba($accent-color, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0;
|
||||||
|
&[data-layout=bubble] {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
&[data-layout=irc] {
|
||||||
|
> a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_line {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,15 +50,21 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
|
$right-gutter: 80px;
|
||||||
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.mx_SettingsFlag {
|
.mx_SettingsFlag {
|
||||||
margin-right: 80px;
|
margin-right: $right-gutter;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-right: $right-gutter;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_SettingsTab_subsectionText .mx_SettingsFlag {
|
&.mx_SettingsTab_subsectionText .mx_SettingsFlag {
|
||||||
margin-right: 0px !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -155,79 +155,6 @@ limitations under the License.
|
||||||
margin-left: calc($font-16px + 10px);
|
margin-left: calc($font-16px + 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
color: $primary-fg-color;
|
|
||||||
|
|
||||||
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 300px;
|
|
||||||
|
|
||||||
border: 1px solid $appearance-tab-border-color;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption,
|
|
||||||
.mx_MessageActionBar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Layout_RadioButton_preview {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
|
|
||||||
border-color: $accent-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton {
|
|
||||||
border-top: 1px solid $appearance-tab-border-color;
|
|
||||||
|
|
||||||
> input + div {
|
|
||||||
border-color: rgba($muted-fg-color, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RadioButton_checked {
|
|
||||||
background-color: rgba($accent-color, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile {
|
|
||||||
margin: 0;
|
|
||||||
&[data-layout=bubble] {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
&[data-layout=irc] {
|
|
||||||
> a {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mx_EventTile_line {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Advanced {
|
.mx_AppearanceUserSettingsTab_Advanced {
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
|
|
@ -28,28 +28,32 @@ limitations under the License.
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken {
|
.mx_HelpUserSettingsTab_copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: solid 1px $light-fg-color;
|
border: solid 1px $light-fg-color;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
width: max-content;
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken_copy {
|
.mx_HelpUserSettingsTab_copyButton {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 20px;
|
|
||||||
display: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
|
||||||
mask-image: url($copy-button-url);
|
|
||||||
background-color: $message-action-bar-fg-color;
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
mask-image: url($copy-button-url);
|
||||||
|
background-color: $message-action-bar-fg-color;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,4 +22,25 @@ limitations under the License.
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
||||||
|
margin-right: 200px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ $spacePanelWidth: 71px;
|
||||||
> p {
|
> p {
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
color: $secondary-fg-color;
|
color: $secondary-fg-color;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceFeedbackPrompt {
|
.mx_SpaceFeedbackPrompt {
|
||||||
|
@ -51,13 +50,6 @@ $spacePanelWidth: 71px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX remove this when spaces leaves Beta
|
|
||||||
.mx_BetaCard_betaPill {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpaceCreateMenuType {
|
.mx_SpaceCreateMenuType {
|
||||||
@mixin SpacePillButton;
|
@mixin SpacePillButton;
|
||||||
}
|
}
|
||||||
|
@ -100,6 +92,11 @@ $spacePanelWidth: 71px;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_disabled {
|
.mx_AccessibleButton_disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
102
res/css/views/voip/CallView/_CallViewButtons.scss
Normal file
102
res/css/views/voip/CallView/_CallViewButtons.scss
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_CallViewButtons {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
bottom: 5px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
z-index: 200; // To be above _all_ feeds
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_hidden {
|
||||||
|
opacity: 0.001; // opacity 0 can cause a re-layout
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewButtons_button {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_dialpad::before {
|
||||||
|
background-image: url('$(res)/img/voip/dialpad.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_micOn::before {
|
||||||
|
background-image: url('$(res)/img/voip/mic-on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_micOff::before {
|
||||||
|
background-image: url('$(res)/img/voip/mic-off.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_vidOn::before {
|
||||||
|
background-image: url('$(res)/img/voip/vid-on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_vidOff::before {
|
||||||
|
background-image: url('$(res)/img/voip/vid-off.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_screensharingOn::before {
|
||||||
|
background-image: url('$(res)/img/voip/screensharing-on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_screensharingOff::before {
|
||||||
|
background-image: url('$(res)/img/voip/screensharing-off.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_sidebarOn::before {
|
||||||
|
background-image: url('$(res)/img/voip/sidebar-on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_sidebarOff::before {
|
||||||
|
background-image: url('$(res)/img/voip/sidebar-off.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_hangup::before {
|
||||||
|
background-image: url('$(res)/img/voip/hangup.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_more::before {
|
||||||
|
background-image: url('$(res)/img/voip/more.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewButtons_button_invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CallPreview {
|
.mx_CallPreview {
|
||||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
||||||
.mx_CallView_pip {
|
.mx_CallView_pip {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background-color: $toast-bg-color;
|
background-color: $system;
|
||||||
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;
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ limitations under the License.
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_callControls {
|
.mx_CallViewButtons {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_callControls_button {
|
.mx_CallViewButtons_button {
|
||||||
&::before {
|
&::before {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
@ -75,8 +75,6 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
// We don't want to collide with the call controls that have 52px of height
|
|
||||||
margin-bottom: 52px;
|
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -201,133 +199,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: left;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callType {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_secondaryCallInfo {
|
|
||||||
&::before {
|
|
||||||
content: '·';
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_controls {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_fullscreen {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_expand {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callInfo {
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_roomName {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_secondaryCall_roomName {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeSmall {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeIcon {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_voice::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_video::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
bottom: 5px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
z-index: 200; // To be above _all_ feeds
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_hidden {
|
|
||||||
opacity: 0.001; // opacity 0 can cause a re-layout
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_presenting {
|
.mx_CallView_presenting {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -347,94 +218,3 @@ limitations under the License.
|
||||||
opacity: 0.001; // opacity 0 can cause a re-layout
|
opacity: 0.001; // opacity 0 can cause a re-layout
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_callControls_button {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: 2px;
|
|
||||||
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_dialpad {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/dialpad.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_micOn {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/mic-on.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_micOff {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/mic-off.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_vidOn {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/vid-on.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_vidOff {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/vid-off.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_screensharingOn {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/screensharing-on.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_screensharingOff {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/screensharing-off.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_sidebarOn {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/sidebar-on.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_sidebarOff {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/sidebar-off.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_hangup {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/hangup.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_more {
|
|
||||||
&::before {
|
|
||||||
background-image: url('$(res)/img/voip/more.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls_button_invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
129
res/css/views/voip/_CallViewHeader.scss
Normal file
129
res/css/views/voip/_CallViewHeader.scss
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_CallViewHeader {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callType {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_secondaryCallInfo {
|
||||||
|
&::before {
|
||||||
|
content: '·';
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_fullscreen {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_expand {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callInfo {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_roomName {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_secondaryCall_roomName {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeSmall {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeIcon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_voice::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_video::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,8 +40,6 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
<path d="m11.068 2c-0.32021 4.772e-4 -0.66852 0.17244-0.96484 0.46875-2.5464 2.5435-5.0905 5.0892-7.6348 7.6348-0.79016 0.7902-0.69302 1.9462 1.1641 1.9707 1.855 0.02447 3.4407-0.56671 3.8281-0.69141l2.4355 3.1445c-0.83503 1.9462-0.86902 4.062-0.058594 5.7949 0.47213 1.0095 1.79 1.0049 2.5781 0.2168l3.2773-3.2773 2.8223 2.8223c1.491 1.491 3.2644 2.0696 3.4512 1.8828s-0.39181-1.9602-1.8828-3.4512l-2.8223-2.8223 3.2773-3.2773c0.788-0.788 0.79075-2.106-0.21875-2.5781-1.733-0.81044-3.8468-0.77643-5.793 0.058594l-3.1445-2.4355c0.1247-0.38742 0.71588-1.9731 0.69141-3.8281-0.015311-1.1607-0.47217-1.6336-1.0059-1.6328z" fill="#737d8c"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 744 B |
|
@ -1,18 +1,35 @@
|
||||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741
|
||||||
$system-dark: #21262C;
|
$accent: #0DBD8B;
|
||||||
|
$alert: #FF5B55;
|
||||||
|
$links: #0086e6;
|
||||||
|
$primary-content: #ffffff;
|
||||||
|
$secondary-content: #A9B2BC;
|
||||||
|
$tertiary-content: #8E99A4;
|
||||||
|
$quaternary-content: #6F7882;
|
||||||
|
$quinary-content: #394049;
|
||||||
|
$system: #21262C;
|
||||||
|
$background: #15191E;
|
||||||
|
$panels: rgba($system, 0.9);
|
||||||
|
$panel-base: #8D97A5; // This color is not intended for use in the app
|
||||||
|
$panel-selected: rgba($panel-base, 0.3);
|
||||||
|
$panel-hover: rgba($panel-base, 0.1);
|
||||||
|
$panel-actions: rgba($panel-base, 0.2);
|
||||||
|
$space-nav: rgba($panel-base, 0.1);
|
||||||
|
|
||||||
|
// TODO: Move userId colors here
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
$bg-color: #15191E;
|
$bg-color: $background;
|
||||||
$base-color: $bg-color;
|
$base-color: $bg-color;
|
||||||
$base-text-color: #ffffff;
|
$base-text-color: $primary-content;
|
||||||
$header-panel-bg-color: #20252B;
|
$header-panel-bg-color: #20252B;
|
||||||
$header-panel-border-color: #000000;
|
$header-panel-border-color: #000000;
|
||||||
$header-panel-text-primary-color: #B9BEC6;
|
$header-panel-text-primary-color: #B9BEC6;
|
||||||
$header-panel-text-secondary-color: #c8c8cd;
|
$header-panel-text-secondary-color: #c8c8cd;
|
||||||
$text-primary-color: #ffffff;
|
$text-primary-color: $primary-content;
|
||||||
$text-secondary-color: #B9BEC6;
|
$text-secondary-color: #B9BEC6;
|
||||||
$quaternary-fg-color: #6F7882;
|
$quaternary-fg-color: $quaternary-content;
|
||||||
$search-bg-color: #181b21;
|
$search-bg-color: #181b21;
|
||||||
$search-placeholder-color: #61708b;
|
$search-placeholder-color: #61708b;
|
||||||
$room-highlight-color: #343a46;
|
$room-highlight-color: #343a46;
|
||||||
|
@ -23,8 +40,8 @@ $primary-bg-color: $bg-color;
|
||||||
$muted-fg-color: $header-panel-text-primary-color;
|
$muted-fg-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// additional text colors
|
// additional text colors
|
||||||
$secondary-fg-color: #A9B2BC;
|
$secondary-fg-color: $secondary-content;
|
||||||
$tertiary-fg-color: #8E99A4;
|
$tertiary-fg-color: $tertiary-content;
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: $header-panel-text-secondary-color;
|
$light-fg-color: $header-panel-text-secondary-color;
|
||||||
|
@ -50,7 +67,7 @@ $inverted-bg-color: $base-color;
|
||||||
$selected-color: $room-highlight-color;
|
$selected-color: $room-highlight-color;
|
||||||
|
|
||||||
// selected for hoverover & selected event tiles
|
// selected for hoverover & selected event tiles
|
||||||
$event-selected-color: $system-dark;
|
$event-selected-color: $system;
|
||||||
|
|
||||||
// used for the hairline dividers in RoomView
|
// used for the hairline dividers in RoomView
|
||||||
$primary-hairline-color: transparent;
|
$primary-hairline-color: transparent;
|
||||||
|
@ -94,7 +111,7 @@ $lightbox-background-bg-color: #000;
|
||||||
$lightbox-background-bg-opacity: 0.85;
|
$lightbox-background-bg-opacity: 0.85;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: $system-dark;
|
$settings-profile-placeholder-bg-color: $system;
|
||||||
$settings-profile-overlay-placeholder-fg-color: #454545;
|
$settings-profile-overlay-placeholder-fg-color: #454545;
|
||||||
$settings-profile-button-bg-color: #e7e7e7;
|
$settings-profile-button-bg-color: #e7e7e7;
|
||||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||||
|
@ -108,20 +125,17 @@ $roomheader-addroom-fg-color: $text-primary-color;
|
||||||
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
||||||
$groupheader-button-color: $header-panel-text-primary-color;
|
$groupheader-button-color: $header-panel-text-primary-color;
|
||||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||||
$icon-button-color: #8E99A4;
|
$icon-button-color: $tertiary-content;
|
||||||
$roomtopic-color: $text-secondary-color;
|
$roomtopic-color: $text-secondary-color;
|
||||||
$eventtile-meta-color: $roomtopic-color;
|
$eventtile-meta-color: $roomtopic-color;
|
||||||
|
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
$quinary-content-color: #394049;
|
|
||||||
$toast-bg-color: $quinary-content-color;
|
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
$dialpad-button-bg-color: #394049;
|
$dialpad-button-bg-color: $quinary-content;
|
||||||
|
|
||||||
$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;
|
||||||
|
@ -164,12 +178,12 @@ $tab-label-icon-bg-color: $text-primary-color;
|
||||||
$tab-label-active-icon-bg-color: $text-primary-color;
|
$tab-label-active-icon-bg-color: $text-primary-color;
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
$button-primary-fg-color: #ffffff;
|
$button-primary-fg-color: $primary-content;
|
||||||
$button-primary-bg-color: $accent-color;
|
$button-primary-bg-color: $accent-color;
|
||||||
$button-secondary-bg-color: transparent;
|
$button-secondary-bg-color: transparent;
|
||||||
$button-danger-fg-color: #ffffff;
|
$button-danger-fg-color: $primary-content;
|
||||||
$button-danger-bg-color: $notice-primary-color;
|
$button-danger-bg-color: $notice-primary-color;
|
||||||
$button-danger-disabled-fg-color: #ffffff;
|
$button-danger-disabled-fg-color: $primary-content;
|
||||||
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
|
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
|
||||||
$button-link-fg-color: $accent-color;
|
$button-link-fg-color: $accent-color;
|
||||||
$button-link-bg-color: transparent;
|
$button-link-bg-color: transparent;
|
||||||
|
@ -178,7 +192,7 @@ $button-link-bg-color: transparent;
|
||||||
$togglesw-off-color: $room-highlight-color;
|
$togglesw-off-color: $room-highlight-color;
|
||||||
|
|
||||||
$progressbar-fg-color: $accent-color;
|
$progressbar-fg-color: $accent-color;
|
||||||
$progressbar-bg-color: $system-dark;
|
$progressbar-bg-color: $system;
|
||||||
|
|
||||||
$visual-bell-bg-color: #800;
|
$visual-bell-bg-color: #800;
|
||||||
|
|
||||||
|
@ -201,19 +215,19 @@ $reaction-row-button-selected-border-color: $accent-color;
|
||||||
$kbd-border-color: #000000;
|
$kbd-border-color: #000000;
|
||||||
|
|
||||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: $primary-content;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: $base-color;
|
$interactive-tooltip-bg-color: $base-color;
|
||||||
$interactive-tooltip-fg-color: #ffffff;
|
$interactive-tooltip-fg-color: $primary-content;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
$message-body-panel-bg-color: $quinary-content;
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $system-dark; // "System Dark"
|
$message-body-panel-icon-bg-color: $system; // "System Dark"
|
||||||
|
|
||||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741
|
||||||
|
$system: #21262C;
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
$bg-color: #181b21;
|
$bg-color: #181b21;
|
||||||
|
@ -111,9 +114,6 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
$quinary-content-color: #394049;
|
|
||||||
$toast-bg-color: $quinary-content-color;
|
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
@ -222,6 +222,13 @@ $appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #14322E;
|
||||||
|
$eventbubble-others-bg: $event-selected-color;
|
||||||
|
$eventbubble-bg-hover: #1C2026;
|
||||||
|
$eventbubble-avatar-outline: $bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji';
|
$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
$system-light: #F4F6FA;
|
$system: #F4F6FA;
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
|
@ -181,8 +181,7 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$composer-e2e-icon-color: #91a1c0;
|
$composer-e2e-icon-color: #91a1c0;
|
||||||
$header-divider-color: #91a1c0;
|
$header-divider-color: #91a1c0;
|
||||||
|
|
||||||
$toast-bg-color: $system-light;
|
$voipcall-plinth-color: $system;
|
||||||
$voipcall-plinth-color: $system-light;
|
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -334,7 +333,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0;
|
$message-body-panel-bg-color: #E3E8F0;
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $system-light;
|
$message-body-panel-icon-bg-color: $system;
|
||||||
|
|
||||||
// See non-legacy _light for variable information
|
// See non-legacy _light for variable information
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
|
@ -352,7 +351,7 @@ $composer-shadow-color: tranparent;
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
$eventbubble-self-bg: #F0FBF8;
|
$eventbubble-self-bg: #F0FBF8;
|
||||||
$eventbubble-others-bg: $system-light;
|
$eventbubble-others-bg: $system;
|
||||||
$eventbubble-bg-hover: #FAFBFD;
|
$eventbubble-bg-hover: #FAFBFD;
|
||||||
$eventbubble-avatar-outline: #fff;
|
$eventbubble-avatar-outline: #fff;
|
||||||
$eventbubble-reply-color: #C1C6CD;
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
|
@ -140,3 +140,10 @@ $event-highlight-bg-color: var(--timeline-highlights-color);
|
||||||
//
|
//
|
||||||
// redirect some variables away from their hardcoded values in the light theme
|
// redirect some variables away from their hardcoded values in the light theme
|
||||||
$settings-grey-fg-color: $primary-fg-color;
|
$settings-grey-fg-color: $primary-fg-color;
|
||||||
|
|
||||||
|
// --eventbubble colors
|
||||||
|
$eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg);
|
||||||
|
$eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg);
|
||||||
|
$eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover);
|
||||||
|
$eventbubble-avatar-outline: var(--eventbubble-avatar-outline, $eventbubble-avatar-outline);
|
||||||
|
$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-color);
|
||||||
|
|
|
@ -8,27 +8,43 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji';
|
$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120
|
||||||
$system-light: #F4F6FA;
|
$accent: #0DBD8B;
|
||||||
|
$alert: #FF5B55;
|
||||||
|
$links: #0086e6;
|
||||||
|
$primary-content: #17191C;
|
||||||
|
$secondary-content: #737D8C;
|
||||||
|
$tertiary-content: #8D97A5;
|
||||||
|
$quaternary-content: #c1c6cd;
|
||||||
|
$quinary-content: #E3E8F0;
|
||||||
|
$system: #F4F6FA;
|
||||||
|
$background: #ffffff;
|
||||||
|
$panels: rgba($system, 0.9);
|
||||||
|
$panel-selected: rgba($tertiary-content, 0.3);
|
||||||
|
$panel-hover: rgba($tertiary-content, 0.1);
|
||||||
|
$panel-actions: rgba($tertiary-content, 0.2);
|
||||||
|
$space-nav: rgba($tertiary-content, 0.15);
|
||||||
|
|
||||||
|
// TODO: Move userId colors here
|
||||||
|
|
||||||
// unified palette
|
// unified palette
|
||||||
// try to use these colors when possible
|
// try to use these colors when possible
|
||||||
$accent-color: #0DBD8B;
|
$accent-color: $accent;
|
||||||
$accent-bg-color: rgba(3, 179, 129, 0.16);
|
$accent-bg-color: rgba(3, 179, 129, 0.16);
|
||||||
$notice-primary-color: #ff4b55;
|
$notice-primary-color: #ff4b55;
|
||||||
$notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
$notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
||||||
$primary-fg-color: #2e2f32;
|
$primary-fg-color: #2e2f32;
|
||||||
$secondary-fg-color: #737D8C;
|
$secondary-fg-color: $secondary-content;
|
||||||
$tertiary-fg-color: #8D99A5;
|
$tertiary-fg-color: #8D99A5;
|
||||||
$quaternary-fg-color: #C1C6CD;
|
$quaternary-fg-color: $quaternary-content;
|
||||||
$header-panel-bg-color: #f3f8fd;
|
$header-panel-bg-color: #f3f8fd;
|
||||||
|
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
$primary-bg-color: #ffffff;
|
$primary-bg-color: $background;
|
||||||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
|
@ -38,7 +54,7 @@ $light-fg-color: #747474;
|
||||||
$focus-bg-color: #dddddd;
|
$focus-bg-color: #dddddd;
|
||||||
|
|
||||||
// button UI (white-on-green in light skin)
|
// button UI (white-on-green in light skin)
|
||||||
$accent-fg-color: #ffffff;
|
$accent-fg-color: $background;
|
||||||
$accent-color-50pct: rgba($accent-color, 0.5);
|
$accent-color-50pct: rgba($accent-color, 0.5);
|
||||||
$accent-color-darker: #92caad;
|
$accent-color-darker: #92caad;
|
||||||
$accent-color-alt: #238CF5;
|
$accent-color-alt: #238CF5;
|
||||||
|
@ -82,7 +98,7 @@ $primary-hairline-color: transparent;
|
||||||
|
|
||||||
// used for the border of input text fields
|
// used for the border of input text fields
|
||||||
$input-border-color: #e7e7e7;
|
$input-border-color: #e7e7e7;
|
||||||
$input-darker-bg-color: #e3e8f0;
|
$input-darker-bg-color: $quinary-content;
|
||||||
$input-darker-fg-color: #9fa9ba;
|
$input-darker-fg-color: #9fa9ba;
|
||||||
$input-lighter-bg-color: #f2f5f8;
|
$input-lighter-bg-color: #f2f5f8;
|
||||||
$input-lighter-fg-color: $input-darker-fg-color;
|
$input-lighter-fg-color: $input-darker-fg-color;
|
||||||
|
@ -90,7 +106,7 @@ $input-focused-border-color: #238cf5;
|
||||||
$input-valid-border-color: $accent-color;
|
$input-valid-border-color: $accent-color;
|
||||||
$input-invalid-border-color: $warning-color;
|
$input-invalid-border-color: $warning-color;
|
||||||
|
|
||||||
$field-focused-label-bg-color: #ffffff;
|
$field-focused-label-bg-color: $background;
|
||||||
|
|
||||||
$button-bg-color: $accent-color;
|
$button-bg-color: $accent-color;
|
||||||
$button-fg-color: white;
|
$button-fg-color: white;
|
||||||
|
@ -112,8 +128,8 @@ $menu-bg-color: #fff;
|
||||||
$menu-box-shadow-color: rgba(118, 131, 156, 0.6);
|
$menu-box-shadow-color: rgba(118, 131, 156, 0.6);
|
||||||
$menu-selected-color: #f5f8fa;
|
$menu-selected-color: #f5f8fa;
|
||||||
|
|
||||||
$avatar-initial-color: #ffffff;
|
$avatar-initial-color: $background;
|
||||||
$avatar-bg-color: #ffffff;
|
$avatar-bg-color: $background;
|
||||||
|
|
||||||
$h3-color: #3d3b39;
|
$h3-color: #3d3b39;
|
||||||
|
|
||||||
|
@ -141,7 +157,7 @@ $blockquote-bar-color: #ddd;
|
||||||
$blockquote-fg-color: #777;
|
$blockquote-fg-color: #777;
|
||||||
|
|
||||||
$settings-grey-fg-color: #a2a2a2;
|
$settings-grey-fg-color: #a2a2a2;
|
||||||
$settings-profile-placeholder-bg-color: $system-light;
|
$settings-profile-placeholder-bg-color: $system;
|
||||||
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
||||||
$settings-profile-button-bg-color: #e7e7e7;
|
$settings-profile-button-bg-color: #e7e7e7;
|
||||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||||
|
@ -163,24 +179,23 @@ $roomheader-addroom-fg-color: #5c6470;
|
||||||
$groupFilterPanel-button-color: #91A1C0;
|
$groupFilterPanel-button-color: #91A1C0;
|
||||||
$groupheader-button-color: #91A1C0;
|
$groupheader-button-color: #91A1C0;
|
||||||
$rightpanel-button-color: #91A1C0;
|
$rightpanel-button-color: #91A1C0;
|
||||||
$icon-button-color: #C1C6CD;
|
$icon-button-color: $quaternary-content;
|
||||||
$roomtopic-color: #9e9e9e;
|
$roomtopic-color: #9e9e9e;
|
||||||
$eventtile-meta-color: $roomtopic-color;
|
$eventtile-meta-color: $roomtopic-color;
|
||||||
|
|
||||||
$composer-e2e-icon-color: #91A1C0;
|
$composer-e2e-icon-color: #91A1C0;
|
||||||
$header-divider-color: #91A1C0;
|
$header-divider-color: #91A1C0;
|
||||||
|
|
||||||
$toast-bg-color: $system-light;
|
$voipcall-plinth-color: $system;
|
||||||
$voipcall-plinth-color: $system-light;
|
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: $quinary-content;
|
||||||
$dialpad-button-bg-color: #e3e8f0;
|
$dialpad-button-bg-color: $quinary-content;
|
||||||
|
|
||||||
|
|
||||||
$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: #ffffff;
|
$roomlist-filter-active-bg-color: $background;
|
||||||
$roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
$roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
||||||
$roomlist-header-color: $tertiary-fg-color;
|
$roomlist-header-color: $tertiary-fg-color;
|
||||||
$roomsublist-divider-color: $primary-fg-color;
|
$roomsublist-divider-color: $primary-fg-color;
|
||||||
|
@ -194,7 +209,7 @@ $roomtile-selected-bg-color: #FFF;
|
||||||
|
|
||||||
$presence-online: $accent-color;
|
$presence-online: $accent-color;
|
||||||
$presence-away: #d9b072;
|
$presence-away: #d9b072;
|
||||||
$presence-offline: #E3E8F0;
|
$presence-offline: $quinary-content;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
@ -257,7 +272,7 @@ $lightbox-border-color: #ffffff;
|
||||||
|
|
||||||
// Tabbed views
|
// Tabbed views
|
||||||
$tab-label-fg-color: #45474a;
|
$tab-label-fg-color: #45474a;
|
||||||
$tab-label-active-fg-color: #ffffff;
|
$tab-label-active-fg-color: $background;
|
||||||
$tab-label-bg-color: transparent;
|
$tab-label-bg-color: transparent;
|
||||||
$tab-label-active-bg-color: $accent-color;
|
$tab-label-active-bg-color: $accent-color;
|
||||||
$tab-label-icon-bg-color: #454545;
|
$tab-label-icon-bg-color: #454545;
|
||||||
|
@ -267,9 +282,9 @@ $tab-label-active-icon-bg-color: $tab-label-active-fg-color;
|
||||||
$button-primary-fg-color: #ffffff;
|
$button-primary-fg-color: #ffffff;
|
||||||
$button-primary-bg-color: $accent-color;
|
$button-primary-bg-color: $accent-color;
|
||||||
$button-secondary-bg-color: $accent-fg-color;
|
$button-secondary-bg-color: $accent-fg-color;
|
||||||
$button-danger-fg-color: #ffffff;
|
$button-danger-fg-color: $background;
|
||||||
$button-danger-bg-color: $notice-primary-color;
|
$button-danger-bg-color: $notice-primary-color;
|
||||||
$button-danger-disabled-fg-color: #ffffff;
|
$button-danger-disabled-fg-color: $background;
|
||||||
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
|
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
|
||||||
$button-link-fg-color: $accent-color;
|
$button-link-fg-color: $accent-color;
|
||||||
$button-link-bg-color: transparent;
|
$button-link-bg-color: transparent;
|
||||||
|
@ -294,7 +309,7 @@ $memberstatus-placeholder-color: $muted-fg-color;
|
||||||
|
|
||||||
$authpage-bg-color: #2e3649;
|
$authpage-bg-color: #2e3649;
|
||||||
$authpage-modal-bg-color: rgba(245, 245, 245, 0.90);
|
$authpage-modal-bg-color: rgba(245, 245, 245, 0.90);
|
||||||
$authpage-body-bg-color: #ffffff;
|
$authpage-body-bg-color: $background;
|
||||||
$authpage-focus-bg-color: #dddddd;
|
$authpage-focus-bg-color: #dddddd;
|
||||||
$authpage-lang-color: #4e5054;
|
$authpage-lang-color: #4e5054;
|
||||||
$authpage-primary-color: #232f32;
|
$authpage-primary-color: #232f32;
|
||||||
|
@ -318,26 +333,26 @@ $kbd-border-color: $reaction-row-button-border-color;
|
||||||
|
|
||||||
$inverted-bg-color: #27303a;
|
$inverted-bg-color: #27303a;
|
||||||
$tooltip-timeline-bg-color: $inverted-bg-color;
|
$tooltip-timeline-bg-color: $inverted-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: $background;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: #27303a;
|
$interactive-tooltip-bg-color: #27303a;
|
||||||
$interactive-tooltip-fg-color: #ffffff;
|
$interactive-tooltip-fg-color: $background;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
$message-body-panel-bg-color: $quinary-content;
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $system-light;
|
$message-body-panel-icon-bg-color: $system;
|
||||||
|
|
||||||
// These two don't change between themes. They are the $warning-color, but we don't
|
// These two don't change between themes. They are the $warning-color, but we don't
|
||||||
// want custom themes to affect them by accident.
|
// want custom themes to affect them by accident.
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
$voice-record-live-circle-color: #ff4b55;
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
|
|
||||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
$voice-record-stop-border-color: $quinary-content;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
$voice-record-icon-color: $tertiary-fg-color;
|
$voice-record-icon-color: $tertiary-fg-color;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||||
|
@ -354,10 +369,10 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
$eventbubble-self-bg: #F0FBF8;
|
$eventbubble-self-bg: #F0FBF8;
|
||||||
$eventbubble-others-bg: $system-light;
|
$eventbubble-others-bg: $system;
|
||||||
$eventbubble-bg-hover: #FAFBFD;
|
$eventbubble-bg-hover: #FAFBFD;
|
||||||
$eventbubble-avatar-outline: $primary-bg-color;
|
$eventbubble-avatar-outline: $primary-bg-color;
|
||||||
$eventbubble-reply-color: #C1C6CD;
|
$eventbubble-reply-color: $quaternary-content;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import RoomViewStore from './stores/RoomViewStore';
|
|
||||||
import { EventSubscription } from 'fbemitter';
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
|
|
||||||
type Listener = (isActive: boolean) => void;
|
type Listener = (isActive: boolean) => void;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -85,6 +86,8 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||||
|
import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||||
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
|
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
|
||||||
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||||
import ToastStore from './stores/ToastStore';
|
import ToastStore from './stores/ToastStore';
|
||||||
|
@ -479,26 +482,44 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing:
|
case CallState.Ringing: {
|
||||||
|
const incomingCallPushRule = (
|
||||||
|
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
|
||||||
|
);
|
||||||
|
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||||
|
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
|
||||||
|
action.set_tweak === TweakName.Sound &&
|
||||||
|
action.value === "ring"
|
||||||
|
));
|
||||||
|
|
||||||
|
if (pushRuleEnabled && tweakSetToRing) {
|
||||||
this.play(AudioID.Ring);
|
this.play(AudioID.Ring);
|
||||||
|
} else {
|
||||||
|
this.silenceCall(call.callId);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CallState.InviteSent:
|
}
|
||||||
|
case CallState.InviteSent: {
|
||||||
this.play(AudioID.Ringback);
|
this.play(AudioID.Ringback);
|
||||||
break;
|
break;
|
||||||
case CallState.Ended:
|
}
|
||||||
{
|
case CallState.Ended: {
|
||||||
const hangupReason = call.hangupReason;
|
const hangupReason = call.hangupReason;
|
||||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||||
this.play(AudioID.Busy);
|
this.play(AudioID.Busy);
|
||||||
|
|
||||||
|
// Don't show a modal when we got rejected/the call was hung up
|
||||||
|
if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
let description;
|
let description;
|
||||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||||
title = _t("User Busy");
|
title = _t("User Busy");
|
||||||
description = _t("The user you called is busy.");
|
description = _t("The user you called is busy.");
|
||||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
} else {
|
||||||
title = _t("Call Failed");
|
title = _t("Call Failed");
|
||||||
description = _t("The call could not be established");
|
description = _t("The call could not be established");
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
|
||||||
return { width, height, img };
|
return { width, height, img };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimum size for image files before we generate a thumbnail for them.
|
||||||
|
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
|
||||||
|
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
|
||||||
|
// We don't apply these thresholds to video thumbnails as a poster image is always useful
|
||||||
|
// and videos tend to be much larger.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
||||||
*
|
*
|
||||||
|
@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
|
||||||
* @param {File} imageFile The image to read and thumbnail.
|
* @param {File} imageFile The image to read and thumbnail.
|
||||||
* @return {Promise} A promise that resolves with the attachment info.
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
*/
|
*/
|
||||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) {
|
||||||
let thumbnailType = "image/png";
|
let thumbnailType = "image/png";
|
||||||
if (imageFile.type === "image/jpeg") {
|
if (imageFile.type === "image/jpeg") {
|
||||||
thumbnailType = "image/jpeg";
|
thumbnailType = "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageInfo;
|
const imageElement = await loadImageElement(imageFile);
|
||||||
return loadImageElement(imageFile).then((r) => {
|
|
||||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||||
}).then((result) => {
|
const imageInfo = result.info;
|
||||||
imageInfo = result.info;
|
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||||
}).then((result) => {
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||||
imageInfo.thumbnail_url = result.url;
|
if (
|
||||||
imageInfo.thumbnail_file = result.file;
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
|
||||||
|
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
|
||||||
|
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
||||||
|
) {
|
||||||
|
delete imageInfo["thumbnail_info"];
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
|
||||||
|
imageInfo["thumbnail_url"] = uploadResult.url;
|
||||||
|
imageInfo["thumbnail_file"] = uploadResult.file;
|
||||||
return imageInfo;
|
return imageInfo;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -123,6 +123,19 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCallTime(delta: Date): string {
|
||||||
|
const hours = delta.getUTCHours();
|
||||||
|
const minutes = delta.getUTCMinutes();
|
||||||
|
const seconds = delta.getUTCSeconds();
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
if (hours) output += `${hours}h `;
|
||||||
|
if (minutes || output) output += `${minutes}m `;
|
||||||
|
if (seconds || output) output += `${seconds}s`;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||||
if (!nextEventDate || !prevEventDate) {
|
if (!nextEventDate || !prevEventDate) {
|
||||||
|
|
|
@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
handleHomeEnd?: boolean;
|
handleHomeEnd?: boolean;
|
||||||
|
handleUpDown?: boolean;
|
||||||
children(renderProps: {
|
children(renderProps: {
|
||||||
onKeyDownHandler(ev: React.KeyboardEvent);
|
onKeyDownHandler(ev: React.KeyboardEvent);
|
||||||
});
|
});
|
||||||
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, onKeyDown }) => {
|
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
|
||||||
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
||||||
activeRef: null,
|
activeRef: null,
|
||||||
refs: [],
|
refs: [],
|
||||||
|
@ -167,22 +168,51 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
||||||
const onKeyDownHandler = useCallback((ev) => {
|
const onKeyDownHandler = useCallback((ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
// Don't interfere with input default keydown behaviour
|
// Don't interfere with input default keydown behaviour
|
||||||
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||||
// check if we actually have any items
|
// check if we actually have any items
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
|
if (handleHomeEnd) {
|
||||||
handled = true;
|
handled = true;
|
||||||
// move focus to first item
|
// move focus to first item
|
||||||
if (context.state.refs.length > 0) {
|
if (context.state.refs.length > 0) {
|
||||||
context.state.refs[0].current.focus();
|
context.state.refs[0].current.focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Key.END:
|
case Key.END:
|
||||||
|
if (handleHomeEnd) {
|
||||||
handled = true;
|
handled = true;
|
||||||
// move focus to last item
|
// move focus to last item
|
||||||
if (context.state.refs.length > 0) {
|
if (context.state.refs.length > 0) {
|
||||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
if (handleUpDown) {
|
||||||
|
handled = true;
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||||
|
if (idx > 0) {
|
||||||
|
context.state.refs[idx - 1].current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
|
if (handleUpDown) {
|
||||||
|
handled = true;
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||||
|
if (idx < context.state.refs.length - 1) {
|
||||||
|
context.state.refs[idx + 1].current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
||||||
} else if (onKeyDown) {
|
} else if (onKeyDown) {
|
||||||
return onKeyDown(ev, context.state);
|
return onKeyDown(ev, context.state);
|
||||||
}
|
}
|
||||||
}, [context.state, onKeyDown, handleHomeEnd]);
|
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
|
||||||
|
|
||||||
return <RovingTabIndexContext.Provider value={context}>
|
return <RovingTabIndexContext.Provider value={context}>
|
||||||
{ children({ onKeyDownHandler }) }
|
{ children({ onKeyDownHandler }) }
|
||||||
|
|
|
@ -38,17 +38,9 @@ function makePlaybackWaveform(input: number[]): number[] {
|
||||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||||
const noiseWaveform = input.map(v => Math.abs(v));
|
const noiseWaveform = input.map(v => Math.abs(v));
|
||||||
|
|
||||||
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
|
||||||
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||||
|
|
||||||
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
|
||||||
// waveform. Most speech happens below the 0.5 mark.
|
|
||||||
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
|
||||||
|
|
||||||
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
|
||||||
// sensible. This is what we return to keep our contract of "values between zero and one".
|
|
||||||
return arrayRescale(filtered, 0, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playback extends EventEmitter implements IDestroyable {
|
export class Playback extends EventEmitter implements IDestroyable {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { IEncryptedFile } from "matrix-js-sdk/src/@types/event";
|
||||||
import { uploadFile } from "../ContentMessages";
|
import { uploadFile } from "../ContentMessages";
|
||||||
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||||
import { clamp } from "../utils/numbers";
|
import { clamp } from "../utils/numbers";
|
||||||
|
import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||||
|
|
||||||
const CHANNELS = 1; // stereo isn't important
|
const CHANNELS = 1; // stereo isn't important
|
||||||
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
||||||
|
@ -113,16 +114,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
});
|
});
|
||||||
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
||||||
|
|
||||||
// Set up our worklet. We use this for timing information and waveform analysis: the
|
|
||||||
// web audio API prefers this be done async to avoid holding the main thread with math.
|
|
||||||
const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
|
|
||||||
if (!mxRecorderWorkletPath) {
|
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
|
||||||
throw new Error("Unable to create recorder: no worklet script registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect our inputs and outputs
|
// Connect our inputs and outputs
|
||||||
if (this.recorderContext.audioWorklet) {
|
if (this.recorderContext.audioWorklet) {
|
||||||
|
// Set up our worklet. We use this for timing information and waveform analysis: the
|
||||||
|
// web audio API prefers this be done async to avoid holding the main thread with math.
|
||||||
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
||||||
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
||||||
this.recorderSource.connect(this.recorderWorklet);
|
this.recorderSource.connect(this.recorderWorklet);
|
||||||
|
|
|
@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
style={style}
|
style={style}
|
||||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||||
onWheel={onWheel}
|
onWheel={onWheel}
|
||||||
tabIndex={tabIndex}
|
// Firefox sometimes makes this element focusable due to
|
||||||
|
// overflow:scroll;, so force it out of tab order by default.
|
||||||
|
tabIndex={tabIndex ?? -1}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
|
||||||
SilencedChanged = "silenced_changed",
|
SilencedChanged = "silenced_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONNECTING_STATES = [
|
||||||
|
CallState.Connecting,
|
||||||
|
CallState.WaitLocalMedia,
|
||||||
|
CallState.CreateOffer,
|
||||||
|
CallState.CreateAnswer,
|
||||||
|
];
|
||||||
|
|
||||||
const SUPPORTED_STATES = [
|
const SUPPORTED_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
CallState.Connecting,
|
|
||||||
CallState.Ringing,
|
CallState.Ringing,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get selectAnswer(): MatrixEvent {
|
||||||
|
return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
|
||||||
|
}
|
||||||
|
|
||||||
public get isVoice(): boolean {
|
public get isVoice(): boolean {
|
||||||
const invite = this.invite;
|
const invite = this.invite;
|
||||||
if (!invite) return;
|
if (!invite) return;
|
||||||
|
@ -82,6 +92,11 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
return Boolean(this.reject);
|
return Boolean(this.reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get duration(): Date {
|
||||||
|
if (!this.hangup || !this.selectAnswer) return;
|
||||||
|
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there are only events from the other side - we missed the call
|
* Returns true if there are only events from the other side - we missed the call
|
||||||
*/
|
*/
|
||||||
|
@ -127,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setState = () => {
|
private setState = () => {
|
||||||
if (SUPPORTED_STATES.includes(this.call?.state)) {
|
if (CONNECTING_STATES.includes(this.call?.state)) {
|
||||||
|
this.state = CallState.Connecting;
|
||||||
|
} else if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||||
this.state = this.call.state;
|
this.state = this.call.state;
|
||||||
} else {
|
} else {
|
||||||
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
||||||
|
|
|
@ -41,6 +41,9 @@ import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { createSpaceFromCommunity } from "../../utils/space";
|
||||||
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
|
||||||
const LONG_DESC_PLACEHOLDER = _td(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
`<h1>HTML for your community's page</h1>
|
`<h1>HTML for your community's page</h1>
|
||||||
|
@ -399,6 +402,8 @@ class FeaturedUser extends React.Component {
|
||||||
const GROUP_JOINPOLICY_OPEN = "open";
|
const GROUP_JOINPOLICY_OPEN = "open";
|
||||||
const GROUP_JOINPOLICY_INVITE = "invite";
|
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||||
|
|
||||||
|
const UPGRADE_NOTICE_LS_KEY = "mx_hide_community_upgrade_notice";
|
||||||
|
|
||||||
@replaceableComponent("structures.GroupView")
|
@replaceableComponent("structures.GroupView")
|
||||||
export default class GroupView extends React.Component {
|
export default class GroupView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -422,6 +427,7 @@ export default class GroupView extends React.Component {
|
||||||
publicityBusy: false,
|
publicityBusy: false,
|
||||||
inviterProfile: null,
|
inviterProfile: null,
|
||||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||||
|
showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -807,6 +813,22 @@ export default class GroupView extends React.Component {
|
||||||
showGroupAddRoomDialog(this.props.groupId);
|
showGroupAddRoomDialog(this.props.groupId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_dismissUpgradeNotice = () => {
|
||||||
|
localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true");
|
||||||
|
this.setState({ showUpgradeNotice: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCreateSpaceClick = () => {
|
||||||
|
createSpaceFromCommunity(this._matrixClient, this.props.groupId);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onAdminsLinkClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.GroupMemberList,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_getGroupSection() {
|
_getGroupSection() {
|
||||||
const groupSettingsSectionClasses = classnames({
|
const groupSettingsSectionClasses = classnames({
|
||||||
"mx_GroupView_group": this.state.editing,
|
"mx_GroupView_group": this.state.editing,
|
||||||
|
@ -843,10 +865,46 @@ export default class GroupView extends React.Component {
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
</div> : <div />;
|
</div> : <div />;
|
||||||
|
|
||||||
|
let communitiesUpgradeNotice;
|
||||||
|
if (this.state.showUpgradeNotice) {
|
||||||
|
let text;
|
||||||
|
if (this.state.isUserPrivileged) {
|
||||||
|
text = _t("You can create a Space from this community <a>here</a>.", {}, {
|
||||||
|
a: sub => <AccessibleButton onClick={this._onCreateSpaceClick} kind="link">
|
||||||
|
{ sub }
|
||||||
|
</AccessibleButton>,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
text = _t("Ask the <a>admins</a> of this community to make it into a Space " +
|
||||||
|
"and keep a look out for the invite.", {}, {
|
||||||
|
a: sub => <AccessibleButton onClick={this._onAdminsLinkClick} kind="link">
|
||||||
|
{ sub }
|
||||||
|
</AccessibleButton>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
communitiesUpgradeNotice = <div className="mx_GroupView_spaceUpgradePrompt">
|
||||||
|
<h2>{ _t("Communities can now be made into Spaces") }</h2>
|
||||||
|
<p>
|
||||||
|
{ _t("Spaces are a new way to make a community, with new features coming.") }
|
||||||
|
|
||||||
|
{ text }
|
||||||
|
|
||||||
|
{ _t("Communities won't receive further updates.") }
|
||||||
|
</p>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_GroupView_spaceUpgradePrompt_close"
|
||||||
|
onClick={this._dismissUpgradeNotice}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className={groupSettingsSectionClasses}>
|
return <div className={groupSettingsSectionClasses}>
|
||||||
{ header }
|
{ header }
|
||||||
{ hostingSignup }
|
{ hostingSignup }
|
||||||
{ changeDelayWarning }
|
{ changeDelayWarning }
|
||||||
|
{ communitiesUpgradeNotice }
|
||||||
{ this._getJoinableNode() }
|
{ this._getJoinableNode() }
|
||||||
{ this._getLongDescriptionNode() }
|
{ this._getLongDescriptionNode() }
|
||||||
{ this._getRoomsNode() }
|
{ this._getRoomsNode() }
|
||||||
|
|
|
@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
<IndicatorScrollbar
|
<IndicatorScrollbar
|
||||||
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||||
verticalScrollsHorizontally={true}
|
verticalScrollsHorizontally={true}
|
||||||
// Firefox sometimes makes this element focusable due to
|
|
||||||
// overflow:scroll;, so force it out of tab order.
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
>
|
||||||
<RoomBreadcrumbs />
|
<RoomBreadcrumbs />
|
||||||
</IndicatorScrollbar>
|
</IndicatorScrollbar>
|
||||||
|
|
|
@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
|
const groupedEvents = [
|
||||||
|
EventType.RoomMember,
|
||||||
|
EventType.RoomThirdPartyInvite,
|
||||||
|
EventType.RoomServerAcl,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
];
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
|
@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
class MemberGrouper extends BaseGrouper {
|
class MemberGrouper extends BaseGrouper {
|
||||||
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
||||||
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
|
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
|
||||||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return membershipTypes.includes(ev.getType() as EventType);
|
return groupedEvents.includes(ev.getType() as EventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode, useMemo, useState } from "react";
|
import React, { ReactNode, KeyboardEvent, useMemo, useState, KeyboardEventHandler } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
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 { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
||||||
|
@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { Key } from "../../Keyboard";
|
||||||
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -80,6 +82,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
|
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -94,11 +97,21 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (joinedRoom) {
|
if (joinedRoom) {
|
||||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
button = <AccessibleButton
|
||||||
|
onClick={onPreviewClick}
|
||||||
|
kind="primary_outline"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("View") }
|
{ _t("View") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else if (onJoinClick) {
|
} else if (onJoinClick) {
|
||||||
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
button = <AccessibleButton
|
||||||
|
onClick={onJoinClick}
|
||||||
|
kind="primary"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("Join") }
|
{ _t("Join") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
@ -106,13 +119,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
let checkbox;
|
let checkbox;
|
||||||
if (onToggleClick) {
|
if (onToggleClick) {
|
||||||
if (hasPermissions) {
|
if (hasPermissions) {
|
||||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
|
||||||
} else {
|
} else {
|
||||||
checkbox = <TextWithTooltip
|
checkbox = <TextWithTooltip
|
||||||
tooltip={_t("You don't have permission")}
|
tooltip={_t("You don't have permission")}
|
||||||
onClick={ev => { ev.stopPropagation(); }}
|
onClick={ev => { ev.stopPropagation(); }}
|
||||||
>
|
>
|
||||||
<StyledCheckbox disabled={true} />
|
<StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
|
||||||
</TextWithTooltip>;
|
</TextWithTooltip>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,8 +185,9 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
let childToggle;
|
let childToggle: JSX.Element;
|
||||||
let childSection;
|
let childSection: JSX.Element;
|
||||||
|
let onKeyDown: KeyboardEventHandler;
|
||||||
if (children) {
|
if (children) {
|
||||||
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
||||||
childToggle = <div
|
childToggle = <div
|
||||||
|
@ -185,25 +199,74 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
toggleShowChildren();
|
toggleShowChildren();
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
if (showChildren) {
|
if (showChildren) {
|
||||||
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
|
const onChildrenKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ARROW_LEFT) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
ref.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
childSection = <div
|
||||||
|
className="mx_SpaceRoomDirectory_subspace_children"
|
||||||
|
onKeyDown={onChildrenKeyDown}
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
{ children }
|
{ children }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown = (e) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case Key.ARROW_LEFT:
|
||||||
|
if (showChildren) {
|
||||||
|
handled = true;
|
||||||
|
toggleShowChildren();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_RIGHT:
|
||||||
|
handled = true;
|
||||||
|
if (showChildren) {
|
||||||
|
const childSection = ref.current?.nextElementSibling;
|
||||||
|
childSection?.querySelector<HTMLDivElement>(".mx_SpaceRoomDirectory_roomTile")?.focus();
|
||||||
|
} else {
|
||||||
|
toggleShowChildren();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
if (handled) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li
|
||||||
|
className="mx_SpaceRoomDirectory_roomTileWrapper"
|
||||||
|
role="treeitem"
|
||||||
|
aria-expanded={children ? showChildren : undefined}
|
||||||
|
>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||||
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
inputRef={ref}
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
{ childToggle }
|
{ childToggle }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ childSection }
|
{ childSection }
|
||||||
</>;
|
</li>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
||||||
|
@ -414,6 +477,15 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (ev: KeyboardEvent, state: IState) => {
|
||||||
|
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) {
|
||||||
|
state.refs[0]?.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO loading state/error state
|
||||||
|
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||||
|
{ ({ onKeyDownHandler }) => {
|
||||||
let content;
|
let content;
|
||||||
if (roomsMap) {
|
if (roomsMap) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||||
|
@ -429,9 +501,13 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let manageButtons;
|
let manageButtons;
|
||||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
if (space.getMyMembership() === "join" &&
|
||||||
|
space.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
||||||
|
) {
|
||||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
return [
|
||||||
|
...selected.get(parentId).values(),
|
||||||
|
].map(childId => [parentId, childId]) as [string, string][];
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||||
|
@ -563,7 +639,12 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||||
{ error }
|
{ error }
|
||||||
</div> }
|
</div> }
|
||||||
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
<AutoHideScrollbar
|
||||||
|
className="mx_SpaceRoomDirectory_list"
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
role="tree"
|
||||||
|
aria-label={_t("Space")}
|
||||||
|
>
|
||||||
{ results }
|
{ results }
|
||||||
{ children }
|
{ children }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
|
@ -572,18 +653,20 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
content = <Spinner />;
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO loading state/error state
|
|
||||||
return <>
|
return <>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_SpaceRoomDirectory_search mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search names and descriptions")}
|
placeholder={_t("Search names and descriptions")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
initialValue={initialText}
|
initialValue={initialText}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ content }
|
{ content }
|
||||||
</>;
|
</>;
|
||||||
|
} }
|
||||||
|
</RovingTabIndexProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -74,6 +74,10 @@ import { BetaPill } from "../views/beta/BetaCard";
|
||||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
||||||
|
import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
|
||||||
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -158,7 +162,33 @@ const onBetaClick = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
// XXX: temporary community migration component
|
||||||
|
const GroupTile = ({ groupId }: { groupId: string }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
|
||||||
|
|
||||||
|
if (!groupSummary) return <Spinner />;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={groupId}
|
||||||
|
groupName={groupSummary.profile.name}
|
||||||
|
groupAvatarUrl={groupSummary.profile.avatar_url}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
resizeMethod='crop'
|
||||||
|
/>
|
||||||
|
{ groupSummary.profile.name }
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ISpacePreviewProps {
|
||||||
|
space: Room;
|
||||||
|
onJoinButtonClicked(): void;
|
||||||
|
onRejectButtonClicked(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
|
|
||||||
|
@ -192,11 +222,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
|
|
||||||
if (inviteSender) {
|
if (inviteSender) {
|
||||||
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||||
<MemberAvatar member={inviter} width={32} height={32} />
|
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_SpaceRoomView_preview_inviter_name">
|
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||||
{ _t("<inviter/> invites you", {}, {
|
{ _t("<inviter/> invites you", {}, {
|
||||||
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||||
|
@ -270,8 +300,18 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let migratedCommunitySection: JSX.Element;
|
||||||
|
const createContent = space.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
|
||||||
|
if (createContent[CreateEventField]) {
|
||||||
|
migratedCommunitySection = <div className="mx_SpaceRoomView_preview_migratedCommunity">
|
||||||
|
{ _t("Created from <Community />", {}, {
|
||||||
|
Community: () => <GroupTile groupId={createContent[CreateEventField]} />,
|
||||||
|
}) }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_SpaceRoomView_preview">
|
return <div className="mx_SpaceRoomView_preview">
|
||||||
<BetaPill onClick={onBetaClick} />
|
{ migratedCommunitySection }
|
||||||
{ inviterSection }
|
{ inviterSection }
|
||||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||||
<h1 className="mx_SpaceRoomView_preview_name">
|
<h1 className="mx_SpaceRoomView_preview_name">
|
||||||
|
|
|
@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||||
|
|
||||||
|
const roomId = this.props.timelineSet.room.roomId;
|
||||||
|
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
|
||||||
|
|
||||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||||
this.props.timelineSet.room.roomId,
|
this.props.timelineSet.room.roomId,
|
||||||
'rm', this.state.readMarkerEventId,
|
'rm', this.state.readMarkerEventId,
|
||||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||||
|
' hidden:' + hiddenRR,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get().setRoomReadMarkers(
|
MatrixClientPeg.get().setRoomReadMarkers(
|
||||||
this.props.timelineSet.room.roomId,
|
roomId,
|
||||||
this.state.readMarkerEventId,
|
this.state.readMarkerEventId,
|
||||||
lastReadEvent, // Could be null, in which case no RR is sent
|
lastReadEvent, // Could be null, in which case no RR is sent
|
||||||
{},
|
{ hidden: hiddenRR },
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||||
|
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { arrayFastResample } from "../../../utils/arrays";
|
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||||
import { percentageOf } from "../../../utils/numbers";
|
|
||||||
import Waveform from "./Waveform";
|
import Waveform from "./Waveform";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
|
|
||||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
waveform: [],
|
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||||
// The incoming data is between zero and one, but typically even screaming into a
|
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
|
||||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
|
||||||
// user.
|
|
||||||
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
|
|
||||||
this.scheduledUpdate.mark();
|
this.scheduledUpdate.mark();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import * as React from "react";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import { createRef } from "react";
|
||||||
|
import AccessibleButton, { ButtonEvent } 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";
|
||||||
|
@ -32,6 +33,8 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.DialpadContextMenu")
|
@replaceableComponent("views.context_menus.DialpadContextMenu")
|
||||||
export default class DialpadContextMenu extends React.Component<IProps, IState> {
|
export default class DialpadContextMenu extends React.Component<IProps, IState> {
|
||||||
|
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -40,9 +43,16 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onDigitPress = (digit) => {
|
onDigitPress = (digit: string, ev: ButtonEvent) => {
|
||||||
this.props.call.sendDtmfDigit(digit);
|
this.props.call.sendDtmfDigit(digit);
|
||||||
this.setState({ value: this.state.value + digit });
|
this.setState({ value: this.state.value + digit });
|
||||||
|
|
||||||
|
// Keep the number field focused so that keyboard entry is still available
|
||||||
|
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||||
|
// i.e someone using keyboard navigation.
|
||||||
|
if (ev.type === "click") {
|
||||||
|
this.numberEntryFieldRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancelClick = () => {
|
onCancelClick = () => {
|
||||||
|
@ -68,6 +78,7 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_header">
|
<div className="mx_DialPadContextMenu_header">
|
||||||
<Field
|
<Field
|
||||||
|
ref={this.numberEntryFieldRef}
|
||||||
className="mx_DialPadContextMenu_dialled"
|
className="mx_DialPadContextMenu_dialled"
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { MenuItem } from "../../structures/ContextMenu";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||||
|
import { createSpaceFromCommunity } from "../../../utils/space";
|
||||||
|
import GroupStore from "../../../stores/GroupStore";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||||
export default class TagTileContextMenu extends React.Component {
|
export default class TagTileContextMenu extends React.Component {
|
||||||
|
@ -49,6 +51,11 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_onCreateSpaceClick = () => {
|
||||||
|
createSpaceFromCommunity(this.context, this.props.tag);
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
_onMoveUp = () => {
|
_onMoveUp = () => {
|
||||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
@ -77,6 +84,16 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let createSpaceOption;
|
||||||
|
if (GroupStore.isUserPrivileged(this.props.tag)) {
|
||||||
|
createSpaceOption = <>
|
||||||
|
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||||
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_createSpace" onClick={this._onCreateSpaceClick}>
|
||||||
|
{ _t("Create Space") }
|
||||||
|
</MenuItem>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||||
{ _t('View Community') }
|
{ _t('View Community') }
|
||||||
|
@ -88,6 +105,7 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||||
{ _t("Unpin") }
|
{ _t("Unpin") }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{ createSpaceOption }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,8 +116,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
|
||||||
opts.parentSpace = this.props.parentSpace;
|
opts.parentSpace = this.props.parentSpace;
|
||||||
|
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||||
opts.joinRule = JoinRule.Restricted;
|
opts.joinRule = JoinRule.Restricted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
/*
|
||||||
|
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 React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||||
|
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
|
import { GroupMember } from "../right_panel/UserInfo";
|
||||||
|
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
|
||||||
|
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
|
||||||
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import InfoDialog from "./InfoDialog";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { UserTab } from "./UserSettingsDialog";
|
||||||
|
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
groupId: string;
|
||||||
|
onFinished(spaceId?: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateEventField = "io.element.migrated_from_community";
|
||||||
|
|
||||||
|
interface IGroupRoom {
|
||||||
|
displayname: string;
|
||||||
|
name?: string;
|
||||||
|
roomId: string;
|
||||||
|
canonicalAlias?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
topic?: string;
|
||||||
|
numJoinedMembers?: number;
|
||||||
|
worldReadable?: boolean;
|
||||||
|
guestCanJoin?: boolean;
|
||||||
|
isPublic?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IGroupSummary {
|
||||||
|
profile: {
|
||||||
|
avatar_url?: string;
|
||||||
|
is_openly_joinable?: boolean;
|
||||||
|
is_public?: boolean;
|
||||||
|
long_description: string;
|
||||||
|
name: string;
|
||||||
|
short_description: string;
|
||||||
|
};
|
||||||
|
rooms_section: {
|
||||||
|
rooms: unknown[];
|
||||||
|
categories: Record<string, unknown>;
|
||||||
|
total_room_count_estimate: number;
|
||||||
|
};
|
||||||
|
user: {
|
||||||
|
is_privileged: boolean;
|
||||||
|
is_public: boolean;
|
||||||
|
is_publicised: boolean;
|
||||||
|
membership: string;
|
||||||
|
};
|
||||||
|
users_section: {
|
||||||
|
users: unknown[];
|
||||||
|
roles: Record<string, unknown>;
|
||||||
|
total_user_count_estimate: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const spaceNameField = useRef<Field>();
|
||||||
|
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
|
||||||
|
const spaceAliasField = useRef<RoomAliasField>();
|
||||||
|
const [topic, setTopic] = useState("");
|
||||||
|
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
|
||||||
|
|
||||||
|
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupSummary) {
|
||||||
|
setName(groupSummary.profile.name || "");
|
||||||
|
setTopic(groupSummary.profile.short_description || "");
|
||||||
|
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [groupSummary]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCreateSpaceClick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (busy) return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setBusy(true);
|
||||||
|
|
||||||
|
// require & validate the space name field
|
||||||
|
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||||
|
setBusy(false);
|
||||||
|
spaceNameField.current.focus();
|
||||||
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// validate the space name alias field but do not require it
|
||||||
|
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||||
|
setBusy(false);
|
||||||
|
spaceAliasField.current.focus();
|
||||||
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rooms, members, invitedMembers] = await Promise.all([
|
||||||
|
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||||
|
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
|
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const viaMap = new Map<string, string[]>();
|
||||||
|
for (const { roomId, canonicalAlias } of rooms) {
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (room) {
|
||||||
|
viaMap.set(roomId, calculateRoomVia(room));
|
||||||
|
} else if (canonicalAlias) {
|
||||||
|
try {
|
||||||
|
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
|
||||||
|
viaMap.set(roomId, servers);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to resolve alias during community migration", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viaMap.get(roomId)?.length) {
|
||||||
|
// XXX: lets guess the via, this might end up being incorrect.
|
||||||
|
const str = canonicalAlias || roomId;
|
||||||
|
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||||
|
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||||
|
creation_content: {
|
||||||
|
[CreateEventField]: groupId,
|
||||||
|
},
|
||||||
|
initial_state: rooms.map(({ roomId }) => ({
|
||||||
|
type: EventType.SpaceChild,
|
||||||
|
state_key: roomId,
|
||||||
|
content: {
|
||||||
|
via: viaMap.get(roomId) || [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
||||||
|
}, {
|
||||||
|
andView: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eagerly remove it from the community panel
|
||||||
|
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||||
|
|
||||||
|
// don't bother awaiting this, as we don't hugely care if it fails
|
||||||
|
cli.setGroupProfile(groupId, {
|
||||||
|
...groupSummary.profile,
|
||||||
|
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
|
||||||
|
_t("This community has been upgraded into a Space") + `</h1></a><br />`
|
||||||
|
+ groupSummary.profile.long_description,
|
||||||
|
} as IGroupSummary["profile"]).catch(e => {
|
||||||
|
console.warn("Failed to update community profile during migration", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
onFinished(roomId);
|
||||||
|
|
||||||
|
const onSpaceClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPreferencesClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Preferences,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let spacesDisabledCopy;
|
||||||
|
if (!SpaceStore.spacesEnabled) {
|
||||||
|
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
|
||||||
|
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.createDialog(InfoDialog, {
|
||||||
|
title: _t("Space created"),
|
||||||
|
description: <>
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
|
||||||
|
<p>
|
||||||
|
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
|
||||||
|
"been invited to it.", {}, {
|
||||||
|
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
|
||||||
|
{ name }
|
||||||
|
</AccessibleButton>,
|
||||||
|
}) }
|
||||||
|
|
||||||
|
{ spacesDisabledCopy }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t("To create a Space from another community, just pick the community in Preferences.") }
|
||||||
|
</p>
|
||||||
|
</>,
|
||||||
|
button: _t("Preferences"),
|
||||||
|
onFinished: (openPreferences: boolean) => {
|
||||||
|
if (openPreferences) {
|
||||||
|
onPreferencesClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let footer;
|
||||||
|
if (error) {
|
||||||
|
footer = <>
|
||||||
|
<img src={require("../../../../res/img/element-icons/warning-badge.svg")} height="24" width="24" alt="" />
|
||||||
|
|
||||||
|
<span className="mx_CreateSpaceFromCommunityDialog_error">
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
|
||||||
|
{ _t("Retry") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
footer = <>
|
||||||
|
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
||||||
|
{ busy ? _t("Creating...") : _t("Create Space") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Create Space from community")}
|
||||||
|
className="mx_CreateSpaceFromCommunityDialog"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||||
|
<p>
|
||||||
|
{ _t("A link to the Space will be put in your community description.") }
|
||||||
|
|
||||||
|
{ _t("All rooms will be added and all community members will be invited.") }
|
||||||
|
</p>
|
||||||
|
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
|
||||||
|
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<SpaceCreateForm
|
||||||
|
busy={busy}
|
||||||
|
onSubmit={onCreateSpaceClick}
|
||||||
|
avatarUrl={groupSummary.profile.avatar_url
|
||||||
|
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
setAvatar={setAvatar}
|
||||||
|
name={name}
|
||||||
|
setName={setName}
|
||||||
|
nameFieldRef={spaceNameField}
|
||||||
|
topic={topic}
|
||||||
|
setTopic={setTopic}
|
||||||
|
alias={alias}
|
||||||
|
setAlias={setAlias}
|
||||||
|
showAliasField={joinRule === JoinRule.Public}
|
||||||
|
aliasFieldRef={spaceAliasField}
|
||||||
|
>
|
||||||
|
<p>{ _t("This description will be shown to people when they view your space") }</p>
|
||||||
|
<JoinRuleDropdown
|
||||||
|
label={_t("Space visibility")}
|
||||||
|
labelInvite={_t("Private space (invite only)")}
|
||||||
|
labelPublic={_t("Public space")}
|
||||||
|
value={joinRule}
|
||||||
|
onChange={setJoinRule}
|
||||||
|
/>
|
||||||
|
<p>{ joinRule === JoinRule.Public
|
||||||
|
? _t("Open space for anyone, best for communities")
|
||||||
|
: _t("Invite only, best for yourself or teams")
|
||||||
|
}</p>
|
||||||
|
{ joinRule !== JoinRule.Public &&
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||||
|
}
|
||||||
|
</SpaceCreateForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_CreateSpaceFromCommunityDialog_footer">
|
||||||
|
{ footer }
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateSpaceFromCommunityDialog;
|
||||||
|
|
|
@ -16,8 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
|
@ -27,8 +26,7 @@ import { BetaPill } from "../beta/BetaCard";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import RoomAliasField from "../elements/RoomAliasField";
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
import { SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||||
import createRoom from "../../../createRoom";
|
|
||||||
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
|
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
|
||||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||||
|
|
||||||
|
@ -81,28 +79,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createRoom({
|
await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace });
|
||||||
createOpts: {
|
|
||||||
preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat,
|
|
||||||
name,
|
|
||||||
power_level_content_override: {
|
|
||||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
|
||||||
events_default: 100,
|
|
||||||
...joinRule === JoinRule.Public ? { invite: 0 } : {},
|
|
||||||
},
|
|
||||||
room_alias_name: joinRule === JoinRule.Public && alias
|
|
||||||
? alias.substr(1, alias.indexOf(":") - 1)
|
|
||||||
: undefined,
|
|
||||||
topic,
|
|
||||||
},
|
|
||||||
avatar,
|
|
||||||
roomType: RoomType.Space,
|
|
||||||
parentSpace,
|
|
||||||
spinner: false,
|
|
||||||
encryption: false,
|
|
||||||
andView: true,
|
|
||||||
inlineErrors: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
onFinished(true);
|
onFinished(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import { getAddressType } from "../../../UserAddress";
|
import { getAddressType } from "../../../UserAddress";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
import { compare } from '../../../utils/strings';
|
import { compare } from '../../../utils/strings';
|
||||||
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
@ -394,6 +394,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
private closeCopiedTooltip: () => void;
|
private closeCopiedTooltip: () => void;
|
||||||
private debounceTimer: number = 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 numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -1283,13 +1284,27 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({ dialPadValue: ev.currentTarget.value });
|
this.setState({ dialPadValue: ev.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDigitPress = digit => {
|
private onDigitPress = (digit: string, ev: ButtonEvent) => {
|
||||||
this.setState({ dialPadValue: this.state.dialPadValue + digit });
|
this.setState({ dialPadValue: this.state.dialPadValue + digit });
|
||||||
|
|
||||||
|
// Keep the number field focused so that keyboard entry is still available
|
||||||
|
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||||
|
// i.e someone using keyboard navigation.
|
||||||
|
if (ev.type === "click") {
|
||||||
|
this.numberEntryFieldRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDeletePress = () => {
|
private onDeletePress = (ev: ButtonEvent) => {
|
||||||
if (this.state.dialPadValue.length === 0) return;
|
if (this.state.dialPadValue.length === 0) return;
|
||||||
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
|
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
|
||||||
|
|
||||||
|
// Keep the number field focused so that keyboard entry is still available
|
||||||
|
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||||
|
// i.e someone using keyboard navigation.
|
||||||
|
if (ev.type === "click") {
|
||||||
|
this.numberEntryFieldRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTabChange = (tabId: TabId) => {
|
private onTabChange = (tabId: TabId) => {
|
||||||
|
@ -1543,6 +1558,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
let dialPadField;
|
let dialPadField;
|
||||||
if (this.state.dialPadValue.length !== 0) {
|
if (this.state.dialPadValue.length !== 0) {
|
||||||
dialPadField = <Field
|
dialPadField = <Field
|
||||||
|
ref={this.numberEntryFieldRef}
|
||||||
className="mx_InviteDialog_dialPadField"
|
className="mx_InviteDialog_dialPadField"
|
||||||
id="dialpad_number"
|
id="dialpad_number"
|
||||||
value={this.state.dialPadValue}
|
value={this.state.dialPadValue}
|
||||||
|
@ -1552,6 +1568,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
dialPadField = <Field
|
dialPadField = <Field
|
||||||
|
ref={this.numberEntryFieldRef}
|
||||||
className="mx_InviteDialog_dialPadField"
|
className="mx_InviteDialog_dialPadField"
|
||||||
id="dialpad_number"
|
id="dialpad_number"
|
||||||
value={this.state.dialPadValue}
|
value={this.state.dialPadValue}
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
UserTab.Preferences,
|
UserTab.Preferences,
|
||||||
_td("Preferences"),
|
_td("Preferences"),
|
||||||
"mx_UserSettingsDialog_preferencesIcon",
|
"mx_UserSettingsDialog_preferencesIcon",
|
||||||
<PreferencesUserSettingsTab />,
|
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||||
));
|
));
|
||||||
|
|
||||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||||
|
|
|
@ -67,7 +67,9 @@ export default function AccessibleButton({
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
if (!disabled) {
|
if (disabled) {
|
||||||
|
newProps["aria-disabled"] = true;
|
||||||
|
} else {
|
||||||
newProps.onClick = onClick;
|
newProps.onClick = onClick;
|
||||||
// We need to consume enter onKeyDown and space onKeyUp
|
// We need to consume enter onKeyDown and space onKeyUp
|
||||||
// otherwise we are risking also activating other keyboard focusable elements
|
// otherwise we are risking also activating other keyboard focusable elements
|
||||||
|
@ -118,7 +120,7 @@ export default function AccessibleButton({
|
||||||
);
|
);
|
||||||
|
|
||||||
// React.createElement expects InputHTMLAttributes
|
// React.createElement expects InputHTMLAttributes
|
||||||
return React.createElement(element, restProps, children);
|
return React.createElement(element, newProps, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessibleButton.defaultProps = {
|
AccessibleButton.defaultProps = {
|
||||||
|
|
|
@ -62,10 +62,10 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Set all this into the initial state
|
// Set all this into the initial state
|
||||||
this.state = {
|
this.state = {
|
||||||
...urlInfo,
|
|
||||||
roomMember,
|
|
||||||
isWrapped: null,
|
|
||||||
widgetDomain: null,
|
widgetDomain: null,
|
||||||
|
isWrapped: null,
|
||||||
|
roomMember,
|
||||||
|
...urlInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Callback for when the button is pressed
|
// Callback for when the button is pressed
|
||||||
onBackspacePress: () => void;
|
onBackspacePress: (ev: ButtonEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
|
export default class DialPadBackspaceButton extends React.PureComponent<IProps> {
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -178,7 +178,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
this.ignoreEvent = ev;
|
this.ignoreEvent = ev;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInputClick = (ev: React.MouseEvent) => {
|
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||||
if (this.props.disabled) return;
|
if (this.props.disabled) return;
|
||||||
|
|
||||||
if (!this.state.expanded) {
|
if (!this.state.expanded) {
|
||||||
|
@ -186,6 +186,10 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
expanded: true,
|
expanded: true,
|
||||||
});
|
});
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||||
|
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
|
||||||
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,7 +208,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
this.props.onOptionChange(dropdownKey);
|
this.props.onOptionChange(dropdownKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInputKeyDown = (e: React.KeyboardEvent) => {
|
private onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
|
|
||||||
// These keys don't generate keypress events and so needs to be on keyup
|
// These keys don't generate keypress events and so needs to be on keyup
|
||||||
|
@ -269,7 +273,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
private prevOption(optionKey: string): string {
|
private prevOption(optionKey: string): string {
|
||||||
const keys = Object.keys(this.childrenByKey);
|
const keys = Object.keys(this.childrenByKey);
|
||||||
const index = keys.indexOf(optionKey);
|
const index = keys.indexOf(optionKey);
|
||||||
return keys[(index - 1) % keys.length];
|
return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollIntoView(node: Element) {
|
private scrollIntoView(node: Element) {
|
||||||
|
@ -320,7 +324,6 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="mx_Dropdown_option"
|
className="mx_Dropdown_option"
|
||||||
onKeyDown={this.onInputKeyDown}
|
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
value={this.state.searchQuery}
|
value={this.state.searchQuery}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
|
@ -329,6 +332,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
aria-owns={`${this.props.id}_listbox`}
|
aria-owns={`${this.props.id}_listbox`}
|
||||||
aria-disabled={this.props.disabled}
|
aria-disabled={this.props.disabled}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -361,13 +365,14 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
||||||
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_Dropdown_input mx_no_textinput"
|
className="mx_Dropdown_input mx_no_textinput"
|
||||||
onClick={this.onInputClick}
|
onClick={this.onAccessibleButtonClick}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={this.state.expanded}
|
aria-expanded={this.state.expanded}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
inputRef={this.buttonRef}
|
inputRef={this.buttonRef}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
aria-describedby={`${this.props.id}_value`}
|
aria-describedby={`${this.props.id}_value`}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{ currentValue }
|
{ currentValue }
|
||||||
<span className="mx_Dropdown_arrow" />
|
<span className="mx_Dropdown_arrow" />
|
||||||
|
|
|
@ -34,7 +34,7 @@ interface IProps {
|
||||||
// The list of room members for which to show avatars next to the summary
|
// The list of room members for which to show avatars next to the summary
|
||||||
summaryMembers?: RoomMember[];
|
summaryMembers?: RoomMember[];
|
||||||
// The text to show as the summary of this event list
|
// The text to show as the summary of this event list
|
||||||
summaryText?: string;
|
summaryText?: string | JSX.Element;
|
||||||
// An array of EventTiles to render when expanded
|
// An array of EventTiles to render when expanded
|
||||||
children: ReactNode[];
|
children: ReactNode[];
|
||||||
// Called when the event list expansion is toggled
|
// Called when the event list expansion is toggled
|
||||||
|
|
|
@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||||
import EventListSummary from "./EventListSummary";
|
import EventListSummary from "./EventListSummary";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||||
|
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||||
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
|
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import { Layout } from '../../../settings/Layout';
|
import { Layout } from '../../../settings/Layout';
|
||||||
|
|
||||||
|
const onPinnedMessagesClick = (): void => {
|
||||||
|
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.PinnedMessages,
|
||||||
|
allowClose: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
||||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||||
summaryLength?: number;
|
summaryLength?: number;
|
||||||
|
@ -60,6 +76,7 @@ enum TransitionType {
|
||||||
ChangedAvatar = "changed_avatar",
|
ChangedAvatar = "changed_avatar",
|
||||||
NoChange = "no_change",
|
NoChange = "no_change",
|
||||||
ServerAcl = "server_acl",
|
ServerAcl = "server_acl",
|
||||||
|
ChangedPins = "pinned_messages"
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEP = ",";
|
const SEP = ",";
|
||||||
|
@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* `Object.keys(eventAggregates)`.
|
* `Object.keys(eventAggregates)`.
|
||||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||||
*/
|
*/
|
||||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
private generateSummary(
|
||||||
|
eventAggregates: Record<string, string[]>,
|
||||||
|
orderedTransitionSequences: string[],
|
||||||
|
): string | JSX.Element {
|
||||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||||
const userNames = eventAggregates[transitions];
|
const userNames = eventAggregates[transitions];
|
||||||
const nameList = this.renderNameList(userNames);
|
const nameList = this.renderNameList(userNames);
|
||||||
|
@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return summaries.join(", ");
|
return jsxJoin(summaries, ", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||||
* @returns {string} the written Human Readable equivalent of the transition.
|
* @returns {string} the written Human Readable equivalent of the transition.
|
||||||
*/
|
*/
|
||||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
private static getDescriptionForTransition(
|
||||||
|
t: TransitionType,
|
||||||
|
userCount: number,
|
||||||
|
repeats: number,
|
||||||
|
): string | JSX.Element {
|
||||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||||
// are there only to show translators to non-English languages
|
// are there only to show translators to non-English languages
|
||||||
// that the verb is conjugated to plural or singular Subject.
|
// that the verb is conjugated to plural or singular Subject.
|
||||||
|
@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
{ severalUsers: "", count: repeats })
|
{ severalUsers: "", count: repeats })
|
||||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||||
break;
|
break;
|
||||||
|
case "pinned_messages":
|
||||||
|
res = (userCount > 1)
|
||||||
|
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
|
{ severalUsers: "", count: repeats },
|
||||||
|
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
|
||||||
|
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||||
|
{ oneUser: "", count: repeats },
|
||||||
|
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* if a transition is not recognised.
|
* if a transition is not recognised.
|
||||||
*/
|
*/
|
||||||
private static getTransition(e: IUserEvents): TransitionType {
|
private static getTransition(e: IUserEvents): TransitionType {
|
||||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
const type = e.mxEvent.getType();
|
||||||
|
|
||||||
|
if (type === EventType.RoomThirdPartyInvite) {
|
||||||
// Handle 3pid invites the same as invites so they get bundled together
|
// Handle 3pid invites the same as invites so they get bundled together
|
||||||
if (!isValid3pidInvite(e.mxEvent)) {
|
if (!isValid3pidInvite(e.mxEvent)) {
|
||||||
return TransitionType.InviteWithdrawal;
|
return TransitionType.InviteWithdrawal;
|
||||||
}
|
}
|
||||||
return TransitionType.Invited;
|
return TransitionType.Invited;
|
||||||
}
|
} else if (type === EventType.RoomServerAcl) {
|
||||||
|
|
||||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
|
||||||
return TransitionType.ServerAcl;
|
return TransitionType.ServerAcl;
|
||||||
|
} else if (type === EventType.RoomPinnedEvents) {
|
||||||
|
return TransitionType.ChangedPins;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (e.mxEvent.getContent().membership) {
|
switch (e.mxEvent.getContent().membership) {
|
||||||
|
@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
// Object mapping user IDs to an array of IUserEvents
|
// Object mapping user IDs to an array of IUserEvents
|
||||||
const userEvents: Record<string, IUserEvents[]> = {};
|
const userEvents: Record<string, IUserEvents[]> = {};
|
||||||
eventsToRender.forEach((e, index) => {
|
eventsToRender.forEach((e, index) => {
|
||||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
const type = e.getType();
|
||||||
|
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
|
||||||
// Initialise a user's events
|
// Initialise a user's events
|
||||||
if (!userEvents[userId]) {
|
if (!userEvents[userId]) {
|
||||||
userEvents[userId] = [];
|
userEvents[userId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.getType() === 'm.room.server_acl') {
|
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||||
latestUserAvatarMember.set(userId, e.sender);
|
latestUserAvatarMember.set(userId, e.sender);
|
||||||
} else if (e.target) {
|
} else if (e.target) {
|
||||||
latestUserAvatarMember.set(userId, e.target);
|
latestUserAvatarMember.set(userId, e.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayName = userId;
|
let displayName = userId;
|
||||||
if (e.getType() === 'm.room.third_party_invite') {
|
if (type === EventType.RoomThirdPartyInvite) {
|
||||||
displayName = e.getContent().display_name;
|
displayName = e.getContent().display_name;
|
||||||
} else if (e.getType() === 'm.room.server_acl') {
|
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||||
displayName = e.sender.name;
|
displayName = e.sender.name;
|
||||||
} else if (e.target) {
|
} else if (e.target) {
|
||||||
displayName = e.target.name;
|
displayName = e.target.name;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
|
import { formatCallTime } from "../../../DateUtils";
|
||||||
|
|
||||||
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
|
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
|
||||||
|
|
||||||
|
@ -161,9 +162,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
// Also, if we don't have a reason
|
// Also, if we don't have a reason
|
||||||
|
const duration = this.props.callEventGrouper.duration;
|
||||||
|
let text = _t("Call ended");
|
||||||
|
if (duration) {
|
||||||
|
text += " • " + formatCallTime(duration);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("Call ended") }
|
{ text }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
|
|
|
@ -25,12 +25,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { Media, mediaFromContent } from "../../../customisations/Media";
|
||||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl?: string;
|
decryptedUrl?: string;
|
||||||
|
@ -157,19 +159,21 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
// this is only used as a fallback in case content.info.w/h is missing
|
// this is only used as a fallback in case content.info.w/h is missing
|
||||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||||
};
|
};
|
||||||
|
|
||||||
protected getContentUrl(): string {
|
protected getContentUrl(): string {
|
||||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
if (this.media.isEncrypted) {
|
||||||
if (media.isEncrypted) {
|
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return media.srcHttp;
|
return this.media.srcHttp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get media(): Media {
|
||||||
|
return mediaFromContent(this.props.mxEvent.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
protected getThumbUrl(): string {
|
protected getThumbUrl(): string {
|
||||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||||
|
@ -347,12 +351,21 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
className="mx_MImageBody_thumbnail"
|
className="mx_MImageBody_thumbnail"
|
||||||
src={thumbUrl}
|
src={thumbUrl}
|
||||||
ref={this.image}
|
ref={this.image}
|
||||||
style={{ maxWidth: `min(100%, ${maxWidth}px)` }}
|
// Force the image to be the full size of the container, even if the
|
||||||
|
// pixel size is smaller. The problem here is that we don't know what
|
||||||
|
// thumbnail size the HS is going to give us, but we have to commit to
|
||||||
|
// a container size immediately and not change it when the image loads
|
||||||
|
// or we'll get a scroll jump (or have to leave blank space).
|
||||||
|
// This will obviously result in an upscaled image which will be a bit
|
||||||
|
// blurry. The best fix would be for the HS to advertise what size thumbnails
|
||||||
|
// it guarantees to produce.
|
||||||
|
style={{ height: '100%' }}
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
onError={this.onImageError}
|
onError={this.onImageError}
|
||||||
onLoad={this.onImageLoad}
|
onLoad={this.onImageLoad}
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,21 +378,41 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_MImageBody_thumbnail': true,
|
||||||
|
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info[BLURHASH_FIELD],
|
||||||
|
});
|
||||||
|
|
||||||
|
// This has incredibly broken types.
|
||||||
|
const C = CSSTransition as any;
|
||||||
const thumbnail = (
|
const thumbnail = (
|
||||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
|
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
|
||||||
{ showPlaceholder &&
|
<SwitchTransition mode="out-in">
|
||||||
<div
|
<C
|
||||||
className="mx_MImageBody_thumbnail"
|
classNames="mx_rtg--fade"
|
||||||
|
key={`img-${showPlaceholder}`}
|
||||||
|
timeout={300}
|
||||||
|
>
|
||||||
|
{ /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
|
||||||
|
<div>
|
||||||
|
{ showPlaceholder && <div
|
||||||
|
className={classes}
|
||||||
style={{
|
style={{
|
||||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||||
maxWidth: `min(100%, ${infoWidth}px)`,
|
maxWidth: `min(100%, ${infoWidth}px)`,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
aspectRatio: `${infoWidth}/${infoHeight}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ placeholder }
|
{ placeholder }
|
||||||
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
}
|
</C>
|
||||||
|
</SwitchTransition>
|
||||||
|
|
||||||
<div style={{ display: !showPlaceholder ? undefined : 'none' }}>
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
}}>
|
||||||
{ img }
|
{ img }
|
||||||
{ gifLabel }
|
{ gifLabel }
|
||||||
</div>
|
</div>
|
||||||
|
@ -401,7 +434,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||||
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
if (blurhash) return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
|
||||||
return (
|
return (
|
||||||
<InlineSpinner w={32} h={32} />
|
<InlineSpinner w={32} h={32} />
|
||||||
);
|
);
|
||||||
|
@ -443,10 +476,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
||||||
const fileBody = this.getFileBody();
|
const fileBody = this.getFileBody();
|
||||||
|
|
||||||
return <div className="mx_MImageBody">
|
return (
|
||||||
|
<div className="mx_MImageBody">
|
||||||
{ thumbnail }
|
{ thumbnail }
|
||||||
{ fileBody }
|
{ fileBody }
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||||
// Calculate how many percent does the pre element take up.
|
// Calculate how many percent does the pre element take up.
|
||||||
// If it's less than 30% we don't add the expansion button.
|
// If it's less than 30% we don't add the expansion button.
|
||||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
// We also round the number as it sometimes can be 29.99...
|
||||||
|
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||||
if (percentageOfViewport < 30) return;
|
if (percentageOfViewport < 30) return;
|
||||||
|
|
||||||
const button = document.createElement("span");
|
const button = document.createElement("span");
|
||||||
|
|
|
@ -851,7 +851,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
return <div />;
|
return <div />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GroupMember {
|
export interface GroupMember {
|
||||||
userId: string;
|
userId: string;
|
||||||
displayname?: string; // XXX: GroupMember objects are inconsistent :((
|
displayname?: string; // XXX: GroupMember objects are inconsistent :((
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -32,7 +31,7 @@ import {
|
||||||
} from '../../../editor/operations';
|
} from '../../../editor/operations';
|
||||||
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||||
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||||
import { getAutoCompleteCreator } from '../../../editor/parts';
|
import { getAutoCompleteCreator, Type } from '../../../editor/parts';
|
||||||
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||||
import { renderModel } from '../../../editor/render';
|
import { renderModel } from '../../../editor/render';
|
||||||
import TypingStore from "../../../stores/TypingStore";
|
import TypingStore from "../../../stores/TypingStore";
|
||||||
|
@ -55,6 +54,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||||
|
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"],
|
||||||
|
["{", "}"],
|
||||||
|
["<", ">"],
|
||||||
|
]);
|
||||||
|
|
||||||
function ctrlShortcutLabel(key: string): string {
|
function ctrlShortcutLabel(key: string): string {
|
||||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +106,7 @@ interface IState {
|
||||||
showVisualBell?: boolean;
|
showVisualBell?: boolean;
|
||||||
autoComplete?: AutocompleteWrapperModel;
|
autoComplete?: AutocompleteWrapperModel;
|
||||||
completionIndex?: number;
|
completionIndex?: number;
|
||||||
|
surroundWith: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||||
|
@ -117,12 +125,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
private readonly emoticonSettingHandle: string;
|
private readonly emoticonSettingHandle: string;
|
||||||
private readonly shouldShowPillAvatarSettingHandle: string;
|
private readonly shouldShowPillAvatarSettingHandle: string;
|
||||||
|
private readonly surroundWithHandle: string;
|
||||||
private readonly historyManager = new HistoryManager();
|
private readonly historyManager = new HistoryManager();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||||
|
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||||
|
@ -130,6 +140,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.configureEmoticonAutoReplace();
|
this.configureEmoticonAutoReplace();
|
||||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||||
this.configureShouldShowPillAvatar);
|
this.configureShouldShowPillAvatar);
|
||||||
|
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||||
|
this.surroundWithSettingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps) {
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
|
@ -157,7 +169,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
range.expandBackwardsWhile((index, offset) => {
|
range.expandBackwardsWhile((index, offset) => {
|
||||||
const part = model.parts[index];
|
const part = model.parts[index];
|
||||||
n -= 1;
|
n -= 1;
|
||||||
return n >= 0 && (part.type === "plain" || part.type === "pill-candidate");
|
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
|
||||||
});
|
});
|
||||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
||||||
if (emoticonMatch) {
|
if (emoticonMatch) {
|
||||||
|
@ -422,6 +434,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||||
const model = this.props.model;
|
const model = this.props.model;
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
|
if (this.state.surroundWith && document.getSelection().type != "Caret") {
|
||||||
|
// This surrounds the selected text with a character. This is
|
||||||
|
// intentionally left out of the keybinding manager as the keybinds
|
||||||
|
// here shouldn't be changeable
|
||||||
|
|
||||||
|
const selectionRange = getRangeForSelection(
|
||||||
|
this.editorRef.current,
|
||||||
|
this.props.model,
|
||||||
|
document.getSelection(),
|
||||||
|
);
|
||||||
|
// trim the range as we want it to exclude leading/trailing spaces
|
||||||
|
selectionRange.trim();
|
||||||
|
|
||||||
|
if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) {
|
||||||
|
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||||
|
this.modifiedFlag = true;
|
||||||
|
toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key));
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MessageComposerAction.FormatBold:
|
case MessageComposerAction.FormatBold:
|
||||||
|
@ -524,9 +558,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
const range = model.startRange(position);
|
const range = model.startRange(position);
|
||||||
range.expandBackwardsWhile((index, offset, part) => {
|
range.expandBackwardsWhile((index, offset, part) => {
|
||||||
return part.text[offset] !== " " && part.text[offset] !== "+" && (
|
return part.text[offset] !== " " && part.text[offset] !== "+" && (
|
||||||
part.type === "plain" ||
|
part.type === Type.Plain ||
|
||||||
part.type === "pill-candidate" ||
|
part.type === Type.PillCandidate ||
|
||||||
part.type === "command"
|
part.type === Type.Command
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const { partCreator } = model;
|
const { partCreator } = model;
|
||||||
|
@ -574,6 +608,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ showPillAvatar });
|
this.setState({ showPillAvatar });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private surroundWithSettingChanged = () => {
|
||||||
|
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
|
||||||
|
this.setState({ surroundWith });
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
@ -581,6 +620,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
||||||
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
||||||
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
||||||
|
SettingsStore.unwatchSetting(this.surroundWithHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -684,7 +724,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||||
<div
|
<div
|
||||||
className={classes}
|
className={classes}
|
||||||
contentEditable="true"
|
contentEditable={this.props.disabled ? null : true}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { getCaretOffsetAndText } from '../../../editor/dom';
|
||||||
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
||||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import { parseEvent } from '../../../editor/deserialize';
|
import { parseEvent } from '../../../editor/deserialize';
|
||||||
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
@ -242,12 +242,12 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
const parts = this.model.parts;
|
const parts = this.model.parts;
|
||||||
const firstPart = parts[0];
|
const firstPart = parts[0];
|
||||||
if (firstPart) {
|
if (firstPart) {
|
||||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +268,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
private getSlashCommand(): [Command, string, string] {
|
private getSlashCommand(): [Command, string, string] {
|
||||||
const commandText = this.model.parts.reduce((text, part) => {
|
const commandText = this.model.parts.reduce((text, part) => {
|
||||||
// use mxid to textify user pills in a command
|
// use mxid to textify user pills in a command
|
||||||
if (part.type === "user-pill") {
|
if (part.type === Type.UserPill) {
|
||||||
return text + part.resourceId;
|
return text + part.resourceId;
|
||||||
}
|
}
|
||||||
return text + part.text;
|
return text + part.text;
|
||||||
|
|
|
@ -58,6 +58,7 @@ function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
|
||||||
interface ISendButtonProps {
|
interface ISendButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
title?: string; // defaults to something generic
|
||||||
}
|
}
|
||||||
|
|
||||||
function SendButton(props: ISendButtonProps) {
|
function SendButton(props: ISendButtonProps) {
|
||||||
|
@ -65,7 +66,7 @@ function SendButton(props: ISendButtonProps) {
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_MessageComposer_sendMessage"
|
className="mx_MessageComposer_sendMessage"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
title={_t('Send message')}
|
title={props.title ?? _t('Send message')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -401,7 +402,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
<SendButton
|
||||||
|
key="controls_send"
|
||||||
|
onClick={this.sendMessage}
|
||||||
|
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (this.state.tombstone) {
|
} else if (this.state.tombstone) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -38,6 +38,8 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ReplyTile")
|
@replaceableComponent("views.rooms.ReplyTile")
|
||||||
export default class ReplyTile extends React.PureComponent<IProps> {
|
export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
|
private anchorElement = createRef<HTMLAnchorElement>();
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onHeightChanged: () => {},
|
onHeightChanged: () => {},
|
||||||
};
|
};
|
||||||
|
@ -71,7 +73,11 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
// Following a link within a reply should not dispatch the `view_room` action
|
// Following a link within a reply should not dispatch the `view_room` action
|
||||||
// so that the browser can direct the user to the correct location
|
// so that the browser can direct the user to the correct location
|
||||||
// The exception being the link wrapping the reply
|
// The exception being the link wrapping the reply
|
||||||
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
|
if (
|
||||||
|
clickTarget.tagName.toLowerCase() !== "a" ||
|
||||||
|
clickTarget.closest("a") === null ||
|
||||||
|
clickTarget === this.anchorElement.current
|
||||||
|
) {
|
||||||
// This allows the permalink to be opened in a new tab/window or copied as
|
// This allows the permalink to be opened in a new tab/window or copied as
|
||||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -141,7 +147,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<a href={permalink} onClick={this.onClick}>
|
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||||
{ sender }
|
{ sender }
|
||||||
<EventTileType
|
<EventTileType
|
||||||
ref="tile"
|
ref="tile"
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
textSerialize,
|
textSerialize,
|
||||||
unescapeMessage,
|
unescapeMessage,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
|
@ -240,14 +240,14 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
const parts = this.model.parts;
|
const parts = this.model.parts;
|
||||||
const firstPart = parts[0];
|
const firstPart = parts[0];
|
||||||
if (firstPart) {
|
if (firstPart) {
|
||||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// be extra resilient when somehow the AutocompleteWrapperModel or
|
// be extra resilient when somehow the AutocompleteWrapperModel or
|
||||||
// CommandPartCreator fails to insert a command part, so we don't send
|
// CommandPartCreator fails to insert a command part, so we don't send
|
||||||
// a command as a message
|
// a command as a message
|
||||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,7 @@ limitations under the License.
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import {
|
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
RecordingState,
|
|
||||||
VoiceRecording,
|
|
||||||
} from "../../../audio/VoiceRecording";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -34,6 +31,11 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||||
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
|
import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -42,6 +44,7 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
recorder?: VoiceRecording;
|
recorder?: VoiceRecording;
|
||||||
recordingPhase?: RecordingState;
|
recordingPhase?: RecordingState;
|
||||||
|
didUploadFail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,9 +72,19 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
|
|
||||||
await this.state.recorder.stop();
|
await this.state.recorder.stop();
|
||||||
|
|
||||||
|
let upload: IUpload;
|
||||||
try {
|
try {
|
||||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error uploading voice message:", e);
|
||||||
|
|
||||||
|
// Flag error and move on. The recording phase will be reset by the upload function.
|
||||||
|
this.setState({ didUploadFail: true });
|
||||||
|
|
||||||
|
return; // don't dispose the recording: the user has a chance to re-upload
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
||||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||||
"body": "Voice message",
|
"body": "Voice message",
|
||||||
|
@ -104,12 +117,11 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error sending/uploading voice message:", e);
|
console.error("Error sending voice message:", e);
|
||||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
|
||||||
title: _t('Upload Failed'),
|
// Voice message should be in the timeline at this point, so let other things take care
|
||||||
description: _t("The voice message failed to upload."),
|
// of error handling. We also shouldn't need the recording anymore, so fall through to
|
||||||
});
|
// disposal.
|
||||||
return; // don't dispose the recording so the user can retry, maybe
|
|
||||||
}
|
}
|
||||||
await this.disposeRecording();
|
await this.disposeRecording();
|
||||||
}
|
}
|
||||||
|
@ -118,7 +130,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
await VoiceRecordingStore.instance.disposeRecording();
|
await VoiceRecordingStore.instance.disposeRecording();
|
||||||
|
|
||||||
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
||||||
this.setState({ recorder: null, recordingPhase: null });
|
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCancel = async () => {
|
private onCancel = async () => {
|
||||||
|
@ -166,6 +178,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// stop any noises which might be happening
|
||||||
|
await PlaybackManager.instance.playOnly(null);
|
||||||
|
|
||||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||||
await recorder.start();
|
await recorder.start();
|
||||||
|
|
||||||
|
@ -200,7 +215,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
let recordingInfo;
|
let stopOrRecordBtn;
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -209,12 +224,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tooltip = _t("Record a voice message");
|
let tooltip = _t("Send voice message");
|
||||||
if (!!this.state.recorder) {
|
if (!!this.state.recorder) {
|
||||||
tooltip = _t("Stop the recording");
|
tooltip = _t("Stop recording");
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
stopOrRecordBtn = <AccessibleTooltipButton
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onRecordStartEndClick}
|
onClick={this.onRecordStartEndClick}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
|
@ -222,22 +237,41 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||||
stopOrRecordBtn = null;
|
stopOrRecordBtn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingInfo = stopOrRecordBtn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||||
deleteButton = <AccessibleTooltipButton
|
deleteButton = <AccessibleTooltipButton
|
||||||
className='mx_VoiceRecordComposerTile_delete'
|
className='mx_VoiceRecordComposerTile_delete'
|
||||||
title={_t("Delete recording")}
|
title={_t("Delete")}
|
||||||
onClick={this.onCancel}
|
onClick={this.onCancel}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadIndicator;
|
||||||
|
if (this.state.recordingPhase === RecordingState.Uploading) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
|
||||||
|
<InlineSpinner w={16} h={16} />
|
||||||
|
</span>;
|
||||||
|
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
|
||||||
|
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
|
||||||
|
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
|
||||||
|
<NotificationBadge
|
||||||
|
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='text-warning'>{ _t("Failed to send") }</span>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||||
|
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||||
|
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||||
return (<>
|
return (<>
|
||||||
|
{ uploadIndicator }
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
|
{ stopOrRecordBtn }
|
||||||
{ this.renderWaveformArea() }
|
{ this.renderWaveformArea() }
|
||||||
{ recordingInfo }
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
133
src/components/views/settings/LayoutSwitcher.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import EventTilePreview from "../elements/EventTilePreview";
|
||||||
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { Layout } from "../../../settings/Layout";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
messagePreviewText: string;
|
||||||
|
onLayoutChanged?: (layout: Layout) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
layout: Layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const layout = e.target.value as Layout;
|
||||||
|
|
||||||
|
this.setState({ layout: layout });
|
||||||
|
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||||
|
this.props.onLayoutChanged(layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||||
|
});
|
||||||
|
const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.Group,
|
||||||
|
});
|
||||||
|
const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||||
|
mx_LayoutSwitcher_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab_section mx_LayoutSwitcher">
|
||||||
|
<span className="mx_SettingsTab_subheading">
|
||||||
|
{ _t("Message layout") }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="mx_LayoutSwitcher_RadioButtons">
|
||||||
|
<label className={ircClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.IRC}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.IRC}
|
||||||
|
checked={this.state.layout === Layout.IRC}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("IRC") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
<label className={groupClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.Group}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.Group}
|
||||||
|
checked={this.state.layout == Layout.Group}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Modern") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
<label className={bubbleClasses}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||||
|
message={this.props.messagePreviewText}
|
||||||
|
layout={Layout.Bubble}
|
||||||
|
userId={this.props.userId}
|
||||||
|
displayName={this.props.displayName}
|
||||||
|
avatarUrl={this.props.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value={Layout.Bubble}
|
||||||
|
checked={this.state.layout == Layout.Bubble}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Message bubbles") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -37,10 +37,9 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { Layout } from "../../../../../settings/Layout";
|
import { Layout } from "../../../../../settings/Layout";
|
||||||
import classNames from 'classnames';
|
|
||||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import { compare } from "../../../../../utils/strings";
|
import { compare } from "../../../../../utils/strings";
|
||||||
|
import LayoutSwitcher from "../../LayoutSwitcher";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -243,17 +242,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
this.setState({ customThemeUrl: e.target.value });
|
this.setState({ customThemeUrl: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onLayoutChanged = (layout: Layout): void => {
|
||||||
let layout;
|
|
||||||
switch (e.target.value) {
|
|
||||||
case "irc": layout = Layout.IRC; break;
|
|
||||||
case "group": layout = Layout.Group; break;
|
|
||||||
case "bubble": layout = Layout.Bubble; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ layout: layout });
|
this.setState({ layout: layout });
|
||||||
|
|
||||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onIRCLayoutChange = (enabled: boolean) => {
|
private onIRCLayoutChange = (enabled: boolean) => {
|
||||||
|
@ -391,75 +381,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLayoutSection = () => {
|
|
||||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
|
||||||
|
|
||||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.IRC}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="irc"
|
|
||||||
checked={this.state.layout === Layout.IRC}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("IRC") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.Group}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="group"
|
|
||||||
checked={this.state.layout == Layout.Group}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("Modern") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
|
||||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
|
||||||
})}>
|
|
||||||
<EventTilePreview
|
|
||||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
|
||||||
message={this.MESSAGE_PREVIEW_TEXT}
|
|
||||||
layout={Layout.Bubble}
|
|
||||||
userId={this.state.userId}
|
|
||||||
displayName={this.state.displayName}
|
|
||||||
avatarUrl={this.state.avatarUrl}
|
|
||||||
/>
|
|
||||||
<StyledRadioButton
|
|
||||||
name="layout"
|
|
||||||
value="bubble"
|
|
||||||
checked={this.state.layout == Layout.Bubble}
|
|
||||||
onChange={this.onLayoutChange}
|
|
||||||
>
|
|
||||||
{ _t("Message bubbles") }
|
|
||||||
</StyledRadioButton>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderAdvancedSection() {
|
private renderAdvancedSection() {
|
||||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||||
|
|
||||||
|
@ -527,6 +448,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
||||||
|
let layoutSection;
|
||||||
|
if (SettingsStore.getValue("feature_new_layout_switcher")) {
|
||||||
|
layoutSection = (
|
||||||
|
<LayoutSwitcher
|
||||||
|
userId={this.state.userId}
|
||||||
|
displayName={this.state.displayName}
|
||||||
|
avatarUrl={this.state.avatarUrl}
|
||||||
|
messagePreviewText={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
onLayoutChanged={this.onLayoutChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
||||||
|
@ -534,7 +468,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||||
</div>
|
</div>
|
||||||
{ this.renderThemeSection() }
|
{ this.renderThemeSection() }
|
||||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
{ layoutSection }
|
||||||
{ this.renderFontSection() }
|
{ this.renderFontSection() }
|
||||||
{ this.renderAdvancedSection() }
|
{ this.renderAdvancedSection() }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
|
||||||
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import createRoom from "../../../../../createRoom";
|
import createRoom from "../../../../../createRoom";
|
||||||
|
@ -69,6 +69,18 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getVersionInfo(): { appVersion: string, olmVersion: string } {
|
||||||
|
const brand = SdkConfig.get().brand;
|
||||||
|
const appVersion = this.state.appVersion || 'unknown';
|
||||||
|
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||||
|
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||||
|
|
||||||
|
return {
|
||||||
|
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
|
||||||
|
olmVersion: `${_t("Olm version:")} ${olmVersion}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private onClearCacheAndReload = (e) => {
|
private onClearCacheAndReload = (e) => {
|
||||||
if (!PlatformPeg.get()) return;
|
if (!PlatformPeg.get()) return;
|
||||||
|
|
||||||
|
@ -173,17 +185,26 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccessTokenCopyClick = async (e) => {
|
private async copy(text: string, e: ButtonEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = e.target; // copy target before we go async and React throws it away
|
const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away
|
||||||
|
|
||||||
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
const successful = await copyPlaintext(text);
|
||||||
const buttonRect = target.getBoundingClientRect();
|
const buttonRect = target.getBoundingClientRect();
|
||||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
...toRightOf(buttonRect, 2),
|
...toRightOf(buttonRect, 2),
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
});
|
||||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAccessTokenCopyClick = (e: ButtonEvent) => {
|
||||||
|
this.copy(MatrixClientPeg.get().getAccessToken(), e);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCopyVersionClicked = (e: ButtonEvent) => {
|
||||||
|
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||||
|
this.copy(`${appVersion}\n${olmVersion}`, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -232,11 +253,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appVersion = this.state.appVersion || 'unknown';
|
|
||||||
|
|
||||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
|
||||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
|
||||||
|
|
||||||
let updateButton = null;
|
let updateButton = null;
|
||||||
if (this.state.canUpdate) {
|
if (this.state.canUpdate) {
|
||||||
updateButton = <UpdateCheckButton />;
|
updateButton = <UpdateCheckButton />;
|
||||||
|
@ -275,6 +291,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
||||||
|
@ -291,8 +309,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
|
<div className="mx_HelpUserSettingsTab_copy">
|
||||||
{ _t("olm version:") } { olmVersion }<br />
|
{ appVersion }<br />
|
||||||
|
{ olmVersion }<br />
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
title={_t("Copy")}
|
||||||
|
onClick={this.onCopyVersionClicked}
|
||||||
|
className="mx_HelpUserSettingsTab_copyButton"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{ updateButton }
|
{ updateButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -308,12 +333,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<summary>{ _t("Access Token") }</summary><br />
|
<summary>{ _t("Access Token") }</summary><br />
|
||||||
<b>{ _t("Your access token gives full access to your account."
|
<b>{ _t("Your access token gives full access to your account."
|
||||||
+ " Do not share it with anyone." ) }</b>
|
+ " Do not share it with anyone." ) }</b>
|
||||||
<div className="mx_HelpUserSettingsTab_accessToken">
|
<div className="mx_HelpUserSettingsTab_copy">
|
||||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
title={_t("Copy")}
|
title={_t("Copy")}
|
||||||
onClick={this.onAccessTokenCopyClick}
|
onClick={this.onAccessTokenCopyClick}
|
||||||
className="mx_HelpUserSettingsTab_accessToken_copy"
|
className="mx_HelpUserSettingsTab_copyButton"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</details><br />
|
</details><br />
|
||||||
|
|
|
@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import * as sdk from "../../../../../index";
|
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import BetaCard from "../../../beta/BetaCard";
|
import BetaCard from "../../../beta/BetaCard";
|
||||||
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
|
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||||
|
|
||||||
export class LabsSettingToggle extends React.Component {
|
export class LabsSettingToggle extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component {
|
||||||
export default class LabsUserSettingsTab extends React.Component {
|
export default class LabsUserSettingsTab extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||||
|
this.setState({ showHiddenReadReceipts });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showHiddenReadReceipts: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -65,15 +74,22 @@ export default class LabsUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
let labsSection;
|
let labsSection;
|
||||||
if (SdkConfig.get()['showLabsSettings']) {
|
if (SdkConfig.get()['showLabsSettings']) {
|
||||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
|
||||||
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
||||||
|
|
||||||
|
let hiddenReadReceipts;
|
||||||
|
if (this.state.showHiddenReadReceipts) {
|
||||||
|
hiddenReadReceipts = (
|
||||||
|
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.DEVICE} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
labsSection = <div className="mx_SettingsTab_section">
|
labsSection = <div className="mx_SettingsTab_section">
|
||||||
{ flags }
|
{ flags }
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
||||||
|
{ hiddenReadReceipts }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
@ -27,6 +29,18 @@ import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
import SpaceStore from "../../../../../stores/SpaceStore";
|
||||||
|
import GroupAvatar from "../../../avatars/GroupAvatar";
|
||||||
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
|
import GroupActions from "../../../../../actions/GroupActions";
|
||||||
|
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||||
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
|
import { CreateEventField, IGroupSummary } from "../../../dialogs/CreateSpaceFromCommunityDialog";
|
||||||
|
import { createSpaceFromCommunity } from "../../../../../utils/space";
|
||||||
|
import Spinner from "../../../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
closeSettingsFn(success: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
autoLaunch: boolean;
|
autoLaunch: boolean;
|
||||||
|
@ -42,8 +56,86 @@ interface IState {
|
||||||
readMarkerOutOfViewThresholdMs: string;
|
readMarkerOutOfViewThresholdMs: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Community = IGroupSummary & {
|
||||||
|
groupId: string;
|
||||||
|
spaceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommunityMigrator = ({ onFinished }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const [communities, setCommunities] = useState<Community[]>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
dis.dispatch(GroupActions.fetchJoinedGroups(cli));
|
||||||
|
}, [cli]);
|
||||||
|
useDispatcher(dis, async payload => {
|
||||||
|
if (payload.action === "GroupActions.fetchJoinedGroups.success") {
|
||||||
|
const communities: Community[] = [];
|
||||||
|
|
||||||
|
const migratedSpaceMap = new Map(cli.getRooms().map(room => {
|
||||||
|
const createContent = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
|
||||||
|
if (createContent?.[CreateEventField]) {
|
||||||
|
return [createContent[CreateEventField], room.roomId] as [string, string];
|
||||||
|
}
|
||||||
|
}).filter(Boolean));
|
||||||
|
|
||||||
|
for (const groupId of payload.result.groups) {
|
||||||
|
const summary = await cli.getGroupSummary(groupId) as IGroupSummary;
|
||||||
|
if (summary.user.is_privileged) {
|
||||||
|
communities.push({
|
||||||
|
...summary,
|
||||||
|
groupId,
|
||||||
|
spaceId: migratedSpaceMap.get(groupId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommunities(communities);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!communities) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_PreferencesUserSettingsTab_CommunityMigrator">
|
||||||
|
{ communities.map(community => (
|
||||||
|
<div key={community.groupId}>
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={community.groupId}
|
||||||
|
groupAvatarUrl={community.profile.avatar_url}
|
||||||
|
groupName={community.profile.name}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
{ community.profile.name }
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary_outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (community.spaceId) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: community.spaceId,
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
} else {
|
||||||
|
createSpaceFromCommunity(cli, community.groupId).then(([spaceId]) => {
|
||||||
|
if (spaceId) {
|
||||||
|
community.spaceId = spaceId;
|
||||||
|
setCommunities([...communities]); // force component re-render
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ community.spaceId ? _t("Open Space") : _t("Create Space") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
|
||||||
export default class PreferencesUserSettingsTab extends React.Component<{}, IState> {
|
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
static ROOM_LIST_SETTINGS = [
|
static ROOM_LIST_SETTINGS = [
|
||||||
'breadcrumbs',
|
'breadcrumbs',
|
||||||
];
|
];
|
||||||
|
@ -52,6 +144,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
||||||
"Spaces.allRoomsInHome",
|
"Spaces.allRoomsInHome",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static COMMUNITIES_SETTINGS = [
|
||||||
|
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
|
||||||
|
];
|
||||||
|
|
||||||
static KEYBINDINGS_SETTINGS = [
|
static KEYBINDINGS_SETTINGS = [
|
||||||
'ctrlFForSearch',
|
'ctrlFForSearch',
|
||||||
];
|
];
|
||||||
|
@ -61,6 +157,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
||||||
'MessageComposerInput.suggestEmoji',
|
'MessageComposerInput.suggestEmoji',
|
||||||
'sendTypingNotifications',
|
'sendTypingNotifications',
|
||||||
'MessageComposerInput.ctrlEnterToSend',
|
'MessageComposerInput.ctrlEnterToSend',
|
||||||
|
'MessageComposerInput.surroundWith',
|
||||||
'MessageComposerInput.showStickersButton',
|
'MessageComposerInput.showStickersButton',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -241,6 +338,19 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
||||||
|
<p>{ _t("Communities have been archived to make way for Spaces but you can convert your " +
|
||||||
|
"communities into Spaces below. Converting will ensure your conversations get the latest " +
|
||||||
|
"features.") }</p>
|
||||||
|
<details>
|
||||||
|
<summary>{ _t("Show my Communities") }</summary>
|
||||||
|
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
||||||
|
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
||||||
|
</details>
|
||||||
|
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
|
||||||
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
|
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const SpaceAvatar = ({
|
||||||
}}
|
}}
|
||||||
kind="link"
|
kind="link"
|
||||||
className="mx_SpaceBasicSettings_avatar_remove"
|
className="mx_SpaceBasicSettings_avatar_remove"
|
||||||
|
aria-label={_t("Delete avatar")}
|
||||||
>
|
>
|
||||||
{ _t("Delete") }
|
{ _t("Delete") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -72,7 +73,11 @@ export const SpaceAvatar = ({
|
||||||
} else {
|
} else {
|
||||||
avatarSection = <React.Fragment>
|
avatarSection = <React.Fragment>
|
||||||
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
||||||
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
|
<AccessibleButton
|
||||||
|
onClick={() => avatarUploadRef.current?.click()}
|
||||||
|
kind="link"
|
||||||
|
aria-label={_t("Upload avatar")}
|
||||||
|
>
|
||||||
{ _t("Upload") }
|
{ _t("Upload") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -18,22 +18,59 @@ import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, u
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import FocusLock from "react-focus-lock";
|
import FocusLock from "react-focus-lock";
|
||||||
|
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
||||||
import createRoom from "../../../createRoom";
|
import createRoom, { IOpts as ICreateOpts } from "../../../createRoom";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
|
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import withValidation from "../elements/Validation";
|
import withValidation from "../elements/Validation";
|
||||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
|
||||||
import RoomAliasField from "../elements/RoomAliasField";
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
|
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||||
|
|
||||||
|
export const createSpace = async (
|
||||||
|
name: string,
|
||||||
|
isPublic: boolean,
|
||||||
|
alias?: string,
|
||||||
|
topic?: string,
|
||||||
|
avatar?: string | File,
|
||||||
|
createOpts: Partial<ICreateRoomOpts> = {},
|
||||||
|
otherOpts: Partial<Omit<ICreateOpts, "createOpts">> = {},
|
||||||
|
) => {
|
||||||
|
return createRoom({
|
||||||
|
createOpts: {
|
||||||
|
name,
|
||||||
|
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
|
||||||
|
power_level_content_override: {
|
||||||
|
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||||
|
events_default: 100,
|
||||||
|
...isPublic ? { invite: 0 } : {},
|
||||||
|
},
|
||||||
|
room_alias_name: isPublic && alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||||
|
topic,
|
||||||
|
...createOpts,
|
||||||
|
},
|
||||||
|
avatar,
|
||||||
|
roomType: RoomType.Space,
|
||||||
|
historyVisibility: isPublic ? HistoryVisibility.WorldReadable : HistoryVisibility.Invited,
|
||||||
|
spinner: false,
|
||||||
|
encryption: false,
|
||||||
|
andView: true,
|
||||||
|
inlineErrors: true,
|
||||||
|
...otherOpts,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -92,7 +129,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BProps = Pick<ComponentProps<typeof SpaceBasicSettings>, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
|
type BProps = Omit<ComponentProps<typeof SpaceBasicSettings>, "nameDisabled" | "topicDisabled" | "avatarDisabled">;
|
||||||
interface ISpaceCreateFormProps extends BProps {
|
interface ISpaceCreateFormProps extends BProps {
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
alias: string;
|
alias: string;
|
||||||
|
@ -106,6 +143,7 @@ interface ISpaceCreateFormProps extends BProps {
|
||||||
export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
busy,
|
busy,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
avatarUrl,
|
||||||
setAvatar,
|
setAvatar,
|
||||||
name,
|
name,
|
||||||
setName,
|
setName,
|
||||||
|
@ -122,7 +160,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
const domain = cli.getDomain();
|
const domain = cli.getDomain();
|
||||||
|
|
||||||
return <form className="mx_SpaceBasicSettings" onSubmit={onSubmit}>
|
return <form className="mx_SpaceBasicSettings" onSubmit={onSubmit}>
|
||||||
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
|
<SpaceAvatar avatarUrl={avatarUrl} setAvatar={setAvatar} avatarDisabled={busy} />
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name="spaceName"
|
name="spaceName"
|
||||||
|
@ -200,30 +238,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createRoom({
|
await createSpace(name, visibility === Visibility.Public, alias, topic, avatar);
|
||||||
createOpts: {
|
|
||||||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
|
||||||
name,
|
|
||||||
power_level_content_override: {
|
|
||||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
|
||||||
events_default: 100,
|
|
||||||
...visibility === Visibility.Public ? { invite: 0 } : {},
|
|
||||||
},
|
|
||||||
room_alias_name: visibility === Visibility.Public && alias
|
|
||||||
? alias.substr(1, alias.indexOf(":") - 1)
|
|
||||||
: undefined,
|
|
||||||
topic,
|
|
||||||
},
|
|
||||||
avatar,
|
|
||||||
roomType: RoomType.Space,
|
|
||||||
historyVisibility: visibility === Visibility.Public
|
|
||||||
? HistoryVisibility.WorldReadable
|
|
||||||
: HistoryVisibility.Invited,
|
|
||||||
spinner: false,
|
|
||||||
encryption: false,
|
|
||||||
andView: true,
|
|
||||||
inlineErrors: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -233,10 +248,23 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
if (visibility === null) {
|
if (visibility === null) {
|
||||||
|
const onCreateSpaceFromCommunityClick = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Preferences,
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
body = <React.Fragment>
|
body = <React.Fragment>
|
||||||
<h2>{ _t("Create a space") }</h2>
|
<h2>{ _t("Create a space") }</h2>
|
||||||
<p>{ _t("Spaces are a new way to group rooms and people. " +
|
<p>
|
||||||
"To join an existing space you'll need an invite.") }</p>
|
{ _t("Spaces are a new way to group rooms and people.") }
|
||||||
|
|
||||||
|
{ _t("What kind of Space do you want to create?") }
|
||||||
|
|
||||||
|
{ _t("You can change this later.") }
|
||||||
|
</p>
|
||||||
|
|
||||||
<SpaceCreateMenuType
|
<SpaceCreateMenuType
|
||||||
title={_t("Public")}
|
title={_t("Public")}
|
||||||
|
@ -251,7 +279,15 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
onClick={() => setVisibility(Visibility.Private)}
|
onClick={() => setVisibility(Visibility.Private)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>{ _t("You can change this later") }</p>
|
<p>
|
||||||
|
{ _t("You can also create a Space from a <a>community</a>.", {}, {
|
||||||
|
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
||||||
|
{ sub }
|
||||||
|
</AccessibleButton>,
|
||||||
|
}) }
|
||||||
|
<br />
|
||||||
|
{ _t("To join an existing space you'll need an invite.") }
|
||||||
|
</p>
|
||||||
|
|
||||||
<SpaceFeedbackPrompt onClick={onFinished} />
|
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
|
||||||
return SpaceStore.instance.allRoomsInHome;
|
return SpaceStore.instance.allRoomsInHome;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <li className={classNames("mx_SpaceItem", {
|
return <li
|
||||||
|
className={classNames("mx_SpaceItem", {
|
||||||
"collapsed": isPanelCollapsed,
|
"collapsed": isPanelCollapsed,
|
||||||
})}>
|
})}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
className="mx_SpaceButton_home"
|
className="mx_SpaceButton_home"
|
||||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||||
|
@ -142,9 +145,12 @@ const CreateSpaceButton = ({
|
||||||
openMenu();
|
openMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
return <li className={classNames("mx_SpaceItem", {
|
return <li
|
||||||
|
className={classNames("mx_SpaceItem", {
|
||||||
"collapsed": isPanelCollapsed,
|
"collapsed": isPanelCollapsed,
|
||||||
})}>
|
})}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
className={classNames("mx_SpaceButton_new", {
|
className={classNames("mx_SpaceButton_new", {
|
||||||
mx_SpaceButton_newCancel: menuDisplayed,
|
mx_SpaceButton_newCancel: menuDisplayed,
|
||||||
|
@ -272,6 +278,8 @@ const SpacePanel = () => {
|
||||||
<ul
|
<ul
|
||||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
|
role="tree"
|
||||||
|
aria-label={_t("Spaces")}
|
||||||
>
|
>
|
||||||
<Droppable droppableId="top-level-spaces">
|
<Droppable droppableId="top-level-spaces">
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
|
|
|
@ -77,11 +77,17 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
|
|
||||||
let notifBadge;
|
let notifBadge;
|
||||||
if (notificationState) {
|
if (notificationState) {
|
||||||
|
let ariaLabel = _t("Jump to first unread room.");
|
||||||
|
if (space?.getMyMembership() === "invite") {
|
||||||
|
ariaLabel = _t("Jump to first invite.");
|
||||||
|
}
|
||||||
|
|
||||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
|
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
|
||||||
forceCount={false}
|
forceCount={false}
|
||||||
notification={notificationState}
|
notification={notificationState}
|
||||||
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +113,6 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={openMenu}
|
onContextMenu={openMenu}
|
||||||
forceHide={!isNarrow || menuDisplayed}
|
forceHide={!isNarrow || menuDisplayed}
|
||||||
role="treeitem"
|
|
||||||
inputRef={handle}
|
inputRef={handle}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
|
@ -284,7 +289,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
/> : null;
|
/> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li {...otherProps} className={itemClasses} ref={innerRef}>
|
<li {...otherProps} className={itemClasses} ref={innerRef} aria-expanded={!collapsed} role="treeitem">
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
space={space}
|
space={space}
|
||||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||||
|
@ -296,9 +301,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
avatarSize={isNested ? 24 : 32}
|
avatarSize={isNested ? 24 : 32}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
aria-expanded={!collapsed}
|
ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined}
|
||||||
ContextMenuComponent={this.props.space.getMyMembership() === "join"
|
|
||||||
? SpaceContextMenu : undefined}
|
|
||||||
>
|
>
|
||||||
{ toggleCollapseButton }
|
{ toggleCollapseButton }
|
||||||
</SpaceButton>
|
</SpaceButton>
|
||||||
|
@ -322,7 +325,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
||||||
isNested,
|
isNested,
|
||||||
parents,
|
parents,
|
||||||
}) => {
|
}) => {
|
||||||
return <ul className="mx_SpaceTreeLevel">
|
return <ul className="mx_SpaceTreeLevel" role="group">
|
||||||
{ spaces.map(s => {
|
{ spaces.map(s => {
|
||||||
return (<SpaceItem
|
return (<SpaceItem
|
||||||
key={s.roomId}
|
key={s.roomId}
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
@ -27,23 +27,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from '../../../stores/UIStore';
|
|
||||||
import { lerp } from '../../../utils/AnimationUtils';
|
|
||||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
|
||||||
import { EventSubscription } from 'fbemitter';
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||||
const PIP_VIEW_WIDTH = 336;
|
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
|
||||||
|
|
||||||
const MOVING_AMT = 0.2;
|
|
||||||
const SNAPPING_AMT = 0.1;
|
|
||||||
|
|
||||||
const PADDING = {
|
|
||||||
top: 58,
|
|
||||||
bottom: 58,
|
|
||||||
left: 76,
|
|
||||||
right: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -66,10 +51,6 @@ interface IState {
|
||||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||||
// they belong to
|
// they belong to
|
||||||
secondaryCall: MatrixCall;
|
secondaryCall: MatrixCall;
|
||||||
|
|
||||||
// Position of the CallPreview
|
|
||||||
translationX: number;
|
|
||||||
translationY: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
|
@ -112,16 +93,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: EventSubscription;
|
private roomStoreToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private settingsWatcherRef: string;
|
private settingsWatcherRef: string;
|
||||||
private callViewWrapper = createRef<HTMLDivElement>();
|
|
||||||
private initX = 0;
|
|
||||||
private initY = 0;
|
|
||||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
|
||||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
|
||||||
private moving = false;
|
|
||||||
private scheduledUpdate = new MarkedExecution(
|
|
||||||
() => this.animationCallback(),
|
|
||||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
roomId,
|
roomId,
|
||||||
primaryCall: primaryCall,
|
primaryCall: primaryCall,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
|
||||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
document.addEventListener("mousemove", this.onMoving);
|
|
||||||
document.addEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
document.removeEventListener("mousemove", this.onMoving);
|
|
||||||
document.removeEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize = (): void => {
|
|
||||||
this.snap(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
private animationCallback = () => {
|
|
||||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
|
||||||
// the desiredTranslation and translation, quit the animationCallback
|
|
||||||
// loop. If that is the case, it means the PiP has snapped into its
|
|
||||||
// position and there is nothing to do. Not doing this would cause an
|
|
||||||
// infinite loop
|
|
||||||
if (
|
|
||||||
!this.moving &&
|
|
||||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
|
||||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
|
||||||
) return;
|
|
||||||
|
|
||||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
|
||||||
this.setState({
|
|
||||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
|
||||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
|
||||||
});
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
|
||||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
|
||||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
|
||||||
|
|
||||||
// Avoid overflow on the x axis
|
|
||||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
|
||||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
|
||||||
} else if (inTranslationX <= 0) {
|
|
||||||
this.desiredTranslationX = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = inTranslationX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid overflow on the y axis
|
|
||||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
|
||||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
|
||||||
} else if (inTranslationY <= 0) {
|
|
||||||
this.desiredTranslationY = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationY = inTranslationY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private snap(animate?: boolean): void {
|
|
||||||
const translationX = this.desiredTranslationX;
|
|
||||||
const translationY = this.desiredTranslationY;
|
|
||||||
// We subtract the PiP size from the window size in order to calculate
|
|
||||||
// the position to snap to from the PiP center and not its top-left
|
|
||||||
// corner
|
|
||||||
const windowWidth = (
|
|
||||||
UIStore.instance.windowWidth -
|
|
||||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
|
||||||
);
|
|
||||||
const windowHeight = (
|
|
||||||
UIStore.instance.windowHeight -
|
|
||||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
// We start animating here because we want the PiP to move when we're
|
|
||||||
// resizing the window
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
translationX: this.desiredTranslationX,
|
|
||||||
translationY: this.desiredTranslationY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = () => {
|
private onRoomViewStoreUpdate = () => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
case 'call_state': {
|
||||||
// listen for call state changes to prod the render method, which
|
// listen for call state changes to prod the render method, which
|
||||||
// may hide the global CallView if the call it is tracking is dead
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
case 'call_state': {
|
|
||||||
this.updateCalls();
|
this.updateCalls();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -300,57 +176,26 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStartMoving = (event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.moving = true;
|
|
||||||
this.initX = event.pageX - this.desiredTranslationX;
|
|
||||||
this.initY = event.pageY - this.desiredTranslationY;
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
|
||||||
if (!this.moving) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onEndMoving = () => {
|
|
||||||
this.moving = false;
|
|
||||||
this.snap(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const pipMode = true;
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
const translatePixelsX = this.state.translationX + "px";
|
|
||||||
const translatePixelsY = this.state.translationY + "px";
|
|
||||||
const style = {
|
|
||||||
transform: `translateX(${translatePixelsX})
|
|
||||||
translateY(${translatePixelsY})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PictureInPictureDragger
|
||||||
className="mx_CallPreview"
|
className="mx_CallPreview"
|
||||||
style={style}
|
draggable={pipMode}
|
||||||
ref={this.callViewWrapper}
|
|
||||||
>
|
>
|
||||||
<CallView
|
{ ({ onStartMoving, onResize }) => <CallView
|
||||||
|
onMouseDownOnHeader={onStartMoving}
|
||||||
call={this.state.primaryCall}
|
call={this.state.primaryCall}
|
||||||
secondaryCall={this.state.secondaryCall}
|
secondaryCall={this.state.secondaryCall}
|
||||||
pipMode={true}
|
pipMode={pipMode}
|
||||||
onMouseDownOnHeader={this.onStartMoving}
|
onResize={onResize}
|
||||||
onResize={this.onResize}
|
/> }
|
||||||
/>
|
</PictureInPictureDragger>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PersistentApp />;
|
return <PersistentApp />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -23,20 +23,19 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import VideoFeed from './VideoFeed';
|
import VideoFeed from './VideoFeed';
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
||||||
import { alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton } from '../../structures/ContextMenu';
|
|
||||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
|
||||||
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
|
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
import CallViewSidebar from './CallViewSidebar';
|
import CallViewSidebar from './CallViewSidebar';
|
||||||
|
import CallViewHeader from './CallView/CallViewHeader';
|
||||||
|
import CallViewButtons from "./CallView/CallViewButtons";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The call for us to display
|
// The call for us to display
|
||||||
|
@ -47,7 +46,7 @@ interface IProps {
|
||||||
|
|
||||||
// a callback which is called when the content in the CallView changes
|
// a callback which is called when the content in the CallView changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: any;
|
onResize?: (event: Event) => void;
|
||||||
|
|
||||||
// Whether this call view is for picture-in-picture mode
|
// Whether this call view is for picture-in-picture mode
|
||||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
|
@ -56,7 +55,7 @@ interface IProps {
|
||||||
pipMode?: boolean;
|
pipMode?: boolean;
|
||||||
|
|
||||||
// Used for dragging the PiP CallView
|
// Used for dragging the PiP CallView
|
||||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -103,19 +102,11 @@ function exitFullscreen() {
|
||||||
if (exitMethod) exitMethod.call(document);
|
if (exitMethod) exitMethod.call(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTROLS_HIDE_DELAY = 2000;
|
|
||||||
// Height of the header duplicated from CSS because we need to subtract it from our max
|
|
||||||
// height to get the max height of the video
|
|
||||||
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
|
||||||
|
|
||||||
@replaceableComponent("views.voip.CallView")
|
@replaceableComponent("views.voip.CallView")
|
||||||
export default class CallView extends React.Component<IProps, IState> {
|
export default class CallView extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private contentRef = createRef<HTMLDivElement>();
|
private contentRef = createRef<HTMLDivElement>();
|
||||||
private controlsHideTimer: number = null;
|
private buttonsRef = createRef<CallViewButtons>();
|
||||||
private dialpadButton = createRef<HTMLDivElement>();
|
|
||||||
private contextMenuButton = createRef<HTMLDivElement>();
|
|
||||||
private contextMenu = createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -231,31 +222,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFullscreenClick = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'video_fullscreen',
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onExpandClick = () => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onControlsHideTimer = () => {
|
|
||||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
|
||||||
this.controlsHideTimer = null;
|
|
||||||
this.setState({
|
|
||||||
controlsVisible: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMouseMove = () => {
|
private onMouseMove = () => {
|
||||||
this.showControls();
|
this.buttonsRef.current?.showControls();
|
||||||
};
|
};
|
||||||
|
|
||||||
private getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } {
|
private getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } {
|
||||||
|
@ -281,29 +249,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
return { primary, secondary };
|
return { primary, secondary };
|
||||||
}
|
}
|
||||||
|
|
||||||
private showControls(): void {
|
|
||||||
if (this.state.showMoreMenu || this.state.showDialpad) return;
|
|
||||||
|
|
||||||
if (!this.state.controlsVisible) {
|
|
||||||
this.setState({
|
|
||||||
controlsVisible: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.controlsHideTimer !== null) {
|
|
||||||
clearTimeout(this.controlsHideTimer);
|
|
||||||
}
|
|
||||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onDialpadClick = (): void => {
|
|
||||||
if (!this.state.showDialpad) {
|
|
||||||
this.setState({ showDialpad: true });
|
|
||||||
this.showControls();
|
|
||||||
} else {
|
|
||||||
this.setState({ showDialpad: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMicMuteClick = (): void => {
|
private onMicMuteClick = (): void => {
|
||||||
const newVal = !this.state.micMuted;
|
const newVal = !this.state.micMuted;
|
||||||
|
|
||||||
|
@ -334,19 +279,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMoreClick = (): void => {
|
|
||||||
this.setState({ showMoreMenu: true });
|
|
||||||
this.showControls();
|
|
||||||
};
|
|
||||||
|
|
||||||
private closeDialpad = (): void => {
|
|
||||||
this.setState({ showDialpad: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
private closeContextMenu = (): void => {
|
|
||||||
this.setState({ showMoreMenu: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||||
// Note that this assumes we always have a CallView on screen at any given time
|
// Note that this assumes we always have a CallView on screen at any given time
|
||||||
// CallHandler would probably be a better place for this
|
// CallHandler would probably be a better place for this
|
||||||
|
@ -359,7 +291,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (ctrlCmdOnly) {
|
if (ctrlCmdOnly) {
|
||||||
this.onMicMuteClick();
|
this.onMicMuteClick();
|
||||||
// show the controls to give feedback
|
// show the controls to give feedback
|
||||||
this.showControls();
|
this.buttonsRef.current?.showControls();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -368,7 +300,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (ctrlCmdOnly) {
|
if (ctrlCmdOnly) {
|
||||||
this.onVidMuteClick();
|
this.onVidMuteClick();
|
||||||
// show the controls to give feedback
|
// show the controls to give feedback
|
||||||
this.showControls();
|
this.buttonsRef.current?.showControls();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -380,32 +312,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallControlsMouseEnter = (): void => {
|
|
||||||
this.setState({ hoveringControls: true });
|
|
||||||
this.showControls();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallControlsMouseLeave = (): void => {
|
|
||||||
this.setState({ hoveringControls: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSecondaryRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallResumeClick = (): void => {
|
private onCallResumeClick = (): void => {
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||||
|
@ -424,180 +330,60 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onToggleSidebar = (): void => {
|
private onToggleSidebar = (): void => {
|
||||||
this.setState({
|
this.setState({ sidebarShown: !this.state.sidebarShown });
|
||||||
sidebarShown: !this.state.sidebarShown,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderCallControls(): JSX.Element {
|
private renderCallControls(): JSX.Element {
|
||||||
const micClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vidClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
|
||||||
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
|
||||||
});
|
|
||||||
|
|
||||||
const screensharingClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_screensharingOn: this.state.screensharing,
|
|
||||||
mx_CallView_callControls_button_screensharingOff: !this.state.screensharing,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sidebarButtonClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown,
|
|
||||||
mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Put the other states of the mic/video icons in the document to make sure they're cached
|
|
||||||
// (otherwise the icon disappears briefly when toggled)
|
|
||||||
const micCacheClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_invisible: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vidCacheClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_invisible: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const callControlsClasses = classNames({
|
|
||||||
mx_CallView_callControls: true,
|
|
||||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
|
||||||
});
|
|
||||||
|
|
||||||
// We don't support call upgrades (yet) so hide the video mute button in voice calls
|
// We don't support call upgrades (yet) so hide the video mute button in voice calls
|
||||||
let vidMuteButton;
|
const vidMuteButtonShown = this.props.call.type === CallType.Video;
|
||||||
if (this.props.call.type === CallType.Video) {
|
|
||||||
vidMuteButton = (
|
|
||||||
<AccessibleButton
|
|
||||||
className={vidClasses}
|
|
||||||
onClick={this.onVidMuteClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Screensharing is possible, if we can send a second stream and
|
// Screensharing is possible, if we can send a second stream and
|
||||||
// identify it using SDPStreamMetadata or if we can replace the already
|
// identify it using SDPStreamMetadata or if we can replace the already
|
||||||
// existing usermedia track by a screensharing track. We also need to be
|
// existing usermedia track by a screensharing track. We also need to be
|
||||||
// connected to know the state of the other side
|
// connected to know the state of the other side
|
||||||
let screensharingButton;
|
const screensharingButtonShown = (
|
||||||
if (
|
|
||||||
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
|
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
|
||||||
this.props.call.state === CallState.Connected
|
this.props.call.state === CallState.Connected
|
||||||
) {
|
|
||||||
screensharingButton = (
|
|
||||||
<AccessibleButton
|
|
||||||
className={screensharingClasses}
|
|
||||||
onClick={this.onScreenshareClick}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// To show the sidebar we need secondary feeds, if we don't have them,
|
// To show the sidebar we need secondary feeds, if we don't have them,
|
||||||
// we can hide this button. If we are in PiP, sidebar is also hidden, so
|
// we can hide this button. If we are in PiP, sidebar is also hidden, so
|
||||||
// we can hide the button too
|
// we can hide the button too
|
||||||
let sidebarButton;
|
const sidebarButtonShown = (
|
||||||
if (
|
|
||||||
!this.props.pipMode &&
|
|
||||||
(
|
|
||||||
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
|
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
|
||||||
this.props.call.isScreensharing()
|
this.props.call.isScreensharing()
|
||||||
)
|
|
||||||
) {
|
|
||||||
sidebarButton = (
|
|
||||||
<AccessibleButton
|
|
||||||
className={sidebarButtonClasses}
|
|
||||||
onClick={this.onToggleSidebar}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// The dial pad & 'more' button actions are only relevant in a connected call
|
// The dial pad & 'more' button actions are only relevant in a connected call
|
||||||
let contextMenuButton;
|
const contextMenuButtonShown = this.state.callState === CallState.Connected;
|
||||||
if (this.state.callState === CallState.Connected) {
|
const dialpadButtonShown = (
|
||||||
contextMenuButton = (
|
this.state.callState === CallState.Connected &&
|
||||||
<ContextMenuButton
|
this.props.call.opponentSupportsDTMF()
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
|
||||||
onClick={this.onMoreClick}
|
|
||||||
inputRef={this.contextMenuButton}
|
|
||||||
isExpanded={this.state.showMoreMenu}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
let dialpadButton;
|
|
||||||
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
|
||||||
dialpadButton = (
|
|
||||||
<ContextMenuButton
|
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
|
||||||
inputRef={this.dialpadButton}
|
|
||||||
onClick={this.onDialpadClick}
|
|
||||||
isExpanded={this.state.showDialpad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dialPad;
|
|
||||||
if (this.state.showDialpad) {
|
|
||||||
dialPad = <DialpadContextMenu
|
|
||||||
{...alwaysAboveRightOf(
|
|
||||||
this.dialpadButton.current.getBoundingClientRect(),
|
|
||||||
ChevronFace.None,
|
|
||||||
CONTEXT_MENU_VPADDING,
|
|
||||||
)}
|
|
||||||
mountAsChild={true}
|
|
||||||
onFinished={this.closeDialpad}
|
|
||||||
call={this.props.call}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contextMenu;
|
|
||||||
if (this.state.showMoreMenu) {
|
|
||||||
contextMenu = <CallContextMenu
|
|
||||||
{...alwaysAboveLeftOf(
|
|
||||||
this.contextMenuButton.current.getBoundingClientRect(),
|
|
||||||
ChevronFace.None,
|
|
||||||
CONTEXT_MENU_VPADDING,
|
|
||||||
)}
|
|
||||||
mountAsChild={true}
|
|
||||||
onFinished={this.closeContextMenu}
|
|
||||||
call={this.props.call}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<CallViewButtons
|
||||||
className={callControlsClasses}
|
ref={this.buttonsRef}
|
||||||
onMouseEnter={this.onCallControlsMouseEnter}
|
call={this.props.call}
|
||||||
onMouseLeave={this.onCallControlsMouseLeave}
|
pipMode={this.props.pipMode}
|
||||||
>
|
handlers={{
|
||||||
{ dialPad }
|
onToggleSidebarClick: this.onToggleSidebar,
|
||||||
{ contextMenu }
|
onScreenshareClick: this.onScreenshareClick,
|
||||||
{ dialpadButton }
|
onHangupClick: this.onHangupClick,
|
||||||
<AccessibleButton
|
onMicMuteClick: this.onMicMuteClick,
|
||||||
className={micClasses}
|
onVidMuteClick: this.onVidMuteClick,
|
||||||
onClick={this.onMicMuteClick}
|
}}
|
||||||
|
buttonsState={{
|
||||||
|
micMuted: this.state.micMuted,
|
||||||
|
vidMuted: this.state.vidMuted,
|
||||||
|
sidebarShown: this.state.sidebarShown,
|
||||||
|
screensharing: this.state.screensharing,
|
||||||
|
}}
|
||||||
|
buttonsVisibility={{
|
||||||
|
vidMute: vidMuteButtonShown,
|
||||||
|
screensharing: screensharingButtonShown,
|
||||||
|
sidebar: sidebarButtonShown,
|
||||||
|
contextMenu: contextMenuButtonShown,
|
||||||
|
dialpad: dialpadButtonShown,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{ vidMuteButton }
|
|
||||||
<div className={micCacheClasses} />
|
|
||||||
<div className={vidCacheClasses} />
|
|
||||||
{ screensharingButton }
|
|
||||||
{ sidebarButton }
|
|
||||||
{ contextMenuButton }
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
|
||||||
onClick={this.onHangupClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,83 +600,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||||
let myClassName;
|
|
||||||
|
|
||||||
let fullScreenButton;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
fullScreenButton = (
|
|
||||||
<div
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
|
||||||
onClick={this.onFullscreenClick}
|
|
||||||
title={_t("Fill Screen")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let expandButton;
|
|
||||||
if (this.props.pipMode) {
|
|
||||||
expandButton = <div
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
|
||||||
onClick={this.onExpandClick}
|
|
||||||
title={_t("Return to call")}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerControls = <div className="mx_CallView_header_controls">
|
|
||||||
{ fullScreenButton }
|
|
||||||
{ expandButton }
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
|
|
||||||
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
|
|
||||||
"mx_CallView_header_callTypeIcon_video": isVideoCall,
|
|
||||||
});
|
|
||||||
|
|
||||||
let header: React.ReactNode;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
header = <div className="mx_CallView_header">
|
|
||||||
<div className={callTypeIconClassName} />
|
|
||||||
<span className="mx_CallView_header_callType">{ callTypeText }</span>
|
|
||||||
{ headerControls }
|
|
||||||
</div>;
|
|
||||||
myClassName = 'mx_CallView_large';
|
|
||||||
} else {
|
|
||||||
let secondaryCallInfo;
|
|
||||||
if (this.props.secondaryCall) {
|
|
||||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
|
||||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
|
||||||
<span className="mx_CallView_secondaryCall_roomName">
|
|
||||||
{ _t("%(name)s on hold", { name: secCallRoom.name }) }
|
|
||||||
</span>
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
header = (
|
|
||||||
<div
|
|
||||||
className="mx_CallView_header"
|
|
||||||
onMouseDown={this.props.onMouseDownOnHeader}
|
|
||||||
>
|
|
||||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_CallView_header_callInfo">
|
|
||||||
<div className="mx_CallView_header_roomName">{ callRoom.name }</div>
|
|
||||||
<div className="mx_CallView_header_callTypeSmall">
|
|
||||||
{ callTypeText }
|
|
||||||
{ secondaryCallInfo }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{ headerControls }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
myClassName = 'mx_CallView_pip';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"mx_CallView " + myClassName}>
|
return <div className={"mx_CallView " + myClassName}>
|
||||||
{ header }
|
<CallViewHeader
|
||||||
|
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||||
|
pipMode={this.props.pipMode}
|
||||||
|
type={this.props.call.type}
|
||||||
|
callRooms={[callRoom, secCallRoom]}
|
||||||
|
/>
|
||||||
{ contentView }
|
{ contentView }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
316
src/components/views/voip/CallView/CallViewButtons.tsx
Normal file
316
src/components/views/voip/CallView/CallViewButtons.tsx
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createRef } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||||
|
import CallContextMenu from "../../context_menus/CallContextMenu";
|
||||||
|
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
||||||
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { Alignment } from "../../elements/Tooltip";
|
||||||
|
import {
|
||||||
|
alwaysAboveLeftOf,
|
||||||
|
alwaysAboveRightOf,
|
||||||
|
ChevronFace,
|
||||||
|
ContextMenuTooltipButton,
|
||||||
|
} from '../../../structures/ContextMenu';
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
|
// Height of the header duplicated from CSS because we need to subtract it from our max
|
||||||
|
// height to get the max height of the video
|
||||||
|
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
||||||
|
|
||||||
|
const TOOLTIP_Y_OFFSET = -24;
|
||||||
|
|
||||||
|
const CONTROLS_HIDE_DELAY = 2000;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
call: MatrixCall;
|
||||||
|
pipMode: boolean;
|
||||||
|
handlers: {
|
||||||
|
onHangupClick: () => void;
|
||||||
|
onScreenshareClick: () => void;
|
||||||
|
onToggleSidebarClick: () => void;
|
||||||
|
onMicMuteClick: () => void;
|
||||||
|
onVidMuteClick: () => void;
|
||||||
|
};
|
||||||
|
buttonsState: {
|
||||||
|
micMuted: boolean;
|
||||||
|
vidMuted: boolean;
|
||||||
|
sidebarShown: boolean;
|
||||||
|
screensharing: boolean;
|
||||||
|
};
|
||||||
|
buttonsVisibility: {
|
||||||
|
screensharing: boolean;
|
||||||
|
vidMute: boolean;
|
||||||
|
sidebar: boolean;
|
||||||
|
dialpad: boolean;
|
||||||
|
contextMenu: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
visible: boolean;
|
||||||
|
showDialpad: boolean;
|
||||||
|
hoveringControls: boolean;
|
||||||
|
showMoreMenu: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||||
|
private dialpadButton = createRef<HTMLDivElement>();
|
||||||
|
private contextMenuButton = createRef<HTMLDivElement>();
|
||||||
|
private controlsHideTimer: number = null;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showDialpad: false,
|
||||||
|
hoveringControls: false,
|
||||||
|
showMoreMenu: false,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
this.showControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
public showControls(): void {
|
||||||
|
if (this.state.showMoreMenu || this.state.showDialpad) return;
|
||||||
|
|
||||||
|
if (!this.state.visible) {
|
||||||
|
this.setState({
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.controlsHideTimer !== null) {
|
||||||
|
clearTimeout(this.controlsHideTimer);
|
||||||
|
}
|
||||||
|
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onControlsHideTimer = (): void => {
|
||||||
|
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||||
|
this.controlsHideTimer = null;
|
||||||
|
this.setState({ visible: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseEnter = (): void => {
|
||||||
|
this.setState({ hoveringControls: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseLeave = (): void => {
|
||||||
|
this.setState({ hoveringControls: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDialpadClick = (): void => {
|
||||||
|
if (!this.state.showDialpad) {
|
||||||
|
this.setState({ showDialpad: true });
|
||||||
|
this.showControls();
|
||||||
|
} else {
|
||||||
|
this.setState({ showDialpad: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoreClick = (): void => {
|
||||||
|
this.setState({ showMoreMenu: true });
|
||||||
|
this.showControls();
|
||||||
|
};
|
||||||
|
|
||||||
|
private closeDialpad = (): void => {
|
||||||
|
this.setState({ showDialpad: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
private closeContextMenu = (): void => {
|
||||||
|
this.setState({ showMoreMenu: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const micClasses = classNames("mx_CallViewButtons_button", {
|
||||||
|
mx_CallViewButtons_button_micOn: !this.props.buttonsState.micMuted,
|
||||||
|
mx_CallViewButtons_button_micOff: this.props.buttonsState.micMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vidClasses = classNames("mx_CallViewButtons_button", {
|
||||||
|
mx_CallViewButtons_button_vidOn: !this.props.buttonsState.vidMuted,
|
||||||
|
mx_CallViewButtons_button_vidOff: this.props.buttonsState.vidMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const screensharingClasses = classNames("mx_CallViewButtons_button", {
|
||||||
|
mx_CallViewButtons_button_screensharingOn: this.props.buttonsState.screensharing,
|
||||||
|
mx_CallViewButtons_button_screensharingOff: !this.props.buttonsState.screensharing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sidebarButtonClasses = classNames("mx_CallViewButtons_button", {
|
||||||
|
mx_CallViewButtons_button_sidebarOn: this.props.buttonsState.sidebarShown,
|
||||||
|
mx_CallViewButtons_button_sidebarOff: !this.props.buttonsState.sidebarShown,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Put the other states of the mic/video icons in the document to make sure they're cached
|
||||||
|
// (otherwise the icon disappears briefly when toggled)
|
||||||
|
const micCacheClasses = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_button_invisible", {
|
||||||
|
mx_CallViewButtons_button_micOn: this.props.buttonsState.micMuted,
|
||||||
|
mx_CallViewButtons_button_micOff: !this.props.buttonsState.micMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vidCacheClasses = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_button_invisible", {
|
||||||
|
mx_CallViewButtons_button_vidOn: this.props.buttonsState.micMuted,
|
||||||
|
mx_CallViewButtons_button_vidOff: !this.props.buttonsState.micMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const callControlsClasses = classNames("mx_CallViewButtons", {
|
||||||
|
mx_CallViewButtons_hidden: !this.state.visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
let vidMuteButton;
|
||||||
|
if (this.props.buttonsVisibility.vidMute) {
|
||||||
|
vidMuteButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={vidClasses}
|
||||||
|
onClick={this.props.handlers.onVidMuteClick}
|
||||||
|
title={this.props.buttonsState.vidMuted ? _t("Start the camera") : _t("Stop the camera")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let screensharingButton;
|
||||||
|
if (this.props.buttonsVisibility.screensharing) {
|
||||||
|
screensharingButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={screensharingClasses}
|
||||||
|
onClick={this.props.handlers.onScreenshareClick}
|
||||||
|
title={this.props.buttonsState.screensharing
|
||||||
|
? _t("Stop sharing your screen")
|
||||||
|
: _t("Start sharing your screen")
|
||||||
|
}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sidebarButton;
|
||||||
|
if (this.props.buttonsVisibility.sidebar) {
|
||||||
|
sidebarButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={sidebarButtonClasses}
|
||||||
|
onClick={this.props.handlers.onToggleSidebarClick}
|
||||||
|
title={this.props.buttonsState.sidebarShown ? _t("Hide sidebar") : _t("Show sidebar")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contextMenuButton;
|
||||||
|
if (this.props.buttonsVisibility.contextMenu) {
|
||||||
|
contextMenuButton = (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_CallViewButtons_button mx_CallViewButtons_button_more"
|
||||||
|
onClick={this.onMoreClick}
|
||||||
|
inputRef={this.contextMenuButton}
|
||||||
|
isExpanded={this.state.showMoreMenu}
|
||||||
|
title={_t("More")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let dialpadButton;
|
||||||
|
if (this.props.buttonsVisibility.dialpad) {
|
||||||
|
dialpadButton = (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_CallViewButtons_button mx_CallViewButtons_dialpad"
|
||||||
|
inputRef={this.dialpadButton}
|
||||||
|
onClick={this.onDialpadClick}
|
||||||
|
isExpanded={this.state.showDialpad}
|
||||||
|
title={_t("Dialpad")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialPad;
|
||||||
|
if (this.state.showDialpad) {
|
||||||
|
dialPad = <DialpadContextMenu
|
||||||
|
{...alwaysAboveRightOf(
|
||||||
|
this.dialpadButton.current.getBoundingClientRect(),
|
||||||
|
ChevronFace.None,
|
||||||
|
CONTEXT_MENU_VPADDING,
|
||||||
|
)}
|
||||||
|
// We mount the context menus as a as a child typically in order to include the
|
||||||
|
// context menus when fullscreening the call content.
|
||||||
|
// However, this does not work as well when the call is embedded in a
|
||||||
|
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
|
||||||
|
mountAsChild={!this.props.pipMode}
|
||||||
|
onFinished={this.closeDialpad}
|
||||||
|
call={this.props.call}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contextMenu;
|
||||||
|
if (this.state.showMoreMenu) {
|
||||||
|
contextMenu = <CallContextMenu
|
||||||
|
{...alwaysAboveLeftOf(
|
||||||
|
this.contextMenuButton.current.getBoundingClientRect(),
|
||||||
|
ChevronFace.None,
|
||||||
|
CONTEXT_MENU_VPADDING,
|
||||||
|
)}
|
||||||
|
mountAsChild={!this.props.pipMode}
|
||||||
|
onFinished={this.closeContextMenu}
|
||||||
|
call={this.props.call}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={callControlsClasses}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
>
|
||||||
|
{ dialPad }
|
||||||
|
{ contextMenu }
|
||||||
|
{ dialpadButton }
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={micClasses}
|
||||||
|
onClick={this.props.handlers.onMicMuteClick}
|
||||||
|
title={this.props.buttonsState.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
{ vidMuteButton }
|
||||||
|
<div className={micCacheClasses} />
|
||||||
|
<div className={vidCacheClasses} />
|
||||||
|
{ screensharingButton }
|
||||||
|
{ sidebarButton }
|
||||||
|
{ contextMenuButton }
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_CallViewButtons_button mx_CallViewButtons_button_hangup"
|
||||||
|
onClick={this.props.handlers.onHangupClick}
|
||||||
|
title={_t("Hangup")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={TOOLTIP_Y_OFFSET}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
135
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CallType } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import React from 'react';
|
||||||
|
import { _t, _td } from '../../../../languageHandler';
|
||||||
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
|
import dis from '../../../../dispatcher/dispatcher';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
|
const callTypeTranslationByType: Record<CallType, string> = {
|
||||||
|
[CallType.Video]: _td("Video Call"),
|
||||||
|
[CallType.Voice]: _td("Voice Call"),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CallViewHeaderProps {
|
||||||
|
pipMode: boolean;
|
||||||
|
type: CallType;
|
||||||
|
callRooms?: Room[];
|
||||||
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoomAvatarClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullscreenClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'video_fullscreen',
|
||||||
|
fullscreen: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpandClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, type, roomId }) => {
|
||||||
|
return <div className="mx_CallViewHeader_controls">
|
||||||
|
{ !pipMode && <AccessibleTooltipButton
|
||||||
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||||
|
onClick={onFullscreenClick}
|
||||||
|
title={_t("Fill Screen")}
|
||||||
|
/> }
|
||||||
|
{ pipMode && <AccessibleTooltipButton
|
||||||
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||||
|
onClick={() => onExpandClick(roomId)}
|
||||||
|
title={_t("Return to call")}
|
||||||
|
/> }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||||
|
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||||
|
<AccessibleButton element='span' onClick={() => onRoomAvatarClick(callRoom.roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||||
|
<span className="mx_CallView_secondaryCall_roomName">
|
||||||
|
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||||
|
</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallTypeIcon: React.FC<{ type: CallType }> = ({ type }) => {
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_CallViewHeader_callTypeIcon': true,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_video': type === CallType.Video,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_voice': type === CallType.Voice,
|
||||||
|
});
|
||||||
|
return <div className={classes} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
|
type,
|
||||||
|
pipMode = false,
|
||||||
|
callRooms = [],
|
||||||
|
onPipMouseDown,
|
||||||
|
}) => {
|
||||||
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
|
const callTypeText = _t(callTypeTranslationByType[type]);
|
||||||
|
const callRoomName = callRoom.name;
|
||||||
|
const { roomId } = callRoom;
|
||||||
|
|
||||||
|
if (!pipMode) {
|
||||||
|
return <div className="mx_CallViewHeader">
|
||||||
|
<CallTypeIcon type={type} />
|
||||||
|
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mx_CallViewHeader"
|
||||||
|
onMouseDown={onPipMouseDown}
|
||||||
|
>
|
||||||
|
<AccessibleButton onClick={() => onRoomAvatarClick(roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
|
</AccessibleButton>
|
||||||
|
<div className="mx_CallViewHeader_callInfo">
|
||||||
|
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||||
|
<div className="mx_CallViewHeader_callTypeSmall">
|
||||||
|
{ callTypeText }
|
||||||
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CallViewHeader;
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } 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', '#'];
|
||||||
|
@ -30,12 +30,12 @@ interface IButtonProps {
|
||||||
kind: DialPadButtonKind;
|
kind: DialPadButtonKind;
|
||||||
digit?: string;
|
digit?: string;
|
||||||
digitSubtext?: string;
|
digitSubtext?: string;
|
||||||
onButtonPress: (string) => void;
|
onButtonPress: (digit: string, ev: ButtonEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DialPadButton extends React.PureComponent<IButtonProps> {
|
class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||||
onClick = () => {
|
onClick = (ev: ButtonEvent) => {
|
||||||
this.props.onButtonPress(this.props.digit);
|
this.props.onButtonPress(this.props.digit, ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -54,10 +54,10 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onDigitPress: (string) => void;
|
onDigitPress: (digit: string, ev: ButtonEvent) => void;
|
||||||
hasDial: boolean;
|
hasDial: boolean;
|
||||||
onDeletePress?: (string) => void;
|
onDeletePress?: (ev: ButtonEvent) => void;
|
||||||
onDialPress?: (string) => void;
|
onDialPress?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.voip.DialPad")
|
@replaceableComponent("views.voip.DialPad")
|
||||||
|
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import { createRef } from "react";
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import DialPad from './DialPad';
|
import DialPad from './DialPad';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -34,6 +35,8 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.voip.DialPadModal")
|
@replaceableComponent("views.voip.DialPadModal")
|
||||||
export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
|
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -54,13 +57,27 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
this.onDialPress();
|
this.onDialPress();
|
||||||
};
|
};
|
||||||
|
|
||||||
onDigitPress = (digit) => {
|
onDigitPress = (digit: string, ev: ButtonEvent) => {
|
||||||
this.setState({ value: this.state.value + digit });
|
this.setState({ value: this.state.value + digit });
|
||||||
|
|
||||||
|
// Keep the number field focused so that keyboard entry is still available.
|
||||||
|
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||||
|
// i.e someone using keyboard navigation.
|
||||||
|
if (ev.type === "click") {
|
||||||
|
this.numberEntryFieldRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeletePress = () => {
|
onDeletePress = (ev: ButtonEvent) => {
|
||||||
if (this.state.value.length === 0) return;
|
if (this.state.value.length === 0) return;
|
||||||
this.setState({ value: this.state.value.slice(0, -1) });
|
this.setState({ value: this.state.value.slice(0, -1) });
|
||||||
|
|
||||||
|
// Keep the number field focused so that keyboard entry is still available
|
||||||
|
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||||
|
// i.e someone using keyboard navigation.
|
||||||
|
if (ev.type === "click") {
|
||||||
|
this.numberEntryFieldRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onDialPress = async () => {
|
onDialPress = async () => {
|
||||||
|
@ -82,6 +99,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
let dialPadField;
|
let dialPadField;
|
||||||
if (this.state.value.length !== 0) {
|
if (this.state.value.length !== 0) {
|
||||||
dialPadField = <Field
|
dialPadField = <Field
|
||||||
|
ref={this.numberEntryFieldRef}
|
||||||
className="mx_DialPadModal_field"
|
className="mx_DialPadModal_field"
|
||||||
id="dialpad_number"
|
id="dialpad_number"
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
|
@ -91,6 +109,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
dialPadField = <Field
|
dialPadField = <Field
|
||||||
|
ref={this.numberEntryFieldRef}
|
||||||
className="mx_DialPadModal_field"
|
className="mx_DialPadModal_field"
|
||||||
id="dialpad_number"
|
id="dialpad_number"
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
|
|
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createRef } from 'react';
|
||||||
|
import UIStore from '../../../stores/UIStore';
|
||||||
|
import { lerp } from '../../../utils/AnimationUtils';
|
||||||
|
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||||
|
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||||
|
|
||||||
|
const PIP_VIEW_WIDTH = 336;
|
||||||
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
|
||||||
|
const MOVING_AMT = 0.2;
|
||||||
|
const SNAPPING_AMT = 0.1;
|
||||||
|
|
||||||
|
const PADDING = {
|
||||||
|
top: 58,
|
||||||
|
bottom: 58,
|
||||||
|
left: 76,
|
||||||
|
right: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IChildrenOptions {
|
||||||
|
onStartMoving: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
onResize: (event: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children: ({ onStartMoving, onResize }: IChildrenOptions) => React.ReactNode;
|
||||||
|
draggable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Position of the PictureInPictureDragger
|
||||||
|
translationX: number;
|
||||||
|
translationY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PictureInPictureDragger shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||||
|
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.voip.PictureInPictureDragger")
|
||||||
|
export default class PictureInPictureDragger extends React.Component<IProps, IState> {
|
||||||
|
private callViewWrapper = createRef<HTMLDivElement>();
|
||||||
|
private initX = 0;
|
||||||
|
private initY = 0;
|
||||||
|
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||||
|
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
|
||||||
|
private moving = false;
|
||||||
|
private scheduledUpdate = new MarkedExecution(
|
||||||
|
() => this.animationCallback(),
|
||||||
|
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||||
|
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
document.addEventListener("mousemove", this.onMoving);
|
||||||
|
document.addEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousemove", this.onMoving);
|
||||||
|
document.removeEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private animationCallback = () => {
|
||||||
|
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||||
|
// the desiredTranslation and translation, quit the animationCallback
|
||||||
|
// loop. If that is the case, it means the PiP has snapped into its
|
||||||
|
// position and there is nothing to do. Not doing this would cause an
|
||||||
|
// infinite loop
|
||||||
|
if (
|
||||||
|
!this.moving &&
|
||||||
|
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||||
|
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||||
|
this.setState({
|
||||||
|
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||||
|
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||||
|
});
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||||
|
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||||
|
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||||
|
|
||||||
|
// Avoid overflow on the x axis
|
||||||
|
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||||
|
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||||
|
} else if (inTranslationX <= 0) {
|
||||||
|
this.desiredTranslationX = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = inTranslationX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid overflow on the y axis
|
||||||
|
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||||
|
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||||
|
} else if (inTranslationY <= 0) {
|
||||||
|
this.desiredTranslationY = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationY = inTranslationY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = (): void => {
|
||||||
|
this.snap(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
private snap = (animate = false) => {
|
||||||
|
const translationX = this.desiredTranslationX;
|
||||||
|
const translationY = this.desiredTranslationY;
|
||||||
|
// We subtract the PiP size from the window size in order to calculate
|
||||||
|
// the position to snap to from the PiP center and not its top-left
|
||||||
|
// corner
|
||||||
|
const windowWidth = (
|
||||||
|
UIStore.instance.windowWidth -
|
||||||
|
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||||
|
);
|
||||||
|
const windowHeight = (
|
||||||
|
UIStore.instance.windowHeight -
|
||||||
|
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
translationX: this.desiredTranslationX,
|
||||||
|
translationY: this.desiredTranslationY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onStartMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.moving = true;
|
||||||
|
this.initX = event.pageX - this.desiredTranslationX;
|
||||||
|
this.initY = event.pageY - this.desiredTranslationY;
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
if (!this.moving) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEndMoving = () => {
|
||||||
|
this.moving = false;
|
||||||
|
this.snap(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
|
const style = {
|
||||||
|
transform: `translateX(${translatePixelsX})
|
||||||
|
translateY(${translatePixelsY})`,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.props.className}
|
||||||
|
style={this.props.draggable ? style : undefined}
|
||||||
|
ref={this.callViewWrapper}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{ this.props.children({
|
||||||
|
onStartMoving: this.onStartMoving,
|
||||||
|
onResize: this.onResize,
|
||||||
|
}) }
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,18 +31,26 @@ export interface IEncryptedFile {
|
||||||
v: string;
|
v: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMediaEventContent {
|
export interface IMediaEventInfo {
|
||||||
body?: string;
|
|
||||||
url?: string; // required on unencrypted media
|
|
||||||
file?: IEncryptedFile; // required for *encrypted* media
|
|
||||||
info?: {
|
|
||||||
thumbnail_url?: string; // eslint-disable-line camelcase
|
thumbnail_url?: string; // eslint-disable-line camelcase
|
||||||
thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase
|
thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase
|
||||||
|
thumbnail_info?: { // eslint-disable-line camelcase
|
||||||
mimetype: string;
|
mimetype: string;
|
||||||
w?: number;
|
w?: number;
|
||||||
h?: number;
|
h?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
mimetype: string;
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMediaEventContent {
|
||||||
|
body?: string;
|
||||||
|
url?: string; // required on unencrypted media
|
||||||
|
file?: IEncryptedFile; // required for *encrypted* media
|
||||||
|
info?: IMediaEventInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPreparedMedia extends IMediaObject {
|
export interface IPreparedMedia extends IMediaObject {
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default class AutocompleteWrapperModel {
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public onEscape(e: KeyboardEvent) {
|
public onEscape(e: KeyboardEvent): void {
|
||||||
this.getAutocompleterComponent().onEscape(e);
|
this.getAutocompleterComponent().onEscape(e);
|
||||||
this.updateCallback({
|
this.updateCallback({
|
||||||
replaceParts: [this.partCreator.plain(this.queryPart.text)],
|
replaceParts: [this.partCreator.plain(this.queryPart.text)],
|
||||||
|
@ -51,27 +51,27 @@ export default class AutocompleteWrapperModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public close() {
|
public close(): void {
|
||||||
this.updateCallback({ close: true });
|
this.updateCallback({ close: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasSelection() {
|
public hasSelection(): boolean {
|
||||||
return this.getAutocompleterComponent().hasSelection();
|
return this.getAutocompleterComponent().hasSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasCompletions() {
|
public hasCompletions(): boolean {
|
||||||
const ac = this.getAutocompleterComponent();
|
const ac = this.getAutocompleterComponent();
|
||||||
return ac && ac.countCompletions() > 0;
|
return ac && ac.countCompletions() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onEnter() {
|
public onEnter(): void {
|
||||||
this.updateCallback({ close: true });
|
this.updateCallback({ close: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there is no current autocompletion, start one and move to the first selection.
|
* If there is no current autocompletion, start one and move to the first selection.
|
||||||
*/
|
*/
|
||||||
public async startSelection() {
|
public async startSelection(): Promise<void> {
|
||||||
const acComponent = this.getAutocompleterComponent();
|
const acComponent = this.getAutocompleterComponent();
|
||||||
if (acComponent.countCompletions() === 0) {
|
if (acComponent.countCompletions() === 0) {
|
||||||
// Force completions to show for the text currently entered
|
// Force completions to show for the text currently entered
|
||||||
|
@ -81,15 +81,15 @@ export default class AutocompleteWrapperModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectPreviousSelection() {
|
public selectPreviousSelection(): void {
|
||||||
this.getAutocompleterComponent().moveSelection(-1);
|
this.getAutocompleterComponent().moveSelection(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectNextSelection() {
|
public selectNextSelection(): void {
|
||||||
this.getAutocompleterComponent().moveSelection(+1);
|
this.getAutocompleterComponent().moveSelection(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onPartUpdate(part: Part, pos: DocumentPosition) {
|
public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> {
|
||||||
// cache the typed value and caret here
|
// cache the typed value and caret here
|
||||||
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
|
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
|
||||||
this.queryPart = part;
|
this.queryPart = part;
|
||||||
|
@ -97,7 +97,7 @@ export default class AutocompleteWrapperModel {
|
||||||
return this.updateQuery(part.text);
|
return this.updateQuery(part.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onComponentSelectionChange(completion: ICompletion) {
|
public onComponentSelectionChange(completion: ICompletion): void {
|
||||||
if (!completion) {
|
if (!completion) {
|
||||||
this.updateCallback({
|
this.updateCallback({
|
||||||
replaceParts: [this.queryPart],
|
replaceParts: [this.queryPart],
|
||||||
|
@ -109,14 +109,14 @@ export default class AutocompleteWrapperModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onComponentConfirm(completion: ICompletion) {
|
public onComponentConfirm(completion: ICompletion): void {
|
||||||
this.updateCallback({
|
this.updateCallback({
|
||||||
replaceParts: this.partForCompletion(completion),
|
replaceParts: this.partForCompletion(completion),
|
||||||
close: true,
|
close: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private partForCompletion(completion: ICompletion) {
|
private partForCompletion(completion: ICompletion): Part[] {
|
||||||
const { completionId } = completion;
|
const { completionId } = completion;
|
||||||
const text = completion.completion;
|
const text = completion.completion;
|
||||||
switch (completion.type) {
|
switch (completion.type) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render";
|
||||||
import Range from "./range";
|
import Range from "./range";
|
||||||
import EditorModel from "./model";
|
import EditorModel from "./model";
|
||||||
import DocumentPosition, { IPosition } from "./position";
|
import DocumentPosition, { IPosition } from "./position";
|
||||||
import { Part } from "./parts";
|
import { Part, Type } from "./parts";
|
||||||
|
|
||||||
export type Caret = Range | DocumentPosition;
|
export type Caret = Range | DocumentPosition;
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||||
// to find newline parts
|
// to find newline parts
|
||||||
for (let i = 0; i <= partIndex; ++i) {
|
for (let i = 0; i <= partIndex; ++i) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
if (part.type === "newline") {
|
if (part.type === Type.Newline) {
|
||||||
lineIndex += 1;
|
lineIndex += 1;
|
||||||
nodeIndex = -1;
|
nodeIndex = -1;
|
||||||
prevPart = null;
|
prevPart = null;
|
||||||
|
@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||||
// and not an adjacent caret node
|
// and not an adjacent caret node
|
||||||
if (i < partIndex) {
|
if (i < partIndex) {
|
||||||
const nextPart = parts[i + 1];
|
const nextPart = parts[i + 1];
|
||||||
const isLastOfLine = !nextPart || nextPart.type === "newline";
|
const isLastOfLine = !nextPart || nextPart.type === Type.Newline;
|
||||||
if (needsCaretNodeAfter(part, isLastOfLine)) {
|
if (needsCaretNodeAfter(part, isLastOfLine)) {
|
||||||
nodeIndex += 1;
|
nodeIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { walkDOMDepthFirst } from "./dom";
|
import { walkDOMDepthFirst } from "./dom";
|
||||||
import { checkBlockNode } from "../HtmlUtils";
|
import { checkBlockNode } from "../HtmlUtils";
|
||||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||||
import { PartCreator } from "./parts";
|
import { PartCreator, Type } from "./parts";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
|
|
||||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||||
|
@ -206,7 +206,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
|
||||||
parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
||||||
}
|
}
|
||||||
for (let i = 0; i < parts.length; i += 1) {
|
for (let i = 0; i < parts.length; i += 1) {
|
||||||
if (parts[i].type === "newline") {
|
if (parts[i].type === Type.Newline) {
|
||||||
parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export interface IDiff {
|
||||||
at?: number;
|
at?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstDiff(a: string, b: string) {
|
function firstDiff(a: string, b: string): number {
|
||||||
const compareLen = Math.min(a.length, b.length);
|
const compareLen = Math.min(a.length, b.length);
|
||||||
for (let i = 0; i < compareLen; ++i) {
|
for (let i = 0; i < compareLen; ++i) {
|
||||||
if (a[i] !== b[i]) {
|
if (a[i] !== b[i]) {
|
||||||
|
|
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