Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18092
This commit is contained in:
commit
64995dfae7
33 changed files with 290 additions and 147 deletions
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @matrix-org/element-web
|
|
@ -193,7 +193,8 @@
|
||||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js"
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!matrix-js-sdk).+$"
|
"/node_modules/(?!matrix-js-sdk).+$"
|
||||||
|
|
|
@ -85,7 +85,7 @@ limitations under the License.
|
||||||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ limitations under the License.
|
||||||
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
.mx_desktopCapturerSourcePicker_source_thumbnail {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
width: 312px;
|
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
@ -53,6 +52,5 @@ limitations under the License.
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 312px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,7 +271,7 @@ limitations under the License.
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
|
|
|
@ -310,14 +310,12 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled {
|
.mx_RoomView_timeline_rr_enabled {
|
||||||
|
.mx_EventTile[data-layout=group] {
|
||||||
.mx_EventTile:not([data-layout=bubble]) {
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
margin-right: 110px;
|
margin-right: 110px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,21 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadingState {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_failedState {
|
||||||
|
margin-right: 21px;
|
||||||
|
|
||||||
|
.mx_VoiceRecordComposerTile_uploadState_badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
|
||||||
// Note: remaining class properties are in the PlayerContainer CSS.
|
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||||
|
|
||||||
|
@ -68,7 +83,7 @@ limitations under the License.
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px; // 12px from the left edge for container padding
|
left: 12px; // 12px from the left edge for container padding
|
||||||
top: 18px; // vertically center (middle align with clock)
|
top: 16px; // vertically center (middle align with clock)
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CallPreview {
|
.mx_CallPreview {
|
||||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
|
|
@ -75,8 +75,6 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
// We don't want to collide with the call controls that have 52px of height
|
|
||||||
margin-bottom: 52px;
|
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -208,6 +206,7 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header_callType {
|
.mx_CallView_header_callType {
|
||||||
|
|
|
@ -40,8 +40,6 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
|
|
||||||
&.mx_VideoFeed_voice {
|
&.mx_VideoFeed_voice {
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_video {
|
.mx_VideoFeed_video {
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
<path d="m11.068 2c-0.32021 4.772e-4 -0.66852 0.17244-0.96484 0.46875-2.5464 2.5435-5.0905 5.0892-7.6348 7.6348-0.79016 0.7902-0.69302 1.9462 1.1641 1.9707 1.855 0.02447 3.4407-0.56671 3.8281-0.69141l2.4355 3.1445c-0.83503 1.9462-0.86902 4.062-0.058594 5.7949 0.47213 1.0095 1.79 1.0049 2.5781 0.2168l3.2773-3.2773 2.8223 2.8223c1.491 1.491 3.2644 2.0696 3.4512 1.8828s-0.39181-1.9602-1.8828-3.4512l-2.8223-2.8223 3.2773-3.2773c0.788-0.788 0.79075-2.106-0.21875-2.5781-1.733-0.81044-3.8468-0.77643-5.793 0.058594l-3.1445-2.4355c0.1247-0.38742 0.71588-1.9731 0.69141-3.8281-0.015311-1.1607-0.47217-1.6336-1.0059-1.6328z" fill="#737d8c"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 744 B |
|
@ -8,9 +8,9 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji';
|
$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
$system-light: #F4F6FA;
|
$system-light: #F4F6FA;
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||||
digits in flowed text to stand out.
|
digits in flowed text to stand out.
|
||||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||||
$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji';
|
$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||||
|
|
||||||
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji';
|
$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
|
||||||
|
|
||||||
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
|
||||||
$system-light: #F4F6FA;
|
$system-light: #F4F6FA;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -60,7 +61,6 @@ import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
|
@ -86,6 +86,9 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||||
|
import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||||
|
import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
|
||||||
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||||
import ToastStore from './stores/ToastStore';
|
import ToastStore from './stores/ToastStore';
|
||||||
import IncomingCallToast from "./toasts/IncomingCallToast";
|
import IncomingCallToast from "./toasts/IncomingCallToast";
|
||||||
|
@ -479,14 +482,28 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing:
|
case CallState.Ringing: {
|
||||||
|
const incomingCallPushRule = (
|
||||||
|
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
|
||||||
|
);
|
||||||
|
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||||
|
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
|
||||||
|
action.set_tweak === TweakName.Sound &&
|
||||||
|
action.value === "ring"
|
||||||
|
));
|
||||||
|
|
||||||
|
if (pushRuleEnabled && tweakSetToRing) {
|
||||||
this.play(AudioID.Ring);
|
this.play(AudioID.Ring);
|
||||||
|
} else {
|
||||||
|
this.silenceCall(call.callId);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CallState.InviteSent:
|
}
|
||||||
|
case CallState.InviteSent: {
|
||||||
this.play(AudioID.Ringback);
|
this.play(AudioID.Ringback);
|
||||||
break;
|
break;
|
||||||
case CallState.Ended:
|
}
|
||||||
{
|
case CallState.Ended: {
|
||||||
const hangupReason = call.hangupReason;
|
const hangupReason = call.hangupReason;
|
||||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
|
@ -1011,14 +1028,10 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
// prevent double clicking the call button
|
// prevent double clicking the call button
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
|
||||||
const hasJitsi = currentJitsiWidgets.length > 0
|
if (jitsiWidget) {
|
||||||
|| WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
|
// If there already is a Jitsi widget pin it
|
||||||
if (hasJitsi) {
|
WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top);
|
||||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
|
||||||
title: _t('Call in Progress'),
|
|
||||||
description: _t('A call is currently being placed!'),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
|
||||||
return { width, height, img };
|
return { width, height, img };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimum size for image files before we generate a thumbnail for them.
|
||||||
|
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
|
||||||
|
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
|
||||||
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
|
||||||
|
// We don't apply these thresholds to video thumbnails as a poster image is always useful
|
||||||
|
// and videos tend to be much larger.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
||||||
*
|
*
|
||||||
|
@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
|
||||||
* @param {File} imageFile The image to read and thumbnail.
|
* @param {File} imageFile The image to read and thumbnail.
|
||||||
* @return {Promise} A promise that resolves with the attachment info.
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
*/
|
*/
|
||||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) {
|
||||||
let thumbnailType = "image/png";
|
let thumbnailType = "image/png";
|
||||||
if (imageFile.type === "image/jpeg") {
|
if (imageFile.type === "image/jpeg") {
|
||||||
thumbnailType = "image/jpeg";
|
thumbnailType = "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageInfo;
|
const imageElement = await loadImageElement(imageFile);
|
||||||
return loadImageElement(imageFile).then((r) => {
|
|
||||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||||
}).then((result) => {
|
const imageInfo = result.info;
|
||||||
imageInfo = result.info;
|
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||||
}).then((result) => {
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
|
||||||
imageInfo.thumbnail_url = result.url;
|
if (
|
||||||
imageInfo.thumbnail_file = result.file;
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
|
||||||
|
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
|
||||||
|
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
|
||||||
|
) {
|
||||||
|
delete imageInfo["thumbnail_info"];
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
|
||||||
|
imageInfo["thumbnail_url"] = uploadResult.url;
|
||||||
|
imageInfo["thumbnail_file"] = uploadResult.file;
|
||||||
return imageInfo;
|
return imageInfo;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,17 +38,9 @@ function makePlaybackWaveform(input: number[]): number[] {
|
||||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||||
const noiseWaveform = input.map(v => Math.abs(v));
|
const noiseWaveform = input.map(v => Math.abs(v));
|
||||||
|
|
||||||
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||||
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
|
||||||
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||||
|
|
||||||
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
|
||||||
// waveform. Most speech happens below the 0.5 mark.
|
|
||||||
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
|
||||||
|
|
||||||
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
|
||||||
// sensible. This is what we return to keep our contract of "values between zero and one".
|
|
||||||
return arrayRescale(filtered, 0, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playback extends EventEmitter implements IDestroyable {
|
export class Playback extends EventEmitter implements IDestroyable {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { IEncryptedFile } from "matrix-js-sdk/src/@types/event";
|
||||||
import { uploadFile } from "../ContentMessages";
|
import { uploadFile } from "../ContentMessages";
|
||||||
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||||
import { clamp } from "../utils/numbers";
|
import { clamp } from "../utils/numbers";
|
||||||
|
import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||||
|
|
||||||
const CHANNELS = 1; // stereo isn't important
|
const CHANNELS = 1; // stereo isn't important
|
||||||
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
||||||
|
@ -113,16 +114,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
});
|
});
|
||||||
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
|
||||||
|
|
||||||
// Set up our worklet. We use this for timing information and waveform analysis: the
|
|
||||||
// web audio API prefers this be done async to avoid holding the main thread with math.
|
|
||||||
const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
|
|
||||||
if (!mxRecorderWorkletPath) {
|
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
|
||||||
throw new Error("Unable to create recorder: no worklet script registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect our inputs and outputs
|
// Connect our inputs and outputs
|
||||||
if (this.recorderContext.audioWorklet) {
|
if (this.recorderContext.audioWorklet) {
|
||||||
|
// Set up our worklet. We use this for timing information and waveform analysis: the
|
||||||
|
// web audio API prefers this be done async to avoid holding the main thread with math.
|
||||||
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
|
||||||
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
|
||||||
this.recorderSource.connect(this.recorderWorklet);
|
this.recorderSource.connect(this.recorderWorklet);
|
||||||
|
|
|
@ -222,11 +222,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
||||||
|
|
||||||
if (inviteSender) {
|
if (inviteSender) {
|
||||||
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||||
<MemberAvatar member={inviter} width={32} height={32} />
|
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_SpaceRoomView_preview_inviter_name">
|
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||||
{ _t("<inviter/> invites you", {}, {
|
{ _t("<inviter/> invites you", {}, {
|
||||||
inviter: () => <b>{ inviter.name || inviteSender }</b>,
|
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
|
||||||
|
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { arrayFastResample } from "../../../utils/arrays";
|
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||||
import { percentageOf } from "../../../utils/numbers";
|
|
||||||
import Waveform from "./Waveform";
|
import Waveform from "./Waveform";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
|
|
||||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
waveform: [],
|
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||||
// The incoming data is between zero and one, but typically even screaming into a
|
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
|
||||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
|
||||||
// user.
|
|
||||||
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
|
|
||||||
this.scheduledUpdate.mark();
|
this.scheduledUpdate.mark();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -117,14 +116,12 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
if (state === CallState.Ended) {
|
if (state === CallState.Ended) {
|
||||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||||
const gotRejected = this.props.callEventGrouper.gotRejected;
|
const gotRejected = this.props.callEventGrouper.gotRejected;
|
||||||
const rejectParty = this.props.callEventGrouper.rejectParty;
|
|
||||||
|
|
||||||
if (gotRejected) {
|
if (gotRejected) {
|
||||||
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
|
{ _t("Call declined") }
|
||||||
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
|
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
|
||||||
|
@ -136,14 +133,14 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
// Also, if we don't have a reason
|
// Also, if we don't have a reason
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("This call has ended") }
|
{ _t("Call ended") }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("They didn't pick up") }
|
{ _t("Missed call") }
|
||||||
{ this.renderCallBackButton(_t("Call again")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -176,7 +173,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
className="mx_CallEvent_content_tooltip"
|
className="mx_CallEvent_content_tooltip"
|
||||||
kind={InfoTooltipKind.Warning}
|
kind={InfoTooltipKind.Warning}
|
||||||
/>
|
/>
|
||||||
{ _t("This call has failed") }
|
{ _t("Connection failed") }
|
||||||
|
{ this.renderCallBackButton(_t("Retry")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +188,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
if (state === CustomCallState.Missed) {
|
if (state === CustomCallState.Missed) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ _t("You missed this call") }
|
{ _t("Missed call") }
|
||||||
{ this.renderCallBackButton(_t("Call back")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -55,6 +55,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||||
|
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"],
|
||||||
|
["{", "}"],
|
||||||
|
["<", ">"],
|
||||||
|
]);
|
||||||
|
|
||||||
function ctrlShortcutLabel(key: string): string {
|
function ctrlShortcutLabel(key: string): string {
|
||||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +107,7 @@ interface IState {
|
||||||
showVisualBell?: boolean;
|
showVisualBell?: boolean;
|
||||||
autoComplete?: AutocompleteWrapperModel;
|
autoComplete?: AutocompleteWrapperModel;
|
||||||
completionIndex?: number;
|
completionIndex?: number;
|
||||||
|
surroundWith: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||||
|
@ -117,12 +126,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
private readonly emoticonSettingHandle: string;
|
private readonly emoticonSettingHandle: string;
|
||||||
private readonly shouldShowPillAvatarSettingHandle: string;
|
private readonly shouldShowPillAvatarSettingHandle: string;
|
||||||
|
private readonly surroundWithHandle: string;
|
||||||
private readonly historyManager = new HistoryManager();
|
private readonly historyManager = new HistoryManager();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||||
|
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||||
|
@ -130,6 +141,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.configureEmoticonAutoReplace();
|
this.configureEmoticonAutoReplace();
|
||||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||||
this.configureShouldShowPillAvatar);
|
this.configureShouldShowPillAvatar);
|
||||||
|
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||||
|
this.surroundWithSettingChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps) {
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
|
@ -422,6 +435,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||||
const model = this.props.model;
|
const model = this.props.model;
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
|
if (this.state.surroundWith && document.getSelection().type != "Caret") {
|
||||||
|
// This surrounds the selected text with a character. This is
|
||||||
|
// intentionally left out of the keybinding manager as the keybinds
|
||||||
|
// here shouldn't be changeable
|
||||||
|
|
||||||
|
const selectionRange = getRangeForSelection(
|
||||||
|
this.editorRef.current,
|
||||||
|
this.props.model,
|
||||||
|
document.getSelection(),
|
||||||
|
);
|
||||||
|
// trim the range as we want it to exclude leading/trailing spaces
|
||||||
|
selectionRange.trim();
|
||||||
|
|
||||||
|
if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) {
|
||||||
|
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||||
|
this.modifiedFlag = true;
|
||||||
|
toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key));
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MessageComposerAction.FormatBold:
|
case MessageComposerAction.FormatBold:
|
||||||
|
@ -574,6 +609,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ showPillAvatar });
|
this.setState({ showPillAvatar });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private surroundWithSettingChanged = () => {
|
||||||
|
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
|
||||||
|
this.setState({ surroundWith });
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
@ -581,6 +621,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
|
||||||
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
|
||||||
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
|
||||||
|
SettingsStore.unwatchSetting(this.surroundWithHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
|
@ -17,10 +17,7 @@ limitations under the License.
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import {
|
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
RecordingState,
|
|
||||||
VoiceRecording,
|
|
||||||
} from "../../../audio/VoiceRecording";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -34,6 +31,11 @@ import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||||
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
|
import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -42,6 +44,7 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
recorder?: VoiceRecording;
|
recorder?: VoiceRecording;
|
||||||
recordingPhase?: RecordingState;
|
recordingPhase?: RecordingState;
|
||||||
|
didUploadFail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,9 +72,19 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
|
|
||||||
await this.state.recorder.stop();
|
await this.state.recorder.stop();
|
||||||
|
|
||||||
|
let upload: IUpload;
|
||||||
try {
|
try {
|
||||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error uploading voice message:", e);
|
||||||
|
|
||||||
|
// Flag error and move on. The recording phase will be reset by the upload function.
|
||||||
|
this.setState({ didUploadFail: true });
|
||||||
|
|
||||||
|
return; // don't dispose the recording: the user has a chance to re-upload
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
||||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||||
"body": "Voice message",
|
"body": "Voice message",
|
||||||
|
@ -104,12 +117,11 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error sending/uploading voice message:", e);
|
console.error("Error sending voice message:", e);
|
||||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
|
||||||
title: _t('Upload Failed'),
|
// Voice message should be in the timeline at this point, so let other things take care
|
||||||
description: _t("The voice message failed to upload."),
|
// of error handling. We also shouldn't need the recording anymore, so fall through to
|
||||||
});
|
// disposal.
|
||||||
return; // don't dispose the recording so the user can retry, maybe
|
|
||||||
}
|
}
|
||||||
await this.disposeRecording();
|
await this.disposeRecording();
|
||||||
}
|
}
|
||||||
|
@ -118,7 +130,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
await VoiceRecordingStore.instance.disposeRecording();
|
await VoiceRecordingStore.instance.disposeRecording();
|
||||||
|
|
||||||
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
||||||
this.setState({ recorder: null, recordingPhase: null });
|
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCancel = async () => {
|
private onCancel = async () => {
|
||||||
|
@ -166,6 +178,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// stop any noises which might be happening
|
||||||
|
await PlaybackManager.instance.playOnly(null);
|
||||||
|
|
||||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||||
await recorder.start();
|
await recorder.start();
|
||||||
|
|
||||||
|
@ -209,9 +224,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tooltip = _t("Record a voice message");
|
let tooltip = _t("Send voice message");
|
||||||
if (!!this.state.recorder) {
|
if (!!this.state.recorder) {
|
||||||
tooltip = _t("Stop the recording");
|
tooltip = _t("Stop recording");
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopOrRecordBtn = <AccessibleTooltipButton
|
let stopOrRecordBtn = <AccessibleTooltipButton
|
||||||
|
@ -229,12 +244,30 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||||
deleteButton = <AccessibleTooltipButton
|
deleteButton = <AccessibleTooltipButton
|
||||||
className='mx_VoiceRecordComposerTile_delete'
|
className='mx_VoiceRecordComposerTile_delete'
|
||||||
title={_t("Delete recording")}
|
title={_t("Delete")}
|
||||||
onClick={this.onCancel}
|
onClick={this.onCancel}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadIndicator;
|
||||||
|
if (this.state.recordingPhase === RecordingState.Uploading) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
|
||||||
|
<InlineSpinner w={16} h={16} />
|
||||||
|
</span>;
|
||||||
|
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||||
|
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
|
||||||
|
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
|
||||||
|
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
|
||||||
|
<NotificationBadge
|
||||||
|
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='text-warning'>{ _t("Failed to send") }</span>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
|
{ uploadIndicator }
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
{ this.renderWaveformArea() }
|
{ this.renderWaveformArea() }
|
||||||
{ recordingInfo }
|
{ recordingInfo }
|
||||||
|
|
|
@ -157,6 +157,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
'MessageComposerInput.suggestEmoji',
|
'MessageComposerInput.suggestEmoji',
|
||||||
'sendTypingNotifications',
|
'sendTypingNotifications',
|
||||||
'MessageComposerInput.ctrlEnterToSend',
|
'MessageComposerInput.ctrlEnterToSend',
|
||||||
|
'MessageComposerInput.surroundWith',
|
||||||
'MessageComposerInput.showStickersButton',
|
'MessageComposerInput.showStickersButton',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ const PIP_VIEW_WIDTH = 336;
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
|
||||||
const MOVING_AMT = 0.2;
|
const MOVING_AMT = 0.2;
|
||||||
const SNAPPING_AMT = 0.05;
|
const SNAPPING_AMT = 0.1;
|
||||||
|
|
||||||
const PADDING = {
|
const PADDING = {
|
||||||
top: 58,
|
top: 58,
|
||||||
|
|
|
@ -23,11 +23,16 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import VideoFeed from './VideoFeed';
|
import VideoFeed from './VideoFeed';
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
|
||||||
import { alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton } from '../../structures/ContextMenu';
|
import {
|
||||||
|
alwaysAboveLeftOf,
|
||||||
|
alwaysAboveRightOf,
|
||||||
|
ChevronFace,
|
||||||
|
ContextMenuTooltipButton,
|
||||||
|
} from '../../structures/ContextMenu';
|
||||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||||
|
@ -37,6 +42,8 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
import CallViewSidebar from './CallViewSidebar';
|
import CallViewSidebar from './CallViewSidebar';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { Alignment } from "../elements/Tooltip";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The call for us to display
|
// The call for us to display
|
||||||
|
@ -75,6 +82,8 @@ interface IState {
|
||||||
sidebarShown: boolean;
|
sidebarShown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tooltipYOffset = -24;
|
||||||
|
|
||||||
function getFullScreenElement() {
|
function getFullScreenElement() {
|
||||||
return (
|
return (
|
||||||
document.fullscreenElement ||
|
document.fullscreenElement ||
|
||||||
|
@ -115,7 +124,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
private controlsHideTimer: number = null;
|
private controlsHideTimer: number = null;
|
||||||
private dialpadButton = createRef<HTMLDivElement>();
|
private dialpadButton = createRef<HTMLDivElement>();
|
||||||
private contextMenuButton = createRef<HTMLDivElement>();
|
private contextMenuButton = createRef<HTMLDivElement>();
|
||||||
private contextMenu = createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -479,9 +487,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let vidMuteButton;
|
let vidMuteButton;
|
||||||
if (this.props.call.type === CallType.Video) {
|
if (this.props.call.type === CallType.Video) {
|
||||||
vidMuteButton = (
|
vidMuteButton = (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={vidClasses}
|
className={vidClasses}
|
||||||
onClick={this.onVidMuteClick}
|
onClick={this.onVidMuteClick}
|
||||||
|
title={this.state.vidMuted ? _t("Start the camera") : _t("Stop the camera")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -496,9 +507,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.props.call.state === CallState.Connected
|
this.props.call.state === CallState.Connected
|
||||||
) {
|
) {
|
||||||
screensharingButton = (
|
screensharingButton = (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={screensharingClasses}
|
className={screensharingClasses}
|
||||||
onClick={this.onScreenshareClick}
|
onClick={this.onScreenshareClick}
|
||||||
|
title={this.state.screensharing
|
||||||
|
? _t("Stop sharing your screen")
|
||||||
|
: _t("Start sharing your screen")
|
||||||
|
}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -518,6 +535,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={sidebarButtonClasses}
|
className={sidebarButtonClasses}
|
||||||
onClick={this.onToggleSidebar}
|
onClick={this.onToggleSidebar}
|
||||||
|
aria-label={this.state.sidebarShown ? _t("Hide sidebar") : _t("Show sidebar")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -526,22 +544,28 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let contextMenuButton;
|
let contextMenuButton;
|
||||||
if (this.state.callState === CallState.Connected) {
|
if (this.state.callState === CallState.Connected) {
|
||||||
contextMenuButton = (
|
contextMenuButton = (
|
||||||
<ContextMenuButton
|
<ContextMenuTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||||
onClick={this.onMoreClick}
|
onClick={this.onMoreClick}
|
||||||
inputRef={this.contextMenuButton}
|
inputRef={this.contextMenuButton}
|
||||||
isExpanded={this.state.showMoreMenu}
|
isExpanded={this.state.showMoreMenu}
|
||||||
|
title={_t("More")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let dialpadButton;
|
let dialpadButton;
|
||||||
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
|
||||||
dialpadButton = (
|
dialpadButton = (
|
||||||
<ContextMenuButton
|
<ContextMenuTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||||
inputRef={this.dialpadButton}
|
inputRef={this.dialpadButton}
|
||||||
onClick={this.onDialpadClick}
|
onClick={this.onDialpadClick}
|
||||||
isExpanded={this.state.showDialpad}
|
isExpanded={this.state.showDialpad}
|
||||||
|
title={_t("Dialpad")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -554,7 +578,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
ChevronFace.None,
|
ChevronFace.None,
|
||||||
CONTEXT_MENU_VPADDING,
|
CONTEXT_MENU_VPADDING,
|
||||||
)}
|
)}
|
||||||
mountAsChild={true}
|
// We mount the context menus as a as a child typically in order to include the
|
||||||
|
// context menus when fullscreening the call content.
|
||||||
|
// However, this does not work as well when the call is embedded in a
|
||||||
|
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
|
||||||
|
mountAsChild={!this.props.pipMode}
|
||||||
onFinished={this.closeDialpad}
|
onFinished={this.closeDialpad}
|
||||||
call={this.props.call}
|
call={this.props.call}
|
||||||
/>;
|
/>;
|
||||||
|
@ -568,7 +596,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
ChevronFace.None,
|
ChevronFace.None,
|
||||||
CONTEXT_MENU_VPADDING,
|
CONTEXT_MENU_VPADDING,
|
||||||
)}
|
)}
|
||||||
mountAsChild={true}
|
mountAsChild={!this.props.pipMode}
|
||||||
onFinished={this.closeContextMenu}
|
onFinished={this.closeContextMenu}
|
||||||
call={this.props.call}
|
call={this.props.call}
|
||||||
/>;
|
/>;
|
||||||
|
@ -583,9 +611,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{ dialPad }
|
{ dialPad }
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
{ dialpadButton }
|
{ dialpadButton }
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className={micClasses}
|
className={micClasses}
|
||||||
onClick={this.onMicMuteClick}
|
onClick={this.onMicMuteClick}
|
||||||
|
title={this.state.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
{ vidMuteButton }
|
{ vidMuteButton }
|
||||||
<div className={micCacheClasses} />
|
<div className={micCacheClasses} />
|
||||||
|
@ -593,9 +624,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{ screensharingButton }
|
{ screensharingButton }
|
||||||
{ sidebarButton }
|
{ sidebarButton }
|
||||||
{ contextMenuButton }
|
{ contextMenuButton }
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||||
onClick={this.onHangupClick}
|
onClick={this.onHangupClick}
|
||||||
|
title={_t("Hangup")}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
yOffset={tooltipYOffset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -820,7 +854,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let fullScreenButton;
|
let fullScreenButton;
|
||||||
if (!this.props.pipMode) {
|
if (!this.props.pipMode) {
|
||||||
fullScreenButton = (
|
fullScreenButton = (
|
||||||
<div
|
<AccessibleTooltipButton
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||||
onClick={this.onFullscreenClick}
|
onClick={this.onFullscreenClick}
|
||||||
title={_t("Fill Screen")}
|
title={_t("Fill Screen")}
|
||||||
|
@ -830,7 +864,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let expandButton;
|
let expandButton;
|
||||||
if (this.props.pipMode) {
|
if (this.props.pipMode) {
|
||||||
expandButton = <div
|
expandButton = <AccessibleTooltipButton
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||||
onClick={this.onExpandClick}
|
onClick={this.onExpandClick}
|
||||||
title={_t("Return to call")}
|
title={_t("Return to call")}
|
||||||
|
|
16
src/emoji.ts
16
src/emoji.ts
|
@ -35,6 +35,16 @@ export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
|
||||||
|
|
||||||
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
||||||
|
|
||||||
|
const isRegionalIndicator = (x: string): boolean => {
|
||||||
|
// First verify that the string is a single character. We use Array.from
|
||||||
|
// to make sure we count by characters, not UTF-8 code units.
|
||||||
|
return Array.from(x).length === 1 &&
|
||||||
|
// Next verify that the character is within the code point range for
|
||||||
|
// regional indicators.
|
||||||
|
// http://unicode.org/charts/PDF/Unicode-6.0/U60-1F100.pdf
|
||||||
|
x >= '\u{1f1e6}' && x <= '\u{1f1ff}';
|
||||||
|
};
|
||||||
|
|
||||||
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
|
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
|
||||||
"people", // smileys
|
"people", // smileys
|
||||||
"people", // actually people
|
"people", // actually people
|
||||||
|
@ -72,7 +82,11 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcode
|
||||||
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
// We manually include regional indicators in the symbols group, since
|
||||||
|
// Emojibase intentionally leaves them uncategorized
|
||||||
|
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group] ??
|
||||||
|
(isRegionalIndicator(emoji.unicode) ? "symbols" : null);
|
||||||
|
|
||||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,6 @@
|
||||||
"Unable to transfer call": "Unable to transfer call",
|
"Unable to transfer call": "Unable to transfer call",
|
||||||
"Transfer Failed": "Transfer Failed",
|
"Transfer Failed": "Transfer Failed",
|
||||||
"Failed to transfer call": "Failed to transfer call",
|
"Failed to transfer call": "Failed to transfer call",
|
||||||
"Call in Progress": "Call in Progress",
|
|
||||||
"A call is currently being placed!": "A call is currently being placed!",
|
|
||||||
"Permission Required": "Permission Required",
|
"Permission Required": "Permission Required",
|
||||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||||
"End conference": "End conference",
|
"End conference": "End conference",
|
||||||
|
@ -849,6 +847,7 @@
|
||||||
"Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline",
|
"Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline",
|
||||||
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
||||||
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
||||||
|
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
|
||||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||||
"Mirror local video feed": "Mirror local video feed",
|
"Mirror local video feed": "Mirror local video feed",
|
||||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||||
|
@ -905,6 +904,17 @@
|
||||||
"sends snowfall": "sends snowfall",
|
"sends snowfall": "sends snowfall",
|
||||||
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||||
"sends space invaders": "sends space invaders",
|
"sends space invaders": "sends space invaders",
|
||||||
|
"Start the camera": "Start the camera",
|
||||||
|
"Stop the camera": "Stop the camera",
|
||||||
|
"Stop sharing your screen": "Stop sharing your screen",
|
||||||
|
"Start sharing your screen": "Start sharing your screen",
|
||||||
|
"Hide sidebar": "Hide sidebar",
|
||||||
|
"Show sidebar": "Show sidebar",
|
||||||
|
"More": "More",
|
||||||
|
"Dialpad": "Dialpad",
|
||||||
|
"Unmute the microphone": "Unmute the microphone",
|
||||||
|
"Mute the microphone": "Mute the microphone",
|
||||||
|
"Hangup": "Hangup",
|
||||||
"unknown person": "unknown person",
|
"unknown person": "unknown person",
|
||||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||||
|
@ -1706,14 +1716,12 @@
|
||||||
"Invited by %(sender)s": "Invited by %(sender)s",
|
"Invited by %(sender)s": "Invited by %(sender)s",
|
||||||
"Jump to first unread message.": "Jump to first unread message.",
|
"Jump to first unread message.": "Jump to first unread message.",
|
||||||
"Mark all as read": "Mark all as read",
|
"Mark all as read": "Mark all as read",
|
||||||
"The voice message failed to upload.": "The voice message failed to upload.",
|
|
||||||
"Unable to access your microphone": "Unable to access your microphone",
|
"Unable to access your microphone": "Unable to access your microphone",
|
||||||
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
||||||
"No microphone found": "No microphone found",
|
"No microphone found": "No microphone found",
|
||||||
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
"We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.",
|
||||||
"Record a voice message": "Record a voice message",
|
"Send voice message": "Send voice message",
|
||||||
"Stop the recording": "Stop the recording",
|
"Stop recording": "Stop recording",
|
||||||
"Delete recording": "Delete recording",
|
|
||||||
"Error updating main address": "Error updating main address",
|
"Error updating main address": "Error updating main address",
|
||||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
||||||
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",
|
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",
|
||||||
|
@ -1874,19 +1882,15 @@
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"Compare emoji": "Compare emoji",
|
||||||
"Connected": "Connected",
|
"Connected": "Connected",
|
||||||
"You declined this call": "You declined this call",
|
"Call declined": "Call declined",
|
||||||
"They declined this call": "They declined this call",
|
|
||||||
"Call back": "Call back",
|
"Call back": "Call back",
|
||||||
"Call again": "Call again",
|
"Missed call": "Missed call",
|
||||||
"This call has ended": "This call has ended",
|
|
||||||
"They didn't pick up": "They didn't pick up",
|
|
||||||
"Could not connect media": "Could not connect media",
|
"Could not connect media": "Could not connect media",
|
||||||
"Connection failed": "Connection failed",
|
"Connection failed": "Connection failed",
|
||||||
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||||
"An unknown error occurred": "An unknown error occurred",
|
"An unknown error occurred": "An unknown error occurred",
|
||||||
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
||||||
"This call has failed": "This call has failed",
|
"Retry": "Retry",
|
||||||
"You missed this call": "You missed this call",
|
|
||||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
|
@ -1909,7 +1913,6 @@
|
||||||
"Error processing audio message": "Error processing audio message",
|
"Error processing audio message": "Error processing audio message",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Retry": "Retry",
|
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
"Download %(text)s": "Download %(text)s",
|
"Download %(text)s": "Download %(text)s",
|
||||||
|
|
|
@ -449,6 +449,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
|
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"MessageComposerInput.surroundWith": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td("Surround selected text when typing special characters"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.autoReplaceEmoji": {
|
"MessageComposerInput.autoReplaceEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Automatically replace plain text Emoji'),
|
displayName: _td('Automatically replace plain text Emoji'),
|
||||||
|
|
13
src/theme.js
13
src/theme.js
|
@ -171,15 +171,10 @@ export async function setTheme(theme) {
|
||||||
// look for the stylesheet elements.
|
// look for the stylesheet elements.
|
||||||
// styleElements is a map from style name to HTMLLinkElement.
|
// styleElements is a map from style name to HTMLLinkElement.
|
||||||
const styleElements = Object.create(null);
|
const styleElements = Object.create(null);
|
||||||
let a;
|
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||||
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
themes.forEach(theme => {
|
||||||
const href = a.getAttribute("href");
|
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||||
// shouldn't we be using the 'title' tag rather than the href?
|
});
|
||||||
const match = href && href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
|
||||||
if (match) {
|
|
||||||
styleElements[match[1]] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(stylesheetName in styleElements)) {
|
if (!(stylesheetName in styleElements)) {
|
||||||
throw new Error("Unknown theme " + stylesheetName);
|
throw new Error("Unknown theme " + stylesheetName);
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class IncomingCallToast extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
silenced: false,
|
silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,8 @@ function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise
|
||||||
|
|
||||||
// Dev note: the reassignment warnings are entirely incorrect here.
|
// Dev note: the reassignment warnings are entirely incorrect here.
|
||||||
|
|
||||||
// @ts-ignore
|
managedIframe.style.display = "none";
|
||||||
// noinspection JSConstantReassignment
|
|
||||||
managedIframe.style = { display: "none" };
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// noinspection JSConstantReassignment
|
// noinspection JSConstantReassignment
|
||||||
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
||||||
|
|
|
@ -96,6 +96,7 @@ export function createTestClient() {
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pushRules: {},
|
||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue