Remove abandoned Voice Broadcasts labs flag (#28548)
* Remove abandoned Voice Broadcasts labs flag Any existing voice broadcasts will be shown as a series of voice messages which will sequence play as normal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove dead code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
5d72735b1f
commit
d8ebc68aa8
174 changed files with 29 additions and 13632 deletions
|
@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
|
||||||
2. `sync_timeline_limit`
|
2. `sync_timeline_limit`
|
||||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||||
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.
|
|
||||||
|
|
|
@ -393,9 +393,3 @@
|
||||||
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
||||||
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
||||||
@import "./views/voip/_VideoFeed.pcss";
|
@import "./views/voip/_VideoFeed.pcss";
|
||||||
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
|
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
|
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
|
|
||||||
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
|
||||||
|
|
|
@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
|
||||||
pointer-events: none; /* makes the avatar non-draggable */
|
pointer-events: none; /* makes the avatar non-draggable */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserMenu_userAvatarLive {
|
|
||||||
align-items: center;
|
|
||||||
background-color: $alert;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: $live-badge-color;
|
|
||||||
display: flex;
|
|
||||||
height: 12px;
|
|
||||||
justify-content: center;
|
|
||||||
left: 25px;
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserMenu_contextMenuButton {
|
.mx_UserMenu_contextMenuButton {
|
||||||
|
|
|
@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details.
|
||||||
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
|
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_voiceBroadcast::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/live.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MessageComposer_plain_text::before {
|
.mx_MessageComposer_plain_text::before {
|
||||||
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
|
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_LiveBadge {
|
|
||||||
align-items: center;
|
|
||||||
background-color: $alert;
|
|
||||||
border-radius: 2px;
|
|
||||||
color: $live-badge-color;
|
|
||||||
display: inline-flex;
|
|
||||||
font-size: $font-12px;
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
gap: $spacing-4;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LiveBadge--grey {
|
|
||||||
background-color: $quaternary-content;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastControl {
|
|
||||||
align-items: center;
|
|
||||||
background-color: $background;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: $secondary-content;
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 32px;
|
|
||||||
height: 32px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastControl-recording {
|
|
||||||
color: $alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastControl-play .mx_Icon {
|
|
||||||
left: 1px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader {
|
|
||||||
align-items: flex-start;
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-8;
|
|
||||||
line-height: 20px;
|
|
||||||
margin-bottom: $spacing-16;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader_content {
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader_room_wrapper {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader_room {
|
|
||||||
font-size: $font-12px;
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader_line {
|
|
||||||
align-items: center;
|
|
||||||
color: $secondary-content;
|
|
||||||
font-size: $font-12px;
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-4;
|
|
||||||
|
|
||||||
.mx_Spinner {
|
|
||||||
flex: 0 0 14px;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader_mic--clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingConnectionError {
|
|
||||||
align-items: center;
|
|
||||||
color: $alert;
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-12;
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
fill: $alert;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
|
|
||||||
align-items: center;
|
|
||||||
color: $alert;
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-4;
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody {
|
|
||||||
background-color: $quinary-content;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: $secondary-content;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: $font-12px;
|
|
||||||
padding: $spacing-12;
|
|
||||||
width: 271px;
|
|
||||||
|
|
||||||
.mx_Clock {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody--pip {
|
|
||||||
background-color: $system;
|
|
||||||
box-shadow: 0 2px 8px 0 #0000004a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody--small {
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-8;
|
|
||||||
width: 192px;
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastHeader {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastControl {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LiveBadge {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody_divider {
|
|
||||||
background-color: $quinary-content;
|
|
||||||
border: 0;
|
|
||||||
height: 1px;
|
|
||||||
margin: $spacing-12 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody_controls {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-32;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: $spacing-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody_timerow {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
|
|
||||||
display: flex;
|
|
||||||
gap: $spacing-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody__small-close {
|
|
||||||
right: 8px;
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
|
@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
|
||||||
}
|
}
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Voice Broadcast */
|
|
||||||
/* ******************** */
|
|
||||||
$live-badge-color: #ffffff;
|
|
||||||
/* ******************** */
|
|
||||||
|
|
||||||
/* One-off colors */
|
/* One-off colors */
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
$progressbar-bg-color: var(--cpd-color-gray-200);
|
$progressbar-bg-color: var(--cpd-color-gray-200);
|
||||||
|
|
|
@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
|
||||||
$location-live-secondary-color: #deddfd;
|
$location-live-secondary-color: #deddfd;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Voice Broadcast */
|
|
||||||
/* ******************** */
|
|
||||||
$live-badge-color: #ffffff;
|
|
||||||
/* ******************** */
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
|
@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
|
||||||
$location-live-secondary-color: #deddfd;
|
$location-live-secondary-color: #deddfd;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Voice Broadcast */
|
|
||||||
/* ******************** */
|
|
||||||
$live-badge-color: #ffffff;
|
|
||||||
/* ******************** */
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900);
|
||||||
$location-live-secondary-color: var(--cpd-color-purple-600);
|
$location-live-secondary-color: var(--cpd-color-purple-600);
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Voice Broadcast */
|
|
||||||
/* ******************** */
|
|
||||||
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
|
|
||||||
/* ******************** */
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
6
src/@types/matrix-js-sdk.d.ts
vendored
6
src/@types/matrix-js-sdk.d.ts
vendored
|
@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
|
||||||
import type { BLURHASH_FIELD } from "../utils/image-media";
|
import type { BLURHASH_FIELD } from "../utils/image-media";
|
||||||
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
|
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
|
||||||
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
|
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
|
||||||
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
|
|
||||||
import type { EncryptedFile } from "matrix-js-sdk/src/types";
|
import type { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
||||||
|
@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" {
|
||||||
"im.vector.modular.widgets": IWidget | {};
|
"im.vector.modular.widgets": IWidget | {};
|
||||||
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
|
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
|
||||||
|
|
||||||
// Unstable voice broadcast state events
|
|
||||||
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
|
|
||||||
|
|
||||||
// Element custom state events
|
// Element custom state events
|
||||||
"im.vector.web.settings": Record<string, any>;
|
"im.vector.web.settings": Record<string, any>;
|
||||||
"org.matrix.room.preview_urls": { disable: boolean };
|
"org.matrix.room.preview_urls": { disable: boolean };
|
||||||
|
@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
|
||||||
waveform?: number[];
|
waveform?: number[];
|
||||||
};
|
};
|
||||||
"org.matrix.msc3245.voice"?: {};
|
"org.matrix.msc3245.voice"?: {};
|
||||||
|
|
||||||
"io.element.voice_broadcast_chunk"?: { sequence: number };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,13 +175,6 @@ export interface IConfigOptions {
|
||||||
sync_timeline_limit?: number;
|
sync_timeline_limit?: number;
|
||||||
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
|
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
|
||||||
|
|
||||||
voice_broadcast?: {
|
|
||||||
// length per voice chunk in seconds
|
|
||||||
chunk_length?: number;
|
|
||||||
// max voice broadcast length in seconds
|
|
||||||
max_length?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
user_notice?: {
|
user_notice?: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
|
@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
|
||||||
import { findDMForUser } from "./utils/dm/findDMForUser";
|
import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||||
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
|
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
|
||||||
import { localNotificationsAreSilenced } from "./utils/notifications";
|
import { localNotificationsAreSilenced } from "./utils/notifications";
|
||||||
import { SdkContextClass } from "./contexts/SDKContext";
|
|
||||||
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
|
|
||||||
import { isNotNull } from "./Typeguards";
|
import { isNotNull } from "./Typeguards";
|
||||||
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||||
import { Jitsi } from "./widgets/Jitsi.ts";
|
import { Jitsi } from "./widgets/Jitsi.ts";
|
||||||
|
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pause current broadcast, if any
|
|
||||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
|
|
||||||
|
|
||||||
if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) {
|
|
||||||
// Do not start a call, if recording a broadcast
|
|
||||||
showCantStartACallDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We might be using managed hybrid widgets
|
// We might be using managed hybrid widgets
|
||||||
if (isManagedHybridWidgetEnabled(room)) {
|
if (isManagedHybridWidgetEnabled(room)) {
|
||||||
await addManagedHybridWidget(room);
|
await addManagedHybridWidget(room);
|
||||||
|
|
|
@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
|
||||||
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
|
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
|
||||||
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||||
import ToastStore from "./stores/ToastStore";
|
import ToastStore from "./stores/ToastStore";
|
||||||
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
|
||||||
import { getSenderName } from "./utils/event/getSenderName";
|
|
||||||
import { stripPlainReply } from "./utils/Reply";
|
import { stripPlainReply } from "./utils/Reply";
|
||||||
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||||
|
|
||||||
|
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
|
||||||
return TextForEvent.textForLocationEvent(event)();
|
return TextForEvent.textForLocationEvent(event)();
|
||||||
},
|
},
|
||||||
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
|
|
||||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
|
|
||||||
// Show a notification for the first broadcast chunk.
|
|
||||||
// At this point a user received something to listen to.
|
|
||||||
return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mute other broadcast chunks
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
|
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||||
|
|
||||||
// XXX: exported for tests
|
// XXX: exported for tests
|
||||||
public evaluateEvent(ev: MatrixEvent): void {
|
public evaluateEvent(ev: MatrixEvent): void {
|
||||||
// Mute notifications for broadcast info events
|
|
||||||
if (ev.getType() === VoiceBroadcastInfoEventType) return;
|
|
||||||
let roomId = ev.getRoomId()!;
|
let roomId = ev.getRoomId()!;
|
||||||
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||||
// Attempt to translate a virtual room to a native one
|
// Attempt to translate a virtual room to a native one
|
||||||
|
|
|
@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
||||||
logo: require("../res/img/element-desktop-logo.svg").default,
|
logo: require("../res/img/element-desktop-logo.svg").default,
|
||||||
url: "https://element.io/get-started",
|
url: "https://element.io/get-started",
|
||||||
},
|
},
|
||||||
voice_broadcast: {
|
|
||||||
chunk_length: 2 * 60, // two minutes
|
|
||||||
max_length: 4 * 60 * 60, // four hours
|
|
||||||
},
|
|
||||||
|
|
||||||
feedback: {
|
feedback: {
|
||||||
existing_issues_url:
|
existing_issues_url:
|
||||||
|
|
|
@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "./stores/right-panel/RightPanelStore";
|
import RightPanelStore from "./stores/right-panel/RightPanelStore";
|
||||||
import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
|
import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
|
||||||
import { ElementCall } from "./models/Call";
|
import { ElementCall } from "./models/Call";
|
||||||
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
|
||||||
import { getSenderName } from "./utils/event/getSenderName";
|
import { getSenderName } from "./utils/event/getSenderName";
|
||||||
import PosthogTrackers from "./PosthogTrackers.ts";
|
import PosthogTrackers from "./PosthogTrackers.ts";
|
||||||
|
|
||||||
|
@ -906,7 +905,6 @@ const stateHandlers: IHandlers = {
|
||||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||||
"im.vector.modular.widgets": textForWidgetEvent,
|
"im.vector.modular.widgets": textForWidgetEvent,
|
||||||
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
|
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
|
||||||
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add all the Mjolnir stuff to the renderer
|
// Add all the Mjolnir stuff to the renderer
|
||||||
|
|
|
@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
|
||||||
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
|
||||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
||||||
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
||||||
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
|
|
||||||
import GenericToast from "../views/toasts/GenericToast";
|
import GenericToast from "../views/toasts/GenericToast";
|
||||||
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
||||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||||
|
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private focusNext: FocusNextType;
|
private focusNext: FocusNextType;
|
||||||
private subTitleStatus: string;
|
private subTitleStatus: string;
|
||||||
private prevWindowWidth: number;
|
private prevWindowWidth: number;
|
||||||
private voiceBroadcastResumer?: VoiceBroadcastResumer;
|
|
||||||
|
|
||||||
private readonly loggedInView = createRef<LoggedInViewType>();
|
private readonly loggedInView = createRef<LoggedInViewType>();
|
||||||
private dispatcherRef?: string;
|
private dispatcherRef?: string;
|
||||||
|
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
window.removeEventListener("resize", this.onWindowResized);
|
window.removeEventListener("resize", this.onWindowResized);
|
||||||
|
|
||||||
this.stores.accountPasswordStore.clearPassword();
|
this.stores.accountPasswordStore.clearPassword();
|
||||||
this.voiceBroadcastResumer?.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWindowResized = (): void => {
|
private onWindowResized = (): void => {
|
||||||
|
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
LegacyCallHandler.instance.hangupAllCalls();
|
LegacyCallHandler.instance.hangupAllCalls();
|
||||||
Promise.all([
|
Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
|
||||||
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()),
|
Lifecycle.logout(this.stores.oidcClientStore),
|
||||||
cleanUpBroadcasts(this.stores),
|
);
|
||||||
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
|
|
||||||
break;
|
break;
|
||||||
case "require_registration":
|
case "require_registration":
|
||||||
startAnyRegistrationFlow(payload as any);
|
startAnyRegistrationFlow(payload as any);
|
||||||
|
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { MutableRefObject, ReactNode, useContext, useRef } from "react";
|
import React, { MutableRefObject, ReactNode, useRef } from "react";
|
||||||
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
|
||||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||||
import {
|
|
||||||
useCurrentVoiceBroadcastPreRecording,
|
|
||||||
useCurrentVoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastPlaybackBody,
|
|
||||||
VoiceBroadcastPreRecording,
|
|
||||||
VoiceBroadcastPreRecordingPip,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingPip,
|
|
||||||
VoiceBroadcastSmallPlaybackBody,
|
|
||||||
} from "../../voice-broadcast";
|
|
||||||
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
|
|
||||||
import { WidgetPip } from "../views/pips/WidgetPip";
|
import { WidgetPip } from "../views/pips/WidgetPip";
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
|
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
|
|
||||||
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
|
|
||||||
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
|
|
||||||
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||||
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
|
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
|
||||||
}
|
}
|
||||||
|
|
||||||
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
|
|
||||||
const content =
|
|
||||||
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
|
|
||||||
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
|
|
||||||
) : (
|
|
||||||
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({ onStartMoving }) => (
|
|
||||||
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createVoiceBroadcastPreRecordingPipContent(
|
|
||||||
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
|
|
||||||
): CreatePipChildren {
|
|
||||||
return ({ onStartMoving }) => (
|
|
||||||
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
|
|
||||||
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createVoiceBroadcastRecordingPipContent(
|
|
||||||
voiceBroadcastRecording: VoiceBroadcastRecording,
|
|
||||||
): CreatePipChildren {
|
|
||||||
return ({ onStartMoving }) => (
|
|
||||||
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
|
|
||||||
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
const pipMode = true;
|
const pipMode = true;
|
||||||
let pipContent: Array<CreatePipChildren> = [];
|
const pipContent: Array<CreatePipChildren> = [];
|
||||||
|
|
||||||
if (this.props.voiceBroadcastRecording) {
|
|
||||||
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
|
|
||||||
} else if (this.props.voiceBroadcastPreRecording) {
|
|
||||||
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
|
|
||||||
} else if (this.props.voiceBroadcastPlayback) {
|
|
||||||
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
// get a ref to call inside the current scope
|
// get a ref to call inside the current scope
|
||||||
|
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PipContainer: React.FC = () => {
|
export const PipContainer: React.FC = () => {
|
||||||
const sdkContext = useContext(SDKContext);
|
|
||||||
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
|
|
||||||
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
|
|
||||||
|
|
||||||
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
|
|
||||||
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
|
|
||||||
|
|
||||||
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
|
|
||||||
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
|
|
||||||
|
|
||||||
const movePersistedElement = useRef<() => void>();
|
const movePersistedElement = useRef<() => void>();
|
||||||
|
|
||||||
return (
|
return <PipContainerInner movePersistedElement={movePersistedElement} />;
|
||||||
<PipContainerInner
|
|
||||||
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
|
|
||||||
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
|
|
||||||
voiceBroadcastRecording={currentVoiceBroadcastRecording}
|
|
||||||
movePersistedElement={movePersistedElement}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||||
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
|
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
|
||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
|
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
|
||||||
import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
|
|
||||||
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
|
|
||||||
import { SDKContext } from "../../contexts/SDKContext";
|
import { SDKContext } from "../../contexts/SDKContext";
|
||||||
import { shouldShowFeedback } from "../../utils/Feedback";
|
import { shouldShowFeedback } from "../../utils/Feedback";
|
||||||
import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg";
|
import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg";
|
||||||
|
@ -58,7 +56,6 @@ interface IState {
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
isHighContrast: boolean;
|
isHighContrast: boolean;
|
||||||
selectedSpace?: Room | null;
|
selectedSpace?: Room | null;
|
||||||
showLiveAvatarAddon: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toRightOf = (rect: PartialDOMRect): MenuProps => {
|
const toRightOf = (rect: PartialDOMRect): MenuProps => {
|
||||||
|
@ -94,7 +91,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
isDarkTheme: this.isUserOnDarkTheme(),
|
isDarkTheme: this.isUserOnDarkTheme(),
|
||||||
isHighContrast: this.isUserOnHighContrastTheme(),
|
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||||
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,19 +98,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
|
return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => {
|
|
||||||
this.setState({
|
|
||||||
showLiveAvatarAddon: recording !== null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||||
this.context.voiceBroadcastRecordingsStore.on(
|
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
|
||||||
this.onCurrentVoiceBroadcastRecordingChanged,
|
|
||||||
);
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||||
}
|
}
|
||||||
|
@ -125,10 +111,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||||
this.context.voiceBroadcastRecordingsStore.off(
|
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
|
||||||
this.onCurrentVoiceBroadcastRecordingChanged,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isUserOnDarkTheme(): boolean {
|
private isUserOnDarkTheme(): boolean {
|
||||||
|
@ -435,12 +417,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
name = <div className="mx_UserMenu_name">{displayName}</div>;
|
name = <div className="mx_UserMenu_name">{displayName}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const liveAvatarAddon = this.state.showLiveAvatarAddon ? (
|
|
||||||
<div className="mx_UserMenu_userAvatarLive" data-testid="user-menu-live-vb">
|
|
||||||
<LiveIcon className="mx_Icon_8" />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserMenu">
|
<div className="mx_UserMenu">
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
|
@ -459,7 +435,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
size={avatarSize + "px"}
|
size={avatarSize + "px"}
|
||||||
className="mx_UserMenu_userAvatar_BaseAvatar"
|
className="mx_UserMenu_userAvatar_BaseAvatar"
|
||||||
/>
|
/>
|
||||||
{liveAvatarAddon}
|
|
||||||
</div>
|
</div>
|
||||||
{name}
|
{name}
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import { BaseGrouper } from "./BaseGrouper";
|
import { BaseGrouper } from "./BaseGrouper";
|
||||||
import MessagePanel, { WrappedEvent } from "../MessagePanel";
|
import MessagePanel, { WrappedEvent } from "../MessagePanel";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast";
|
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import DateSeparator from "../../views/messages/DateSeparator";
|
import DateSeparator from "../../views/messages/DateSeparator";
|
||||||
|
@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VoiceBroadcastInfoEventType === eventType) {
|
|
||||||
// always show voice broadcast info events in timeline
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isState() && event.getSender() === createEvent.getSender()) {
|
if (event.isState() && event.getSender() === createEvent.getSender()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { MutableRefObject } from "react";
|
|
||||||
|
|
||||||
import { toLeftOrRightOf } from "../../structures/ContextMenu";
|
|
||||||
import IconizedContextMenu, {
|
|
||||||
IconizedContextMenuOptionList,
|
|
||||||
IconizedContextMenuRadio,
|
|
||||||
} from "../context_menus/IconizedContextMenu";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
containerRef: MutableRefObject<HTMLElement | null>;
|
|
||||||
currentDevice: MediaDeviceInfo | null;
|
|
||||||
devices: MediaDeviceInfo[];
|
|
||||||
onDeviceSelect: (device: MediaDeviceInfo) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DevicesContextMenu: React.FC<Props> = ({ containerRef, currentDevice, devices, onDeviceSelect }) => {
|
|
||||||
const deviceOptions = devices.map((d: MediaDeviceInfo) => {
|
|
||||||
return (
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
key={d.deviceId}
|
|
||||||
active={d.deviceId === currentDevice?.deviceId}
|
|
||||||
onClick={() => onDeviceSelect(d)}
|
|
||||||
label={d.label}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconizedContextMenu
|
|
||||||
mountAsChild={false}
|
|
||||||
onFinished={() => {}}
|
|
||||||
{...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})}
|
|
||||||
>
|
|
||||||
<IconizedContextMenuOptionList>{deviceOptions}</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import InfoDialog from "./InfoDialog";
|
|
||||||
|
|
||||||
export const createCantStartVoiceMessageBroadcastDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voice_message|cant_start_broadcast_title"),
|
|
||||||
description: <p>{_t("voice_message|cant_start_broadcast_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { IRedactOpts, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { IRedactOpts, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent";
|
|
||||||
import ErrorDialog from "./ErrorDialog";
|
import ErrorDialog from "./ErrorDialog";
|
||||||
import TextInputDialog from "./TextInputDialog";
|
import TextInputDialog from "./TextInputDialog";
|
||||||
|
|
||||||
|
@ -70,18 +68,6 @@ export function createRedactEventDialog({
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {};
|
const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {};
|
||||||
|
|
||||||
// redact related events if this is a voice broadcast started event and
|
|
||||||
// server has support for relation based redactions
|
|
||||||
if (isVoiceBroadcastStartedEvent(mxEvent)) {
|
|
||||||
const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions);
|
|
||||||
if (
|
|
||||||
relationBasedRedactionsSupport &&
|
|
||||||
relationBasedRedactionsSupport !== ServerSupport.Unsupported
|
|
||||||
) {
|
|
||||||
withRelTypes.with_rel_types = [RelationType.Reference];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onCloseDialog?.();
|
onCloseDialog?.();
|
||||||
await cli.redactEvent(roomId, eventId, undefined, {
|
await cli.redactEvent(roomId, eventId, undefined, {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account
|
||||||
import SettingsFlag from "../elements/SettingsFlag";
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import ServerInfo from "./devtools/ServerInfo";
|
import ServerInfo from "./devtools/ServerInfo";
|
||||||
import { Features } from "../../../settings/Settings";
|
|
||||||
import CopyableText from "../elements/CopyableText";
|
import CopyableText from "../elements/CopyableText";
|
||||||
import RoomNotifications from "./devtools/RoomNotifications";
|
import RoomNotifications from "./devtools/RoomNotifications";
|
||||||
|
|
||||||
|
@ -100,7 +99,6 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
||||||
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name={Features.VoiceBroadcastForceSmallChunks} level={SettingLevel.DEVICE} />
|
|
||||||
</div>
|
</div>
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
|
|
|
@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
|
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
|
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import PinningUtils from "../../../utils/PinningUtils";
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers.ts";
|
import PosthogTrackers from "../../../PosthogTrackers.ts";
|
||||||
|
@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
||||||
* until cross-platform support
|
* until cross-platform support
|
||||||
* (PSF-1041)
|
* (PSF-1041)
|
||||||
*/
|
*/
|
||||||
!M_BEACON_INFO.matches(this.props.mxEvent.getType()) &&
|
!M_BEACON_INFO.matches(this.props.mxEvent.getType());
|
||||||
!(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
|
|
||||||
|
|
||||||
return inNotThreadTimeline && isAllowedMessageType;
|
return inNotThreadTimeline && isAllowedMessageType;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ import MjolnirBody from "./MjolnirBody";
|
||||||
import MBeaconBody from "./MBeaconBody";
|
import MBeaconBody from "./MBeaconBody";
|
||||||
import { DecryptionFailureBody } from "./DecryptionFailureBody";
|
import { DecryptionFailureBody } from "./DecryptionFailureBody";
|
||||||
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
|
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
|
||||||
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
|
|
||||||
|
|
||||||
// onMessageAllowed is handled internally
|
// onMessageAllowed is handled internally
|
||||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||||
|
@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||||
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
|
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
|
||||||
BodyType = MLocationBody;
|
BodyType = MLocationBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) {
|
|
||||||
BodyType = VoiceBroadcastBody;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_mjolnir")) {
|
if (SettingsStore.getValue("feature_mjolnir")) {
|
||||||
|
|
|
@ -48,14 +48,9 @@ import MessageComposerButtons from "./MessageComposerButtons";
|
||||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import { Features } from "../../../settings/Settings";
|
|
||||||
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||||
import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/";
|
import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/";
|
||||||
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
||||||
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import { VoiceBroadcastInfoState } from "../../../voice-broadcast";
|
|
||||||
import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog";
|
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { formatTimeLeft } from "../../../DateUtils";
|
import { formatTimeLeft } from "../../../DateUtils";
|
||||||
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
|
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
|
||||||
|
@ -101,7 +96,6 @@ interface IState {
|
||||||
isStickerPickerOpen: boolean;
|
isStickerPickerOpen: boolean;
|
||||||
showStickersButton: boolean;
|
showStickersButton: boolean;
|
||||||
showPollsButton: boolean;
|
showPollsButton: boolean;
|
||||||
showVoiceBroadcastButton: boolean;
|
|
||||||
isWysiwygLabEnabled: boolean;
|
isWysiwygLabEnabled: boolean;
|
||||||
isRichTextEnabled: boolean;
|
isRichTextEnabled: boolean;
|
||||||
initialComposerContent: string;
|
initialComposerContent: string;
|
||||||
|
@ -127,7 +121,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public static defaultProps = {
|
public static defaultProps = {
|
||||||
compact: false,
|
compact: false,
|
||||||
showVoiceBroadcastButton: false,
|
|
||||||
isRichTextEnabled: true,
|
isRichTextEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -155,7 +148,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
isStickerPickerOpen: false,
|
isStickerPickerOpen: false,
|
||||||
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
||||||
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
||||||
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
|
||||||
isWysiwygLabEnabled: isWysiwygLabEnabled,
|
isWysiwygLabEnabled: isWysiwygLabEnabled,
|
||||||
isRichTextEnabled: isRichTextEnabled,
|
isRichTextEnabled: isRichTextEnabled,
|
||||||
initialComposerContent: initialComposerContent,
|
initialComposerContent: initialComposerContent,
|
||||||
|
@ -250,7 +242,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
||||||
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
|
||||||
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
|
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -301,12 +292,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Features.VoiceBroadcast: {
|
|
||||||
if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) {
|
|
||||||
this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "feature_wysiwyg_composer": {
|
case "feature_wysiwyg_composer": {
|
||||||
if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) {
|
if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) {
|
||||||
this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) });
|
this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) });
|
||||||
|
@ -533,13 +518,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRecordStartEndClick = (): void => {
|
private onRecordStartEndClick = (): void => {
|
||||||
const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent();
|
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||||
|
|
||||||
if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) {
|
|
||||||
createCantStartVoiceMessageBroadcastDialog();
|
|
||||||
} else {
|
|
||||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.context.narrow) {
|
if (this.context.narrow) {
|
||||||
this.toggleButtonMenu();
|
this.toggleButtonMenu();
|
||||||
|
@ -698,17 +677,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
onComposerModeClick={this.onRichTextToggle}
|
onComposerModeClick={this.onRichTextToggle}
|
||||||
toggleButtonMenu={this.toggleButtonMenu}
|
toggleButtonMenu={this.toggleButtonMenu}
|
||||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
|
||||||
onStartVoiceBroadcastClick={() => {
|
|
||||||
setUpVoiceBroadcastPreRecording(
|
|
||||||
this.props.room,
|
|
||||||
MatrixClientPeg.safeGet(),
|
|
||||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
|
||||||
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
|
||||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
|
||||||
);
|
|
||||||
this.toggleButtonMenu();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showSendButton && (
|
{showSendButton && (
|
||||||
|
|
|
@ -43,8 +43,6 @@ interface IProps {
|
||||||
showPollsButton: boolean;
|
showPollsButton: boolean;
|
||||||
showStickersButton: boolean;
|
showStickersButton: boolean;
|
||||||
toggleButtonMenu: () => void;
|
toggleButtonMenu: () => void;
|
||||||
showVoiceBroadcastButton: boolean;
|
|
||||||
onStartVoiceBroadcastClick: () => void;
|
|
||||||
isRichTextEnabled: boolean;
|
isRichTextEnabled: boolean;
|
||||||
onComposerModeClick: () => void;
|
onComposerModeClick: () => void;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +78,6 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
uploadButton(), // props passed via UploadButtonContext
|
uploadButton(), // props passed via UploadButtonContext
|
||||||
showStickersButton(props),
|
showStickersButton(props),
|
||||||
voiceRecordingButton(props, narrow),
|
voiceRecordingButton(props, narrow),
|
||||||
startVoiceBroadcastButton(props),
|
|
||||||
props.showPollsButton ? pollButton(room, props.relation) : null,
|
props.showPollsButton ? pollButton(room, props.relation) : null,
|
||||||
showLocationButton(props, room, matrixClient),
|
showLocationButton(props, room, matrixClient),
|
||||||
];
|
];
|
||||||
|
@ -100,7 +97,6 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
showStickersButton(props),
|
showStickersButton(props),
|
||||||
voiceRecordingButton(props, narrow),
|
voiceRecordingButton(props, narrow),
|
||||||
startVoiceBroadcastButton(props),
|
|
||||||
props.showPollsButton ? pollButton(room, props.relation) : null,
|
props.showPollsButton ? pollButton(room, props.relation) : null,
|
||||||
showLocationButton(props, room, matrixClient),
|
showLocationButton(props, room, matrixClient),
|
||||||
];
|
];
|
||||||
|
@ -254,18 +250,6 @@ function showStickersButton(props: IProps): ReactElement | null {
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
|
|
||||||
return props.showVoiceBroadcastButton ? (
|
|
||||||
<CollapsibleButton
|
|
||||||
key="start_voice_broadcast"
|
|
||||||
className="mx_MessageComposer_button"
|
|
||||||
iconClassName="mx_MessageComposer_voiceBroadcast"
|
|
||||||
onClick={props.onStartVoiceBroadcastClick}
|
|
||||||
title={_t("voice_broadcast|action")}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
|
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
|
||||||
// XXX: recording UI does not work well in narrow mode, so hide for now
|
// XXX: recording UI does not work well in narrow mode, so hide for now
|
||||||
return narrow ? null : (
|
return narrow ? null : (
|
||||||
|
|
|
@ -39,7 +39,6 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast";
|
|
||||||
import { RoomTileSubtitle } from "./RoomTileSubtitle";
|
import { RoomTileSubtitle } from "./RoomTileSubtitle";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
import { UIComponent } from "../../../settings/UIFeature";
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
|
@ -53,10 +52,6 @@ interface Props {
|
||||||
tag: TagID;
|
tag: TagID;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClassProps extends Props {
|
|
||||||
hasLiveVoiceBroadcast: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -77,13 +72,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect): MenuProps => {
|
||||||
return { left, top, chevronFace };
|
return { left, top, chevronFace };
|
||||||
};
|
};
|
||||||
|
|
||||||
export class RoomTile extends React.PureComponent<ClassProps, State> {
|
class RoomTile extends React.PureComponent<Props, State> {
|
||||||
private dispatcherRef?: string;
|
private dispatcherRef?: string;
|
||||||
private roomTileRef = createRef<HTMLDivElement>();
|
private roomTileRef = createRef<HTMLDivElement>();
|
||||||
private notificationState: NotificationState;
|
private notificationState: NotificationState;
|
||||||
private roomProps: RoomEchoChamber;
|
private roomProps: RoomEchoChamber;
|
||||||
|
|
||||||
public constructor(props: ClassProps) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -370,15 +365,10 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
/**
|
/**
|
||||||
* RoomTile has a subtile if one of the following applies:
|
* RoomTile has a subtile if one of the following applies:
|
||||||
* - there is a call
|
* - there is a call
|
||||||
* - there is a live voice broadcast
|
|
||||||
* - message previews are enabled and there is a previewable message
|
* - message previews are enabled and there is a previewable message
|
||||||
*/
|
*/
|
||||||
private get shouldRenderSubtitle(): boolean {
|
private get shouldRenderSubtitle(): boolean {
|
||||||
return (
|
return !!this.state.call || (this.props.showMessagePreview && !!this.state.messagePreview);
|
||||||
!!this.state.call ||
|
|
||||||
this.props.hasLiveVoiceBroadcast ||
|
|
||||||
(this.props.showMessagePreview && !!this.state.messagePreview)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
|
@ -409,7 +399,6 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
const subtitle = this.shouldRenderSubtitle ? (
|
const subtitle = this.shouldRenderSubtitle ? (
|
||||||
<RoomTileSubtitle
|
<RoomTileSubtitle
|
||||||
call={this.state.call}
|
call={this.state.call}
|
||||||
hasLiveVoiceBroadcast={this.props.hasLiveVoiceBroadcast}
|
|
||||||
messagePreview={this.state.messagePreview}
|
messagePreview={this.state.messagePreview}
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
showMessagePreview={this.props.showMessagePreview}
|
showMessagePreview={this.props.showMessagePreview}
|
||||||
|
@ -491,9 +480,4 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomTileHOC: React.FC<Props> = (props: Props) => {
|
export default RoomTile;
|
||||||
const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room);
|
|
||||||
return <RoomTile {...props} hasLiveVoiceBroadcast={hasLiveVoiceBroadcast} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomTileHOC;
|
|
||||||
|
|
|
@ -13,11 +13,9 @@ import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons"
|
||||||
import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import { Call } from "../../../models/Call";
|
import { Call } from "../../../models/Call";
|
||||||
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||||
import { VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
call: Call | null;
|
call: Call | null;
|
||||||
hasLiveVoiceBroadcast: boolean;
|
|
||||||
messagePreview: MessagePreview | null;
|
messagePreview: MessagePreview | null;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
showMessagePreview: boolean;
|
showMessagePreview: boolean;
|
||||||
|
@ -25,13 +23,7 @@ interface Props {
|
||||||
|
|
||||||
const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`;
|
const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`;
|
||||||
|
|
||||||
export const RoomTileSubtitle: React.FC<Props> = ({
|
export const RoomTileSubtitle: React.FC<Props> = ({ call, messagePreview, roomId, showMessagePreview }) => {
|
||||||
call,
|
|
||||||
hasLiveVoiceBroadcast,
|
|
||||||
messagePreview,
|
|
||||||
roomId,
|
|
||||||
showMessagePreview,
|
|
||||||
}) => {
|
|
||||||
if (call) {
|
if (call) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomTile_subtitle">
|
<div className="mx_RoomTile_subtitle">
|
||||||
|
@ -40,10 +32,6 @@ export const RoomTileSubtitle: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLiveVoiceBroadcast) {
|
|
||||||
return <VoiceBroadcastRoomSubtitle />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showMessagePreview && messagePreview) {
|
if (showMessagePreview && messagePreview) {
|
||||||
const className = classNames("mx_RoomTile_subtitle", {
|
const className = classNames("mx_RoomTile_subtitle", {
|
||||||
"mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply,
|
"mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply,
|
||||||
|
|
|
@ -19,7 +19,6 @@ import ErrorDialog from "../../../dialogs/ErrorDialog";
|
||||||
import PowerSelector from "../../../elements/PowerSelector";
|
import PowerSelector from "../../../elements/PowerSelector";
|
||||||
import SettingsFieldset from "../../SettingsFieldset";
|
import SettingsFieldset from "../../SettingsFieldset";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../../../../voice-broadcast";
|
|
||||||
import { ElementCall } from "../../../../../models/Call";
|
import { ElementCall } from "../../../../../models/Call";
|
||||||
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
||||||
import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
|
import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
|
||||||
|
@ -62,7 +61,6 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
|
||||||
|
|
||||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||||
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
||||||
[VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||||
|
@ -289,7 +287,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR
|
||||||
|
|
||||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||||
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
|
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
|
||||||
[VoiceBroadcastInfoEventType]: _td("room_settings|permissions|io.element.voice_broadcast_info"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// MSC3401: Native Group VoIP signaling
|
// MSC3401: Native Group VoIP signaling
|
||||||
|
|
|
@ -25,11 +25,6 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
|
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
|
||||||
import { OidcClientStore } from "../stores/oidc/OidcClientStore";
|
import { OidcClientStore } from "../stores/oidc/OidcClientStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore from "../stores/WidgetStore";
|
||||||
import {
|
|
||||||
VoiceBroadcastPlaybacksStore,
|
|
||||||
VoiceBroadcastPreRecordingStore,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
} from "../voice-broadcast";
|
|
||||||
|
|
||||||
// This context is available to components under MatrixChat,
|
// This context is available to components under MatrixChat,
|
||||||
// the context must not be used by components outside a SdkContextClass tree.
|
// the context must not be used by components outside a SdkContextClass tree.
|
||||||
|
@ -68,9 +63,6 @@ export class SdkContextClass {
|
||||||
protected _SpaceStore?: SpaceStoreClass;
|
protected _SpaceStore?: SpaceStoreClass;
|
||||||
protected _LegacyCallHandler?: LegacyCallHandler;
|
protected _LegacyCallHandler?: LegacyCallHandler;
|
||||||
protected _TypingStore?: TypingStore;
|
protected _TypingStore?: TypingStore;
|
||||||
protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
|
|
||||||
protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
|
|
||||||
protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore;
|
|
||||||
protected _AccountPasswordStore?: AccountPasswordStore;
|
protected _AccountPasswordStore?: AccountPasswordStore;
|
||||||
protected _UserProfilesStore?: UserProfilesStore;
|
protected _UserProfilesStore?: UserProfilesStore;
|
||||||
protected _OidcClientStore?: OidcClientStore;
|
protected _OidcClientStore?: OidcClientStore;
|
||||||
|
@ -157,27 +149,6 @@ export class SdkContextClass {
|
||||||
return this._TypingStore;
|
return this._TypingStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get voiceBroadcastRecordingsStore(): VoiceBroadcastRecordingsStore {
|
|
||||||
if (!this._VoiceBroadcastRecordingsStore) {
|
|
||||||
this._VoiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
|
|
||||||
}
|
|
||||||
return this._VoiceBroadcastRecordingsStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get voiceBroadcastPreRecordingStore(): VoiceBroadcastPreRecordingStore {
|
|
||||||
if (!this._VoiceBroadcastPreRecordingStore) {
|
|
||||||
this._VoiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
|
|
||||||
}
|
|
||||||
return this._VoiceBroadcastPreRecordingStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get voiceBroadcastPlaybacksStore(): VoiceBroadcastPlaybacksStore {
|
|
||||||
if (!this._VoiceBroadcastPlaybacksStore) {
|
|
||||||
this._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(this.voiceBroadcastRecordingsStore);
|
|
||||||
}
|
|
||||||
return this._VoiceBroadcastPlaybacksStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get accountPasswordStore(): AccountPasswordStore {
|
public get accountPasswordStore(): AccountPasswordStore {
|
||||||
if (!this._AccountPasswordStore) {
|
if (!this._AccountPasswordStore) {
|
||||||
this._AccountPasswordStore = new AccountPasswordStore();
|
this._AccountPasswordStore = new AccountPasswordStore();
|
||||||
|
|
|
@ -41,13 +41,7 @@ import { getMessageModerationState, MessageModerationState } from "../utils/Even
|
||||||
import HiddenBody from "../components/views/messages/HiddenBody";
|
import HiddenBody from "../components/views/messages/HiddenBody";
|
||||||
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
||||||
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
||||||
import { shouldDisplayAsVoiceBroadcastTile } from "../voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile";
|
|
||||||
import { ElementCall } from "../models/Call";
|
import { ElementCall } from "../models/Call";
|
||||||
import {
|
|
||||||
isRelatedToVoiceBroadcast,
|
|
||||||
shouldDisplayAsVoiceBroadcastStoppedText,
|
|
||||||
VoiceBroadcastChunkEventType,
|
|
||||||
} from "../voice-broadcast";
|
|
||||||
|
|
||||||
// Subset of EventTile's IProps plus some mixins
|
// Subset of EventTile's IProps plus some mixins
|
||||||
export interface EventTileTypeProps
|
export interface EventTileTypeProps
|
||||||
|
@ -223,12 +217,6 @@ export function pickFactory(
|
||||||
return MessageEventFactory;
|
return MessageEventFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDisplayAsVoiceBroadcastTile(mxEvent)) {
|
|
||||||
return MessageEventFactory;
|
|
||||||
} else if (shouldDisplayAsVoiceBroadcastStoppedText(mxEvent)) {
|
|
||||||
return TextualEventFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== "") {
|
if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== "") {
|
||||||
return noEventFactoryFactory(); // improper event type to render
|
return noEventFactoryFactory(); // improper event type to render
|
||||||
}
|
}
|
||||||
|
@ -249,16 +237,6 @@ export function pickFactory(
|
||||||
return noEventFactoryFactory();
|
return noEventFactoryFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEvent.getContent()[VoiceBroadcastChunkEventType]) {
|
|
||||||
// hide voice broadcast chunks
|
|
||||||
return noEventFactoryFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showHiddenEvents && mxEvent.isDecryptionFailure() && isRelatedToVoiceBroadcast(mxEvent, cli)) {
|
|
||||||
// hide utd events related to a broadcast
|
|
||||||
return noEventFactoryFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory();
|
return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
|
||||||
import { M_POLL_END, M_POLL_START, M_BEACON_INFO, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { M_POLL_END, M_POLL_START, M_BEACON_INFO, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
|
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get forwardable event for a given event
|
* Get forwardable event for a given event
|
||||||
|
@ -20,8 +19,6 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() === VoiceBroadcastInfoEventType) return null;
|
|
||||||
|
|
||||||
// Live location beacons should forward their latest location as a static pin location
|
// Live location beacons should forward their latest location as a static pin location
|
||||||
// If the beacon is not live, or doesn't have a location forwarding is not allowed
|
// If the beacon is not live, or doesn't have a location forwarding is not allowed
|
||||||
if (M_BEACON_INFO.matches(event.getType())) {
|
if (M_BEACON_INFO.matches(event.getType())) {
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { _t } from "../languageHandler";
|
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
|
||||||
import { requestMediaPermissions } from "../utils/media/requestMediaPermissions";
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
devices: MediaDeviceInfo[];
|
|
||||||
device: MediaDeviceInfo | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAudioDeviceSelection = (
|
|
||||||
onDeviceChanged?: (device: MediaDeviceInfo) => void,
|
|
||||||
): {
|
|
||||||
currentDevice: MediaDeviceInfo | null;
|
|
||||||
currentDeviceLabel: string;
|
|
||||||
devices: MediaDeviceInfo[];
|
|
||||||
setDevice(device: MediaDeviceInfo): void;
|
|
||||||
} => {
|
|
||||||
const shouldRequestPermissionsRef = useRef<boolean>(true);
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
devices: [],
|
|
||||||
device: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldRequestPermissionsRef.current) {
|
|
||||||
shouldRequestPermissionsRef.current = false;
|
|
||||||
requestMediaPermissions(false).then((stream: MediaStream | undefined) => {
|
|
||||||
MediaDeviceHandler.getDevices().then((devices) => {
|
|
||||||
if (!devices) return;
|
|
||||||
const { audioinput } = devices;
|
|
||||||
MediaDeviceHandler.getDefaultDevice(audioinput);
|
|
||||||
const deviceFromSettings = MediaDeviceHandler.getAudioInput();
|
|
||||||
const device =
|
|
||||||
audioinput.find((d) => {
|
|
||||||
return d.deviceId === deviceFromSettings;
|
|
||||||
}) || audioinput[0];
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
devices: audioinput,
|
|
||||||
device,
|
|
||||||
});
|
|
||||||
stream?.getTracks().forEach((t) => t.stop());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDevice = (device: MediaDeviceInfo): void => {
|
|
||||||
const shouldNotify = device.deviceId !== state.device?.deviceId;
|
|
||||||
MediaDeviceHandler.instance.setDevice(device.deviceId, MediaDeviceKindEnum.AudioInput);
|
|
||||||
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
device,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldNotify) {
|
|
||||||
onDeviceChanged?.(device);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentDevice: state.device,
|
|
||||||
currentDeviceLabel: state.device?.label || _t("voip|default_device"),
|
|
||||||
devices: state.devices,
|
|
||||||
setDevice,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1087,10 +1087,6 @@
|
||||||
},
|
},
|
||||||
"error_user_not_logged_in": "User is not logged in",
|
"error_user_not_logged_in": "User is not logged in",
|
||||||
"event_preview": {
|
"event_preview": {
|
||||||
"io.element.voice_broadcast_info": {
|
|
||||||
"user": "%(senderName)s ended a voice broadcast",
|
|
||||||
"you": "You ended a voice broadcast"
|
|
||||||
},
|
|
||||||
"m.call.answer": {
|
"m.call.answer": {
|
||||||
"dm": "Call in progress",
|
"dm": "Call in progress",
|
||||||
"user": "%(senderName)s joined the call",
|
"user": "%(senderName)s joined the call",
|
||||||
|
@ -1491,8 +1487,6 @@
|
||||||
"video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.",
|
"video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.",
|
||||||
"video_rooms_faq2_question": "Can I use text chat alongside the video call?",
|
"video_rooms_faq2_question": "Can I use text chat alongside the video call?",
|
||||||
"video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
"video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
||||||
"voice_broadcast": "Voice broadcast",
|
|
||||||
"voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length",
|
|
||||||
"wysiwyg_composer": "Rich text editor"
|
"wysiwyg_composer": "Rich text editor"
|
||||||
},
|
},
|
||||||
"labs_mjolnir": {
|
"labs_mjolnir": {
|
||||||
|
@ -1638,7 +1632,6 @@
|
||||||
"mute_description": "You won't get any notifications"
|
"mute_description": "You won't get any notifications"
|
||||||
},
|
},
|
||||||
"notifier": {
|
"notifier": {
|
||||||
"io.element.voice_broadcast_chunk": "%(senderName)s started a voice broadcast",
|
|
||||||
"m.key.verification.request": "%(name)s is requesting verification"
|
"m.key.verification.request": "%(name)s is requesting verification"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
|
@ -2253,7 +2246,6 @@
|
||||||
"error_unbanning": "Failed to unban",
|
"error_unbanning": "Failed to unban",
|
||||||
"events_default": "Send messages",
|
"events_default": "Send messages",
|
||||||
"invite": "Invite users",
|
"invite": "Invite users",
|
||||||
"io.element.voice_broadcast_info": "Voice broadcasts",
|
|
||||||
"kick": "Remove users",
|
"kick": "Remove users",
|
||||||
"m.call": "Start %(brand)s calls",
|
"m.call": "Start %(brand)s calls",
|
||||||
"m.call.member": "Join %(brand)s calls",
|
"m.call.member": "Join %(brand)s calls",
|
||||||
|
@ -3287,10 +3279,6 @@
|
||||||
"error_rendering_message": "Can't load this message",
|
"error_rendering_message": "Can't load this message",
|
||||||
"historical_messages_unavailable": "You can't see earlier messages",
|
"historical_messages_unavailable": "You can't see earlier messages",
|
||||||
"in_room_name": " in <strong>%(room)s</strong>",
|
"in_room_name": " in <strong>%(room)s</strong>",
|
||||||
"io.element.voice_broadcast_info": {
|
|
||||||
"user": "%(senderName)s ended a <a>voice broadcast</a>",
|
|
||||||
"you": "You ended a <a>voice broadcast</a>"
|
|
||||||
},
|
|
||||||
"io.element.widgets.layout": "%(senderName)s has updated the room layout",
|
"io.element.widgets.layout": "%(senderName)s has updated the room layout",
|
||||||
"late_event_separator": "Originally sent %(dateTime)s",
|
"late_event_separator": "Originally sent %(dateTime)s",
|
||||||
"load_error": {
|
"load_error": {
|
||||||
|
@ -3840,38 +3828,6 @@
|
||||||
"switch_theme_dark": "Switch to dark mode",
|
"switch_theme_dark": "Switch to dark mode",
|
||||||
"switch_theme_light": "Switch to light mode"
|
"switch_theme_light": "Switch to light mode"
|
||||||
},
|
},
|
||||||
"voice_broadcast": {
|
|
||||||
"30s_backward": "30s backward",
|
|
||||||
"30s_forward": "30s forward",
|
|
||||||
"action": "Voice broadcast",
|
|
||||||
"buffering": "Buffering…",
|
|
||||||
"confirm_listen_affirm": "Yes, end my recording",
|
|
||||||
"confirm_listen_description": "If you start listening to this live broadcast, your current live broadcast recording will be ended.",
|
|
||||||
"confirm_listen_title": "Listen to live broadcast?",
|
|
||||||
"confirm_stop_affirm": "Yes, stop broadcast",
|
|
||||||
"confirm_stop_description": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.",
|
|
||||||
"confirm_stop_title": "Stop live broadcasting?",
|
|
||||||
"connection_error": "Connection error - Recording paused",
|
|
||||||
"failed_already_recording_description": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.",
|
|
||||||
"failed_already_recording_title": "Can't start a new voice broadcast",
|
|
||||||
"failed_decrypt": "Unable to decrypt voice broadcast",
|
|
||||||
"failed_generic": "Unable to play this voice broadcast",
|
|
||||||
"failed_insufficient_permission_description": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.",
|
|
||||||
"failed_insufficient_permission_title": "Can't start a new voice broadcast",
|
|
||||||
"failed_no_connection_description": "Unfortunately we're unable to start a recording right now. Please try again later.",
|
|
||||||
"failed_no_connection_title": "Connection error",
|
|
||||||
"failed_others_already_recording_description": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.",
|
|
||||||
"failed_others_already_recording_title": "Can't start a new voice broadcast",
|
|
||||||
"go_live": "Go live",
|
|
||||||
"live": "Live",
|
|
||||||
"pause": "pause voice broadcast",
|
|
||||||
"play": "play voice broadcast",
|
|
||||||
"resume": "resume voice broadcast"
|
|
||||||
},
|
|
||||||
"voice_message": {
|
|
||||||
"cant_start_broadcast_description": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.",
|
|
||||||
"cant_start_broadcast_title": "Can't start voice message"
|
|
||||||
},
|
|
||||||
"voip": {
|
"voip": {
|
||||||
"already_in_call": "Already in call",
|
"already_in_call": "Already in call",
|
||||||
"already_in_call_person": "You're already in a call with this person.",
|
"already_in_call_person": "You're already in a call with this person.",
|
||||||
|
@ -3891,7 +3847,6 @@
|
||||||
"camera_disabled": "Your camera is turned off",
|
"camera_disabled": "Your camera is turned off",
|
||||||
"camera_enabled": "Your camera is still enabled",
|
"camera_enabled": "Your camera is still enabled",
|
||||||
"cannot_call_yourself_description": "You cannot place a call with yourself.",
|
"cannot_call_yourself_description": "You cannot place a call with yourself.",
|
||||||
"change_input_device": "Change input device",
|
|
||||||
"close_lobby": "Close lobby",
|
"close_lobby": "Close lobby",
|
||||||
"connecting": "Connecting",
|
"connecting": "Connecting",
|
||||||
"connection_lost": "Connectivity to the server has been lost",
|
"connection_lost": "Connectivity to the server has been lost",
|
||||||
|
@ -3910,8 +3865,6 @@
|
||||||
"enable_camera": "Turn on camera",
|
"enable_camera": "Turn on camera",
|
||||||
"enable_microphone": "Unmute microphone",
|
"enable_microphone": "Unmute microphone",
|
||||||
"expand": "Return to call",
|
"expand": "Return to call",
|
||||||
"failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
|
||||||
"failed_call_live_broadcast_title": "Can’t start a call",
|
|
||||||
"get_call_link": "Share call link",
|
"get_call_link": "Share call link",
|
||||||
"hangup": "Hangup",
|
"hangup": "Hangup",
|
||||||
"hide_sidebar_button": "Hide sidebar",
|
"hide_sidebar_button": "Hide sidebar",
|
||||||
|
|
|
@ -85,8 +85,6 @@ export enum LabGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Features {
|
export enum Features {
|
||||||
VoiceBroadcast = "feature_voice_broadcast",
|
|
||||||
VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks",
|
|
||||||
NotificationSettings2 = "feature_notification_settings2",
|
NotificationSettings2 = "feature_notification_settings2",
|
||||||
OidcNativeFlow = "feature_oidc_native_flow",
|
OidcNativeFlow = "feature_oidc_native_flow",
|
||||||
ReleaseAnnouncement = "feature_release_announcement",
|
ReleaseAnnouncement = "feature_release_announcement",
|
||||||
|
@ -440,19 +438,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
shouldWarn: true,
|
shouldWarn: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
[Features.VoiceBroadcast]: {
|
|
||||||
isFeature: true,
|
|
||||||
labsGroup: LabGroup.Messaging,
|
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
|
|
||||||
supportedLevelsAreOrdered: true,
|
|
||||||
displayName: _td("labs|voice_broadcast"),
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
[Features.VoiceBroadcastForceSmallChunks]: {
|
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
|
||||||
displayName: _td("labs|voice_broadcast_force_small_chunks"),
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
[Features.OidcNativeFlow]: {
|
[Features.OidcNativeFlow]: {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Developer,
|
labsGroup: LabGroup.Developer,
|
||||||
|
|
|
@ -42,15 +42,6 @@ import { UPDATE_EVENT } from "./AsyncStore";
|
||||||
import { SdkContextClass } from "../contexts/SDKContext";
|
import { SdkContextClass } from "../contexts/SDKContext";
|
||||||
import { CallStore } from "./CallStore";
|
import { CallStore } from "./CallStore";
|
||||||
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
|
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
|
||||||
import {
|
|
||||||
doClearCurrentVoiceBroadcastPlaybackIfStopped,
|
|
||||||
doMaybeSetCurrentVoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingsStoreEvent,
|
|
||||||
} from "../voice-broadcast";
|
|
||||||
import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators";
|
|
||||||
import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog";
|
|
||||||
import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom";
|
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload";
|
import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload";
|
||||||
import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
|
@ -164,10 +155,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.resetDispatcher(dis);
|
this.resetDispatcher(dis);
|
||||||
this.stores.voiceBroadcastRecordingsStore.addListener(
|
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
|
||||||
this.onCurrentBroadcastRecordingChanged,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRoomListener(roomId: string, fn: Listener): void {
|
public addRoomListener(roomId: string, fn: Listener): void {
|
||||||
|
@ -182,16 +169,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
this.emit(roomId, isActive);
|
this.emit(roomId, isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCurrentBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => {
|
|
||||||
if (recording === null) {
|
|
||||||
const room = this.stores.client?.getRoom(this.state.roomId || undefined);
|
|
||||||
|
|
||||||
if (room) {
|
|
||||||
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private setState(newState: Partial<State>): void {
|
private setState(newState: Partial<State>): void {
|
||||||
// If values haven't changed, there's nothing to do.
|
// If values haven't changed, there's nothing to do.
|
||||||
// This only tries a shallow comparison, so unchanged objects will slip
|
// This only tries a shallow comparison, so unchanged objects will slip
|
||||||
|
@ -207,16 +184,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState.viewingCall) {
|
|
||||||
// Pause current broadcast, if any
|
|
||||||
this.stores.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
|
|
||||||
|
|
||||||
if (this.stores.voiceBroadcastRecordingsStore.getCurrent()) {
|
|
||||||
showCantStartACallDialog();
|
|
||||||
newState.viewingCall = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastRoomId = this.state.roomId;
|
const lastRoomId = this.state.roomId;
|
||||||
this.state = Object.assign(this.state, newState);
|
this.state = Object.assign(this.state, newState);
|
||||||
if (lastRoomId !== this.state.roomId) {
|
if (lastRoomId !== this.state.roomId) {
|
||||||
|
@ -235,29 +202,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
this.emit(UPDATE_EVENT);
|
this.emit(UPDATE_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void {
|
|
||||||
if (!this.stores.client) return;
|
|
||||||
doMaybeSetCurrentVoiceBroadcastPlayback(
|
|
||||||
room,
|
|
||||||
this.stores.client,
|
|
||||||
this.stores.voiceBroadcastPlaybacksStore,
|
|
||||||
this.stores.voiceBroadcastRecordingsStore,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomStateEvents(event: MatrixEvent): void {
|
|
||||||
const roomId = event.getRoomId?.();
|
|
||||||
|
|
||||||
// no room or not current room
|
|
||||||
if (!roomId || roomId !== this.state.roomId) return;
|
|
||||||
|
|
||||||
const room = this.stores.client?.getRoom(roomId);
|
|
||||||
|
|
||||||
if (room) {
|
|
||||||
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onDispatch(payload: ActionPayload): void {
|
private onDispatch(payload: ActionPayload): void {
|
||||||
// eslint-disable-line @typescript-eslint/naming-convention
|
// eslint-disable-line @typescript-eslint/naming-convention
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
@ -283,10 +227,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
wasContextSwitch: false,
|
wasContextSwitch: false,
|
||||||
viewingCall: false,
|
viewingCall: false,
|
||||||
});
|
});
|
||||||
doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore);
|
|
||||||
break;
|
|
||||||
case "MatrixActions.RoomState.events":
|
|
||||||
this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event);
|
|
||||||
break;
|
break;
|
||||||
case Action.ViewRoomError:
|
case Action.ViewRoomError:
|
||||||
this.viewRoomError(payload as ViewRoomErrorPayload);
|
this.viewRoomError(payload as ViewRoomErrorPayload);
|
||||||
|
@ -489,9 +429,6 @@ export class RoomViewStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore);
|
|
||||||
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
|
|
||||||
|
|
||||||
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
|
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
|
||||||
}
|
}
|
||||||
} else if (payload.room_alias) {
|
} else if (payload.room_alias) {
|
||||||
|
|
|
@ -22,8 +22,6 @@ import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
import { IPreview } from "./previews/IPreview";
|
import { IPreview } from "./previews/IPreview";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../voice-broadcast";
|
|
||||||
import { VoiceBroadcastPreview } from "./previews/VoiceBroadcastPreview";
|
|
||||||
import shouldHideEvent from "../../shouldHideEvent";
|
import shouldHideEvent from "../../shouldHideEvent";
|
||||||
|
|
||||||
// Emitted event for when a room's preview has changed. First argument will the room for which
|
// Emitted event for when a room's preview has changed. First argument will the room for which
|
||||||
|
@ -69,10 +67,6 @@ const PREVIEWS: Record<
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new PollStartEventPreview(),
|
previewer: new PollStartEventPreview(),
|
||||||
},
|
},
|
||||||
[VoiceBroadcastInfoEventType]: {
|
|
||||||
isState: true,
|
|
||||||
previewer: new VoiceBroadcastPreview(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The maximum number of events we're willing to look back on to get a preview.
|
// The maximum number of events we're willing to look back on to get a preview.
|
||||||
|
|
|
@ -14,15 +14,11 @@ import { _t, sanitizeForTranslation } from "../../../languageHandler";
|
||||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||||
import { getHtmlText } from "../../../HtmlUtils";
|
import { getHtmlText } from "../../../HtmlUtils";
|
||||||
import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply";
|
import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply";
|
||||||
import { VoiceBroadcastChunkEventType } from "../../../voice-broadcast/types";
|
|
||||||
|
|
||||||
export class MessageEventPreview implements IPreview {
|
export class MessageEventPreview implements IPreview {
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
|
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
|
||||||
let eventContent = event.getContent();
|
let eventContent = event.getContent();
|
||||||
|
|
||||||
// no preview for broadcast chunks
|
|
||||||
if (eventContent[VoiceBroadcastChunkEventType]) return null;
|
|
||||||
|
|
||||||
if (event.isRelation(RelationType.Replace)) {
|
if (event.isRelation(RelationType.Replace)) {
|
||||||
// It's an edit, generate the preview on the new text
|
// It's an edit, generate the preview on the new text
|
||||||
eventContent = event.getContent()["m.new_content"];
|
eventContent = event.getContent()["m.new_content"];
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoState } from "../../../voice-broadcast/types";
|
|
||||||
import { textForVoiceBroadcastStoppedEventWithoutLink } from "../../../voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink";
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
|
|
||||||
export class VoiceBroadcastPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: string, isThread?: boolean): string | null {
|
|
||||||
if (!event.isRedacted() && event.getContent()?.state === VoiceBroadcastInfoState.Stopped) {
|
|
||||||
return textForVoiceBroadcastStoppedEventWithoutLink(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -284,10 +284,6 @@ export class StopGapWidget extends EventEmitter {
|
||||||
});
|
});
|
||||||
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
|
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
|
||||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
||||||
this.messaging.on(`action:${ElementWidgetActions.JoinCall}`, () => {
|
|
||||||
// pause voice broadcast recording when any widget sends a "join"
|
|
||||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always attach a handler for ViewRoom, but permission check it internally
|
// Always attach a handler for ViewRoom, but permission check it internally
|
||||||
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import SettingsStore from "../settings/SettingsStore";
|
||||||
import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory";
|
import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory";
|
||||||
import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils";
|
import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils";
|
||||||
import { ElementCall } from "../models/Call";
|
import { ElementCall } from "../models/Call";
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast";
|
|
||||||
|
|
||||||
const calcIsInfoMessage = (
|
const calcIsInfoMessage = (
|
||||||
eventType: EventType | string,
|
eventType: EventType | string,
|
||||||
|
@ -38,8 +37,7 @@ const calcIsInfoMessage = (
|
||||||
eventType !== EventType.RoomCreate &&
|
eventType !== EventType.RoomCreate &&
|
||||||
!M_POLL_START.matches(eventType) &&
|
!M_POLL_START.matches(eventType) &&
|
||||||
!M_POLL_END.matches(eventType) &&
|
!M_POLL_END.matches(eventType) &&
|
||||||
!M_BEACON_INFO.matches(eventType) &&
|
!M_BEACON_INFO.matches(eventType)
|
||||||
!(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -91,8 +89,7 @@ export function getEventDisplayInfo(
|
||||||
(eventType === EventType.RoomMessage && msgtype === MsgType.Emote) ||
|
(eventType === EventType.RoomMessage && msgtype === MsgType.Emote) ||
|
||||||
M_POLL_START.matches(eventType) ||
|
M_POLL_START.matches(eventType) ||
|
||||||
M_BEACON_INFO.matches(eventType) ||
|
M_BEACON_INFO.matches(eventType) ||
|
||||||
isLocationEvent(mxEvent) ||
|
isLocationEvent(mxEvent);
|
||||||
eventType === VoiceBroadcastInfoEventType;
|
|
||||||
|
|
||||||
// If we're showing hidden events in the timeline, we should use the
|
// If we're showing hidden events in the timeline, we should use the
|
||||||
// source tile when there's no regular tile for an event and also for
|
// source tile when there's no regular tile for an event and also for
|
||||||
|
|
|
@ -30,7 +30,6 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||||
import { launchPollEditor } from "../components/views/messages/MPollBody";
|
import { launchPollEditor } from "../components/views/messages/MPollBody";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether an event should allow actions like reply, reactions, edit, etc.
|
* Returns whether an event should allow actions like reply, reactions, edit, etc.
|
||||||
|
@ -56,9 +55,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
|
||||||
mxEvent.getType() === "m.sticker" ||
|
mxEvent.getType() === "m.sticker" ||
|
||||||
M_POLL_START.matches(mxEvent.getType()) ||
|
M_POLL_START.matches(mxEvent.getType()) ||
|
||||||
M_POLL_END.matches(mxEvent.getType()) ||
|
M_POLL_END.matches(mxEvent.getType()) ||
|
||||||
M_BEACON_INFO.matches(mxEvent.getType()) ||
|
M_BEACON_INFO.matches(mxEvent.getType())
|
||||||
(mxEvent.getType() === VoiceBroadcastInfoEventType &&
|
|
||||||
mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { isEqual } from "lodash";
|
|
||||||
import { Optional } from "matrix-events-sdk";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { getChunkLength } from "..";
|
|
||||||
import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording";
|
|
||||||
import { concat } from "../../utils/arrays";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { Singleflight } from "../../utils/Singleflight";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastRecorderEvent {
|
|
||||||
ChunkRecorded = "chunk_recorded",
|
|
||||||
CurrentChunkLengthUpdated = "current_chunk_length_updated",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
[VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void;
|
|
||||||
[VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChunkRecordedPayload {
|
|
||||||
buffer: Uint8Array;
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// char sequence of "OpusHead"
|
|
||||||
const OpusHead = [79, 112, 117, 115, 72, 101, 97, 100];
|
|
||||||
|
|
||||||
// char sequence of "OpusTags"
|
|
||||||
const OpusTags = [79, 112, 117, 115, 84, 97, 103, 115];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class provides the function to seamlessly record fixed length chunks.
|
|
||||||
* Subscribe with on(VoiceBroadcastRecordingEvents.ChunkRecorded, (payload: ChunkRecordedPayload) => {})
|
|
||||||
* to retrieve chunks while recording.
|
|
||||||
*/
|
|
||||||
export class VoiceBroadcastRecorder
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastRecorderEvent, EventMap>
|
|
||||||
implements IDestroyable
|
|
||||||
{
|
|
||||||
private opusHead?: Uint8Array;
|
|
||||||
private opusTags?: Uint8Array;
|
|
||||||
private chunkBuffer = new Uint8Array(0);
|
|
||||||
// position of the previous chunk in seconds
|
|
||||||
private previousChunkEndTimePosition = 0;
|
|
||||||
// current chunk length in seconds
|
|
||||||
private currentChunkLength = 0;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private voiceRecording: VoiceRecording,
|
|
||||||
public readonly targetChunkLength: number,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.voiceRecording.onDataAvailable = this.onDataAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
|
||||||
await this.voiceRecording.start();
|
|
||||||
this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => {
|
|
||||||
this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the recording and returns the remaining chunk (if any).
|
|
||||||
*/
|
|
||||||
public async stop(): Promise<Optional<ChunkRecordedPayload>> {
|
|
||||||
try {
|
|
||||||
await this.voiceRecording.stop();
|
|
||||||
} catch {
|
|
||||||
// Ignore if the recording raises any error.
|
|
||||||
}
|
|
||||||
|
|
||||||
// forget about that call, so that we can stop it again later
|
|
||||||
Singleflight.forgetAllFor(this.voiceRecording);
|
|
||||||
const chunk = this.extractChunk();
|
|
||||||
this.currentChunkLength = 0;
|
|
||||||
this.previousChunkEndTimePosition = 0;
|
|
||||||
return chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get contentType(): string {
|
|
||||||
return this.voiceRecording.contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCurrentChunkLength(currentChunkLength: number): void {
|
|
||||||
if (this.currentChunkLength === currentChunkLength) return;
|
|
||||||
|
|
||||||
this.currentChunkLength = currentChunkLength;
|
|
||||||
this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrentChunkLength(): number {
|
|
||||||
return this.currentChunkLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onDataAvailable = (data: ArrayBuffer): void => {
|
|
||||||
const dataArray = new Uint8Array(data);
|
|
||||||
|
|
||||||
// extract the part, that contains the header type info
|
|
||||||
const headerType = Array.from(dataArray.slice(28, 36));
|
|
||||||
|
|
||||||
if (isEqual(OpusHead, headerType)) {
|
|
||||||
// data seems to be an "OpusHead" header
|
|
||||||
this.opusHead = dataArray;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEqual(OpusTags, headerType)) {
|
|
||||||
// data seems to be an "OpusTags" header
|
|
||||||
this.opusTags = dataArray;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setCurrentChunkLength(this.voiceRecording.recorderSeconds! - this.previousChunkEndTimePosition);
|
|
||||||
this.handleData(dataArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleData(data: Uint8Array): void {
|
|
||||||
this.chunkBuffer = concat(this.chunkBuffer, data);
|
|
||||||
this.emitChunkIfTargetLengthReached();
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitChunkIfTargetLengthReached(): void {
|
|
||||||
if (this.getCurrentChunkLength() >= this.targetChunkLength) {
|
|
||||||
this.emitAndResetChunk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the current chunk and resets the buffer.
|
|
||||||
*/
|
|
||||||
private extractChunk(): Optional<ChunkRecordedPayload> {
|
|
||||||
if (this.chunkBuffer.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.opusHead || !this.opusTags) {
|
|
||||||
logger.warn("Broadcast chunk cannot be extracted. OpusHead or OpusTags is missing.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRecorderTime = this.voiceRecording.recorderSeconds!;
|
|
||||||
const payload: ChunkRecordedPayload = {
|
|
||||||
buffer: concat(this.opusHead!, this.opusTags!, this.chunkBuffer),
|
|
||||||
length: this.getCurrentChunkLength(),
|
|
||||||
};
|
|
||||||
this.chunkBuffer = new Uint8Array(0);
|
|
||||||
this.setCurrentChunkLength(0);
|
|
||||||
this.previousChunkEndTimePosition = currentRecorderTime;
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitAndResetChunk(): void {
|
|
||||||
if (this.chunkBuffer.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, this.extractChunk()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.removeAllListeners();
|
|
||||||
this.voiceRecording.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => {
|
|
||||||
const voiceRecording = new VoiceRecording();
|
|
||||||
voiceRecording.disableMaxLength();
|
|
||||||
return new VoiceBroadcastRecorder(voiceRecording, getChunkLength());
|
|
||||||
};
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
|
||||||
import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastRecordingBody,
|
|
||||||
shouldDisplayAsVoiceBroadcastRecordingTile,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
VoiceBroadcastPlaybackBody,
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
} from "..";
|
|
||||||
import { IBodyProps } from "../../components/views/messages/IBodyProps";
|
|
||||||
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
|
||||||
import { SDKContext } from "../../contexts/SDKContext";
|
|
||||||
import { useMatrixClientContext } from "../../contexts/MatrixClientContext";
|
|
||||||
|
|
||||||
export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
|
|
||||||
const sdkContext = useContext(SDKContext);
|
|
||||||
const client = useMatrixClientContext();
|
|
||||||
const [infoState, setInfoState] = useState(mxEvent.getContent()?.state || VoiceBroadcastInfoState.Stopped);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onInfoEvent = (event: MatrixEvent): void => {
|
|
||||||
if (event.getContent()?.state === VoiceBroadcastInfoState.Stopped) {
|
|
||||||
// only a stopped event can change the tile state
|
|
||||||
setInfoState(VoiceBroadcastInfoState.Stopped);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const relationsHelper = new RelationsHelper(
|
|
||||||
mxEvent,
|
|
||||||
RelationType.Reference,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent);
|
|
||||||
relationsHelper.emitCurrent();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
relationsHelper.destroy();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldDisplayAsVoiceBroadcastRecordingTile(infoState, client, mxEvent)) {
|
|
||||||
const recording = sdkContext.voiceBroadcastRecordingsStore.getByInfoEvent(mxEvent, client);
|
|
||||||
return <VoiceBroadcastRecordingBody recording={recording} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playback = sdkContext.voiceBroadcastPlaybacksStore.getByInfoEvent(mxEvent, client);
|
|
||||||
return <VoiceBroadcastPlaybackBody playback={playback} />;
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
grey?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LiveBadge: React.FC<Props> = ({ grey = false }) => {
|
|
||||||
const liveBadgeClasses = classNames("mx_LiveBadge", {
|
|
||||||
"mx_LiveBadge--grey": grey,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={liveBadgeClasses}>
|
|
||||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("voice_broadcast|live")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SeekButton: React.FC<Props> = ({ onClick, icon: Icon, label }) => {
|
|
||||||
return (
|
|
||||||
<AccessibleButton kind="secondary_content" onClick={onClick} aria-label={label}>
|
|
||||||
<Icon className="mx_Icon mx_Icon_24" />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import classNames from "classnames";
|
|
||||||
import React, { ReactElement } from "react";
|
|
||||||
|
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: string;
|
|
||||||
icon: ReactElement;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastControl: React.FC<Props> = ({ className = "", icon, label, onClick }) => {
|
|
||||||
return (
|
|
||||||
<AccessibleButton
|
|
||||||
className={classNames("mx_VoiceBroadcastControl", className)}
|
|
||||||
onClick={onClick}
|
|
||||||
aria-label={label}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastError: React.FC<Props> = ({ message }) => {
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastRecordingConnectionError">
|
|
||||||
<WarningIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,139 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
|
||||||
import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid";
|
|
||||||
|
|
||||||
import { LiveBadge, VoiceBroadcastLiveness } from "../..";
|
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg";
|
|
||||||
import { Icon as TimerIcon } from "../../../../res/img/compound/timer-16px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../../../components/views/elements/AccessibleButton";
|
|
||||||
import Clock from "../../../components/views/audio_messages/Clock";
|
|
||||||
import { formatTimeLeft } from "../../../DateUtils";
|
|
||||||
import Spinner from "../../../components/views/elements/Spinner";
|
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
|
|
||||||
interface VoiceBroadcastHeaderProps {
|
|
||||||
linkToRoom?: boolean;
|
|
||||||
live?: VoiceBroadcastLiveness;
|
|
||||||
liveBadgePosition?: "middle" | "right";
|
|
||||||
onCloseClick?: () => void;
|
|
||||||
onMicrophoneLineClick?: ((e: ButtonEvent) => void | Promise<void>) | null;
|
|
||||||
room: Room;
|
|
||||||
microphoneLabel?: string;
|
|
||||||
showBroadcast?: boolean;
|
|
||||||
showBuffering?: boolean;
|
|
||||||
bufferingPosition?: "line" | "title";
|
|
||||||
timeLeft?: number;
|
|
||||||
showClose?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
|
||||||
linkToRoom = false,
|
|
||||||
live = "not-live",
|
|
||||||
liveBadgePosition = "right",
|
|
||||||
onCloseClick = (): void => {},
|
|
||||||
onMicrophoneLineClick = null,
|
|
||||||
room,
|
|
||||||
microphoneLabel,
|
|
||||||
showBroadcast = false,
|
|
||||||
showBuffering = false,
|
|
||||||
bufferingPosition = "line",
|
|
||||||
showClose = false,
|
|
||||||
timeLeft,
|
|
||||||
}) => {
|
|
||||||
const broadcast = showBroadcast && (
|
|
||||||
<div className="mx_VoiceBroadcastHeader_line">
|
|
||||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("voice_broadcast|action")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const liveBadge = live !== "not-live" && <LiveBadge grey={live === "grey"} />;
|
|
||||||
|
|
||||||
const closeButton = showClose && (
|
|
||||||
<AccessibleButton onClick={onCloseClick}>
|
|
||||||
<CloseIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeLeftLine = timeLeft && (
|
|
||||||
<div className="mx_VoiceBroadcastHeader_line">
|
|
||||||
<TimerIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
<Clock formatFn={formatTimeLeft} seconds={timeLeft} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const bufferingLine = showBuffering && bufferingPosition === "line" && (
|
|
||||||
<div className="mx_VoiceBroadcastHeader_line">
|
|
||||||
<Spinner w={14} h={14} />
|
|
||||||
{_t("voice_broadcast|buffering")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const microphoneLineClasses = classNames({
|
|
||||||
mx_VoiceBroadcastHeader_line: true,
|
|
||||||
["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick,
|
|
||||||
});
|
|
||||||
|
|
||||||
const microphoneLine = microphoneLabel && (
|
|
||||||
<AccessibleButton
|
|
||||||
className={microphoneLineClasses}
|
|
||||||
onClick={onMicrophoneLineClick}
|
|
||||||
title={_t("voip|change_input_device")}
|
|
||||||
>
|
|
||||||
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
<span>{microphoneLabel}</span>
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRoomAvatarOrNameClick = (): void => {
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
metricsTrigger: undefined, // other
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let roomAvatar = <RoomAvatar room={room} size="32px" />;
|
|
||||||
let roomName = (
|
|
||||||
<div className="mx_VoiceBroadcastHeader_room_wrapper">
|
|
||||||
<div className="mx_VoiceBroadcastHeader_room">{room.name}</div>
|
|
||||||
{showBuffering && bufferingPosition === "title" && <Spinner w={12} h={12} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (linkToRoom) {
|
|
||||||
roomAvatar = <AccessibleButton onClick={onRoomAvatarOrNameClick}>{roomAvatar}</AccessibleButton>;
|
|
||||||
|
|
||||||
roomName = <AccessibleButton onClick={onRoomAvatarOrNameClick}>{roomName}</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastHeader">
|
|
||||||
{roomAvatar}
|
|
||||||
<div className="mx_VoiceBroadcastHeader_content">
|
|
||||||
{roomName}
|
|
||||||
{microphoneLine}
|
|
||||||
{timeLeftLine}
|
|
||||||
{broadcast}
|
|
||||||
{bufferingLine}
|
|
||||||
{liveBadgePosition === "middle" && liveBadge}
|
|
||||||
</div>
|
|
||||||
{liveBadgePosition === "right" && liveBadge}
|
|
||||||
{closeButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { ReactElement } from "react";
|
|
||||||
import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
|
|
||||||
import PlayIcon from "@vector-im/compound-design-tokens/assets/web/icons/play-solid";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { VoiceBroadcastControl, VoiceBroadcastPlaybackState } from "../..";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClick: () => void;
|
|
||||||
state: VoiceBroadcastPlaybackState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastPlaybackControl: React.FC<Props> = ({ onClick, state }) => {
|
|
||||||
let controlIcon: ReactElement | null = null;
|
|
||||||
let controlLabel: string | null = null;
|
|
||||||
let className = "";
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case VoiceBroadcastPlaybackState.Stopped:
|
|
||||||
controlIcon = <PlayIcon className="mx_Icon mx_Icon_16" />;
|
|
||||||
className = "mx_VoiceBroadcastControl-play";
|
|
||||||
controlLabel = _t("voice_broadcast|play");
|
|
||||||
break;
|
|
||||||
case VoiceBroadcastPlaybackState.Paused:
|
|
||||||
controlIcon = <PlayIcon className="mx_Icon mx_Icon_16" />;
|
|
||||||
className = "mx_VoiceBroadcastControl-play";
|
|
||||||
controlLabel = _t("voice_broadcast|resume");
|
|
||||||
break;
|
|
||||||
case VoiceBroadcastPlaybackState.Buffering:
|
|
||||||
case VoiceBroadcastPlaybackState.Playing:
|
|
||||||
controlIcon = <PauseIcon className="mx_Icon mx_Icon_12" />;
|
|
||||||
controlLabel = _t("voice_broadcast|pause");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlIcon && controlLabel) {
|
|
||||||
return (
|
|
||||||
<VoiceBroadcastControl className={className} label={controlLabel} icon={controlIcon} onClick={onClick} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export const VoiceBroadcastRecordingConnectionError: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastRecordingConnectionError">
|
|
||||||
<WarningIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("voice_broadcast|connection_error")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export const VoiceBroadcastRoomSubtitle: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomTile_subtitle mx_RoomTile_subtitle--voice-broadcast">
|
|
||||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("voice_broadcast|live")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import BaseDialog from "../../../components/views/dialogs/BaseDialog";
|
|
||||||
import DialogButtons from "../../../components/views/elements/DialogButtons";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onFinished: (confirmed?: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfirmListenBroadcastStopCurrentDialog: React.FC<Props> = ({ onFinished }) => {
|
|
||||||
return (
|
|
||||||
<BaseDialog title={_t("voice_broadcast|confirm_listen_title")} hasCancel={true} onFinished={onFinished}>
|
|
||||||
<p>{_t("voice_broadcast|confirm_listen_description")}</p>
|
|
||||||
<DialogButtons
|
|
||||||
onPrimaryButtonClick={() => onFinished(true)}
|
|
||||||
primaryButton={_t("voice_broadcast|confirm_listen_affirm")}
|
|
||||||
cancelButton={_t("action|no")}
|
|
||||||
onCancel={() => onFinished(false)}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showConfirmListenBroadcastStopCurrentDialog = async (): Promise<boolean> => {
|
|
||||||
const { finished } = Modal.createDialog(ConfirmListenBroadcastStopCurrentDialog);
|
|
||||||
const [confirmed] = await finished;
|
|
||||||
return !!confirmed;
|
|
||||||
};
|
|
|
@ -1,102 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { ReactElement } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastError,
|
|
||||||
VoiceBroadcastHeader,
|
|
||||||
VoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastPlaybackControl,
|
|
||||||
VoiceBroadcastPlaybackState,
|
|
||||||
} from "../..";
|
|
||||||
import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback";
|
|
||||||
import { Icon as Back30sIcon } from "../../../../res/img/compound/back-30s-24px.svg";
|
|
||||||
import { Icon as Forward30sIcon } from "../../../../res/img/compound/forward-30s-24px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Clock from "../../../components/views/audio_messages/Clock";
|
|
||||||
import SeekBar from "../../../components/views/audio_messages/SeekBar";
|
|
||||||
import { SeekButton } from "../atoms/SeekButton";
|
|
||||||
|
|
||||||
const SEEK_TIME = 30;
|
|
||||||
|
|
||||||
interface VoiceBroadcastPlaybackBodyProps {
|
|
||||||
pip?: boolean;
|
|
||||||
playback: VoiceBroadcastPlayback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProps> = ({ pip = false, playback }) => {
|
|
||||||
const { times, liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback);
|
|
||||||
|
|
||||||
let seekBackwardButton: ReactElement | null = null;
|
|
||||||
let seekForwardButton: ReactElement | null = null;
|
|
||||||
|
|
||||||
if (playbackState !== VoiceBroadcastPlaybackState.Stopped) {
|
|
||||||
const onSeekBackwardButtonClick = (): void => {
|
|
||||||
playback.skipTo(Math.max(0, times.position - SEEK_TIME));
|
|
||||||
};
|
|
||||||
|
|
||||||
seekBackwardButton = (
|
|
||||||
<SeekButton
|
|
||||||
icon={Back30sIcon}
|
|
||||||
label={_t("voice_broadcast|30s_backward")}
|
|
||||||
onClick={onSeekBackwardButtonClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSeekForwardButtonClick = (): void => {
|
|
||||||
playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME));
|
|
||||||
};
|
|
||||||
|
|
||||||
seekForwardButton = (
|
|
||||||
<SeekButton
|
|
||||||
icon={Forward30sIcon}
|
|
||||||
label={_t("voice_broadcast|30s_forward")}
|
|
||||||
onClick={onSeekForwardButtonClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classNames({
|
|
||||||
mx_VoiceBroadcastBody: true,
|
|
||||||
["mx_VoiceBroadcastBody--pip"]: pip,
|
|
||||||
});
|
|
||||||
|
|
||||||
const content =
|
|
||||||
playbackState === VoiceBroadcastPlaybackState.Error ? (
|
|
||||||
<VoiceBroadcastError message={playback.errorMessage} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="mx_VoiceBroadcastBody_controls">
|
|
||||||
{seekBackwardButton}
|
|
||||||
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
|
|
||||||
{seekForwardButton}
|
|
||||||
</div>
|
|
||||||
<SeekBar playback={playback} />
|
|
||||||
<div className="mx_VoiceBroadcastBody_timerow">
|
|
||||||
<Clock seconds={times.position} />
|
|
||||||
<Clock seconds={-times.timeLeft} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes}>
|
|
||||||
<VoiceBroadcastHeader
|
|
||||||
linkToRoom={pip}
|
|
||||||
live={liveness}
|
|
||||||
microphoneLabel={sender?.name}
|
|
||||||
room={room}
|
|
||||||
showBroadcast={playbackState !== VoiceBroadcastPlaybackState.Buffering}
|
|
||||||
showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering}
|
|
||||||
/>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { VoiceBroadcastHeader } from "../..";
|
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
|
||||||
import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording";
|
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection";
|
|
||||||
import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
voiceBroadcastPreRecording: VoiceBroadcastPreRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
showDeviceSelect: boolean;
|
|
||||||
disableStartButton: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({ voiceBroadcastPreRecording }) => {
|
|
||||||
const pipRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection();
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
showDeviceSelect: false,
|
|
||||||
disableStartButton: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDeviceSelect = (device: MediaDeviceInfo): void => {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
showDeviceSelect: false,
|
|
||||||
}));
|
|
||||||
setDevice(device);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStartBroadcastClick = (): void => {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
disableStartButton: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
voiceBroadcastPreRecording.start();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" ref={pipRef}>
|
|
||||||
<VoiceBroadcastHeader
|
|
||||||
linkToRoom={true}
|
|
||||||
onCloseClick={voiceBroadcastPreRecording.cancel}
|
|
||||||
onMicrophoneLineClick={(): void => setState({ ...state, showDeviceSelect: true })}
|
|
||||||
room={voiceBroadcastPreRecording.room}
|
|
||||||
microphoneLabel={currentDeviceLabel}
|
|
||||||
showClose={true}
|
|
||||||
/>
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_VoiceBroadcastBody_blockButton"
|
|
||||||
kind="danger"
|
|
||||||
onClick={onStartBroadcastClick}
|
|
||||||
disabled={state.disableStartButton}
|
|
||||||
>
|
|
||||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("voice_broadcast|go_live")}
|
|
||||||
</AccessibleButton>
|
|
||||||
{state.showDeviceSelect && (
|
|
||||||
<DevicesContextMenu
|
|
||||||
containerRef={pipRef}
|
|
||||||
currentDevice={currentDevice}
|
|
||||||
devices={devices}
|
|
||||||
onDeviceSelect={onDeviceSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
useVoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastHeader,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingConnectionError,
|
|
||||||
} from "../..";
|
|
||||||
|
|
||||||
interface VoiceBroadcastRecordingBodyProps {
|
|
||||||
recording: VoiceBroadcastRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastRecordingBody: React.FC<VoiceBroadcastRecordingBodyProps> = ({ recording }) => {
|
|
||||||
const { live, room, sender, recordingState } = useVoiceBroadcastRecording(recording);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastBody">
|
|
||||||
<VoiceBroadcastHeader live={live ? "live" : "grey"} microphoneLabel={sender?.name} room={room} />
|
|
||||||
{recordingState === "connection_error" && <VoiceBroadcastRecordingConnectionError />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,116 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useRef, useState } from "react";
|
|
||||||
import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
|
|
||||||
import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastControl,
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingConnectionError,
|
|
||||||
VoiceBroadcastRecordingState,
|
|
||||||
} from "../..";
|
|
||||||
import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording";
|
|
||||||
import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader";
|
|
||||||
import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg";
|
|
||||||
import { Icon as RecordIcon } from "../../../../res/img/compound/record-10px.svg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection";
|
|
||||||
import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu";
|
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
|
||||||
|
|
||||||
interface VoiceBroadcastRecordingPipProps {
|
|
||||||
recording: VoiceBroadcastRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
|
|
||||||
const pipRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const { live, timeLeft, recordingState, room, stopRecording, toggleRecording } =
|
|
||||||
useVoiceBroadcastRecording(recording);
|
|
||||||
const { currentDevice, devices, setDevice } = useAudioDeviceSelection();
|
|
||||||
|
|
||||||
const onDeviceSelect = async (device: MediaDeviceInfo): Promise<void> => {
|
|
||||||
setShowDeviceSelect(false);
|
|
||||||
|
|
||||||
if (currentDevice?.deviceId === device.deviceId) {
|
|
||||||
// device unchanged
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDevice(device);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
[VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped] as VoiceBroadcastRecordingState[]
|
|
||||||
).includes(recordingState)
|
|
||||||
) {
|
|
||||||
// Nothing to do in these cases. Resume will use the selected device.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pause and resume to switch the input device
|
|
||||||
await recording.pause();
|
|
||||||
await recording.resume();
|
|
||||||
};
|
|
||||||
|
|
||||||
const [showDeviceSelect, setShowDeviceSelect] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const toggleControl =
|
|
||||||
recordingState === VoiceBroadcastInfoState.Paused ? (
|
|
||||||
<VoiceBroadcastControl
|
|
||||||
className="mx_VoiceBroadcastControl-recording"
|
|
||||||
onClick={toggleRecording}
|
|
||||||
icon={<RecordIcon className="mx_Icon mx_Icon_12" />}
|
|
||||||
label={_t("voice_broadcast|resume")}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<VoiceBroadcastControl
|
|
||||||
onClick={toggleRecording}
|
|
||||||
icon={<PauseIcon className="mx_Icon mx_Icon_12" />}
|
|
||||||
label={_t("voice_broadcast|pause")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const controls =
|
|
||||||
recordingState === "connection_error" ? (
|
|
||||||
<VoiceBroadcastRecordingConnectionError />
|
|
||||||
) : (
|
|
||||||
<div className="mx_VoiceBroadcastBody_controls">
|
|
||||||
{toggleControl}
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={(): void => setShowDeviceSelect(true)}
|
|
||||||
title={_t("voip|change_input_device")}
|
|
||||||
>
|
|
||||||
<MicrophoneIcon className="mx_Icon mx_Icon_16 mx_Icon_alert" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<VoiceBroadcastControl
|
|
||||||
icon={<StopIcon className="mx_Icon mx_Icon_16" />}
|
|
||||||
label="Stop Recording"
|
|
||||||
onClick={stopRecording}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" ref={pipRef}>
|
|
||||||
<VoiceBroadcastHeader linkToRoom={true} live={live ? "live" : "grey"} room={room} timeLeft={timeLeft} />
|
|
||||||
<hr className="mx_VoiceBroadcastBody_divider" />
|
|
||||||
{controls}
|
|
||||||
{showDeviceSelect && (
|
|
||||||
<DevicesContextMenu
|
|
||||||
containerRef={pipRef}
|
|
||||||
currentDevice={currentDevice}
|
|
||||||
devices={devices}
|
|
||||||
onDeviceSelect={onDeviceSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastHeader,
|
|
||||||
VoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastPlaybackControl,
|
|
||||||
VoiceBroadcastPlaybackState,
|
|
||||||
} from "../..";
|
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
|
||||||
import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback";
|
|
||||||
|
|
||||||
interface VoiceBroadcastSmallPlaybackBodyProps {
|
|
||||||
playback: VoiceBroadcastPlayback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceBroadcastSmallPlaybackBody: React.FC<VoiceBroadcastSmallPlaybackBodyProps> = ({ playback }) => {
|
|
||||||
const { liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback);
|
|
||||||
return (
|
|
||||||
<div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small">
|
|
||||||
<VoiceBroadcastHeader
|
|
||||||
linkToRoom={true}
|
|
||||||
live={liveness}
|
|
||||||
liveBadgePosition="middle"
|
|
||||||
microphoneLabel={sender?.name}
|
|
||||||
room={room}
|
|
||||||
showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering}
|
|
||||||
bufferingPosition="title"
|
|
||||||
/>
|
|
||||||
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
|
|
||||||
<AccessibleButton onClick={() => playback.stop()}>
|
|
||||||
<CloseIcon className="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" />
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
|
||||||
import { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback";
|
|
||||||
import {
|
|
||||||
VoiceBroadcastPlaybacksStore,
|
|
||||||
VoiceBroadcastPlaybacksStoreEvent,
|
|
||||||
} from "../stores/VoiceBroadcastPlaybacksStore";
|
|
||||||
|
|
||||||
export const useCurrentVoiceBroadcastPlayback = (
|
|
||||||
voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
): {
|
|
||||||
currentVoiceBroadcastPlayback: VoiceBroadcastPlayback | null;
|
|
||||||
} => {
|
|
||||||
const currentVoiceBroadcastPlayback = useTypedEventEmitterState(
|
|
||||||
voiceBroadcastPlaybackStore,
|
|
||||||
VoiceBroadcastPlaybacksStoreEvent.CurrentChanged,
|
|
||||||
(playback?: VoiceBroadcastPlayback) => {
|
|
||||||
return playback ?? voiceBroadcastPlaybackStore.getCurrent();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentVoiceBroadcastPlayback,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
|
||||||
import { VoiceBroadcastPreRecordingStore } from "../stores/VoiceBroadcastPreRecordingStore";
|
|
||||||
import { VoiceBroadcastPreRecording } from "../models/VoiceBroadcastPreRecording";
|
|
||||||
|
|
||||||
export const useCurrentVoiceBroadcastPreRecording = (
|
|
||||||
voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore,
|
|
||||||
): {
|
|
||||||
currentVoiceBroadcastPreRecording: VoiceBroadcastPreRecording | null;
|
|
||||||
} => {
|
|
||||||
const currentVoiceBroadcastPreRecording = useTypedEventEmitterState(
|
|
||||||
voiceBroadcastPreRecordingStore,
|
|
||||||
"changed",
|
|
||||||
(preRecording?: VoiceBroadcastPreRecording) => {
|
|
||||||
return preRecording ?? voiceBroadcastPreRecordingStore.getCurrent();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentVoiceBroadcastPreRecording,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from "..";
|
|
||||||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
|
||||||
|
|
||||||
export const useCurrentVoiceBroadcastRecording = (
|
|
||||||
voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
): {
|
|
||||||
currentVoiceBroadcastRecording: VoiceBroadcastRecording | null;
|
|
||||||
} => {
|
|
||||||
const currentVoiceBroadcastRecording = useTypedEventEmitterState(
|
|
||||||
voiceBroadcastRecordingsStore,
|
|
||||||
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
|
|
||||||
(recording?: VoiceBroadcastRecording) => {
|
|
||||||
return recording ?? voiceBroadcastRecordingsStore.getCurrent();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentVoiceBroadcastRecording,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast";
|
|
||||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
|
||||||
import { SDKContext } from "../../contexts/SDKContext";
|
|
||||||
|
|
||||||
export const useHasRoomLiveVoiceBroadcast = (room: Room): boolean => {
|
|
||||||
const sdkContext = useContext(SDKContext);
|
|
||||||
const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(false);
|
|
||||||
|
|
||||||
const update = useMemo(() => {
|
|
||||||
return sdkContext?.client
|
|
||||||
? () => {
|
|
||||||
hasRoomLiveVoiceBroadcast(sdkContext.client!, room).then(
|
|
||||||
({ hasBroadcast }) => {
|
|
||||||
setHasLiveVoiceBroadcast(hasBroadcast);
|
|
||||||
},
|
|
||||||
() => {}, // no update on error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: () => {}; // noop without client
|
|
||||||
}, [room, sdkContext, setHasLiveVoiceBroadcast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
update();
|
|
||||||
}, [update]);
|
|
||||||
|
|
||||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => update());
|
|
||||||
return hasLiveVoiceBroadcast;
|
|
||||||
};
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
||||||
import {
|
|
||||||
VoiceBroadcastLiveness,
|
|
||||||
VoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastPlaybackEvent,
|
|
||||||
VoiceBroadcastPlaybackState,
|
|
||||||
VoiceBroadcastPlaybackTimes,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
export const useVoiceBroadcastPlayback = (
|
|
||||||
playback: VoiceBroadcastPlayback,
|
|
||||||
): {
|
|
||||||
times: {
|
|
||||||
duration: number;
|
|
||||||
position: number;
|
|
||||||
timeLeft: number;
|
|
||||||
};
|
|
||||||
sender: RoomMember | null;
|
|
||||||
liveness: VoiceBroadcastLiveness;
|
|
||||||
playbackState: VoiceBroadcastPlaybackState;
|
|
||||||
toggle(): void;
|
|
||||||
room: Room;
|
|
||||||
} => {
|
|
||||||
const client = MatrixClientPeg.safeGet();
|
|
||||||
const room = client.getRoom(playback.infoEvent.getRoomId());
|
|
||||||
|
|
||||||
if (!room) {
|
|
||||||
throw new Error(`Voice Broadcast room not found (event ${playback.infoEvent.getId()})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = playback.infoEvent.sender;
|
|
||||||
|
|
||||||
if (!sender) {
|
|
||||||
throw new Error(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const playbackToggle = (): void => {
|
|
||||||
playback.toggle();
|
|
||||||
};
|
|
||||||
|
|
||||||
const playbackState = useTypedEventEmitterState(
|
|
||||||
playback,
|
|
||||||
VoiceBroadcastPlaybackEvent.StateChanged,
|
|
||||||
(state?: VoiceBroadcastPlaybackState) => {
|
|
||||||
return state ?? playback.getState();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const times = useTypedEventEmitterState(
|
|
||||||
playback,
|
|
||||||
VoiceBroadcastPlaybackEvent.TimesChanged,
|
|
||||||
(t?: VoiceBroadcastPlaybackTimes) => {
|
|
||||||
return (
|
|
||||||
t ?? {
|
|
||||||
duration: playback.durationSeconds,
|
|
||||||
position: playback.timeSeconds,
|
|
||||||
timeLeft: playback.timeLeftSeconds,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const liveness = useTypedEventEmitterState(
|
|
||||||
playback,
|
|
||||||
VoiceBroadcastPlaybackEvent.LivenessChanged,
|
|
||||||
(l?: VoiceBroadcastLiveness) => {
|
|
||||||
return l ?? playback.getLiveness();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
times,
|
|
||||||
liveness: liveness,
|
|
||||||
playbackState,
|
|
||||||
room: room,
|
|
||||||
sender,
|
|
||||||
toggle: playbackToggle,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingEvent,
|
|
||||||
VoiceBroadcastRecordingState,
|
|
||||||
} from "..";
|
|
||||||
import QuestionDialog from "../../components/views/dialogs/QuestionDialog";
|
|
||||||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
||||||
import Modal from "../../Modal";
|
|
||||||
|
|
||||||
const showStopBroadcastingDialog = async (): Promise<boolean> => {
|
|
||||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
|
||||||
title: _t("voice_broadcast|confirm_stop_title"),
|
|
||||||
description: <p>{_t("voice_broadcast|confirm_stop_description")}</p>,
|
|
||||||
button: _t("voice_broadcast|confirm_stop_affirm"),
|
|
||||||
});
|
|
||||||
const [confirmed] = await finished;
|
|
||||||
return !!confirmed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useVoiceBroadcastRecording = (
|
|
||||||
recording: VoiceBroadcastRecording,
|
|
||||||
): {
|
|
||||||
live: boolean;
|
|
||||||
timeLeft: number;
|
|
||||||
recordingState: VoiceBroadcastRecordingState;
|
|
||||||
room: Room;
|
|
||||||
sender: RoomMember | null;
|
|
||||||
stopRecording(): void;
|
|
||||||
toggleRecording(): void;
|
|
||||||
} => {
|
|
||||||
const client = MatrixClientPeg.safeGet();
|
|
||||||
const roomId = recording.infoEvent.getRoomId();
|
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("Unable to find voice broadcast room with Id: " + roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = recording.infoEvent.sender;
|
|
||||||
|
|
||||||
if (!sender) {
|
|
||||||
throw new Error(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopRecording = async (): Promise<void> => {
|
|
||||||
const confirmed = await showStopBroadcastingDialog();
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
await recording.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const recordingState = useTypedEventEmitterState(
|
|
||||||
recording,
|
|
||||||
VoiceBroadcastRecordingEvent.StateChanged,
|
|
||||||
(state?: VoiceBroadcastRecordingState) => {
|
|
||||||
return state ?? recording.getState();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeLeft = useTypedEventEmitterState(
|
|
||||||
recording,
|
|
||||||
VoiceBroadcastRecordingEvent.TimeLeftChanged,
|
|
||||||
(t?: number) => {
|
|
||||||
return t ?? recording.getTimeLeft();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const live = (
|
|
||||||
[VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[]
|
|
||||||
).includes(recordingState);
|
|
||||||
|
|
||||||
return {
|
|
||||||
live,
|
|
||||||
timeLeft,
|
|
||||||
recordingState,
|
|
||||||
room,
|
|
||||||
sender,
|
|
||||||
stopRecording,
|
|
||||||
toggleRecording: recording.toggle,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Voice Broadcast module
|
|
||||||
* {@link https://github.com/vector-im/element-meta/discussions/632}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./types";
|
|
||||||
export * from "./models/VoiceBroadcastPlayback";
|
|
||||||
export * from "./models/VoiceBroadcastPreRecording";
|
|
||||||
export * from "./models/VoiceBroadcastRecording";
|
|
||||||
export * from "./audio/VoiceBroadcastRecorder";
|
|
||||||
export * from "./components/VoiceBroadcastBody";
|
|
||||||
export * from "./components/atoms/LiveBadge";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastControl";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastError";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastHeader";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastPlaybackControl";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastRecordingConnectionError";
|
|
||||||
export * from "./components/atoms/VoiceBroadcastRoomSubtitle";
|
|
||||||
export * from "./components/molecules/ConfirmListenBroadcastStopCurrent";
|
|
||||||
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
|
||||||
export * from "./components/molecules/VoiceBroadcastSmallPlaybackBody";
|
|
||||||
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
|
|
||||||
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
|
||||||
export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
|
||||||
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
|
|
||||||
export * from "./hooks/useCurrentVoiceBroadcastRecording";
|
|
||||||
export * from "./hooks/useHasRoomLiveVoiceBroadcast";
|
|
||||||
export * from "./hooks/useVoiceBroadcastRecording";
|
|
||||||
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
|
||||||
export * from "./stores/VoiceBroadcastPreRecordingStore";
|
|
||||||
export * from "./stores/VoiceBroadcastRecordingsStore";
|
|
||||||
export * from "./utils/checkVoiceBroadcastPreConditions";
|
|
||||||
export * from "./utils/cleanUpBroadcasts";
|
|
||||||
export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped";
|
|
||||||
export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback";
|
|
||||||
export * from "./utils/getChunkLength";
|
|
||||||
export * from "./utils/getMaxBroadcastLength";
|
|
||||||
export * from "./utils/hasRoomLiveVoiceBroadcast";
|
|
||||||
export * from "./utils/isRelatedToVoiceBroadcast";
|
|
||||||
export * from "./utils/isVoiceBroadcastStartedEvent";
|
|
||||||
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
|
|
||||||
export * from "./utils/retrieveStartedInfoEvent";
|
|
||||||
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
|
|
||||||
export * from "./utils/shouldDisplayAsVoiceBroadcastTile";
|
|
||||||
export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText";
|
|
||||||
export * from "./utils/startNewVoiceBroadcastRecording";
|
|
||||||
export * from "./utils/textForVoiceBroadcastStoppedEvent";
|
|
||||||
export * from "./utils/textForVoiceBroadcastStoppedEventWithoutLink";
|
|
||||||
export * from "./utils/VoiceBroadcastResumer";
|
|
|
@ -1,651 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
EventType,
|
|
||||||
MatrixClient,
|
|
||||||
MatrixEvent,
|
|
||||||
MatrixEventEvent,
|
|
||||||
MsgType,
|
|
||||||
RelationType,
|
|
||||||
TypedEventEmitter,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { SimpleObservable } from "matrix-widget-api";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
|
||||||
|
|
||||||
import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback";
|
|
||||||
import { PlaybackManager } from "../../audio/PlaybackManager";
|
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
|
||||||
import { MediaEventHelper } from "../../utils/MediaEventHelper";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import {
|
|
||||||
VoiceBroadcastLiveness,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastInfoEventContent,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
showConfirmListenBroadcastStopCurrentDialog,
|
|
||||||
} from "..";
|
|
||||||
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
|
||||||
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
|
|
||||||
import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybackState {
|
|
||||||
Paused = "pause",
|
|
||||||
Playing = "playing",
|
|
||||||
Stopped = "stopped",
|
|
||||||
Buffering = "buffering",
|
|
||||||
Error = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybackEvent {
|
|
||||||
TimesChanged = "times_changed",
|
|
||||||
LivenessChanged = "liveness_changed",
|
|
||||||
StateChanged = "state_changed",
|
|
||||||
InfoStateChanged = "info_state_changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VoiceBroadcastPlaybackTimes = {
|
|
||||||
duration: number;
|
|
||||||
position: number;
|
|
||||||
timeLeft: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
[VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void;
|
|
||||||
[VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void;
|
|
||||||
[VoiceBroadcastPlaybackEvent.StateChanged]: (
|
|
||||||
state: VoiceBroadcastPlaybackState,
|
|
||||||
playback: VoiceBroadcastPlayback,
|
|
||||||
) => void;
|
|
||||||
[VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceBroadcastPlayback
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastPlaybackEvent, EventMap>
|
|
||||||
implements IDestroyable, PlaybackInterface
|
|
||||||
{
|
|
||||||
private state = VoiceBroadcastPlaybackState.Stopped;
|
|
||||||
private chunkEvents = new VoiceBroadcastChunkEvents();
|
|
||||||
/** @var Map: event Id → undecryptable event */
|
|
||||||
private utdChunkEvents: Map<string, MatrixEvent> = new Map();
|
|
||||||
private playbacks = new Map<string, Playback>();
|
|
||||||
private currentlyPlaying: MatrixEvent | null = null;
|
|
||||||
/** @var total duration of all chunks in milliseconds */
|
|
||||||
private duration = 0;
|
|
||||||
/** @var current playback position in milliseconds */
|
|
||||||
private position = 0;
|
|
||||||
public readonly liveData = new SimpleObservable<number[]>();
|
|
||||||
private liveness: VoiceBroadcastLiveness = "not-live";
|
|
||||||
|
|
||||||
// set via addInfoEvent() in constructor
|
|
||||||
private infoState!: VoiceBroadcastInfoState;
|
|
||||||
private lastInfoEvent!: MatrixEvent;
|
|
||||||
|
|
||||||
// set via setUpRelationsHelper() in constructor
|
|
||||||
private chunkRelationHelper!: RelationsHelper;
|
|
||||||
private infoRelationHelper!: RelationsHelper;
|
|
||||||
|
|
||||||
private skipToNext?: number;
|
|
||||||
private skipToDeferred?: IDeferred<void>;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public readonly infoEvent: MatrixEvent,
|
|
||||||
private client: MatrixClient,
|
|
||||||
private recordings: VoiceBroadcastRecordingsStore,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.addInfoEvent(this.infoEvent);
|
|
||||||
this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
|
||||||
this.setUpRelationsHelper();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setUpRelationsHelper(): Promise<void> {
|
|
||||||
this.infoRelationHelper = new RelationsHelper(
|
|
||||||
this.infoEvent,
|
|
||||||
RelationType.Reference,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
this.client,
|
|
||||||
);
|
|
||||||
this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent);
|
|
||||||
|
|
||||||
if (this.infoState !== VoiceBroadcastInfoState.Stopped) {
|
|
||||||
// Only required if not stopped. Stopped is the final state.
|
|
||||||
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.infoRelationHelper.emitFetchCurrent();
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn("error fetching server side relation for voice broadcast info", err);
|
|
||||||
// fall back to local events
|
|
||||||
this.infoRelationHelper.emitCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chunkRelationHelper = new RelationsHelper(
|
|
||||||
this.infoEvent,
|
|
||||||
RelationType.Reference,
|
|
||||||
EventType.RoomMessage,
|
|
||||||
this.client,
|
|
||||||
);
|
|
||||||
this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO Michael W: only fetch events if needed, blocked by PSF-1708
|
|
||||||
await this.chunkRelationHelper.emitFetchCurrent();
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn("error fetching server side relation for voice broadcast chunks", err);
|
|
||||||
// fall back to local events
|
|
||||||
this.chunkRelationHelper.emitCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
|
|
||||||
if (!event.getId() && !event.getTxnId()) {
|
|
||||||
// skip events without id and txn id
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isDecryptionFailure()) {
|
|
||||||
this.onChunkEventDecryptionFailure(event);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getContent()?.msgtype !== MsgType.Audio) {
|
|
||||||
// skip non-audio event
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chunkEvents.addEvent(event);
|
|
||||||
this.setDuration(this.chunkEvents.getLength());
|
|
||||||
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Buffering) {
|
|
||||||
await this.startOrPlayNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChunkEventDecryptionFailure = (event: MatrixEvent): void => {
|
|
||||||
const eventId = event.getId();
|
|
||||||
|
|
||||||
if (!eventId) {
|
|
||||||
// This should not happen, as the existence of the Id is checked before the call.
|
|
||||||
// Log anyway and return.
|
|
||||||
logger.warn("Broadcast chunk decryption failure for event without Id", {
|
|
||||||
broadcast: this.infoEvent.getId(),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.utdChunkEvents.has(eventId)) {
|
|
||||||
event.once(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.utdChunkEvents.set(eventId, event);
|
|
||||||
this.setError();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChunkEventDecrypted = async (event: MatrixEvent): Promise<void> => {
|
|
||||||
const eventId = event.getId();
|
|
||||||
|
|
||||||
if (!eventId) {
|
|
||||||
// This should not happen, as the existence of the Id is checked before the call.
|
|
||||||
// Log anyway and return.
|
|
||||||
logger.warn("Broadcast chunk decrypted for event without Id", { broadcast: this.infoEvent.getId() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.utdChunkEvents.delete(eventId);
|
|
||||||
await this.addChunkEvent(event);
|
|
||||||
|
|
||||||
if (this.utdChunkEvents.size === 0) {
|
|
||||||
// no more UTD events, recover from error to paused
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Paused);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private startOrPlayNext = async (): Promise<void> => {
|
|
||||||
if (this.currentlyPlaying) {
|
|
||||||
return this.playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.start();
|
|
||||||
};
|
|
||||||
|
|
||||||
private addInfoEvent = (event: MatrixEvent): void => {
|
|
||||||
if (this.lastInfoEvent && this.lastInfoEvent.getTs() >= event.getTs()) {
|
|
||||||
// Only handle newer events
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = event.getContent()?.state;
|
|
||||||
|
|
||||||
if (!Object.values(VoiceBroadcastInfoState).includes(state)) {
|
|
||||||
// Do not handle unknown voice broadcast states
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastInfoEvent = event;
|
|
||||||
this.setInfoState(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onBeforeRedaction = (): void => {
|
|
||||||
if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) {
|
|
||||||
this.stop();
|
|
||||||
// destroy cleans up everything
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise<void> {
|
|
||||||
try {
|
|
||||||
return await this.loadPlayback(chunkEvent);
|
|
||||||
} catch (err: any) {
|
|
||||||
logger.warn("Unable to load broadcast playback", {
|
|
||||||
message: err.message,
|
|
||||||
broadcastId: this.infoEvent.getId(),
|
|
||||||
chunkId: chunkEvent.getId(),
|
|
||||||
});
|
|
||||||
this.setError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadPlayback(chunkEvent: MatrixEvent): Promise<void> {
|
|
||||||
const eventId = chunkEvent.getId();
|
|
||||||
|
|
||||||
if (!eventId) {
|
|
||||||
throw new Error("Broadcast chunk event without Id occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
const helper = new MediaEventHelper(chunkEvent);
|
|
||||||
const blob = await helper.sourceBlob.value;
|
|
||||||
const buffer = await blob.arrayBuffer();
|
|
||||||
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
|
|
||||||
await playback.prepare();
|
|
||||||
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
|
|
||||||
this.playbacks.set(eventId, playback);
|
|
||||||
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(chunkEvent, state));
|
|
||||||
playback.clockInfo.liveData.onUpdate(([position]) => {
|
|
||||||
this.onPlaybackPositionUpdate(chunkEvent, position);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private unloadPlayback(event: MatrixEvent): void {
|
|
||||||
const playback = this.playbacks.get(event.getId()!);
|
|
||||||
if (!playback) return;
|
|
||||||
|
|
||||||
playback.destroy();
|
|
||||||
this.playbacks.delete(event.getId()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPlaybackPositionUpdate = (event: MatrixEvent, position: number): void => {
|
|
||||||
if (event !== this.currentlyPlaying) return;
|
|
||||||
|
|
||||||
const newPosition = this.chunkEvents.getLengthTo(event) + position * 1000; // observable sends seconds
|
|
||||||
|
|
||||||
// do not jump backwards - this can happen when transiting from one to another chunk
|
|
||||||
if (newPosition < this.position) return;
|
|
||||||
|
|
||||||
this.setPosition(newPosition);
|
|
||||||
};
|
|
||||||
|
|
||||||
private setDuration(duration: number): void {
|
|
||||||
if (this.duration === duration) return;
|
|
||||||
|
|
||||||
this.duration = duration;
|
|
||||||
this.emitTimesChanged();
|
|
||||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setPosition(position: number): void {
|
|
||||||
if (this.position === position) return;
|
|
||||||
|
|
||||||
this.position = position;
|
|
||||||
this.emitTimesChanged();
|
|
||||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitTimesChanged(): void {
|
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
|
||||||
duration: this.durationSeconds,
|
|
||||||
position: this.timeSeconds,
|
|
||||||
timeLeft: this.timeLeftSeconds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise<void> => {
|
|
||||||
if (event !== this.currentlyPlaying) return;
|
|
||||||
if (newState !== PlaybackState.Stopped) return;
|
|
||||||
|
|
||||||
await this.playNext();
|
|
||||||
this.unloadPlayback(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
private async playNext(): Promise<void> {
|
|
||||||
if (!this.currentlyPlaying) return;
|
|
||||||
|
|
||||||
const next = this.chunkEvents.getNext(this.currentlyPlaying);
|
|
||||||
|
|
||||||
if (next) {
|
|
||||||
return this.playEvent(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.getInfoState() === VoiceBroadcastInfoState.Stopped &&
|
|
||||||
this.chunkEvents.getSequenceForEvent(this.currentlyPlaying) === this.lastChunkSequence
|
|
||||||
) {
|
|
||||||
this.stop();
|
|
||||||
} else {
|
|
||||||
// No more chunks available, although the broadcast is not finished → enter buffering state.
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Buffering);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {number} The last chunk sequence from the latest info event.
|
|
||||||
* Falls back to the length of received chunks if the info event does not provide the number.
|
|
||||||
*/
|
|
||||||
private get lastChunkSequence(): number {
|
|
||||||
return (
|
|
||||||
this.lastInfoEvent.getContent<VoiceBroadcastInfoEventContent>()?.last_chunk_sequence ||
|
|
||||||
this.chunkEvents.getNumberOfEvents()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async playEvent(event: MatrixEvent): Promise<void> {
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
this.currentlyPlaying = event;
|
|
||||||
const playback = await this.tryGetOrLoadPlaybackForEvent(event);
|
|
||||||
playback?.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
|
|
||||||
try {
|
|
||||||
return await this.getOrLoadPlaybackForEvent(event);
|
|
||||||
} catch (err: any) {
|
|
||||||
logger.warn("Unable to load broadcast playback", {
|
|
||||||
message: err.message,
|
|
||||||
broadcastId: this.infoEvent.getId(),
|
|
||||||
chunkId: event.getId(),
|
|
||||||
});
|
|
||||||
this.setError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
|
|
||||||
const eventId = event.getId();
|
|
||||||
|
|
||||||
if (!eventId) {
|
|
||||||
throw new Error("Broadcast chunk event without Id occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.playbacks.has(eventId)) {
|
|
||||||
// set to buffering while loading the chunk data
|
|
||||||
const currentState = this.getState();
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Buffering);
|
|
||||||
await this.loadPlayback(event);
|
|
||||||
this.setState(currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
const playback = this.playbacks.get(eventId);
|
|
||||||
|
|
||||||
if (!playback) {
|
|
||||||
throw new Error(`Unable to find playback for event ${event.getId()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to load the playback for the next event for a smooth(er) playback
|
|
||||||
const nextEvent = this.chunkEvents.getNext(event);
|
|
||||||
if (nextEvent) this.tryLoadPlayback(nextEvent);
|
|
||||||
|
|
||||||
return playback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCurrentPlayback(): Playback | undefined {
|
|
||||||
if (!this.currentlyPlaying) return;
|
|
||||||
return this.playbacks.get(this.currentlyPlaying.getId()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLiveness(): VoiceBroadcastLiveness {
|
|
||||||
return this.liveness;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setLiveness(liveness: VoiceBroadcastLiveness): void {
|
|
||||||
if (this.liveness === liveness) return;
|
|
||||||
|
|
||||||
this.liveness = liveness;
|
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.LivenessChanged, liveness);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get currentState(): PlaybackState {
|
|
||||||
return PlaybackState.Playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get timeSeconds(): number {
|
|
||||||
return this.position / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get durationSeconds(): number {
|
|
||||||
return this.duration / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get timeLeftSeconds(): number {
|
|
||||||
// Sometimes the meta data and the audio files are a little bit out of sync.
|
|
||||||
// Be sure it never returns a negative value.
|
|
||||||
return Math.max(0, Math.round(this.durationSeconds) - this.timeSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async skipTo(timeSeconds: number): Promise<void> {
|
|
||||||
this.skipToNext = timeSeconds;
|
|
||||||
|
|
||||||
if (this.skipToDeferred) {
|
|
||||||
// Skip to position is already in progress. Return the promise for that.
|
|
||||||
return this.skipToDeferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skipToDeferred = defer();
|
|
||||||
|
|
||||||
while (this.skipToNext !== undefined) {
|
|
||||||
// Skip to position until skipToNext is undefined.
|
|
||||||
// skipToNext can be set if skipTo is called while already skipping.
|
|
||||||
const skipToNext = this.skipToNext;
|
|
||||||
this.skipToNext = undefined;
|
|
||||||
await this.doSkipTo(skipToNext);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skipToDeferred.resolve();
|
|
||||||
this.skipToDeferred = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async doSkipTo(timeSeconds: number): Promise<void> {
|
|
||||||
const time = timeSeconds * 1000;
|
|
||||||
const event = this.chunkEvents.findByTime(time);
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
logger.warn("voice broadcast chunk event to skip to not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlayback = this.getCurrentPlayback();
|
|
||||||
const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event);
|
|
||||||
const currentPlaybackEvent = this.currentlyPlaying;
|
|
||||||
|
|
||||||
if (!skipToPlayback) {
|
|
||||||
logger.warn("voice broadcast chunk to skip to not found", event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentlyPlaying = event;
|
|
||||||
|
|
||||||
if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) {
|
|
||||||
// only stop and unload the playback here without triggering other effects, e.g. play next
|
|
||||||
currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange);
|
|
||||||
await currentPlayback.stop();
|
|
||||||
currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange);
|
|
||||||
this.unloadPlayback(currentPlaybackEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetInChunk = time - this.chunkEvents.getLengthTo(event);
|
|
||||||
await skipToPlayback.skipTo(offsetInChunk / 1000);
|
|
||||||
|
|
||||||
if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) {
|
|
||||||
await skipToPlayback.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setPosition(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
|
||||||
if (this.state === VoiceBroadcastPlaybackState.Playing) return;
|
|
||||||
|
|
||||||
const currentRecording = this.recordings.getCurrent();
|
|
||||||
|
|
||||||
if (currentRecording && currentRecording.getState() !== VoiceBroadcastInfoState.Stopped) {
|
|
||||||
const shouldStopRecording = await showConfirmListenBroadcastStopCurrentDialog();
|
|
||||||
|
|
||||||
if (!shouldStopRecording) {
|
|
||||||
// keep recording
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.recordings.getCurrent()?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunkEvents = this.chunkEvents.getEvents();
|
|
||||||
|
|
||||||
const toPlay =
|
|
||||||
this.getInfoState() === VoiceBroadcastInfoState.Stopped
|
|
||||||
? chunkEvents[0] // start at the beginning for an ended voice broadcast
|
|
||||||
: chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast
|
|
||||||
|
|
||||||
if (toPlay) {
|
|
||||||
return this.playEvent(toPlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Buffering);
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop(): void {
|
|
||||||
// error is a final state
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Stopped);
|
|
||||||
this.getCurrentPlayback()?.stop();
|
|
||||||
this.currentlyPlaying = null;
|
|
||||||
this.setPosition(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public pause(): void {
|
|
||||||
// error is a final state
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
|
|
||||||
|
|
||||||
// stopped voice broadcasts cannot be paused
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return;
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Paused);
|
|
||||||
this.getCurrentPlayback()?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public resume(): void {
|
|
||||||
// error is a final state
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
|
|
||||||
|
|
||||||
if (!this.currentlyPlaying) {
|
|
||||||
// no playback to resume, start from the beginning
|
|
||||||
this.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
this.getCurrentPlayback()?.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the playback:
|
|
||||||
* stopped → playing
|
|
||||||
* playing → paused
|
|
||||||
* paused → playing
|
|
||||||
*/
|
|
||||||
public async toggle(): Promise<void> {
|
|
||||||
// error is a final state
|
|
||||||
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
|
|
||||||
|
|
||||||
if (this.state === VoiceBroadcastPlaybackState.Stopped) {
|
|
||||||
await this.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state === VoiceBroadcastPlaybackState.Paused) {
|
|
||||||
this.resume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getState(): VoiceBroadcastPlaybackState {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setState(state: VoiceBroadcastPlaybackState): void {
|
|
||||||
if (this.state === state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = state;
|
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set error state. Stop current playback, if any.
|
|
||||||
*/
|
|
||||||
private setError(): void {
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Error);
|
|
||||||
this.getCurrentPlayback()?.stop();
|
|
||||||
this.currentlyPlaying = null;
|
|
||||||
this.setPosition(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInfoState(): VoiceBroadcastInfoState {
|
|
||||||
return this.infoState;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setInfoState(state: VoiceBroadcastInfoState): void {
|
|
||||||
if (this.infoState === state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.infoState = state;
|
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state);
|
|
||||||
this.setLiveness(determineVoiceBroadcastLiveness(this.infoState));
|
|
||||||
}
|
|
||||||
|
|
||||||
public get errorMessage(): string {
|
|
||||||
if (this.getState() !== VoiceBroadcastPlaybackState.Error) return "";
|
|
||||||
if (this.utdChunkEvents.size) return _t("voice_broadcast|failed_decrypt");
|
|
||||||
return _t("voice_broadcast|failed_generic");
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
for (const [, utdEvent] of this.utdChunkEvents) {
|
|
||||||
utdEvent.off(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.utdChunkEvents.clear();
|
|
||||||
|
|
||||||
this.chunkRelationHelper.destroy();
|
|
||||||
this.infoRelationHelper.destroy();
|
|
||||||
this.removeAllListeners();
|
|
||||||
|
|
||||||
this.chunkEvents = new VoiceBroadcastChunkEvents();
|
|
||||||
this.playbacks.forEach((p) => p.destroy());
|
|
||||||
this.playbacks = new Map<string, Playback>();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, Room, RoomMember, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore";
|
|
||||||
import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore";
|
|
||||||
import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording";
|
|
||||||
|
|
||||||
type VoiceBroadcastPreRecordingEvent = "dismiss";
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
dismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceBroadcastPreRecording
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap>
|
|
||||||
implements IDestroyable
|
|
||||||
{
|
|
||||||
public constructor(
|
|
||||||
public room: Room,
|
|
||||||
public sender: RoomMember,
|
|
||||||
private client: MatrixClient,
|
|
||||||
private playbacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
private recordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public start = async (): Promise<void> => {
|
|
||||||
await startNewVoiceBroadcastRecording(this.room, this.client, this.playbacksStore, this.recordingsStore);
|
|
||||||
this.emit("dismiss", this);
|
|
||||||
};
|
|
||||||
|
|
||||||
public cancel = (): void => {
|
|
||||||
this.emit("dismiss", this);
|
|
||||||
};
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.removeAllListeners();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,441 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import {
|
|
||||||
ClientEvent,
|
|
||||||
ClientEventHandlerMap,
|
|
||||||
EventType,
|
|
||||||
MatrixClient,
|
|
||||||
MatrixEvent,
|
|
||||||
MatrixEventEvent,
|
|
||||||
MsgType,
|
|
||||||
RelationType,
|
|
||||||
TypedEventEmitter,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { AudioContent, EncryptedFile } from "matrix-js-sdk/src/types";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChunkRecordedPayload,
|
|
||||||
createVoiceBroadcastRecorder,
|
|
||||||
getMaxBroadcastLength,
|
|
||||||
VoiceBroadcastInfoEventContent,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastRecorder,
|
|
||||||
VoiceBroadcastRecorderEvent,
|
|
||||||
} from "..";
|
|
||||||
import { uploadFile } from "../../ContentMessages";
|
|
||||||
import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import dis from "../../dispatcher/dispatcher";
|
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
|
||||||
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
|
|
||||||
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
|
||||||
import { createReconnectedListener } from "../../utils/connection";
|
|
||||||
import { localNotificationsAreSilenced } from "../../utils/notifications";
|
|
||||||
import { BackgroundAudio } from "../../audio/BackgroundAudio";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastRecordingEvent {
|
|
||||||
StateChanged = "liveness_changed",
|
|
||||||
TimeLeftChanged = "time_left_changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VoiceBroadcastRecordingState = VoiceBroadcastInfoState | "connection_error";
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
[VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastRecordingState) => void;
|
|
||||||
[VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceBroadcastRecording
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastRecordingEvent, EventMap>
|
|
||||||
implements IDestroyable
|
|
||||||
{
|
|
||||||
private state: VoiceBroadcastRecordingState;
|
|
||||||
private recorder: VoiceBroadcastRecorder | null = null;
|
|
||||||
private dispatcherRef: string;
|
|
||||||
private chunkEvents = new VoiceBroadcastChunkEvents();
|
|
||||||
private chunkRelationHelper: RelationsHelper;
|
|
||||||
private maxLength: number;
|
|
||||||
private timeLeft: number;
|
|
||||||
private toRetry: Array<() => Promise<void>> = [];
|
|
||||||
private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
|
|
||||||
private roomId: string;
|
|
||||||
private infoEventId: string;
|
|
||||||
private backgroundAudio = new BackgroundAudio();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing.
|
|
||||||
* This variable holds the last sequence number.
|
|
||||||
* Starts with 0 because there is no chunk at the beginning of a broadcast.
|
|
||||||
* Will be incremented when a chunk message is created.
|
|
||||||
*/
|
|
||||||
private sequence = 0;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public readonly infoEvent: MatrixEvent,
|
|
||||||
private client: MatrixClient,
|
|
||||||
initialState?: VoiceBroadcastInfoState,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.maxLength = getMaxBroadcastLength();
|
|
||||||
this.timeLeft = this.maxLength;
|
|
||||||
this.infoEventId = this.determineEventIdFromInfoEvent();
|
|
||||||
this.roomId = this.determineRoomIdFromInfoEvent();
|
|
||||||
|
|
||||||
if (initialState) {
|
|
||||||
this.state = initialState;
|
|
||||||
} else {
|
|
||||||
this.state = this.determineInitialStateFromInfoEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Michael W: listen for state updates
|
|
||||||
|
|
||||||
this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
this.chunkRelationHelper = this.initialiseChunkEventRelation();
|
|
||||||
this.reconnectedListener = createReconnectedListener(this.onReconnect);
|
|
||||||
this.client.on(ClientEvent.Sync, this.reconnectedListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private initialiseChunkEventRelation(): RelationsHelper {
|
|
||||||
const relationsHelper = new RelationsHelper(
|
|
||||||
this.infoEvent,
|
|
||||||
RelationType.Reference,
|
|
||||||
EventType.RoomMessage,
|
|
||||||
this.client,
|
|
||||||
);
|
|
||||||
relationsHelper.on(RelationsHelperEvent.Add, this.onChunkEvent);
|
|
||||||
|
|
||||||
relationsHelper.emitFetchCurrent().catch((err) => {
|
|
||||||
logger.warn("error fetching server side relation for voice broadcast chunks", err);
|
|
||||||
// fall back to local events
|
|
||||||
relationsHelper.emitCurrent();
|
|
||||||
});
|
|
||||||
|
|
||||||
return relationsHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onChunkEvent = (event: MatrixEvent): void => {
|
|
||||||
if (
|
|
||||||
(!event.getId() && !event.getTxnId()) ||
|
|
||||||
event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chunkEvents.addEvent(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
private determineEventIdFromInfoEvent(): string {
|
|
||||||
const infoEventId = this.infoEvent.getId();
|
|
||||||
|
|
||||||
if (!infoEventId) {
|
|
||||||
throw new Error("Cannot create broadcast for info event without Id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return infoEventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private determineRoomIdFromInfoEvent(): string {
|
|
||||||
const roomId = this.infoEvent.getRoomId();
|
|
||||||
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error(`Cannot create broadcast for unknown room (info event ${this.infoEventId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return roomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the initial broadcast state.
|
|
||||||
* Checks all related events. If one has the "stopped" state → stopped, else started.
|
|
||||||
*/
|
|
||||||
private determineInitialStateFromInfoEvent(): VoiceBroadcastRecordingState {
|
|
||||||
const room = this.client.getRoom(this.roomId);
|
|
||||||
const relations = room
|
|
||||||
?.getUnfilteredTimelineSet()
|
|
||||||
?.relations?.getChildEventsForEvent(this.infoEventId, RelationType.Reference, VoiceBroadcastInfoEventType);
|
|
||||||
const relatedEvents = relations?.getRelations();
|
|
||||||
return !relatedEvents?.find((event: MatrixEvent) => {
|
|
||||||
return event.getContent()?.state === VoiceBroadcastInfoState.Stopped;
|
|
||||||
})
|
|
||||||
? VoiceBroadcastInfoState.Started
|
|
||||||
: VoiceBroadcastInfoState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTimeLeft(): number {
|
|
||||||
return this.timeLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retries failed actions on reconnect.
|
|
||||||
*/
|
|
||||||
private onReconnect = async (): Promise<void> => {
|
|
||||||
// Do nothing if not in connection_error state.
|
|
||||||
if (this.state !== "connection_error") return;
|
|
||||||
|
|
||||||
// Copy the array, so that it is possible to remove elements from it while iterating over the original.
|
|
||||||
const toRetryCopy = [...this.toRetry];
|
|
||||||
|
|
||||||
for (const retryFn of this.toRetry) {
|
|
||||||
try {
|
|
||||||
await retryFn();
|
|
||||||
// Successfully retried. Remove from array copy.
|
|
||||||
toRetryCopy.splice(toRetryCopy.indexOf(retryFn), 1);
|
|
||||||
} catch {
|
|
||||||
// The current retry callback failed. Stop the loop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toRetry = toRetryCopy;
|
|
||||||
|
|
||||||
if (this.toRetry.length === 0) {
|
|
||||||
// Everything has been successfully retried. Recover from error state to paused.
|
|
||||||
await this.pause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private async setTimeLeft(timeLeft: number): Promise<void> {
|
|
||||||
if (timeLeft <= 0) {
|
|
||||||
// time is up - stop the recording
|
|
||||||
return await this.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// do never increase time left; no action if equals
|
|
||||||
if (timeLeft >= this.timeLeft) return;
|
|
||||||
|
|
||||||
this.timeLeft = timeLeft;
|
|
||||||
this.emit(VoiceBroadcastRecordingEvent.TimeLeftChanged, timeLeft);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
|
||||||
return this.getRecorder().start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
|
||||||
if (this.state === VoiceBroadcastInfoState.Stopped) return;
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastInfoState.Stopped);
|
|
||||||
await this.stopRecorder();
|
|
||||||
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
// stopped or already paused recordings cannot be paused
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
[VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused] as VoiceBroadcastRecordingState[]
|
|
||||||
).includes(this.state)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastInfoState.Paused);
|
|
||||||
await this.stopRecorder();
|
|
||||||
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Paused);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async resume(): Promise<void> {
|
|
||||||
if (this.state !== VoiceBroadcastInfoState.Paused) return;
|
|
||||||
|
|
||||||
this.setState(VoiceBroadcastInfoState.Resumed);
|
|
||||||
await this.getRecorder().start();
|
|
||||||
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Resumed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggle = async (): Promise<void> => {
|
|
||||||
if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume();
|
|
||||||
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
[VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[]
|
|
||||||
).includes(this.getState())
|
|
||||||
) {
|
|
||||||
return this.pause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public getState(): VoiceBroadcastRecordingState {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRecorder(): VoiceBroadcastRecorder {
|
|
||||||
if (!this.recorder) {
|
|
||||||
this.recorder = createVoiceBroadcastRecorder();
|
|
||||||
this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded);
|
|
||||||
this.recorder.on(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, this.onCurrentChunkLengthUpdated);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.recorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async destroy(): Promise<void> {
|
|
||||||
if (this.recorder) {
|
|
||||||
this.recorder.stop();
|
|
||||||
this.recorder.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
|
||||||
this.removeAllListeners();
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
this.chunkEvents = new VoiceBroadcastChunkEvents();
|
|
||||||
this.chunkRelationHelper.destroy();
|
|
||||||
this.client.off(ClientEvent.Sync, this.reconnectedListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onBeforeRedaction = (): void => {
|
|
||||||
if (this.getState() !== VoiceBroadcastInfoState.Stopped) {
|
|
||||||
this.setState(VoiceBroadcastInfoState.Stopped);
|
|
||||||
// destroy cleans up everything
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload): void => {
|
|
||||||
if (payload.action !== "call_state") return;
|
|
||||||
|
|
||||||
// pause on any call action
|
|
||||||
this.pause();
|
|
||||||
};
|
|
||||||
|
|
||||||
private setState(state: VoiceBroadcastRecordingState): void {
|
|
||||||
this.state = state;
|
|
||||||
this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCurrentChunkLengthUpdated = (currentChunkLength: number): void => {
|
|
||||||
this.setTimeLeft(this.maxLength - this.chunkEvents.getLengthSeconds() - currentChunkLength);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise<void> => {
|
|
||||||
const uploadAndSendFn = async (): Promise<void> => {
|
|
||||||
const { url, file } = await this.uploadFile(chunk);
|
|
||||||
await this.sendVoiceMessage(chunk, url, file);
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.callWithRetry(uploadAndSendFn);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called on connection errors.
|
|
||||||
* It sets the connection error state and stops the recorder.
|
|
||||||
*/
|
|
||||||
private async onConnectionError(): Promise<void> {
|
|
||||||
this.playConnectionErrorAudioNotification().catch(() => {
|
|
||||||
// Error logged in playConnectionErrorAudioNotification().
|
|
||||||
});
|
|
||||||
await this.stopRecorder(false);
|
|
||||||
this.setState("connection_error");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async playConnectionErrorAudioNotification(): Promise<void> {
|
|
||||||
if (localNotificationsAreSilenced(this.client)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.backgroundAudio.pickFormatAndPlay("./media/error", ["mp3", "ogg"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadFile(chunk: ChunkRecordedPayload): ReturnType<typeof uploadFile> {
|
|
||||||
return uploadFile(
|
|
||||||
this.client,
|
|
||||||
this.roomId,
|
|
||||||
new Blob([chunk.buffer], {
|
|
||||||
type: this.getRecorder().contentType,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendVoiceMessage(chunk: ChunkRecordedPayload, url?: string, file?: EncryptedFile): Promise<void> {
|
|
||||||
/**
|
|
||||||
* Increment the last sequence number and use it for this message.
|
|
||||||
* Done outside of the sendMessageFn to get a scoped value.
|
|
||||||
* Also see {@link VoiceBroadcastRecording.sequence}.
|
|
||||||
*/
|
|
||||||
const sequence = ++this.sequence;
|
|
||||||
|
|
||||||
const sendMessageFn = async (): Promise<void> => {
|
|
||||||
const content = createVoiceMessageContent(
|
|
||||||
url,
|
|
||||||
this.getRecorder().contentType,
|
|
||||||
Math.round(chunk.length * 1000),
|
|
||||||
chunk.buffer.length,
|
|
||||||
file,
|
|
||||||
);
|
|
||||||
content["m.relates_to"] = {
|
|
||||||
rel_type: RelationType.Reference,
|
|
||||||
event_id: this.infoEventId,
|
|
||||||
};
|
|
||||||
(<AudioContent>content)["io.element.voice_broadcast_chunk"] = {
|
|
||||||
sequence,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.client.sendMessage(this.roomId, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.callWithRetry(sendMessageFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an info state event with given state.
|
|
||||||
* On error stores a resend function and setState(state) in {@link toRetry} and
|
|
||||||
* sets the broadcast state to connection_error.
|
|
||||||
*/
|
|
||||||
private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise<void> {
|
|
||||||
const sendEventFn = async (): Promise<void> => {
|
|
||||||
await this.client.sendStateEvent(
|
|
||||||
this.roomId,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
{
|
|
||||||
device_id: this.client.getDeviceId(),
|
|
||||||
state,
|
|
||||||
last_chunk_sequence: this.sequence,
|
|
||||||
["m.relates_to"]: {
|
|
||||||
rel_type: RelationType.Reference,
|
|
||||||
event_id: this.infoEventId,
|
|
||||||
},
|
|
||||||
} as VoiceBroadcastInfoEventContent,
|
|
||||||
this.client.getSafeUserId(),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.callWithRetry(sendEventFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the function.
|
|
||||||
* On failure adds it to the retry list and triggers connection error.
|
|
||||||
* {@link toRetry}
|
|
||||||
* {@link onConnectionError}
|
|
||||||
*/
|
|
||||||
private async callWithRetry(retryAbleFn: () => Promise<void>): Promise<void> {
|
|
||||||
try {
|
|
||||||
await retryAbleFn();
|
|
||||||
} catch {
|
|
||||||
this.toRetry.push(retryAbleFn);
|
|
||||||
this.onConnectionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async stopRecorder(emit = true): Promise<void> {
|
|
||||||
if (!this.recorder) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lastChunk = await this.recorder.stop();
|
|
||||||
if (lastChunk && emit) {
|
|
||||||
await this.onChunkRecorded(lastChunk);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn("error stopping voice broadcast recorder", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastPlayback,
|
|
||||||
VoiceBroadcastPlaybackEvent,
|
|
||||||
VoiceBroadcastPlaybackState,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
} from "..";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybacksStoreEvent {
|
|
||||||
CurrentChanged = "current_changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
[VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This store manages VoiceBroadcastPlaybacks:
|
|
||||||
* - access the currently playing voice broadcast
|
|
||||||
* - ensures that only once broadcast is playing at a time
|
|
||||||
*/
|
|
||||||
export class VoiceBroadcastPlaybacksStore
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastPlaybacksStoreEvent, EventMap>
|
|
||||||
implements IDestroyable
|
|
||||||
{
|
|
||||||
private current: VoiceBroadcastPlayback | null = null;
|
|
||||||
|
|
||||||
/** Playbacks indexed by their info event id. */
|
|
||||||
private playbacks = new Map<string, VoiceBroadcastPlayback>();
|
|
||||||
|
|
||||||
public constructor(private recordings: VoiceBroadcastRecordingsStore) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCurrent(current: VoiceBroadcastPlayback): void {
|
|
||||||
if (this.current === current) return;
|
|
||||||
|
|
||||||
this.current = current;
|
|
||||||
this.addPlayback(current);
|
|
||||||
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCurrent(): void {
|
|
||||||
if (this.current === null) return;
|
|
||||||
|
|
||||||
this.current = null;
|
|
||||||
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrent(): VoiceBroadcastPlayback | null {
|
|
||||||
return this.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastPlayback {
|
|
||||||
const infoEventId = infoEvent.getId()!;
|
|
||||||
|
|
||||||
if (!this.playbacks.has(infoEventId)) {
|
|
||||||
this.addPlayback(new VoiceBroadcastPlayback(infoEvent, client, this.recordings));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.playbacks.get(infoEventId)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addPlayback(playback: VoiceBroadcastPlayback): void {
|
|
||||||
const infoEventId = playback.infoEvent.getId()!;
|
|
||||||
|
|
||||||
if (this.playbacks.has(infoEventId)) return;
|
|
||||||
|
|
||||||
this.playbacks.set(infoEventId, playback);
|
|
||||||
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPlaybackStateChanged = (state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback): void => {
|
|
||||||
switch (state) {
|
|
||||||
case VoiceBroadcastPlaybackState.Buffering:
|
|
||||||
case VoiceBroadcastPlaybackState.Playing:
|
|
||||||
this.pauseExcept(playback);
|
|
||||||
this.setCurrent(playback);
|
|
||||||
break;
|
|
||||||
case VoiceBroadcastPlaybackState.Stopped:
|
|
||||||
this.clearCurrent();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private pauseExcept(playbackNotToPause: VoiceBroadcastPlayback): void {
|
|
||||||
for (const playback of this.playbacks.values()) {
|
|
||||||
if (playback !== playbackNotToPause) {
|
|
||||||
playback.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.removeAllListeners();
|
|
||||||
|
|
||||||
for (const playback of this.playbacks.values()) {
|
|
||||||
playback.off(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playbacks = new Map();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastPreRecording } from "..";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
|
|
||||||
export type VoiceBroadcastPreRecordingEvent = "changed";
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
changed: (preRecording: VoiceBroadcastPreRecording | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceBroadcastPreRecordingStore
|
|
||||||
extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap>
|
|
||||||
implements IDestroyable
|
|
||||||
{
|
|
||||||
private current: VoiceBroadcastPreRecording | null = null;
|
|
||||||
|
|
||||||
public setCurrent(current: VoiceBroadcastPreRecording): void {
|
|
||||||
if (this.current === current) return;
|
|
||||||
|
|
||||||
if (this.current) {
|
|
||||||
this.current.off("dismiss", this.onCancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = current;
|
|
||||||
current.on("dismiss", this.onCancel);
|
|
||||||
this.emit("changed", current);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCurrent(): void {
|
|
||||||
if (this.current === null) return;
|
|
||||||
|
|
||||||
this.current.off("dismiss", this.onCancel);
|
|
||||||
this.current = null;
|
|
||||||
this.emit("changed", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrent(): VoiceBroadcastPreRecording | null {
|
|
||||||
return this.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.removeAllListeners();
|
|
||||||
|
|
||||||
if (this.current) {
|
|
||||||
this.current.off("dismiss", this.onCancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCancel = (voiceBroadcastPreRecording: VoiceBroadcastPreRecording): void => {
|
|
||||||
if (this.current === voiceBroadcastPreRecording) {
|
|
||||||
this.clearCurrent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
VoiceBroadcastRecordingEvent,
|
|
||||||
VoiceBroadcastRecordingState,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastRecordingsStoreEvent {
|
|
||||||
CurrentChanged = "current_changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventMap {
|
|
||||||
[VoiceBroadcastRecordingsStoreEvent.CurrentChanged]: (recording: VoiceBroadcastRecording | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This store provides access to the current and specific Voice Broadcast recordings.
|
|
||||||
*/
|
|
||||||
export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadcastRecordingsStoreEvent, EventMap> {
|
|
||||||
private current: VoiceBroadcastRecording | null = null;
|
|
||||||
private recordings = new Map<string, VoiceBroadcastRecording>();
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCurrent(current: VoiceBroadcastRecording): void {
|
|
||||||
if (this.current === current) return;
|
|
||||||
|
|
||||||
const infoEventId = current.infoEvent.getId();
|
|
||||||
|
|
||||||
if (!infoEventId) {
|
|
||||||
throw new Error("Got broadcast info event without Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.current) {
|
|
||||||
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = current;
|
|
||||||
this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
|
||||||
this.recordings.set(infoEventId, current);
|
|
||||||
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrent(): VoiceBroadcastRecording | null {
|
|
||||||
return this.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasCurrent(): boolean {
|
|
||||||
return this.current !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCurrent(): void {
|
|
||||||
if (!this.current) return;
|
|
||||||
|
|
||||||
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
|
||||||
this.current = null;
|
|
||||||
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording {
|
|
||||||
const infoEventId = infoEvent.getId();
|
|
||||||
|
|
||||||
if (!infoEventId) {
|
|
||||||
throw new Error("Got broadcast info event without Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const recording = this.recordings.get(infoEventId) || new VoiceBroadcastRecording(infoEvent, client);
|
|
||||||
this.recordings.set(infoEventId, recording);
|
|
||||||
return recording;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCurrentStateChanged = (state: VoiceBroadcastRecordingState): void => {
|
|
||||||
if (state === VoiceBroadcastInfoState.Stopped) {
|
|
||||||
this.clearCurrent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RelationType } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info";
|
|
||||||
export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk";
|
|
||||||
|
|
||||||
export type VoiceBroadcastLiveness = "live" | "not-live" | "grey";
|
|
||||||
|
|
||||||
export enum VoiceBroadcastInfoState {
|
|
||||||
Started = "started",
|
|
||||||
Paused = "paused",
|
|
||||||
Resumed = "resumed",
|
|
||||||
Stopped = "stopped",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VoiceBroadcastInfoEventContent {
|
|
||||||
device_id: string;
|
|
||||||
state: VoiceBroadcastInfoState;
|
|
||||||
chunk_length?: number;
|
|
||||||
last_chunk_sequence?: number;
|
|
||||||
["m.relates_to"]?: {
|
|
||||||
rel_type: RelationType;
|
|
||||||
event_id: string;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastChunkEventType } from "..";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Voice broadcast chunk collection.
|
|
||||||
* Orders chunks by sequence (if available) or timestamp.
|
|
||||||
*/
|
|
||||||
export class VoiceBroadcastChunkEvents {
|
|
||||||
private events: MatrixEvent[] = [];
|
|
||||||
|
|
||||||
public getEvents(): MatrixEvent[] {
|
|
||||||
return [...this.events];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNext(event: MatrixEvent): MatrixEvent | undefined {
|
|
||||||
return this.events[this.events.indexOf(event) + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
public addEvent(event: MatrixEvent): void {
|
|
||||||
if (this.addOrReplaceEvent(event)) {
|
|
||||||
this.sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addEvents(events: MatrixEvent[]): void {
|
|
||||||
const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => {
|
|
||||||
return this.addOrReplaceEvent(event) || newSoFar;
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
if (atLeastOneNew) {
|
|
||||||
this.sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public includes(event: MatrixEvent): boolean {
|
|
||||||
return !!this.events.find((e) => this.equalByTxnIdOrId(event, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {number} Length in milliseconds
|
|
||||||
*/
|
|
||||||
public getLength(): number {
|
|
||||||
return this.events.reduce((length: number, event: MatrixEvent) => {
|
|
||||||
return length + this.calculateChunkLength(event);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLengthSeconds(): number {
|
|
||||||
return this.getLength() / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the accumulated length to (excl.) a chunk event.
|
|
||||||
*/
|
|
||||||
public getLengthTo(event: MatrixEvent): number {
|
|
||||||
let length = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.events.indexOf(event); i++) {
|
|
||||||
length += this.calculateChunkLength(this.events[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public findByTime(time: number): MatrixEvent | null {
|
|
||||||
let lengthSoFar = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.events.length; i++) {
|
|
||||||
lengthSoFar += this.calculateChunkLength(this.events[i]);
|
|
||||||
|
|
||||||
if (lengthSoFar >= time) {
|
|
||||||
return this.events[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isLast(event: MatrixEvent): boolean {
|
|
||||||
return this.events.indexOf(event) >= this.events.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSequenceForEvent(event: MatrixEvent): number | null {
|
|
||||||
const sequence = parseInt(event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10);
|
|
||||||
if (!isNaN(sequence)) return sequence;
|
|
||||||
|
|
||||||
if (this.events.includes(event)) return this.events.indexOf(event) + 1;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNumberOfEvents(): number {
|
|
||||||
return this.events.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateChunkLength(event: MatrixEvent): number {
|
|
||||||
return event.getContent()?.["org.matrix.msc1767.audio"]?.duration || event.getContent()?.info?.duration || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addOrReplaceEvent = (event: MatrixEvent): boolean => {
|
|
||||||
this.events = this.events.filter((e) => !this.equalByTxnIdOrId(event, e));
|
|
||||||
this.events.push(event);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
private equalByTxnIdOrId(eventA: MatrixEvent, eventB: MatrixEvent): boolean {
|
|
||||||
return (
|
|
||||||
(eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId()) ||
|
|
||||||
eventA.getId() === eventB.getId()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort by sequence, if available for all events.
|
|
||||||
* Else fall back to timestamp.
|
|
||||||
*/
|
|
||||||
private sort(): void {
|
|
||||||
const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp;
|
|
||||||
this.events.sort(compareFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => {
|
|
||||||
const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
|
|
||||||
const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
|
|
||||||
return aSequence - bSequence;
|
|
||||||
};
|
|
||||||
|
|
||||||
private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => {
|
|
||||||
return a.getTs() - b.getTs();
|
|
||||||
};
|
|
||||||
|
|
||||||
private allHaveSequence(): boolean {
|
|
||||||
return !this.events.some((event: MatrixEvent) => {
|
|
||||||
const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence;
|
|
||||||
return parseInt(sequence, 10) !== sequence;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles voice broadcasts on app resume (after logging in, reload, crash…).
|
|
||||||
*/
|
|
||||||
export class VoiceBroadcastResumer implements IDestroyable {
|
|
||||||
public constructor(private client: MatrixClient) {
|
|
||||||
if (client.isInitialSyncComplete()) {
|
|
||||||
this.resume();
|
|
||||||
} else {
|
|
||||||
// wait for initial sync
|
|
||||||
client.on(ClientEvent.Sync, this.onClientSync);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClientSync = (): void => {
|
|
||||||
if (this.client.getSyncState() === SyncState.Syncing) {
|
|
||||||
this.client.off(ClientEvent.Sync, this.onClientSync);
|
|
||||||
this.resume();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private resume(): void {
|
|
||||||
const userId = this.client.getUserId();
|
|
||||||
const deviceId = this.client.getDeviceId();
|
|
||||||
|
|
||||||
if (!userId || !deviceId) {
|
|
||||||
// Resuming a voice broadcast only makes sense if there is a user.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.getRooms().forEach((room: Room) => {
|
|
||||||
const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(room, userId, deviceId);
|
|
||||||
|
|
||||||
if (infoEvent) {
|
|
||||||
// Found a live broadcast event from current device; stop it.
|
|
||||||
// Stopping it is a temporary solution (see PSF-1669).
|
|
||||||
this.sendStopVoiceBroadcastStateEvent(infoEvent);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendStopVoiceBroadcastStateEvent(infoEvent: MatrixEvent): void {
|
|
||||||
const userId = this.client.getUserId();
|
|
||||||
const deviceId = this.client.getDeviceId();
|
|
||||||
const roomId = infoEvent.getRoomId();
|
|
||||||
|
|
||||||
if (!userId || !deviceId || !roomId) {
|
|
||||||
// We can only send a state event if we know all the IDs.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content: VoiceBroadcastInfoEventContent = {
|
|
||||||
device_id: deviceId,
|
|
||||||
state: VoiceBroadcastInfoState.Stopped,
|
|
||||||
};
|
|
||||||
|
|
||||||
// all events should reference the started event
|
|
||||||
const referencedEventId =
|
|
||||||
infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started
|
|
||||||
? infoEvent.getId()
|
|
||||||
: infoEvent.getContent()?.["m.relates_to"]?.event_id;
|
|
||||||
|
|
||||||
if (referencedEventId) {
|
|
||||||
content["m.relates_to"] = {
|
|
||||||
rel_type: RelationType.Reference,
|
|
||||||
event_id: referencedEventId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.sendStateEvent(roomId, VoiceBroadcastInfoEventType, content, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.client.off(ClientEvent.Sync, this.onClientSync);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { MatrixClient, Room, SyncState } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from "..";
|
|
||||||
import InfoDialog from "../../components/views/dialogs/InfoDialog";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import Modal from "../../Modal";
|
|
||||||
|
|
||||||
const showAlreadyRecordingDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voice_broadcast|failed_already_recording_title"),
|
|
||||||
description: <p>{_t("voice_broadcast|failed_already_recording_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showInsufficientPermissionsDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voice_broadcast|failed_insufficient_permission_title"),
|
|
||||||
description: <p>{_t("voice_broadcast|failed_insufficient_permission_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showOthersAlreadyRecordingDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voice_broadcast|failed_others_already_recording_title"),
|
|
||||||
description: <p>{_t("voice_broadcast|failed_others_already_recording_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showNoConnectionDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voice_broadcast|failed_no_connection_title"),
|
|
||||||
description: <p>{_t("voice_broadcast|failed_no_connection_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkVoiceBroadcastPreConditions = async (
|
|
||||||
room: Room,
|
|
||||||
client: MatrixClient,
|
|
||||||
recordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
if (recordingsStore.getCurrent()) {
|
|
||||||
showAlreadyRecordingDialog();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUserId = client.getUserId();
|
|
||||||
|
|
||||||
if (!currentUserId) return false;
|
|
||||||
|
|
||||||
if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) {
|
|
||||||
showInsufficientPermissionsDialog();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.getSyncState() === SyncState.Error) {
|
|
||||||
showNoConnectionDialog();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId);
|
|
||||||
|
|
||||||
if (hasBroadcast && startedByUser) {
|
|
||||||
showAlreadyRecordingDialog();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasBroadcast) {
|
|
||||||
showOthersAlreadyRecordingDialog();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
|
||||||
|
|
||||||
export const cleanUpBroadcasts = async (stores: SdkContextClass): Promise<void> => {
|
|
||||||
stores.voiceBroadcastPlaybacksStore.getCurrent()?.stop();
|
|
||||||
stores.voiceBroadcastPlaybacksStore.clearCurrent();
|
|
||||||
|
|
||||||
await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop();
|
|
||||||
stores.voiceBroadcastRecordingsStore.clearCurrent();
|
|
||||||
|
|
||||||
stores.voiceBroadcastPreRecordingStore.getCurrent()?.cancel();
|
|
||||||
stores.voiceBroadcastPreRecordingStore.clearCurrent();
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from "..";
|
|
||||||
|
|
||||||
const stateLivenessMap: Map<VoiceBroadcastInfoState, VoiceBroadcastLiveness> = new Map([
|
|
||||||
["started", "live"],
|
|
||||||
["resumed", "live"],
|
|
||||||
["paused", "grey"],
|
|
||||||
["stopped", "not-live"],
|
|
||||||
] as Array<[VoiceBroadcastInfoState, VoiceBroadcastLiveness]>);
|
|
||||||
|
|
||||||
export const determineVoiceBroadcastLiveness = (infoState: VoiceBroadcastInfoState): VoiceBroadcastLiveness => {
|
|
||||||
return stateLivenessMap.get(infoState) ?? "not-live";
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from "..";
|
|
||||||
|
|
||||||
export const doClearCurrentVoiceBroadcastPlaybackIfStopped = (
|
|
||||||
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
): void => {
|
|
||||||
if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) {
|
|
||||||
// clear current if stopped
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
hasRoomLiveVoiceBroadcast,
|
|
||||||
VoiceBroadcastPlaybacksStore,
|
|
||||||
VoiceBroadcastPlaybackState,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a live voice broadcast is in the room and
|
|
||||||
* another voice broadcast is not currently being listened to or recorded
|
|
||||||
* the live broadcast in the room is set as the current broadcast to listen to.
|
|
||||||
* When there is no live broadcast in the room: clear current broadcast.
|
|
||||||
*
|
|
||||||
* @param {Room} room The room to check for a live voice broadcast
|
|
||||||
* @param {MatrixClient} client
|
|
||||||
* @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore
|
|
||||||
* @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore
|
|
||||||
*/
|
|
||||||
export const doMaybeSetCurrentVoiceBroadcastPlayback = async (
|
|
||||||
room: Room,
|
|
||||||
client: MatrixClient,
|
|
||||||
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
): Promise<void> => {
|
|
||||||
// do not disturb the current recording
|
|
||||||
if (voiceBroadcastRecordingsStore.hasCurrent()) return;
|
|
||||||
|
|
||||||
const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent();
|
|
||||||
|
|
||||||
if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) {
|
|
||||||
// do not disturb the current playback
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { infoEvent } = await hasRoomLiveVoiceBroadcast(client, room);
|
|
||||||
|
|
||||||
if (infoEvent) {
|
|
||||||
// live broadcast in the room + no recording + not listening yet: set the current broadcast
|
|
||||||
const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client);
|
|
||||||
voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no broadcast; not listening: clear current
|
|
||||||
voiceBroadcastPlaybacksStore.clearCurrent();
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
|
||||||
|
|
||||||
export const findRoomLiveVoiceBroadcastFromUserAndDevice = (
|
|
||||||
room: Room,
|
|
||||||
userId: string,
|
|
||||||
deviceId: string,
|
|
||||||
): MatrixEvent | null => {
|
|
||||||
const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId);
|
|
||||||
|
|
||||||
// no broadcast from that user
|
|
||||||
if (!stateEvent) return null;
|
|
||||||
|
|
||||||
const content = stateEvent.getContent() || {};
|
|
||||||
|
|
||||||
// stopped broadcast
|
|
||||||
if (content.state === VoiceBroadcastInfoState.Stopped) return null;
|
|
||||||
|
|
||||||
return content.device_id === deviceId ? stateEvent : null;
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
|
||||||
import { Features } from "../../settings/Settings";
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the target chunk length for voice broadcasts:
|
|
||||||
* - If {@see Features.VoiceBroadcastForceSmallChunks} is enabled uses 15s chunk length
|
|
||||||
* - Otherwise to get the value from the voice_broadcast.chunk_length config
|
|
||||||
* - If that fails from DEFAULTS
|
|
||||||
* - If that fails fall back to 120 (two minutes)
|
|
||||||
*/
|
|
||||||
export const getChunkLength = (): number => {
|
|
||||||
if (SettingsStore.getValue(Features.VoiceBroadcastForceSmallChunks)) return 15;
|
|
||||||
return SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast?.chunk_length || 120;
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the max length for voice broadcasts:
|
|
||||||
* - Tries to get the value from the voice_broadcast.max_length config
|
|
||||||
* - If that fails from DEFAULTS
|
|
||||||
* - If that fails fall back to four hours
|
|
||||||
*/
|
|
||||||
export const getMaxBroadcastLength = (): number => {
|
|
||||||
return SdkConfig.get("voice_broadcast")?.max_length || DEFAULTS.voice_broadcast?.max_length || 4 * 60 * 60;
|
|
||||||
};
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { retrieveStartedInfoEvent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
|
||||||
import { asyncEvery } from "../../utils/arrays";
|
|
||||||
|
|
||||||
interface Result {
|
|
||||||
// whether there is a live broadcast in the room
|
|
||||||
hasBroadcast: boolean;
|
|
||||||
// info event of any live broadcast in the room
|
|
||||||
infoEvent: MatrixEvent | null;
|
|
||||||
// whether the broadcast was started by the user
|
|
||||||
startedByUser: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasRoomLiveVoiceBroadcast = async (client: MatrixClient, room: Room, userId?: string): Promise<Result> => {
|
|
||||||
let hasBroadcast = false;
|
|
||||||
let startedByUser = false;
|
|
||||||
let infoEvent: MatrixEvent | null = null;
|
|
||||||
|
|
||||||
const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType);
|
|
||||||
await asyncEvery(stateEvents, async (event: MatrixEvent) => {
|
|
||||||
const state = event.getContent()?.state;
|
|
||||||
|
|
||||||
if (state && state !== VoiceBroadcastInfoState.Stopped) {
|
|
||||||
const startEvent = await retrieveStartedInfoEvent(event, client);
|
|
||||||
|
|
||||||
// skip if started voice broadcast event is redacted
|
|
||||||
if (startEvent?.isRedacted()) return true;
|
|
||||||
|
|
||||||
hasBroadcast = true;
|
|
||||||
infoEvent = startEvent;
|
|
||||||
|
|
||||||
// state key = sender's MXID
|
|
||||||
if (event.getStateKey() === userId) {
|
|
||||||
startedByUser = true;
|
|
||||||
// break here, because more than true / true is not possible
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasBroadcast,
|
|
||||||
infoEvent,
|
|
||||||
startedByUser,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventType } from "../types";
|
|
||||||
|
|
||||||
export const isRelatedToVoiceBroadcast = (event: MatrixEvent, client: MatrixClient): boolean => {
|
|
||||||
const relation = event.getRelation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
relation?.rel_type === RelationType.Reference &&
|
|
||||||
!!relation.event_id &&
|
|
||||||
client.getRoom(event.getRoomId())?.findEventById(relation.event_id)?.getType() === VoiceBroadcastInfoEventType
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../types";
|
|
||||||
|
|
||||||
export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => {
|
|
||||||
return (
|
|
||||||
event.getType() === VoiceBroadcastInfoEventType && event.getContent()?.state === VoiceBroadcastInfoState.Started
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastPlaybacksStore } from "..";
|
|
||||||
|
|
||||||
export const pauseNonLiveBroadcastFromOtherRoom = (
|
|
||||||
room: Room,
|
|
||||||
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
): void => {
|
|
||||||
const playingBroadcast = voiceBroadcastPlaybacksStore.getCurrent();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!playingBroadcast ||
|
|
||||||
playingBroadcast?.getLiveness() === "live" ||
|
|
||||||
playingBroadcast?.infoEvent.getRoomId() === room.roomId
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
voiceBroadcastPlaybacksStore.clearCurrent();
|
|
||||||
playingBroadcast.pause();
|
|
||||||
};
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoState } from "..";
|
|
||||||
|
|
||||||
export const retrieveStartedInfoEvent = async (
|
|
||||||
event: MatrixEvent,
|
|
||||||
client: MatrixClient,
|
|
||||||
): Promise<MatrixEvent | null> => {
|
|
||||||
// started event passed as argument
|
|
||||||
if (event.getContent()?.state === VoiceBroadcastInfoState.Started) return event;
|
|
||||||
|
|
||||||
const relatedEventId = event.getRelation()?.event_id;
|
|
||||||
|
|
||||||
// no related event
|
|
||||||
if (!relatedEventId) return null;
|
|
||||||
|
|
||||||
const roomId = event.getRoomId() || "";
|
|
||||||
const relatedEventFromRoom = client.getRoom(roomId)?.findEventById(relatedEventId);
|
|
||||||
|
|
||||||
// event found
|
|
||||||
if (relatedEventFromRoom) return relatedEventFromRoom;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const relatedEventData = await client.fetchRoomEvent(roomId, relatedEventId);
|
|
||||||
return new MatrixEvent(relatedEventData);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
checkVoiceBroadcastPreConditions,
|
|
||||||
VoiceBroadcastPlaybacksStore,
|
|
||||||
VoiceBroadcastPreRecording,
|
|
||||||
VoiceBroadcastPreRecordingStore,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
export const setUpVoiceBroadcastPreRecording = async (
|
|
||||||
room: Room,
|
|
||||||
client: MatrixClient,
|
|
||||||
playbacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
recordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
preRecordingStore: VoiceBroadcastPreRecordingStore,
|
|
||||||
): Promise<VoiceBroadcastPreRecording | null> => {
|
|
||||||
if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = client.getUserId();
|
|
||||||
if (!userId) return null;
|
|
||||||
|
|
||||||
const sender = room.getMember(userId);
|
|
||||||
if (!sender) return null;
|
|
||||||
|
|
||||||
// pause and clear current playback (if any)
|
|
||||||
playbacksStore.getCurrent()?.pause();
|
|
||||||
playbacksStore.clearCurrent();
|
|
||||||
|
|
||||||
const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
|
|
||||||
preRecordingStore.setCurrent(preRecording);
|
|
||||||
return preRecording;
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoState } from "..";
|
|
||||||
|
|
||||||
export const shouldDisplayAsVoiceBroadcastRecordingTile = (
|
|
||||||
state: VoiceBroadcastInfoState,
|
|
||||||
client: MatrixClient,
|
|
||||||
event: MatrixEvent,
|
|
||||||
): boolean => {
|
|
||||||
const userId = client.getUserId();
|
|
||||||
return (
|
|
||||||
!!userId &&
|
|
||||||
userId === event.getSender() &&
|
|
||||||
client.getDeviceId() === event.getContent<VoiceBroadcastInfoEventContent>()?.device_id &&
|
|
||||||
state !== VoiceBroadcastInfoState.Stopped
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
|
||||||
|
|
||||||
export const shouldDisplayAsVoiceBroadcastStoppedText = (event: MatrixEvent): boolean =>
|
|
||||||
event.getType() === VoiceBroadcastInfoEventType &&
|
|
||||||
event.getContent()?.state === VoiceBroadcastInfoState.Stopped &&
|
|
||||||
!event.isRedacted();
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
|
||||||
|
|
||||||
export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent): boolean =>
|
|
||||||
event.getType?.() === VoiceBroadcastInfoEventType &&
|
|
||||||
(event.getContent?.()?.state === VoiceBroadcastInfoState.Started || event.isRedacted());
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import InfoDialog from "../../components/views/dialogs/InfoDialog";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import Modal from "../../Modal";
|
|
||||||
|
|
||||||
export const showCantStartACallDialog = (): void => {
|
|
||||||
Modal.createDialog(InfoDialog, {
|
|
||||||
title: _t("voip|failed_call_live_broadcast_title"),
|
|
||||||
description: <p>{_t("voip|failed_call_live_broadcast_description")}</p>,
|
|
||||||
hasCloseButton: true,
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ISendEventResponse, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { defer } from "matrix-js-sdk/src/utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VoiceBroadcastInfoEventContent,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
VoiceBroadcastInfoState,
|
|
||||||
VoiceBroadcastRecordingsStore,
|
|
||||||
VoiceBroadcastRecording,
|
|
||||||
getChunkLength,
|
|
||||||
VoiceBroadcastPlaybacksStore,
|
|
||||||
} from "..";
|
|
||||||
import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions";
|
|
||||||
|
|
||||||
const startBroadcast = async (
|
|
||||||
room: Room,
|
|
||||||
client: MatrixClient,
|
|
||||||
recordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
): Promise<VoiceBroadcastRecording> => {
|
|
||||||
const { promise, resolve, reject } = defer<VoiceBroadcastRecording>();
|
|
||||||
|
|
||||||
const userId = client.getUserId();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
reject("unable to start voice broadcast if current user is unknown");
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: ISendEventResponse | null = null;
|
|
||||||
|
|
||||||
const onRoomStateEvents = (): void => {
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
const voiceBroadcastEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId);
|
|
||||||
|
|
||||||
if (voiceBroadcastEvent?.getId() === result.event_id) {
|
|
||||||
room.off(RoomStateEvent.Events, onRoomStateEvents);
|
|
||||||
const recording = new VoiceBroadcastRecording(voiceBroadcastEvent, client);
|
|
||||||
recordingsStore.setCurrent(recording);
|
|
||||||
recording.start();
|
|
||||||
resolve(recording);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
room.on(RoomStateEvent.Events, onRoomStateEvents);
|
|
||||||
|
|
||||||
// XXX Michael W: refactor to live event
|
|
||||||
result = await client.sendStateEvent(
|
|
||||||
room.roomId,
|
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
{
|
|
||||||
device_id: client.getDeviceId(),
|
|
||||||
state: VoiceBroadcastInfoState.Started,
|
|
||||||
chunk_length: getChunkLength(),
|
|
||||||
} as VoiceBroadcastInfoEventContent,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a new Voice Broadcast Recording, if
|
|
||||||
* - the user has the permissions to do so in the room
|
|
||||||
* - the user is not already recording a voice broadcast
|
|
||||||
* - there is no other broadcast being recorded in the room, yet
|
|
||||||
* Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state.
|
|
||||||
*/
|
|
||||||
export const startNewVoiceBroadcastRecording = async (
|
|
||||||
room: Room,
|
|
||||||
client: MatrixClient,
|
|
||||||
playbacksStore: VoiceBroadcastPlaybacksStore,
|
|
||||||
recordingsStore: VoiceBroadcastRecordingsStore,
|
|
||||||
): Promise<VoiceBroadcastRecording | null> => {
|
|
||||||
if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pause and clear current playback (if any)
|
|
||||||
playbacksStore.getCurrent()?.pause();
|
|
||||||
playbacksStore.clearCurrent();
|
|
||||||
|
|
||||||
return startBroadcast(room, client, recordingsStore);
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
||||||
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
|
||||||
import { highlightEvent } from "../../utils/EventUtils";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import { getSenderName } from "../../utils/event/getSenderName";
|
|
||||||
|
|
||||||
export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent, client: MatrixClient): (() => ReactNode) => {
|
|
||||||
return (): ReactNode => {
|
|
||||||
const ownUserId = MatrixClientPeg.get()?.getUserId();
|
|
||||||
const startEventId = event.getRelation()?.event_id;
|
|
||||||
const roomId = event.getRoomId();
|
|
||||||
|
|
||||||
const templateTags = {
|
|
||||||
a: (text: string) =>
|
|
||||||
startEventId && roomId ? (
|
|
||||||
<AccessibleButton kind="link_inline" onClick={(): void => highlightEvent(roomId, startEventId)}>
|
|
||||||
{text}
|
|
||||||
</AccessibleButton>
|
|
||||||
) : (
|
|
||||||
text
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ownUserId && ownUserId === event.getSender()) {
|
|
||||||
return _t("timeline|io.element.voice_broadcast_info|you", {}, templateTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _t("timeline|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }, templateTags);
|
|
||||||
};
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue