mirror of https://github.com/vector-im/riot-web
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18092
commit
db951b43a3
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
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));
|
||||
- run: unzip previewbuild.zip && rm previewbuild.zip
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: .
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
enable-pull-request-comment: true
|
||||
enable-commit-comment: false
|
||||
overwrites-pull-request-comment: true
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
|
|
@ -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 }}
|
|
@ -241,6 +241,7 @@
|
|||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||
@import "./views/settings/_EmailAddresses.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
@import "./views/settings/_LayoutSwitcher.scss";
|
||||
@import "./views/settings/_Notifications.scss";
|
||||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
@import "./views/settings/_ProfileSettings.scss";
|
||||
|
@ -274,6 +275,7 @@
|
|||
@import "./views/voip/_CallPreview.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
@import "./views/voip/_CallViewHeader.scss";
|
||||
@import "./views/voip/_CallViewSidebar.scss";
|
||||
@import "./views/voip/_DialPad.scss";
|
||||
@import "./views/voip/_DialPadContextMenu.scss";
|
||||
|
|
|
@ -24,33 +24,33 @@ limitations under the License.
|
|||
align-items: flex-start;
|
||||
height: 500px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 8px;
|
||||
}
|
||||
.mx_desktopCapturerSourcePicker_source {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
|
||||
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $accent-color;
|
||||
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source_name {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_desktopCapturerSourcePicker_source_name {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
height: 28px;
|
||||
border: 2px solid $voice-record-stop-border-color;
|
||||
border-radius: 32px;
|
||||
margin-right: 16px; // between us and the send button
|
||||
margin-right: 8px; // between us and the waveform component
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
|
@ -64,6 +64,10 @@ limitations under the License.
|
|||
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
||||
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||
|
||||
// fixed height to reduce layout jumps with the play button appearing
|
||||
// https://github.com/vector-im/element-web/issues/18431
|
||||
height: 32px;
|
||||
|
||||
margin: 6px; // force the composer area to put a gutter around us
|
||||
margin-right: 12px; // isolate from stop/send button
|
||||
|
||||
|
@ -83,7 +87,7 @@ limitations under the License.
|
|||
height: 10px;
|
||||
position: absolute;
|
||||
left: 12px; // 12px from the left edge for container padding
|
||||
top: 16px; // vertically center (middle align with clock)
|
||||
top: 17px; // vertically center (middle align with clock)
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -155,79 +155,6 @@ limitations under the License.
|
|||
margin-left: calc($font-16px + 10px);
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
|
||||
color: $primary-fg-color;
|
||||
|
||||
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 300px;
|
||||
|
||||
border: 1px solid $appearance-tab-border-color;
|
||||
border-radius: 10px;
|
||||
|
||||
.mx_EventTile_msgOption,
|
||||
.mx_MessageActionBar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_Layout_RadioButton_preview {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_RadioButton {
|
||||
flex-grow: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mx_EventTile_content {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
|
||||
border-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RadioButton {
|
||||
border-top: 1px solid $appearance-tab-border-color;
|
||||
|
||||
> input + div {
|
||||
border-color: rgba($muted-fg-color, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RadioButton_checked {
|
||||
background-color: rgba($accent-color, 0.08);
|
||||
}
|
||||
|
||||
.mx_EventTile {
|
||||
margin: 0;
|
||||
&[data-layout=bubble] {
|
||||
margin-right: 40px;
|
||||
}
|
||||
&[data-layout=irc] {
|
||||
> a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.mx_EventTile_line {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_Advanced {
|
||||
color: $primary-fg-color;
|
||||
|
||||
|
|
|
@ -28,28 +28,32 @@ limitations under the License.
|
|||
user-select: all;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken {
|
||||
.mx_HelpUserSettingsTab_copy {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
border: solid 1px $light-fg-color;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
width: max-content;
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
display: inherit;
|
||||
}
|
||||
.mx_HelpUserSettingsTab_copyButton {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
display: block;
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
||||
mask-image: url($copy-button-url);
|
||||
background-color: $message-action-bar-fg-color;
|
||||
margin-left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
mask-image: url($copy-button-url);
|
||||
background-color: $message-action-bar-fg-color;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,120 +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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -222,6 +222,13 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
$composer-shadow-color: tranparent;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #14322E;
|
||||
$eventbubble-others-bg: $event-selected-color;
|
||||
$eventbubble-bg-hover: #1C2026;
|
||||
$eventbubble-avatar-outline: $bg-color;
|
||||
$eventbubble-reply-color: #C1C6CD;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -140,3 +140,10 @@ $event-highlight-bg-color: var(--timeline-highlights-color);
|
|||
//
|
||||
// redirect some variables away from their hardcoded values in the light theme
|
||||
$settings-grey-fg-color: $primary-fg-color;
|
||||
|
||||
// --eventbubble colors
|
||||
$eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg);
|
||||
$eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg);
|
||||
$eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover);
|
||||
$eventbubble-avatar-outline: var(--eventbubble-avatar-outline, $eventbubble-avatar-outline);
|
||||
$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-color);
|
||||
|
|
|
@ -509,13 +509,17 @@ export default class CallHandler extends EventEmitter {
|
|||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
|
||||
this.play(AudioID.Busy);
|
||||
|
||||
// Don't show a modal when we got rejected/the call was hung up
|
||||
if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break;
|
||||
|
||||
let title;
|
||||
let description;
|
||||
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
|
||||
if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||
title = _t("User Busy");
|
||||
description = _t("The user you called is busy.");
|
||||
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
|
||||
} else {
|
||||
title = _t("Call Failed");
|
||||
description = _t("The call could not be established");
|
||||
}
|
||||
|
|
|
@ -123,6 +123,19 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
|||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||
}
|
||||
|
||||
export function formatCallTime(delta: Date): string {
|
||||
const hours = delta.getUTCHours();
|
||||
const minutes = delta.getUTCMinutes();
|
||||
const seconds = delta.getUTCSeconds();
|
||||
|
||||
let output = "";
|
||||
if (hours) output += `${hours}h `;
|
||||
if (minutes || output) output += `${minutes}m `;
|
||||
if (seconds || output) output += `${seconds}s`;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
|
|
|
@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
|
|||
SilencedChanged = "silenced_changed",
|
||||
}
|
||||
|
||||
const CONNECTING_STATES = [
|
||||
CallState.Connecting,
|
||||
CallState.WaitLocalMedia,
|
||||
CallState.CreateOffer,
|
||||
CallState.CreateAnswer,
|
||||
];
|
||||
|
||||
const SUPPORTED_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.Connecting,
|
||||
CallState.Ringing,
|
||||
];
|
||||
|
||||
|
@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
||||
}
|
||||
|
||||
private get selectAnswer(): MatrixEvent {
|
||||
return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
|
||||
}
|
||||
|
||||
public get isVoice(): boolean {
|
||||
const invite = this.invite;
|
||||
if (!invite) return;
|
||||
|
@ -82,6 +92,11 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
return Boolean(this.reject);
|
||||
}
|
||||
|
||||
public get duration(): Date {
|
||||
if (!this.hangup || !this.selectAnswer) return;
|
||||
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are only events from the other side - we missed the call
|
||||
*/
|
||||
|
@ -127,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
}
|
||||
|
||||
private setState = () => {
|
||||
if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||
if (CONNECTING_STATES.includes(this.call?.state)) {
|
||||
this.state = CallState.Connecting;
|
||||
} else if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||
this.state = this.call.state;
|
||||
} else {
|
||||
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
||||
|
|
|
@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
|||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
|
||||
const groupedEvents = [
|
||||
EventType.RoomMember,
|
||||
EventType.RoomThirdPartyInvite,
|
||||
EventType.RoomServerAcl,
|
||||
EventType.RoomPinnedEvents,
|
||||
];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
|
@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
|
|||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
class MemberGrouper extends BaseGrouper {
|
||||
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
||||
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
|
||||
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
|
|||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return membershipTypes.includes(ev.getType() as EventType);
|
||||
return groupedEvents.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||
|
|
|
@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
|
||||
|
||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||
this.props.timelineSet.room.roomId,
|
||||
'rm', this.state.readMarkerEventId,
|
||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||
' hidden:' + hiddenRR,
|
||||
);
|
||||
MatrixClientPeg.get().setRoomReadMarkers(
|
||||
this.props.timelineSet.room.roomId,
|
||||
roomId,
|
||||
this.state.readMarkerEventId,
|
||||
lastReadEvent, // Could be null, in which case no RR is sent
|
||||
{},
|
||||
{ hidden: hiddenRR },
|
||||
).catch((e) => {
|
||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||
|
|
|
@ -62,10 +62,10 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
|||
|
||||
// Set all this into the initial state
|
||||
this.state = {
|
||||
...urlInfo,
|
||||
roomMember,
|
||||
isWrapped: null,
|
||||
widgetDomain: null,
|
||||
isWrapped: null,
|
||||
roomMember,
|
||||
...urlInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ interface IProps {
|
|||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[];
|
||||
// The text to show as the summary of this event list
|
||||
summaryText?: string;
|
||||
summaryText?: string | JSX.Element;
|
||||
// An array of EventTiles to render when expanded
|
||||
children: ReactNode[];
|
||||
// Called when the event list expansion is toggled
|
||||
|
|
|
@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
|||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
|
||||
const onPinnedMessagesClick = (): void => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.PinnedMessages,
|
||||
allowClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength?: number;
|
||||
|
@ -60,6 +76,7 @@ enum TransitionType {
|
|||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
ServerAcl = "server_acl",
|
||||
ChangedPins = "pinned_messages"
|
||||
}
|
||||
|
||||
const SEP = ",";
|
||||
|
@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* `Object.keys(eventAggregates)`.
|
||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||
*/
|
||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
||||
private generateSummary(
|
||||
eventAggregates: Record<string, string[]>,
|
||||
orderedTransitionSequences: string[],
|
||||
): string | JSX.Element {
|
||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this.renderNameList(userNames);
|
||||
|
@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
return summaries.join(", ");
|
||||
return jsxJoin(summaries, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
||||
private static getDescriptionForTransition(
|
||||
t: TransitionType,
|
||||
userCount: number,
|
||||
repeats: number,
|
||||
): string | JSX.Element {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
|
@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
{ severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "pinned_messages":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ severalUsers: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
|
||||
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
{ oneUser: "", count: repeats },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* if a transition is not recognised.
|
||||
*/
|
||||
private static getTransition(e: IUserEvents): TransitionType {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
const type = e.mxEvent.getType();
|
||||
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return TransitionType.InviteWithdrawal;
|
||||
}
|
||||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
||||
} else if (type === EventType.RoomServerAcl) {
|
||||
return TransitionType.ServerAcl;
|
||||
} else if (type === EventType.RoomPinnedEvents) {
|
||||
return TransitionType.ChangedPins;
|
||||
}
|
||||
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
|
@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
// Object mapping user IDs to an array of IUserEvents
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
||||
const type = e.getType();
|
||||
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
}
|
||||
|
||||
if (e.getType() === 'm.room.server_acl') {
|
||||
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
latestUserAvatarMember.set(userId, e.sender);
|
||||
} else if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
if (e.getType() === 'm.room.third_party_invite') {
|
||||
if (type === EventType.RoomThirdPartyInvite) {
|
||||
displayName = e.getContent().display_name;
|
||||
} else if (e.getType() === 'm.room.server_acl') {
|
||||
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
|
||||
displayName = e.sender.name;
|
||||
} else if (e.target) {
|
||||
displayName = e.target.name;
|
||||
|
|
|
@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
import { formatCallTime } from "../../../DateUtils";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -131,9 +132,14 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
// https://github.com/vector-im/riot-android/issues/2623
|
||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||
// Also, if we don't have a reason
|
||||
const duration = this.props.callEventGrouper.duration;
|
||||
let text = _t("Call ended");
|
||||
if (duration) {
|
||||
text += " • " + formatCallTime(duration);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("Call ended") }
|
||||
{ text }
|
||||
</div>
|
||||
);
|
||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||
|
|
|
@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
// We also round the number as it sometimes can be 29.99...
|
||||
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -725,7 +724,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||
<div
|
||||
className={classes}
|
||||
contentEditable="true"
|
||||
contentEditable={this.props.disabled ? null : true}
|
||||
tabIndex={0}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
|
|
|
@ -58,6 +58,7 @@ function ComposerAvatar(props: IComposerAvatarProps) {
|
|||
|
||||
interface ISendButtonProps {
|
||||
onClick: () => void;
|
||||
title?: string; // defaults to something generic
|
||||
}
|
||||
|
||||
function SendButton(props: ISendButtonProps) {
|
||||
|
@ -65,7 +66,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={_t('Send message')}
|
||||
title={props.title ?? _t('Send message')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -401,7 +402,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -38,6 +38,8 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.rooms.ReplyTile")
|
||||
export default class ReplyTile extends React.PureComponent<IProps> {
|
||||
private anchorElement = createRef<HTMLAnchorElement>();
|
||||
|
||||
static defaultProps = {
|
||||
onHeightChanged: () => {},
|
||||
};
|
||||
|
@ -71,7 +73,11 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
// Following a link within a reply should not dispatch the `view_room` action
|
||||
// so that the browser can direct the user to the correct location
|
||||
// The exception being the link wrapping the reply
|
||||
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
|
||||
if (
|
||||
clickTarget.tagName.toLowerCase() !== "a" ||
|
||||
clickTarget.closest("a") === null ||
|
||||
clickTarget === this.anchorElement.current
|
||||
) {
|
||||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
|
@ -141,7 +147,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a href={permalink} onClick={this.onClick}>
|
||||
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||
{ sender }
|
||||
<EventTileType
|
||||
ref="tile"
|
||||
|
|
|
@ -215,7 +215,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
let recordingInfo;
|
||||
let stopOrRecordBtn;
|
||||
let deleteButton;
|
||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
||||
const classes = classNames({
|
||||
|
@ -229,7 +229,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
||||
stopOrRecordBtn = <AccessibleTooltipButton
|
||||
className={classes}
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
|
@ -237,8 +237,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopOrRecordBtn = null;
|
||||
}
|
||||
|
||||
recordingInfo = stopOrRecordBtn;
|
||||
}
|
||||
|
||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||
|
@ -266,11 +264,14 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
</span>;
|
||||
}
|
||||
|
||||
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
||||
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
||||
// rendered when we're not recording, so the record button ends up in the correct spot.
|
||||
return (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopOrRecordBtn }
|
||||
{ this.renderWaveformArea() }
|
||||
{ recordingInfo }
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import EventTilePreview from "../elements/EventTilePreview";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
messagePreviewText: string;
|
||||
onLayoutChanged?: (layout: Layout) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
export default class LayoutSwitcher extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
};
|
||||
}
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const layout = e.target.value as Layout;
|
||||
|
||||
this.setState({ layout: layout });
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
this.props.onLayoutChanged(layout);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||
});
|
||||
const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.Group,
|
||||
});
|
||||
const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", {
|
||||
mx_LayoutSwitcher_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_LayoutSwitcher">
|
||||
<span className="mx_SettingsTab_subheading">
|
||||
{ _t("Message layout") }
|
||||
</span>
|
||||
|
||||
<div className="mx_LayoutSwitcher_RadioButtons">
|
||||
<label className={ircClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.IRC}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.IRC}
|
||||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("IRC") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={groupClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.Group}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.Group}
|
||||
checked={this.state.layout == Layout.Group}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Modern") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={bubbleClasses}>
|
||||
<EventTilePreview
|
||||
className="mx_LayoutSwitcher_RadioButton_preview"
|
||||
message={this.props.messagePreviewText}
|
||||
layout={Layout.Bubble}
|
||||
userId={this.props.userId}
|
||||
displayName={this.props.displayName}
|
||||
avatarUrl={this.props.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value={Layout.Bubble}
|
||||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Message bubbles") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -37,10 +37,9 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import classNames from 'classnames';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
import LayoutSwitcher from "../../LayoutSwitcher";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -243,17 +242,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({ customThemeUrl: e.target.value });
|
||||
};
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let layout;
|
||||
switch (e.target.value) {
|
||||
case "irc": layout = Layout.IRC; break;
|
||||
case "group": layout = Layout.Group; break;
|
||||
case "bubble": layout = Layout.Bubble; break;
|
||||
}
|
||||
|
||||
private onLayoutChanged = (layout: Layout): void => {
|
||||
this.setState({ layout: layout });
|
||||
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
};
|
||||
|
||||
private onIRCLayoutChange = (enabled: boolean) => {
|
||||
|
@ -391,75 +381,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.IRC}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="irc"
|
||||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("IRC") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Group}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="group"
|
||||
checked={this.state.layout == Layout.Group}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Modern") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Bubble}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="bubble"
|
||||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Message bubbles") }
|
||||
</StyledRadioButton>
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
|
@ -527,6 +448,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let layoutSection;
|
||||
if (SettingsStore.getValue("feature_new_layout_switcher")) {
|
||||
layoutSection = (
|
||||
<LayoutSwitcher
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
messagePreviewText={this.MESSAGE_PREVIEW_TEXT}
|
||||
onLayoutChanged={this.onLayoutChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
||||
|
@ -534,7 +468,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||
</div>
|
||||
{ this.renderThemeSection() }
|
||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
||||
{ layoutSection }
|
||||
{ this.renderFontSection() }
|
||||
{ this.renderAdvancedSection() }
|
||||
</div>
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
|
@ -69,6 +69,18 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
private getVersionInfo(): { appVersion: string, olmVersion: string } {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const appVersion = this.state.appVersion || 'unknown';
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
|
||||
return {
|
||||
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
|
||||
olmVersion: `${_t("Olm version:")} ${olmVersion}`,
|
||||
};
|
||||
}
|
||||
|
||||
private onClearCacheAndReload = (e) => {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
|
@ -173,17 +185,26 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
onAccessTokenCopyClick = async (e) => {
|
||||
private async copy(text: string, e: ButtonEvent) {
|
||||
e.preventDefault();
|
||||
const target = e.target; // copy target before we go async and React throws it away
|
||||
const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away
|
||||
|
||||
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
||||
const successful = await copyPlaintext(text);
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||
}
|
||||
|
||||
private onAccessTokenCopyClick = (e: ButtonEvent) => {
|
||||
this.copy(MatrixClientPeg.get().getAccessToken(), e);
|
||||
};
|
||||
|
||||
private onCopyVersionClicked = (e: ButtonEvent) => {
|
||||
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||
this.copy(`${appVersion}\n${olmVersion}`, e);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -232,11 +253,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
const appVersion = this.state.appVersion || 'unknown';
|
||||
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
|
||||
let updateButton = null;
|
||||
if (this.state.canUpdate) {
|
||||
updateButton = <UpdateCheckButton />;
|
||||
|
@ -275,6 +291,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
const { appVersion, olmVersion } = this.getVersionInfo();
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
||||
|
@ -291,8 +309,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
|
||||
{ _t("olm version:") } { olmVersion }<br />
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
{ appVersion }<br />
|
||||
{ olmVersion }<br />
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyVersionClicked}
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
{ updateButton }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -308,12 +333,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
<summary>{ _t("Access Token") }</summary><br />
|
||||
<b>{ _t("Your access token gives full access to your account."
|
||||
+ " Do not share it with anyone." ) }</b>
|
||||
<div className="mx_HelpUserSettingsTab_accessToken">
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onAccessTokenCopyClick}
|
||||
className="mx_HelpUserSettingsTab_accessToken_copy"
|
||||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
</details><br />
|
||||
|
|
|
@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler";
|
|||
import PropTypes from "prop-types";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import BetaCard from "../../../beta/BetaCard";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component {
|
|||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||
this.setState({ showHiddenReadReceipts });
|
||||
});
|
||||
|
||||
this.state = {
|
||||
showHiddenReadReceipts: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -65,15 +74,22 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
|
||||
let labsSection;
|
||||
if (SdkConfig.get()['showLabsSettings']) {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
||||
|
||||
let hiddenReadReceipts;
|
||||
if (this.state.showHiddenReadReceipts) {
|
||||
hiddenReadReceipts = (
|
||||
<SettingsFlag name="feature_hidden_read_receipts" level={SettingLevel.ACCOUNT} />
|
||||
);
|
||||
}
|
||||
|
||||
labsSection = <div className="mx_SettingsTab_section">
|
||||
{ flags }
|
||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
|
||||
{ hiddenReadReceipts }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -27,23 +27,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from '../../../stores/UIStore';
|
||||
import { lerp } from '../../../utils/AnimationUtils';
|
||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
|
||||
const PIP_VIEW_WIDTH = 336;
|
||||
const PIP_VIEW_HEIGHT = 232;
|
||||
|
||||
const MOVING_AMT = 0.2;
|
||||
const SNAPPING_AMT = 0.1;
|
||||
|
||||
const PADDING = {
|
||||
top: 58,
|
||||
bottom: 58,
|
||||
left: 76,
|
||||
right: 8,
|
||||
};
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -66,10 +51,6 @@ interface IState {
|
|||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
|
||||
// Position of the CallPreview
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
}
|
||||
|
||||
// Splits a list of calls into one 'primary' one and a list
|
||||
|
@ -112,16 +93,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
private roomStoreToken: EventSubscription;
|
||||
private dispatcherRef: string;
|
||||
private settingsWatcherRef: string;
|
||||
private callViewWrapper = createRef<HTMLDivElement>();
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
||||
private moving = false;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.animationCallback(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
document.addEventListener("mousemove", this.onMoving);
|
||||
document.addEventListener("mouseup", this.onEndMoving);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
document.removeEventListener("mousemove", this.onMoving);
|
||||
document.removeEventListener("mouseup", this.onEndMoving);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
}
|
||||
|
||||
private onResize = (): void => {
|
||||
this.snap(false);
|
||||
};
|
||||
|
||||
private animationCallback = () => {
|
||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||
// the desiredTranslation and translation, quit the animationCallback
|
||||
// loop. If that is the case, it means the PiP has snapped into its
|
||||
// position and there is nothing to do. Not doing this would cause an
|
||||
// infinite loop
|
||||
if (
|
||||
!this.moving &&
|
||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||
) return;
|
||||
|
||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||
this.setState({
|
||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||
});
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||
|
||||
// Avoid overflow on the x axis
|
||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||
} else if (inTranslationX <= 0) {
|
||||
this.desiredTranslationX = 0;
|
||||
} else {
|
||||
this.desiredTranslationX = inTranslationX;
|
||||
}
|
||||
|
||||
// Avoid overflow on the y axis
|
||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||
} else if (inTranslationY <= 0) {
|
||||
this.desiredTranslationY = 0;
|
||||
} else {
|
||||
this.desiredTranslationY = inTranslationY;
|
||||
}
|
||||
}
|
||||
|
||||
private snap(animate?: boolean): void {
|
||||
const translationX = this.desiredTranslationX;
|
||||
const translationY = this.desiredTranslationY;
|
||||
// We subtract the PiP size from the window size in order to calculate
|
||||
// the position to snap to from the PiP center and not its top-left
|
||||
// corner
|
||||
const windowWidth = (
|
||||
UIStore.instance.windowWidth -
|
||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||
);
|
||||
const windowHeight = (
|
||||
UIStore.instance.windowHeight -
|
||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||
);
|
||||
|
||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||
} else {
|
||||
this.desiredTranslationX = PADDING.left;
|
||||
this.desiredTranslationY = PADDING.top;
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
// We start animating here because we want the PiP to move when we're
|
||||
// resizing the window
|
||||
this.scheduledUpdate.mark();
|
||||
} else {
|
||||
this.setState({
|
||||
translationX: this.desiredTranslationX,
|
||||
translationY: this.desiredTranslationY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
|
||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state': {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
|
||||
this.updateCalls();
|
||||
break;
|
||||
}
|
||||
|
@ -300,57 +176,26 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onStartMoving = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.moving = true;
|
||||
this.initX = event.pageX - this.desiredTranslationX;
|
||||
this.initY = event.pageY - this.desiredTranslationY;
|
||||
this.scheduledUpdate.mark();
|
||||
};
|
||||
|
||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||
if (!this.moving) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||
};
|
||||
|
||||
private onEndMoving = () => {
|
||||
this.moving = false;
|
||||
this.snap(true);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const pipMode = true;
|
||||
if (this.state.primaryCall) {
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
const translatePixelsY = this.state.translationY + "px";
|
||||
const style = {
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<PictureInPictureDragger
|
||||
className="mx_CallPreview"
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
draggable={pipMode}
|
||||
>
|
||||
<CallView
|
||||
{ ({ onStartMoving, onResize }) => <CallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={true}
|
||||
onMouseDownOnHeader={this.onStartMoving}
|
||||
onResize={this.onResize}
|
||||
/>
|
||||
</div>
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
/> }
|
||||
</PictureInPictureDragger>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return <PersistentApp />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,28 +42,29 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
|||
import Modal from '../../../Modal';
|
||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||
import CallViewSidebar from './CallViewSidebar';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
// The call for us to display
|
||||
call: MatrixCall;
|
||||
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: (event: Event) => void;
|
||||
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
||||
// Used for dragging the PiP CallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -152,6 +153,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener('keydown', this.onNativeKeyDown);
|
||||
this.showControls();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -239,21 +241,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onFullscreenClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'video_fullscreen',
|
||||
fullscreen: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onControlsHideTimer = () => {
|
||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||
this.controlsHideTimer = null;
|
||||
|
@ -397,23 +384,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
this.setState({ hoveringControls: false });
|
||||
};
|
||||
|
||||
private onRoomAvatarClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onSecondaryRoomAvatarClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onCallResumeClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
|
@ -726,7 +696,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
|
@ -746,7 +716,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
contentView =(
|
||||
contentView = (
|
||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div
|
||||
|
@ -848,83 +818,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
||||
let myClassName;
|
||||
|
||||
let fullScreenButton;
|
||||
if (!this.props.pipMode) {
|
||||
fullScreenButton = (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||
onClick={this.onFullscreenClick}
|
||||
title={_t("Fill Screen")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let expandButton;
|
||||
if (this.props.pipMode) {
|
||||
expandButton = <AccessibleTooltipButton
|
||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||
onClick={this.onExpandClick}
|
||||
title={_t("Return to call")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const headerControls = <div className="mx_CallView_header_controls">
|
||||
{ fullScreenButton }
|
||||
{ expandButton }
|
||||
</div>;
|
||||
|
||||
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
|
||||
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
|
||||
"mx_CallView_header_callTypeIcon_video": isVideoCall,
|
||||
});
|
||||
|
||||
let header: React.ReactNode;
|
||||
if (!this.props.pipMode) {
|
||||
header = <div className="mx_CallView_header">
|
||||
<div className={callTypeIconClassName} />
|
||||
<span className="mx_CallView_header_callType">{ callTypeText }</span>
|
||||
{ headerControls }
|
||||
</div>;
|
||||
myClassName = 'mx_CallView_large';
|
||||
} else {
|
||||
let secondaryCallInfo;
|
||||
if (this.props.secondaryCall) {
|
||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{ _t("%(name)s on hold", { name: secCallRoom.name }) }
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
}
|
||||
|
||||
header = (
|
||||
<div
|
||||
className="mx_CallView_header"
|
||||
onMouseDown={this.props.onMouseDownOnHeader}
|
||||
>
|
||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
</AccessibleButton>
|
||||
<div className="mx_CallView_header_callInfo">
|
||||
<div className="mx_CallView_header_roomName">{ callRoom.name }</div>
|
||||
<div className="mx_CallView_header_callTypeSmall">
|
||||
{ callTypeText }
|
||||
{ secondaryCallInfo }
|
||||
</div>
|
||||
</div>
|
||||
{ headerControls }
|
||||
</div>
|
||||
);
|
||||
myClassName = 'mx_CallView_pip';
|
||||
}
|
||||
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||
|
||||
return <div className={"mx_CallView " + myClassName}>
|
||||
{ header }
|
||||
<CallViewHeader
|
||||
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||
pipMode={this.props.pipMode}
|
||||
type={this.props.call.type}
|
||||
callRooms={[callRoom, secCallRoom]}
|
||||
/>
|
||||
{ contentView }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -822,6 +822,7 @@
|
|||
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||
"Don't send read receipts": "Don't send read receipts",
|
||||
"Font size": "Font size",
|
||||
"Use custom size": "Use custom size",
|
||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||
|
@ -1140,6 +1141,10 @@
|
|||
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
"Message bubbles": "Message bubbles",
|
||||
"Messages containing keywords": "Messages containing keywords",
|
||||
"Error saving notification preferences": "Error saving notification preferences",
|
||||
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||
|
@ -1252,10 +1257,6 @@
|
|||
"Custom theme URL": "Custom theme URL",
|
||||
"Add theme": "Add theme",
|
||||
"Theme": "Theme",
|
||||
"Message layout": "Message layout",
|
||||
"IRC": "IRC",
|
||||
"Modern": "Modern",
|
||||
"Message bubbles": "Message bubbles",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
|
@ -1276,6 +1277,8 @@
|
|||
"Deactivate Account": "Deactivate Account",
|
||||
"Deactivate account": "Deactivate account",
|
||||
"Discovery": "Discovery",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"Olm version:": "Olm version:",
|
||||
"Legal": "Legal",
|
||||
"Credits": "Credits",
|
||||
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
||||
|
@ -1289,13 +1292,11 @@
|
|||
"FAQ": "FAQ",
|
||||
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
||||
"Versions": "Versions",
|
||||
"%(brand)s version:": "%(brand)s version:",
|
||||
"olm version:": "olm version:",
|
||||
"Copy": "Copy",
|
||||
"Homeserver is": "Homeserver is",
|
||||
"Identity server is": "Identity server is",
|
||||
"Access Token": "Access Token",
|
||||
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
||||
"Copy": "Copy",
|
||||
"Clear cache and reload": "Clear cache and reload",
|
||||
"Labs": "Labs",
|
||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
|
||||
|
@ -1546,6 +1547,7 @@
|
|||
"Send a reply…": "Send a reply…",
|
||||
"Send an encrypted message…": "Send an encrypted message…",
|
||||
"Send a message…": "Send a message…",
|
||||
"Send voice message": "Send voice message",
|
||||
"The conversation continues here.": "The conversation continues here.",
|
||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||
|
@ -1720,7 +1722,6 @@
|
|||
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
"No microphone found": "No microphone found",
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
||||
"Send voice message": "Send voice message",
|
||||
"Stop recording": "Stop recording",
|
||||
"Error updating main address": "Error updating main address",
|
||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
||||
|
@ -2094,6 +2095,8 @@
|
|||
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
|
||||
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
|
||||
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
|
||||
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
|
||||
"Power level": "Power level",
|
||||
"Custom level": "Custom level",
|
||||
"QR Code": "QR Code",
|
||||
|
|
|
@ -311,6 +311,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"feature_hidden_read_receipts": {
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td(
|
||||
"Don't send read receipts",
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
"baseFontSize": {
|
||||
displayName: _td("Font size"),
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import { jsxJoin } from './ReactUtils';
|
||||
|
||||
/**
|
||||
* formats numbers to fit into ~3 characters, suitable for badge counts
|
||||
|
@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string {
|
|||
* @returns {string} a string constructed by joining `items` with a comma
|
||||
* between each item, but with the last item appended as " and [lastItem]".
|
||||
*/
|
||||
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
|
||||
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
|
@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
|
|||
return items[0];
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <span>hello world</span>
|
||||
* @param array the array of element to join
|
||||
* @param joiner the string/JSX.Element to join with
|
||||
* @returns the joined array
|
||||
*/
|
||||
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
|
||||
const newArray = [];
|
||||
array.forEach((element, index) => {
|
||||
newArray.push(element, (index === array.length - 1) ? null : joiner);
|
||||
});
|
||||
return (
|
||||
<span>{ newArray }</span>
|
||||
);
|
||||
}
|
|
@ -12,12 +12,6 @@
|
|||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
"jsx": "react",
|
||||
"types": [
|
||||
"node",
|
||||
"react",
|
||||
"flux",
|
||||
"react-transition-group"
|
||||
],
|
||||
"lib": [
|
||||
"es2019",
|
||||
"dom",
|
||||
|
@ -27,5 +21,5 @@
|
|||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
],
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue