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