Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer
This commit is contained in:
commit
4d089dcc05
109 changed files with 2374 additions and 1011 deletions
|
@ -39,7 +39,6 @@ import PlatformPeg from "./PlatformPeg";
|
|||
import { sendLoginRequest } from "./Login";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import TypingStore from "./stores/TypingStore";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import { IntegrationManagers } from "./integrations/IntegrationManagers";
|
||||
import { Mjolnir } from "./mjolnir/Mjolnir";
|
||||
|
@ -62,6 +61,7 @@ import { DialogOpener } from "./utils/DialogOpener";
|
|||
import { Action } from "./dispatcher/actions";
|
||||
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
||||
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
||||
import { SdkContextClass } from './contexts/SDKContext';
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
@ -797,7 +797,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
dis.dispatch({ action: 'will_start_client' }, true);
|
||||
|
||||
// reset things first just in case
|
||||
TypingStore.sharedInstance().reset();
|
||||
SdkContextClass.instance.typingStore.reset();
|
||||
ToastStore.sharedInstance().reset();
|
||||
|
||||
DialogOpener.instance.prepare();
|
||||
|
@ -927,7 +927,7 @@ export function stopMatrixClient(unsetClient = true): void {
|
|||
Notifier.stop();
|
||||
LegacyCallHandler.instance.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
SdkContextClass.instance.typingStore.reset();
|
||||
Presence.stop();
|
||||
ActiveWidgetStore.instance.stop();
|
||||
IntegrationManagers.sharedInstance().stopWatching();
|
||||
|
|
|
@ -47,7 +47,7 @@ export const DEFAULTS: IConfigOptions = {
|
|||
url: "https://element.io/get-started",
|
||||
},
|
||||
voice_broadcast: {
|
||||
chunk_length: 60, // one minute
|
||||
chunk_length: 120, // two minutes
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import liveIcon from "../../../res/img/element-icons/live.svg";
|
||||
import microphoneIcon from "../../../res/img/voip/call-view/mic-on.svg";
|
||||
import pauseIcon from "../../../res/img/element-icons/pause.svg";
|
||||
import playIcon from "../../../res/img/element-icons/play.svg";
|
||||
import stopIcon from "../../../res/img/element-icons/Stop.svg";
|
||||
|
||||
export enum IconType {
|
||||
Live,
|
||||
Microphone,
|
||||
Pause,
|
||||
Play,
|
||||
Stop,
|
||||
}
|
||||
|
||||
const iconTypeMap = new Map([
|
||||
[IconType.Live, liveIcon],
|
||||
[IconType.Microphone, microphoneIcon],
|
||||
[IconType.Pause, pauseIcon],
|
||||
[IconType.Play, playIcon],
|
||||
[IconType.Stop, stopIcon],
|
||||
]);
|
||||
|
||||
export enum IconColour {
|
||||
Accent = "accent",
|
||||
LiveBadge = "live-badge",
|
||||
CompoundSecondaryContent = "compound-secondary-content",
|
||||
}
|
||||
|
||||
export enum IconSize {
|
||||
S16 = "16",
|
||||
}
|
||||
|
||||
interface IconProps {
|
||||
colour?: IconColour;
|
||||
size?: IconSize;
|
||||
type: IconType;
|
||||
}
|
||||
|
||||
export const Icon: React.FC<IconProps> = ({
|
||||
size = IconSize.S16,
|
||||
colour = IconColour.Accent,
|
||||
type,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = [
|
||||
"mx_Icon",
|
||||
`mx_Icon_${size}`,
|
||||
`mx_Icon_${colour}`,
|
||||
];
|
||||
|
||||
const styles: React.CSSProperties = {
|
||||
maskImage: `url("${iconTypeMap.get(type)}")`,
|
||||
WebkitMaskImage: `url("${iconTypeMap.get(type)}")`,
|
||||
};
|
||||
|
||||
return (
|
||||
<i
|
||||
aria-hidden
|
||||
className={classes.join(" ")}
|
||||
role="presentation"
|
||||
style={styles}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { _t } from "../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
parent: HTMLElement;
|
||||
parent: HTMLElement | null;
|
||||
onFileDrop(dataTransfer: DataTransfer): void;
|
||||
}
|
||||
|
||||
|
@ -90,20 +90,20 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
|||
}));
|
||||
};
|
||||
|
||||
parent.addEventListener("drop", onDrop);
|
||||
parent.addEventListener("dragover", onDragOver);
|
||||
parent.addEventListener("dragenter", onDragEnter);
|
||||
parent.addEventListener("dragleave", onDragLeave);
|
||||
parent?.addEventListener("drop", onDrop);
|
||||
parent?.addEventListener("dragover", onDragOver);
|
||||
parent?.addEventListener("dragenter", onDragEnter);
|
||||
parent?.addEventListener("dragleave", onDragLeave);
|
||||
|
||||
return () => {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
parent.removeEventListener("drop", onDrop);
|
||||
parent.removeEventListener("dragover", onDragOver);
|
||||
parent.removeEventListener("dragenter", onDragEnter);
|
||||
parent.removeEventListener("dragleave", onDragLeave);
|
||||
parent?.removeEventListener("drop", onDrop);
|
||||
parent?.removeEventListener("dragover", onDragOver);
|
||||
parent?.removeEventListener("dragenter", onDragEnter);
|
||||
parent?.removeEventListener("dragleave", onDragLeave);
|
||||
};
|
||||
}, [parent, onFileDrop]);
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
|||
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
||||
import { SdkContextClass, SDKContext } from '../../contexts/SDKContext';
|
||||
import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings';
|
||||
import { VoiceBroadcastResumer } from '../../voice-broadcast';
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
@ -234,6 +235,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private focusComposer: boolean;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
private voiceBroadcastResumer: VoiceBroadcastResumer;
|
||||
|
||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||
private readonly dispatcherRef: string;
|
||||
|
@ -433,6 +435,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
window.removeEventListener("resize", this.onWindowResized);
|
||||
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
|
||||
}
|
||||
|
||||
private onWindowResized = (): void => {
|
||||
|
@ -1618,6 +1621,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { createRef, KeyboardEvent } from 'react';
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
@ -70,6 +70,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
thread?: Thread;
|
||||
lastReply?: MatrixEvent | null;
|
||||
layout: Layout;
|
||||
editState?: EditorStateTransfer;
|
||||
replyToEvent?: MatrixEvent;
|
||||
|
@ -88,9 +89,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const thread = this.props.room.getThread(this.props.mxEvent.getId());
|
||||
|
||||
this.setupThreadListeners(thread);
|
||||
this.state = {
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
narrow: false,
|
||||
thread,
|
||||
lastReply: thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
}),
|
||||
};
|
||||
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||
|
@ -99,6 +107,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.state.thread) {
|
||||
this.postThreadUpdate(this.state.thread);
|
||||
}
|
||||
this.setupThread(this.props.mxEvent);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -189,19 +200,49 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private updateThreadRelation = (): void => {
|
||||
this.setState({
|
||||
lastReply: this.threadLastReply,
|
||||
});
|
||||
};
|
||||
|
||||
private get threadLastReply(): MatrixEvent | undefined {
|
||||
return this.state.thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
});
|
||||
}
|
||||
|
||||
private updateThread = (thread?: Thread) => {
|
||||
if (thread && this.state.thread !== thread) {
|
||||
if (this.state.thread === thread) return;
|
||||
|
||||
this.setupThreadListeners(thread, this.state.thread);
|
||||
if (thread) {
|
||||
this.setState({
|
||||
thread,
|
||||
}, async () => {
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
await thread.fetchInitialEvents();
|
||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
this.timelinePanel.current?.refreshTimeline();
|
||||
});
|
||||
lastReply: this.threadLastReply,
|
||||
}, async () => this.postThreadUpdate(thread));
|
||||
}
|
||||
};
|
||||
|
||||
private async postThreadUpdate(thread: Thread): Promise<void> {
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
await thread.fetchInitialEvents();
|
||||
this.updateThreadRelation();
|
||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
this.timelinePanel.current?.refreshTimeline();
|
||||
}
|
||||
|
||||
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
|
||||
if (oldThread) {
|
||||
this.state.thread.off(ThreadEvent.NewReply, this.updateThreadRelation);
|
||||
this.props.room.off(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
|
||||
}
|
||||
if (thread) {
|
||||
thread.on(ThreadEvent.NewReply, this.updateThreadRelation);
|
||||
this.props.room.on(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
|
||||
}
|
||||
}
|
||||
|
||||
private resetJumpToEvent = (event?: string): void => {
|
||||
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
||||
event === this.props.initialEvent?.getId()) {
|
||||
|
@ -242,14 +283,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private nextBatch: string;
|
||||
private nextBatch: string | undefined | null = null;
|
||||
|
||||
private onPaginationRequest = async (
|
||||
timelineWindow: TimelineWindow | null,
|
||||
direction = Direction.Backward,
|
||||
limit = 20,
|
||||
): Promise<boolean> => {
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
if (!Thread.hasServerSideSupport && timelineWindow) {
|
||||
timelineWindow.extend(direction, limit);
|
||||
return true;
|
||||
}
|
||||
|
@ -262,40 +303,50 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
opts.from = this.nextBatch;
|
||||
}
|
||||
|
||||
const { nextBatch } = await this.state.thread.fetchEvents(opts);
|
||||
|
||||
this.nextBatch = nextBatch;
|
||||
let nextBatch: string | null | undefined = null;
|
||||
if (this.state.thread) {
|
||||
const response = await this.state.thread.fetchEvents(opts);
|
||||
nextBatch = response.nextBatch;
|
||||
this.nextBatch = nextBatch;
|
||||
}
|
||||
|
||||
// Advances the marker on the TimelineWindow to define the correct
|
||||
// window of events to display on screen
|
||||
timelineWindow.extend(direction, limit);
|
||||
timelineWindow?.extend(direction, limit);
|
||||
|
||||
return !!nextBatch;
|
||||
};
|
||||
|
||||
private onFileDrop = (dataTransfer: DataTransfer) => {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.threadRelation,
|
||||
MatrixClientPeg.get(),
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
if (roomId) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
roomId,
|
||||
this.threadRelation,
|
||||
MatrixClientPeg.get(),
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
} else {
|
||||
console.warn("Unknwon roomId for event", this.props.mxEvent);
|
||||
}
|
||||
};
|
||||
|
||||
private get threadRelation(): IEventRelation {
|
||||
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
});
|
||||
|
||||
return {
|
||||
const relation = {
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": this.state.thread?.id,
|
||||
"is_falling_back": true,
|
||||
"m.in_reply_to": {
|
||||
"event_id": lastThreadReply?.getId() ?? this.state.thread?.id,
|
||||
},
|
||||
};
|
||||
|
||||
const fallbackEventId = this.state.lastReply?.getId() ?? this.state.thread?.id;
|
||||
if (fallbackEventId) {
|
||||
relation["m.in_reply_to"] = {
|
||||
"event_id": fallbackEventId,
|
||||
};
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
private renderThreadViewHeader = (): JSX.Element => {
|
||||
|
@ -314,7 +365,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
const threadRelation = this.threadRelation;
|
||||
|
||||
let timeline: JSX.Element;
|
||||
let timeline: JSX.Element | null;
|
||||
if (this.state.thread) {
|
||||
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
|
||||
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",
|
||||
|
|
|
@ -370,7 +370,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_LoginWithQR">
|
||||
<div data-testid="login-with-qr" className="mx_LoginWithQR">
|
||||
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
|
||||
{ backButton ?
|
||||
<AccessibleButton
|
||||
|
|
|
@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
interface IProps {
|
||||
export interface ThreadListContextMenuProps {
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
onMenuToggle?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => {
|
|||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
const ThreadListContextMenu: React.FC<IProps> = ({
|
||||
const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
|
||||
mxEvent,
|
||||
permalinkCreator,
|
||||
onMenuToggle,
|
||||
|
@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
closeThreadOptions();
|
||||
}, [mxEvent, closeThreadOptions]);
|
||||
|
||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||
await copyPlaintext(matrixToUrl);
|
||||
closeThreadOptions();
|
||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => {
|
||||
if (permalinkCreator) {
|
||||
evt?.preventDefault();
|
||||
evt?.stopPropagation();
|
||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||
await copyPlaintext(matrixToUrl);
|
||||
closeThreadOptions();
|
||||
}
|
||||
}, [mxEvent, closeThreadOptions, permalinkCreator]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
title={_t("Thread options")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
data-testid="threadlist-dropdown-button"
|
||||
/>
|
||||
{ menuDisplayed && (<IconizedContextMenu
|
||||
onFinished={closeThreadOptions}
|
||||
|
@ -102,11 +105,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
{ permalinkCreator &&
|
||||
<IconizedContextMenuOption
|
||||
data-testid="copy-thread-link"
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>) }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -21,10 +21,11 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
|
||||
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
widget: Widget;
|
||||
|
@ -57,7 +58,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||
if (this.state.rememberSelection) {
|
||||
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
|
||||
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
SdkContextClass.instance.widgetPermissionStore.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||
);
|
||||
|
|
|
@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent<IProps> {
|
|||
style={{ width: w, height: h }}
|
||||
aria-label={_t("Loading...")}
|
||||
role="progressbar"
|
||||
data-testid="spinner"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -57,7 +57,7 @@ type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transfor
|
|||
|
||||
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||
private static container: HTMLElement;
|
||||
private parent: Element;
|
||||
private parent: Element | null = null;
|
||||
|
||||
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
|
||||
// so we expose the Alignment options off of us statically.
|
||||
|
@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
|
||||
|
||||
this.updatePosition();
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
// positioned, also taking into account any window zoom
|
||||
private updatePosition = (): void => {
|
||||
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
|
||||
if (!this.props.visible) return;
|
||||
if (!this.props.visible || !this.parent) return;
|
||||
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
const width = UIStore.instance.windowWidth;
|
||||
|
|
|
@ -31,7 +31,6 @@ import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
|||
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
|
||||
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||
import { renderModel } from '../../../editor/render';
|
||||
import TypingStore from "../../../stores/TypingStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { IS_MAC, Key } from "../../../Keyboard";
|
||||
import { EMOTICON_TO_EMOJI } from "../../../emoji";
|
||||
|
@ -47,6 +46,7 @@ import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
|||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { linkify } from '../../../linkify-matrix';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||
|
@ -246,7 +246,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
isTyping = false;
|
||||
}
|
||||
}
|
||||
TypingStore.sharedInstance().setSelfTyping(
|
||||
SdkContextClass.instance.typingStore.setSelfTyping(
|
||||
this.props.room.roomId,
|
||||
this.props.threadId,
|
||||
isTyping,
|
||||
|
@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
aria-activedescendant={activeDescendant}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
data-testid="basicmessagecomposer"
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={props.title ?? _t('Send message')}
|
||||
data-testid="sendmessagebtn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -379,7 +379,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
isExpanded={mainMenuDisplayed}
|
||||
className="mx_RoomListHeader_contextMenuButton"
|
||||
title={activeSpace
|
||||
? _t("%(spaceName)s menu", { spaceName })
|
||||
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
|
||||
: _t("Home options")}
|
||||
>
|
||||
{ title }
|
||||
|
|
|
@ -25,7 +25,9 @@ import { RoomNotificationStateStore } from "../stores/notifications/RoomNotifica
|
|||
import RightPanelStore from "../stores/right-panel/RightPanelStore";
|
||||
import { RoomViewStore } from "../stores/RoomViewStore";
|
||||
import SpaceStore, { SpaceStoreClass } from "../stores/spaces/SpaceStore";
|
||||
import TypingStore from "../stores/TypingStore";
|
||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
|
||||
export const SDKContext = createContext<SdkContextClass>(undefined);
|
||||
|
@ -50,6 +52,7 @@ export class SdkContextClass {
|
|||
public client?: MatrixClient;
|
||||
|
||||
// All protected fields to make it easier to derive test stores
|
||||
protected _WidgetPermissionStore?: WidgetPermissionStore;
|
||||
protected _RightPanelStore?: RightPanelStore;
|
||||
protected _RoomNotificationStateStore?: RoomNotificationStateStore;
|
||||
protected _RoomViewStore?: RoomViewStore;
|
||||
|
@ -59,6 +62,7 @@ export class SdkContextClass {
|
|||
protected _SlidingSyncManager?: SlidingSyncManager;
|
||||
protected _SpaceStore?: SpaceStoreClass;
|
||||
protected _LegacyCallHandler?: LegacyCallHandler;
|
||||
protected _TypingStore?: TypingStore;
|
||||
|
||||
/**
|
||||
* Automatically construct stores which need to be created eagerly so they can register with
|
||||
|
@ -100,6 +104,12 @@ export class SdkContextClass {
|
|||
}
|
||||
return this._WidgetLayoutStore;
|
||||
}
|
||||
public get widgetPermissionStore(): WidgetPermissionStore {
|
||||
if (!this._WidgetPermissionStore) {
|
||||
this._WidgetPermissionStore = new WidgetPermissionStore(this);
|
||||
}
|
||||
return this._WidgetPermissionStore;
|
||||
}
|
||||
public get widgetStore(): WidgetStore {
|
||||
if (!this._WidgetStore) {
|
||||
this._WidgetStore = WidgetStore.instance;
|
||||
|
@ -124,4 +134,11 @@ export class SdkContextClass {
|
|||
}
|
||||
return this._SpaceStore;
|
||||
}
|
||||
public get typingStore(): TypingStore {
|
||||
if (!this._TypingStore) {
|
||||
this._TypingStore = new TypingStore(this);
|
||||
window.mxTypingStore = this._TypingStore;
|
||||
}
|
||||
return this._TypingStore;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -941,7 +941,7 @@
|
|||
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
|
||||
"You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
|
||||
"Call failed due to misconfigured server": "Volání selhalo, protože je rozbitá konfigurace serveru",
|
||||
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého homeserveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
|
||||
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého domovského serveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
|
||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Můžete také zkusit použít veřejný server na adrese <code>turn.matrix.org</code>, ale ten nebude tak spolehlivý a bude sdílet vaši IP adresu s tímto serverem. To můžete spravovat také v Nastavení.",
|
||||
"Try using turn.matrix.org": "Zkuste použít turn.matrix.org",
|
||||
"Messages": "Zprávy",
|
||||
|
@ -1441,7 +1441,7 @@
|
|||
"Manually Verify by Text": "Manuální textové ověření",
|
||||
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
|
||||
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
|
||||
"Manually verify all remote sessions": "Manuálně ověřit všechny relace",
|
||||
"Manually verify all remote sessions": "Ručně ověřit všechny relace",
|
||||
"cached locally": "uložen lokálně",
|
||||
"not found locally": "nenalezen lolálně",
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individuálně ověřit každou uživatelovu relaci a označit jí za důvěryhodnou, bez důvěry v křížový podpis.",
|
||||
|
@ -3635,5 +3635,34 @@
|
|||
"Notifications silenced": "Oznámení ztlumena",
|
||||
"Yes, stop broadcast": "Ano, zastavit vysílání",
|
||||
"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.": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.",
|
||||
"Stop live broadcasting?": "Ukončit živé vysílání?"
|
||||
"Stop live broadcasting?": "Ukončit živé vysílání?",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a spusťte nové.",
|
||||
"Can't start a new voice broadcast": "Nelze spustit nové hlasové vysílání",
|
||||
"Completing set up of your new device": "Dokončování nastavení nového zařízení",
|
||||
"Waiting for device to sign in": "Čekání na přihlášení zařízení",
|
||||
"Connecting...": "Připojování...",
|
||||
"Review and approve the sign in": "Zkontrolovat a schválit přihlášení",
|
||||
"Select 'Scan QR code'": "Vyberte \"Naskenovat QR kód\"",
|
||||
"Start at the sign in screen": "Začněte na přihlašovací obrazovce",
|
||||
"Scan the QR code below with your device that's signed out.": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.",
|
||||
"By approving access for this device, it will have full access to your account.": "Schválením přístupu tohoto zařízení získá zařízení plný přístup k vašemu účtu.",
|
||||
"Check that the code below matches with your other device:": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:",
|
||||
"Devices connected": "Zařízení byla propojena",
|
||||
"The homeserver doesn't support signing in another device.": "Domovský server nepodporuje přihlášení pomocí jiného zařízení.",
|
||||
"An unexpected error occurred.": "Došlo k neočekávané chybě.",
|
||||
"The request was cancelled.": "Požadavek byl zrušen.",
|
||||
"The other device isn't signed in.": "Druhé zařízení není přihlášeno.",
|
||||
"The other device is already signed in.": "Druhé zařízení je již přihlášeno.",
|
||||
"The request was declined on the other device.": "Požadavek byl na druhém zařízení odmítnut.",
|
||||
"Linking with this device is not supported.": "Propojení s tímto zařízením není podporováno.",
|
||||
"The scanned code is invalid.": "Naskenovaný kód je neplatný.",
|
||||
"The linking wasn't completed in the required time.": "Propojení nebylo dokončeno v požadovaném čase.",
|
||||
"Sign in new device": "Přihlásit nové zařízení",
|
||||
"Show QR code": "Zobrazit QR kód",
|
||||
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Toto zařízení můžete použít k přihlášení nového zařízení pomocí QR kódu. QR kód zobrazený na tomto zařízení musíte naskenovat pomocí odhlášeného zařízení.",
|
||||
"Sign in with QR code": "Přihlásit se pomocí QR kódu",
|
||||
"Browser": "Prohlížeč",
|
||||
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Povolit zobrazení QR kódu ve správci relací pro přihlášení do jiného zařízení (vyžaduje kompatibilní domovský server)"
|
||||
}
|
||||
|
|
|
@ -919,7 +919,7 @@
|
|||
"Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Server-Betreibenden vertraust.",
|
||||
"Trust": "Vertrauen",
|
||||
"Custom (%(level)s)": "Benutzerdefiniert (%(level)s)",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in Rohtext, ohne sie als Markdown darzustellen",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Sendet eine Nachricht als Klartext, ohne sie als Markdown darzustellen",
|
||||
"Use an identity server to invite by email. Manage in Settings.": "Verwende einen Identitäts-Server, um per E-Mail einladen zu können. Lege einen in den Einstellungen fest.",
|
||||
"%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
|
||||
"Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren",
|
||||
|
@ -1166,7 +1166,7 @@
|
|||
"%(creator)s created and configured the room.": "%(creator)s hat den Raum erstellt und konfiguriert.",
|
||||
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Bewahre eine Kopie an einem sicheren Ort, wie einem Passwort-Manager oder in einem Safe auf.",
|
||||
"Copy": "Kopieren",
|
||||
"Sends a message as html, without interpreting it as markdown": "Verschickt eine Nachricht im HTML-Format, ohne sie als Markdown zu darzustellen",
|
||||
"Sends a message as html, without interpreting it as markdown": "Sendet eine Nachricht als HTML, ohne sie als Markdown darzustellen",
|
||||
"Show rooms with unread notifications first": "Zeige Räume mit ungelesenen Benachrichtigungen zuerst an",
|
||||
"Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen",
|
||||
"Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen",
|
||||
|
@ -3624,11 +3624,40 @@
|
|||
"resume voice broadcast": "Sprachübertragung fortsetzen",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Unterstrichen",
|
||||
"Try out the rich text editor (plain text mode coming soon)": "Probiere den Rich-Text-Editor aus (bald auch mit Plain-Text-Modus)",
|
||||
"Try out the rich text editor (plain text mode coming soon)": "Probiere den Textverarbeitungs-Editor (bald auch mit Klartext-Modus)",
|
||||
"You have already joined this call from another device": "Du nimmst an diesem Anruf bereits mit einem anderen Gerät teil",
|
||||
"stop voice broadcast": "Sprachübertragung beenden",
|
||||
"Notifications silenced": "Benachrichtigungen stummgeschaltet",
|
||||
"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.": "Willst du die Sprachübertragung wirklich beenden? Damit endet auch die Aufnahme.",
|
||||
"Yes, stop broadcast": "Ja, Sprachübertragung beenden",
|
||||
"Stop live broadcasting?": "Sprachübertragung beenden?"
|
||||
"Stop live broadcasting?": "Sprachübertragung beenden?",
|
||||
"Sign in with QR code": "Mit QR-Code anmelden",
|
||||
"Browser": "Browser",
|
||||
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Erlaube es andere Geräte mittels QR-Code in der Sitzungsverwaltung anzumelden (kompatibler Heim-Server benötigt)",
|
||||
"Completing set up of your new device": "Schließe Anmeldung deines neuen Gerätes ab",
|
||||
"Waiting for device to sign in": "Warte auf Anmeldung des Gerätes",
|
||||
"Connecting...": "Verbinde …",
|
||||
"Review and approve the sign in": "Überprüfe und genehmige die Anmeldung",
|
||||
"Select 'Scan QR code'": "Wähle „QR-Code einlesen“",
|
||||
"Start at the sign in screen": "Beginne auf dem Anmeldebildschirm",
|
||||
"Scan the QR code below with your device that's signed out.": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.",
|
||||
"By approving access for this device, it will have full access to your account.": "Indem du den Zugriff dieses Gerätes bestätigst, erhält es vollen Zugang zu deinem Account.",
|
||||
"Check that the code below matches with your other device:": "Überprüfe, dass der unten angezeigte Code mit deinem anderen Gerät übereinstimmt:",
|
||||
"Devices connected": "Geräte verbunden",
|
||||
"The homeserver doesn't support signing in another device.": "Der Heim-Server unterstützt die Anmeldung eines anderen Gerätes nicht.",
|
||||
"An unexpected error occurred.": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"The request was cancelled.": "Die Anfrage wurde abgebrochen.",
|
||||
"The other device isn't signed in.": "Das andere Gerät ist nicht angemeldet.",
|
||||
"The other device is already signed in.": "Das andere Gerät ist bereits angemeldet.",
|
||||
"The request was declined on the other device.": "Die Anfrage wurde auf dem anderen Gerät abgelehnt.",
|
||||
"Linking with this device is not supported.": "Die Verbindung mit diesem Gerät wird nicht unterstützt.",
|
||||
"The scanned code is invalid.": "Der gescannte Code ist ungültig.",
|
||||
"The linking wasn't completed in the required time.": "Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden.",
|
||||
"Sign in new device": "Neues Gerät anmelden",
|
||||
"Show QR code": "QR-Code anzeigen",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.",
|
||||
"Can't start a new voice broadcast": "Sprachübertragung kann nicht gestartet werden",
|
||||
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Du kannst dieses Gerät verwenden, um ein neues Gerät per QR-Code anzumelden. Dazu musst du den auf diesem Gerät angezeigten QR-Code mit deinem nicht angemeldeten Gerät einlesen."
|
||||
}
|
||||
|
|
|
@ -644,10 +644,10 @@
|
|||
"Stop live broadcasting?": "Stop live broadcasting?",
|
||||
"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.": "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.",
|
||||
"Yes, stop broadcast": "Yes, stop broadcast",
|
||||
"Live": "Live",
|
||||
"pause voice broadcast": "pause voice broadcast",
|
||||
"play voice broadcast": "play voice broadcast",
|
||||
"resume voice broadcast": "resume voice broadcast",
|
||||
"stop voice broadcast": "stop voice broadcast",
|
||||
"pause voice broadcast": "pause voice broadcast",
|
||||
"Live": "Live",
|
||||
"Voice broadcast": "Voice broadcast",
|
||||
"Cannot reach homeserver": "Cannot reach homeserver",
|
||||
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",
|
||||
|
|
|
@ -3631,5 +3631,37 @@
|
|||
"New session manager": "Uus sessioonihaldur",
|
||||
"Use new session manager": "Kasuta uut sessioonihaldurit",
|
||||
"Try out the rich text editor (plain text mode coming soon)": "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)",
|
||||
"Notifications silenced": "Teavitused on summutatud"
|
||||
"Notifications silenced": "Teavitused on summutatud",
|
||||
"Completing set up of your new device": "Lõpetame uue seadme seadistamise",
|
||||
"Waiting for device to sign in": "Ootame, et teine seade logiks võrku",
|
||||
"Connecting...": "Ühendamisel…",
|
||||
"Review and approve the sign in": "Vaata üle ja kinnita sisselogimine Matrixi'i võrku",
|
||||
"Select 'Scan QR code'": "Vali „Loe QR-koodi“",
|
||||
"Start at the sign in screen": "Alusta sisselogimisvaatest",
|
||||
"Scan the QR code below with your device that's signed out.": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.",
|
||||
"By approving access for this device, it will have full access to your account.": "Lubades ligipääsu sellele seadmele, annad talle ka täismahulise ligipääsu oma kasutajakontole.",
|
||||
"Check that the code below matches with your other device:": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:",
|
||||
"Devices connected": "Seadmed on ühendatud",
|
||||
"The homeserver doesn't support signing in another device.": "Koduserver ei toeta muude seadmete võrku logimise võimalust.",
|
||||
"An unexpected error occurred.": "Tekkis teadmata viga.",
|
||||
"The request was cancelled.": "Päring katkestati.",
|
||||
"The other device isn't signed in.": "Teine seade ei ole võrku loginud.",
|
||||
"The other device is already signed in.": "Teine seade on juba võrku loginud.",
|
||||
"The request was declined on the other device.": "Teine seade lükkas päringu tagasi.",
|
||||
"Linking with this device is not supported.": "Sidumine selle seadmega ei ole toetatud.",
|
||||
"The scanned code is invalid.": "Skaneeritud QR-kood on vigane.",
|
||||
"The linking wasn't completed in the required time.": "Sidumine ei lõppenud etteantud aja jooksul.",
|
||||
"Sign in new device": "Logi sisse uus seade",
|
||||
"Show QR code": "Näita QR-koodi",
|
||||
"Sign in with QR code": "Logi sisse QR-koodi abil",
|
||||
"Browser": "Brauser",
|
||||
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Sa saad kasutada seda seadet mõne muu seadme logimiseks Matrix'i võrku QR-koodi alusel. Selleks skaneeri võrgust väljalogitud seadmega seda QR-koodi.",
|
||||
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Teise seadme sisselogimiseks luba QR-koodi kuvamine sessioonihalduris (eeldab, et koduserver sellist võimalust toetab)",
|
||||
"Yes, stop broadcast": "Jah, lõpeta",
|
||||
"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.": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.",
|
||||
"Stop live broadcasting?": "Kas lõpetame otseeetri?",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.",
|
||||
"Can't start a new voice broadcast": "Uue ringhäälingukõne alustamine pole võimalik"
|
||||
}
|
||||
|
|
|
@ -3631,5 +3631,33 @@
|
|||
"You do not have sufficient permissions to change this.": "Nincs megfelelő jogosultság a megváltoztatáshoz.",
|
||||
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s végpontok között titkosított de jelenleg csak kevés számú résztvevővel működik.",
|
||||
"Enable %(brand)s as an additional calling option in this room": "%(brand)s engedélyezése mint további opció hívásokhoz a szobában",
|
||||
"Notifications silenced": "Értesítések elnémítva"
|
||||
"Notifications silenced": "Értesítések elnémítva",
|
||||
"Stop live broadcasting?": "Megszakítja az élő közvetítést?",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Valaki már elindított egy hang közvetítést. Várja meg a közvetítés végét az új indításához.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nincs jogosultsága hang közvetítést indítani ebben a szobában. Vegye fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Egy hang közvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához.",
|
||||
"Can't start a new voice broadcast": "Az új hang közvetítés nem indítható el",
|
||||
"Completing set up of your new device": "Új eszköz beállításának elvégzése",
|
||||
"Waiting for device to sign in": "Várakozás a másik eszköz bejelentkezésére",
|
||||
"Connecting...": "Kapcsolás…",
|
||||
"Select 'Scan QR code'": "Válassza ezt: „QR kód beolvasása”",
|
||||
"Start at the sign in screen": "Kezdje a bejelentkező képernyőn",
|
||||
"Scan the QR code below with your device that's signed out.": "A kijelentkezett eszközzel olvasd be a QR kódot alább.",
|
||||
"By approving access for this device, it will have full access to your account.": "Ennek az eszköznek a hozzáférés engedélyezése után az eszköznek teljes hozzáférése lesz a fiókjához.",
|
||||
"Check that the code below matches with your other device:": "Ellenőrizze, hogy az alábbi kód megegyezik a másik eszközödön lévővel:",
|
||||
"Devices connected": "Összekötött eszközök",
|
||||
"The homeserver doesn't support signing in another device.": "A matrix szerver nem támogatja más eszköz bejelentkeztetését.",
|
||||
"An unexpected error occurred.": "Nemvárt hiba történt.",
|
||||
"The request was cancelled.": "A kérés megszakítva.",
|
||||
"The other device isn't signed in.": "A másik eszköz még nincs bejelentkezve.",
|
||||
"The other device is already signed in.": "A másik eszköz már bejelentkezett.",
|
||||
"The request was declined on the other device.": "A kérést elutasították a másik eszközön.",
|
||||
"Linking with this device is not supported.": "Összekötés ezzel az eszközzel nem támogatott.",
|
||||
"The scanned code is invalid.": "A beolvasott kód érvénytelen.",
|
||||
"The linking wasn't completed in the required time.": "Az összekötés az elvárt időn belül nem fejeződött be.",
|
||||
"Sign in new device": "Új eszköz bejelentkeztetése",
|
||||
"Show QR code": "QR kód beolvasása",
|
||||
"Sign in with QR code": "Belépés QR kóddal",
|
||||
"Browser": "Böngésző",
|
||||
"Yes, stop broadcast": "Igen, közvetítés megállítása"
|
||||
}
|
||||
|
|
|
@ -2267,8 +2267,8 @@
|
|||
"Value": "Valore",
|
||||
"Setting ID": "ID impostazione",
|
||||
"Show chat effects (animations when receiving e.g. confetti)": "Mostra effetti chat (animazioni quando si ricevono ad es. coriandoli)",
|
||||
"Original event source": "Fonte dell'evento originale",
|
||||
"Decrypted event source": "Fonte dell'evento decifrato",
|
||||
"Original event source": "Sorgente dell'evento originale",
|
||||
"Decrypted event source": "Sorgente dell'evento decifrato",
|
||||
"Inviting...": "Invito...",
|
||||
"Invite by username": "Invita per nome utente",
|
||||
"Invite your teammates": "Invita la tua squadra",
|
||||
|
@ -3635,5 +3635,8 @@
|
|||
"stop voice broadcast": "ferma broadcast voce",
|
||||
"Yes, stop broadcast": "Sì, ferma il broadcast",
|
||||
"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.": "Vuoi davvero fermare il tuo broadcast in diretta? Verrà terminato il broadcast e la registrazione completa sarà disponibile nella stanza.",
|
||||
"Stop live broadcasting?": "Fermare il broadcast in diretta?"
|
||||
"Stop live broadcasting?": "Fermare il broadcast in diretta?",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Non hai l'autorizzazione necessaria per iniziare un broadcast vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Stai già registrando un broadcast vocale. Termina quello in corso per iniziarne uno nuovo.",
|
||||
"Can't start a new voice broadcast": "Impossibile iniziare un nuovo broadcast vocale"
|
||||
}
|
||||
|
|
|
@ -3632,5 +3632,37 @@
|
|||
"stop voice broadcast": "zastaviť hlasové vysielanie",
|
||||
"resume voice broadcast": "obnoviť hlasové vysielanie",
|
||||
"pause voice broadcast": "pozastaviť hlasové vysielanie",
|
||||
"Notifications silenced": "Oznámenia stlmené"
|
||||
"Notifications silenced": "Oznámenia stlmené",
|
||||
"Completing set up of your new device": "Dokončenie nastavenia nového zariadenia",
|
||||
"Waiting for device to sign in": "Čaká sa na prihlásenie zariadenia",
|
||||
"Connecting...": "Pripájanie…",
|
||||
"Review and approve the sign in": "Skontrolujte a schváľte prihlásenie",
|
||||
"Select 'Scan QR code'": "Vyberte možnosť \"Skenovať QR kód\"",
|
||||
"Start at the sign in screen": "Začnite na prihlasovacej obrazovke",
|
||||
"Scan the QR code below with your device that's signed out.": "Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.",
|
||||
"By approving access for this device, it will have full access to your account.": "Schválením prístupu pre toto zariadenie bude mať plný prístup k vášmu účtu.",
|
||||
"Check that the code below matches with your other device:": "Skontrolujte, či sa nižšie uvedený kód zhoduje s vaším druhým zariadením:",
|
||||
"Devices connected": "Zariadenia pripojené",
|
||||
"The homeserver doesn't support signing in another device.": "Domovský server nepodporuje prihlasovanie do iného zariadenia.",
|
||||
"An unexpected error occurred.": "Vyskytla sa neočakávaná chyba.",
|
||||
"The request was cancelled.": "Žiadosť bola zrušená.",
|
||||
"The other device isn't signed in.": "Druhé zariadenie nie je prihlásené.",
|
||||
"The other device is already signed in.": "Druhé zariadenie je už prihlásené.",
|
||||
"The request was declined on the other device.": "Žiadosť bola na druhom zariadení zamietnutá.",
|
||||
"Linking with this device is not supported.": "Prepojenie s týmto zariadením nie je podporované.",
|
||||
"The scanned code is invalid.": "Naskenovaný kód je neplatný.",
|
||||
"The linking wasn't completed in the required time.": "Prepojenie nebolo dokončené v požadovanom čase.",
|
||||
"Sign in new device": "Prihlásiť nové zariadenie",
|
||||
"Show QR code": "Zobraziť QR kód",
|
||||
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Toto zariadenie môžete použiť na prihlásenie nového zariadenia pomocou QR kódu. QR kód zobrazený na tomto zariadení musíte naskenovať pomocou zariadenia, ktoré je odhlásené.",
|
||||
"Sign in with QR code": "Prihlásiť sa pomocou QR kódu",
|
||||
"Browser": "Prehliadač",
|
||||
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Umožniť zobrazenie QR kódu v správcovi relácií na prihlásenie do iného zariadenia (vyžaduje kompatibilný domovský server)",
|
||||
"Yes, stop broadcast": "Áno, zastaviť vysielanie",
|
||||
"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.": "Určite chcete zastaviť vaše vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.",
|
||||
"Stop live broadcasting?": "Zastaviť vysielanie naživo?",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.",
|
||||
"Can't start a new voice broadcast": "Nemôžete spustiť nové hlasové vysielanie"
|
||||
}
|
||||
|
|
|
@ -3632,5 +3632,37 @@
|
|||
"pause voice broadcast": "призупинити голосове мовлення",
|
||||
"You have already joined this call from another device": "Ви вже приєдналися до цього виклику з іншого пристрою",
|
||||
"stop voice broadcast": "припинити голосове мовлення",
|
||||
"Notifications silenced": "Сповіщення стишено"
|
||||
"Notifications silenced": "Сповіщення стишено",
|
||||
"Sign in with QR code": "Увійти за допомогою QR-коду",
|
||||
"Browser": "Браузер",
|
||||
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Дозволити показ QR-коду в менеджері сеансів для входу на іншому пристрої (потрібен сумісний домашній сервер)",
|
||||
"Yes, stop broadcast": "Так, припинити мовлення",
|
||||
"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.": "Ви впевнені, що хочете припинити голосове мовлення? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.",
|
||||
"Stop live broadcasting?": "Припинити голосове мовлення?",
|
||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Хтось інший вже записує голосову трансляцію. Зачекайте, поки вона завершиться, щоб почати нову.",
|
||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Ви не маєте необхідних дозволів для початку голосового мовлення в цій кімнаті. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи.",
|
||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Ви вже записуєте голосову трансляцію. Завершіть поточну трансляцію, щоб розпочати нову.",
|
||||
"Can't start a new voice broadcast": "Не вдалося розпочати нову голосове мовлення",
|
||||
"Completing set up of your new device": "Завершення налаштування нового пристрою",
|
||||
"Waiting for device to sign in": "Очікування входу з пристрою",
|
||||
"Connecting...": "З'єднання...",
|
||||
"Review and approve the sign in": "Розглянути та схвалити вхід",
|
||||
"Select 'Scan QR code'": "Виберіть «Сканувати QR-код»",
|
||||
"Start at the sign in screen": "Почніть з екрана входу",
|
||||
"Scan the QR code below with your device that's signed out.": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.",
|
||||
"By approving access for this device, it will have full access to your account.": "Затвердивши доступ для цього пристрою, ви надасте йому повний доступ до вашого облікового запису.",
|
||||
"Check that the code below matches with your other device:": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:",
|
||||
"Devices connected": "Пристрої під'єднано",
|
||||
"The homeserver doesn't support signing in another device.": "Домашній сервер не підтримує вхід на іншому пристрої.",
|
||||
"An unexpected error occurred.": "Виникла непередбачувана помилка.",
|
||||
"The request was cancelled.": "Запит було скасовано.",
|
||||
"The other device isn't signed in.": "На іншому пристрої вхід не виконано.",
|
||||
"The other device is already signed in.": "На іншому пристрої вхід було виконано.",
|
||||
"The request was declined on the other device.": "На іншому пристрої запит відхилено.",
|
||||
"Linking with this device is not supported.": "Зв'язок з цим пристроєм не підтримується.",
|
||||
"The scanned code is invalid.": "Сканований код недійсний.",
|
||||
"The linking wasn't completed in the required time.": "У встановлені терміни з'єднання не було виконано.",
|
||||
"Sign in new device": "Увійти на новому пристрої",
|
||||
"Show QR code": "Показати QR-код",
|
||||
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Ви можете використовувати цей пристрій для входу на новому пристрої за допомогою QR-коду. Вам потрібно буде сканувати QR-код, показаний на цьому пристрої, своїм пристроєм, на якому ви вийшли."
|
||||
}
|
||||
|
|
|
@ -43,6 +43,8 @@ import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widge
|
|||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
|
||||
import PlatformPeg from "../PlatformPeg";
|
||||
import { getCurrentLanguage } from "../languageHandler";
|
||||
import DesktopCapturerSourcePicker from "../components/views/elements/DesktopCapturerSourcePicker";
|
||||
import Modal from "../Modal";
|
||||
|
||||
const TIMEOUT_MS = 16000;
|
||||
|
||||
|
@ -639,10 +641,6 @@ export class ElementCall extends Call {
|
|||
baseUrl: client.baseUrl,
|
||||
lang: getCurrentLanguage().replace("_", "-"),
|
||||
});
|
||||
// Currently, the screen-sharing support is the same is it is for Jitsi
|
||||
if (!PlatformPeg.get().supportsJitsiScreensharing()) {
|
||||
params.append("hideScreensharing", "");
|
||||
}
|
||||
url.hash = `#?${params.toString()}`;
|
||||
|
||||
// To use Element Call without touching room state, we create a virtual
|
||||
|
@ -818,6 +816,7 @@ export class ElementCall extends Call {
|
|||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare);
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
|
@ -831,8 +830,9 @@ export class ElementCall extends Call {
|
|||
public setDisconnected() {
|
||||
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout);
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
|
@ -951,4 +951,20 @@ export class ElementCall extends Call {
|
|||
this.layout = Layout.Spotlight;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private onScreenshare = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (PlatformPeg.get().supportsDesktopCapturer()) {
|
||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
const [source] = await finished;
|
||||
|
||||
await this.messaging!.transport.reply(ev.detail, {
|
||||
failed: !source,
|
||||
desktopCapturerSourceId: source,
|
||||
});
|
||||
} else {
|
||||
await this.messaging!.transport.reply(ev.detail, {});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { SdkContextClass } from "../contexts/SDKContext";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { isLocalRoom } from "../utils/localRoom/isLocalRoom";
|
||||
import Timer from "../utils/Timer";
|
||||
|
@ -34,17 +34,10 @@ export default class TypingStore {
|
|||
};
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(private readonly context: SdkContextClass) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public static sharedInstance(): TypingStore {
|
||||
if (window.mxTypingStore === undefined) {
|
||||
window.mxTypingStore = new TypingStore();
|
||||
}
|
||||
return window.mxTypingStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached typing states. Intended to be called when the
|
||||
* MatrixClientPeg client changes.
|
||||
|
@ -108,6 +101,6 @@ export default class TypingStore {
|
|||
} else currentTyping.userTimer.restart();
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendTyping(roomId, isTyping, TYPING_SERVER_TIMEOUT);
|
||||
this.context.client?.sendTyping(roomId, isTyping, TYPING_SERVER_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export enum ElementWidgetActions {
|
|||
// Actions for switching layouts
|
||||
TileLayout = "io.element.tile_layout",
|
||||
SpotlightLayout = "io.element.spotlight_layout",
|
||||
Screenshare = "io.element.screenshare",
|
||||
|
||||
OpenIntegrationManager = "integration_manager_open",
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ import Modal from "../../Modal";
|
|||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
|
||||
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
|
||||
import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore";
|
||||
import { OIDCState } from "./WidgetPermissionStore";
|
||||
import { WidgetType } from "../../widgets/WidgetType";
|
||||
import { CHAT_EFFECTS } from "../../effects";
|
||||
import { containsEmoji } from "../../effects/utils";
|
||||
|
@ -350,7 +350,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
}
|
||||
|
||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
||||
const oidcState = WidgetPermissionStore.instance.getOIDCState(
|
||||
const oidcState = SdkContextClass.instance.widgetPermissionStore.getOIDCState(
|
||||
this.forWidget, this.forWidgetKind, this.inRoomId,
|
||||
);
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
import { Widget, WidgetKind } from "matrix-widget-api";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
|
||||
export enum OIDCState {
|
||||
Allowed, // user has set the remembered value as allowed
|
||||
|
@ -27,16 +27,7 @@ export enum OIDCState {
|
|||
}
|
||||
|
||||
export class WidgetPermissionStore {
|
||||
private static internalInstance: WidgetPermissionStore;
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
public static get instance(): WidgetPermissionStore {
|
||||
if (!WidgetPermissionStore.internalInstance) {
|
||||
WidgetPermissionStore.internalInstance = new WidgetPermissionStore();
|
||||
}
|
||||
return WidgetPermissionStore.internalInstance;
|
||||
public constructor(private readonly context: SdkContextClass) {
|
||||
}
|
||||
|
||||
// TODO (all functions here): Merge widgetKind with the widget definition
|
||||
|
@ -44,7 +35,7 @@ export class WidgetPermissionStore {
|
|||
private packSettingKey(widget: Widget, kind: WidgetKind, roomId?: string): string {
|
||||
let location = roomId;
|
||||
if (kind !== WidgetKind.Room) {
|
||||
location = MatrixClientPeg.get().getUserId();
|
||||
location = this.context.client?.getUserId();
|
||||
}
|
||||
if (kind === WidgetKind.Modal) {
|
||||
location = '*MODAL*-' + location; // to guarantee differentiation from whatever spawned it
|
||||
|
@ -71,7 +62,10 @@ export class WidgetPermissionStore {
|
|||
public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState) {
|
||||
const settingsKey = this.packSettingKey(widget, kind, roomId);
|
||||
|
||||
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
let currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (!currentValues) {
|
||||
currentValues = {};
|
||||
}
|
||||
if (!currentValues.allow) currentValues.allow = [];
|
||||
if (!currentValues.deny) currentValues.deny = [];
|
||||
|
||||
|
|
|
@ -17,10 +17,11 @@ limitations under the License.
|
|||
import { Optional } from "matrix-events-sdk";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||
|
||||
import { getChunkLength } from "..";
|
||||
import { VoiceRecording } from "../../audio/VoiceRecording";
|
||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
||||
import { concat } from "../../utils/arrays";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { Singleflight } from "../../utils/Singleflight";
|
||||
|
||||
export enum VoiceBroadcastRecorderEvent {
|
||||
ChunkRecorded = "chunk_recorded",
|
||||
|
@ -65,6 +66,8 @@ export class VoiceBroadcastRecorder
|
|||
*/
|
||||
public async stop(): Promise<Optional<ChunkRecordedPayload>> {
|
||||
await this.voiceRecording.stop();
|
||||
// forget about that call, so that we can stop it again later
|
||||
Singleflight.forgetAllFor(this.voiceRecording);
|
||||
return this.extractChunk();
|
||||
}
|
||||
|
||||
|
@ -136,6 +139,5 @@ export class VoiceBroadcastRecorder
|
|||
}
|
||||
|
||||
export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => {
|
||||
const targetChunkLength = SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast!.chunk_length;
|
||||
return new VoiceBroadcastRecorder(new VoiceRecording(), targetChunkLength);
|
||||
return new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength());
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
|
|||
client,
|
||||
);
|
||||
relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent);
|
||||
relationsHelper.emitCurrent();
|
||||
|
||||
return () => {
|
||||
relationsHelper.destroy();
|
||||
|
|
|
@ -16,12 +16,12 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
|
||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export const LiveBadge: React.FC = () => {
|
||||
return <div className="mx_LiveBadge">
|
||||
<Icon type={IconType.Live} colour={IconColour.LiveBadge} />
|
||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||
{ _t("Live") }
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { VoiceBroadcastPlaybackState } from "../..";
|
||||
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const stateIconMap = new Map([
|
||||
[VoiceBroadcastPlaybackState.Playing, IconType.Pause],
|
||||
[VoiceBroadcastPlaybackState.Paused, IconType.Play],
|
||||
[VoiceBroadcastPlaybackState.Stopped, IconType.Play],
|
||||
]);
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
state: VoiceBroadcastPlaybackState;
|
||||
}
|
||||
|
||||
export const PlaybackControlButton: React.FC<Props> = ({
|
||||
onClick,
|
||||
state,
|
||||
}) => {
|
||||
const ariaLabel = state === VoiceBroadcastPlaybackState.Playing
|
||||
? _t("pause voice broadcast")
|
||||
: _t("resume voice broadcast");
|
||||
|
||||
return <AccessibleButton
|
||||
className="mx_BroadcastPlaybackControlButton"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<Icon
|
||||
colour={IconColour.CompoundSecondaryContent}
|
||||
type={stateIconMap.get(state)}
|
||||
/>
|
||||
</AccessibleButton>;
|
||||
};
|
|
@ -14,27 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const StopButton: React.FC<Props> = ({
|
||||
export const VoiceBroadcastControl: React.FC<Props> = ({
|
||||
className = "",
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
}) => {
|
||||
return <AccessibleButton
|
||||
className="mx_BroadcastPlaybackControlButton"
|
||||
className={classNames("mx_VoiceBroadcastControl", className)}
|
||||
onClick={onClick}
|
||||
aria-label={_t("stop voice broadcast")}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon
|
||||
colour={IconColour.CompoundSecondaryContent}
|
||||
type={IconType.Stop}
|
||||
/>
|
||||
<Icon className="mx_Icon mx_Icon_16" />
|
||||
</AccessibleButton>;
|
||||
};
|
|
@ -15,7 +15,8 @@ import React from "react";
|
|||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LiveBadge } from "../..";
|
||||
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
|
||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||
import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
|
||||
|
||||
|
@ -34,7 +35,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
|||
}) => {
|
||||
const broadcast = showBroadcast
|
||||
? <div className="mx_VoiceBroadcastHeader_line">
|
||||
<Icon type={IconType.Live} colour={IconColour.CompoundSecondaryContent} />
|
||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||
{ _t("Voice broadcast") }
|
||||
</div>
|
||||
: null;
|
||||
|
@ -46,7 +47,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
|||
{ room.name }
|
||||
</div>
|
||||
<div className="mx_VoiceBroadcastHeader_line">
|
||||
<Icon type={IconType.Microphone} colour={IconColour.CompoundSecondaryContent} />
|
||||
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
|
||||
<span>{ sender.name }</span>
|
||||
</div>
|
||||
{ broadcast }
|
||||
|
|
|
@ -17,13 +17,16 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import {
|
||||
PlaybackControlButton,
|
||||
VoiceBroadcastControl,
|
||||
VoiceBroadcastHeader,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybackState,
|
||||
} from "../..";
|
||||
import Spinner from "../../../components/views/elements/Spinner";
|
||||
import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback";
|
||||
import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg";
|
||||
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface VoiceBroadcastPlaybackBodyProps {
|
||||
playback: VoiceBroadcastPlayback;
|
||||
|
@ -40,9 +43,35 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
|||
playbackState,
|
||||
} = useVoiceBroadcastPlayback(playback);
|
||||
|
||||
const control = playbackState === VoiceBroadcastPlaybackState.Buffering
|
||||
? <Spinner />
|
||||
: <PlaybackControlButton onClick={toggle} state={playbackState} />;
|
||||
let control: React.ReactNode;
|
||||
|
||||
if (playbackState === VoiceBroadcastPlaybackState.Buffering) {
|
||||
control = <Spinner />;
|
||||
} else {
|
||||
let controlIcon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
let controlLabel: string;
|
||||
|
||||
switch (playbackState) {
|
||||
case VoiceBroadcastPlaybackState.Stopped:
|
||||
controlIcon = PlayIcon;
|
||||
controlLabel = _t("play voice broadcast");
|
||||
break;
|
||||
case VoiceBroadcastPlaybackState.Paused:
|
||||
controlIcon = PlayIcon;
|
||||
controlLabel = _t("resume voice broadcast");
|
||||
break;
|
||||
case VoiceBroadcastPlaybackState.Playing:
|
||||
controlIcon = PauseIcon;
|
||||
controlLabel = _t("pause voice broadcast");
|
||||
break;
|
||||
}
|
||||
|
||||
control = <VoiceBroadcastControl
|
||||
label={controlLabel}
|
||||
icon={controlIcon}
|
||||
onClick={toggle}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_VoiceBroadcastPlaybackBody">
|
||||
|
|
|
@ -17,11 +17,16 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import {
|
||||
StopButton,
|
||||
VoiceBroadcastControl,
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecording,
|
||||
} from "../..";
|
||||
import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording";
|
||||
import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader";
|
||||
import { Icon as StopIcon } from "../../../../res/img/element-icons/Stop.svg";
|
||||
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
|
||||
import { Icon as RecordIcon } from "../../../../res/img/element-icons/Record.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface VoiceBroadcastRecordingPipProps {
|
||||
recording: VoiceBroadcastRecording;
|
||||
|
@ -30,11 +35,22 @@ interface VoiceBroadcastRecordingPipProps {
|
|||
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
|
||||
const {
|
||||
live,
|
||||
sender,
|
||||
recordingState,
|
||||
room,
|
||||
sender,
|
||||
stopRecording,
|
||||
toggleRecording,
|
||||
} = useVoiceBroadcastRecording(recording);
|
||||
|
||||
const toggleControl = recordingState === VoiceBroadcastInfoState.Paused
|
||||
? <VoiceBroadcastControl
|
||||
className="mx_VoiceBroadcastControl-recording"
|
||||
onClick={toggleRecording}
|
||||
icon={RecordIcon}
|
||||
label={_t("resume voice broadcast")}
|
||||
/>
|
||||
: <VoiceBroadcastControl onClick={toggleRecording} icon={PauseIcon} label={_t("pause voice broadcast")} />;
|
||||
|
||||
return <div
|
||||
className="mx_VoiceBroadcastRecordingPip"
|
||||
>
|
||||
|
@ -45,7 +61,12 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
|||
/>
|
||||
<hr className="mx_VoiceBroadcastRecordingPip_divider" />
|
||||
<div className="mx_VoiceBroadcastRecordingPip_controls">
|
||||
<StopButton onClick={stopRecording} />
|
||||
{ toggleControl }
|
||||
<VoiceBroadcastControl
|
||||
icon={StopIcon}
|
||||
label="Stop Recording"
|
||||
onClick={stopRecording}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingEvent,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "..";
|
||||
import QuestionDialog from "../../components/views/dialogs/QuestionDialog";
|
||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||
|
@ -53,24 +52,31 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
|
|||
const confirmed = await showStopBroadcastingDialog();
|
||||
|
||||
if (confirmed) {
|
||||
recording.stop();
|
||||
VoiceBroadcastRecordingsStore.instance().clearCurrent();
|
||||
await recording.stop();
|
||||
}
|
||||
};
|
||||
|
||||
const [live, setLive] = useState(recording.getState() === VoiceBroadcastInfoState.Started);
|
||||
const [recordingState, setRecordingState] = useState(recording.getState());
|
||||
useTypedEventEmitter(
|
||||
recording,
|
||||
VoiceBroadcastRecordingEvent.StateChanged,
|
||||
(state: VoiceBroadcastInfoState, _recording: VoiceBroadcastRecording) => {
|
||||
setLive(state === VoiceBroadcastInfoState.Started);
|
||||
setRecordingState(state);
|
||||
},
|
||||
);
|
||||
|
||||
const live = [
|
||||
VoiceBroadcastInfoState.Started,
|
||||
VoiceBroadcastInfoState.Paused,
|
||||
VoiceBroadcastInfoState.Running,
|
||||
].includes(recordingState);
|
||||
|
||||
return {
|
||||
live,
|
||||
recordingState,
|
||||
room,
|
||||
sender: recording.infoEvent.sender,
|
||||
stopRecording,
|
||||
toggleRecording: recording.toggle,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,8 +26,7 @@ export * from "./models/VoiceBroadcastRecording";
|
|||
export * from "./audio/VoiceBroadcastRecorder";
|
||||
export * from "./components/VoiceBroadcastBody";
|
||||
export * from "./components/atoms/LiveBadge";
|
||||
export * from "./components/atoms/PlaybackControlButton";
|
||||
export * from "./components/atoms/StopButton";
|
||||
export * from "./components/atoms/VoiceBroadcastControl";
|
||||
export * from "./components/atoms/VoiceBroadcastHeader";
|
||||
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
||||
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
||||
|
@ -35,10 +34,14 @@ export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
|||
export * from "./hooks/useVoiceBroadcastRecording";
|
||||
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
||||
export * from "./stores/VoiceBroadcastRecordingsStore";
|
||||
export * from "./utils/getChunkLength";
|
||||
export * from "./utils/hasRoomLiveVoiceBroadcast";
|
||||
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
|
||||
export * from "./utils/resumeVoiceBroadcastInRoom";
|
||||
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
|
||||
export * from "./utils/shouldDisplayAsVoiceBroadcastTile";
|
||||
export * from "./utils/startNewVoiceBroadcastRecording";
|
||||
export * from "./utils/VoiceBroadcastResumer";
|
||||
|
||||
export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info";
|
||||
export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk";
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||
|
||||
import {
|
||||
|
@ -52,9 +52,23 @@ export class VoiceBroadcastRecording
|
|||
public constructor(
|
||||
public readonly infoEvent: MatrixEvent,
|
||||
private client: MatrixClient,
|
||||
initialState?: VoiceBroadcastInfoState,
|
||||
) {
|
||||
super();
|
||||
|
||||
if (initialState) {
|
||||
this.state = initialState;
|
||||
} else {
|
||||
this.setInitialStateFromInfoEvent();
|
||||
}
|
||||
|
||||
// TODO Michael W: listen for state updates
|
||||
//
|
||||
this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
private setInitialStateFromInfoEvent(): void {
|
||||
const room = this.client.getRoom(this.infoEvent.getRoomId());
|
||||
const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
|
||||
this.infoEvent.getId(),
|
||||
|
@ -65,9 +79,6 @@ export class VoiceBroadcastRecording
|
|||
this.state = !relatedEvents?.find((event: MatrixEvent) => {
|
||||
return event.getContent()?.state === VoiceBroadcastInfoState.Stopped;
|
||||
}) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped;
|
||||
// TODO Michael W: add listening for updates
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
|
@ -75,11 +86,38 @@ export class VoiceBroadcastRecording
|
|||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
if (this.state === VoiceBroadcastInfoState.Stopped) return;
|
||||
|
||||
this.setState(VoiceBroadcastInfoState.Stopped);
|
||||
await this.stopRecorder();
|
||||
await this.sendStoppedStateEvent();
|
||||
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped);
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
// stopped or already paused recordings cannot be paused
|
||||
if ([VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused].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.Running);
|
||||
await this.getRecorder().start();
|
||||
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Running);
|
||||
}
|
||||
|
||||
public toggle = async (): Promise<void> => {
|
||||
if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume();
|
||||
|
||||
if ([VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Running].includes(this.getState())) {
|
||||
return this.pause();
|
||||
}
|
||||
};
|
||||
|
||||
public getState(): VoiceBroadcastInfoState {
|
||||
return this.state;
|
||||
}
|
||||
|
@ -99,10 +137,19 @@ export class VoiceBroadcastRecording
|
|||
this.recorder.stop();
|
||||
}
|
||||
|
||||
this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
this.removeAllListeners();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onBeforeRedaction = () => {
|
||||
if (this.getState() !== VoiceBroadcastInfoState.Stopped) {
|
||||
this.setState(VoiceBroadcastInfoState.Stopped);
|
||||
// destroy cleans up everything
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action !== "call_state") return;
|
||||
|
||||
|
@ -152,14 +199,14 @@ export class VoiceBroadcastRecording
|
|||
await this.client.sendMessage(this.infoEvent.getRoomId(), content);
|
||||
}
|
||||
|
||||
private async sendStoppedStateEvent(): Promise<void> {
|
||||
private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise<void> {
|
||||
// TODO Michael W: add error handling for state event
|
||||
await this.client.sendStateEvent(
|
||||
this.infoEvent.getRoomId(),
|
||||
VoiceBroadcastInfoEventType,
|
||||
{
|
||||
device_id: this.client.getDeviceId(),
|
||||
state: VoiceBroadcastInfoState.Stopped,
|
||||
state,
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: this.infoEvent.getId(),
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||
|
||||
import { VoiceBroadcastRecording } from "..";
|
||||
import { VoiceBroadcastInfoState, VoiceBroadcastRecording, VoiceBroadcastRecordingEvent } from "..";
|
||||
|
||||
export enum VoiceBroadcastRecordingsStoreEvent {
|
||||
CurrentChanged = "current_changed",
|
||||
|
@ -41,7 +41,12 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
|
|||
public setCurrent(current: VoiceBroadcastRecording): void {
|
||||
if (this.current === current) return;
|
||||
|
||||
if (this.current) {
|
||||
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
||||
}
|
||||
|
||||
this.current = current;
|
||||
this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
||||
this.recordings.set(current.infoEvent.getId(), current);
|
||||
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current);
|
||||
}
|
||||
|
@ -51,8 +56,9 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
|
|||
}
|
||||
|
||||
public clearCurrent(): void {
|
||||
if (this.current === null) return;
|
||||
if (!this.current) return;
|
||||
|
||||
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
|
||||
this.current = null;
|
||||
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null);
|
||||
}
|
||||
|
@ -67,6 +73,12 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
|
|||
return this.recordings.get(infoEventId);
|
||||
}
|
||||
|
||||
private onCurrentStateChanged = (state: VoiceBroadcastInfoState) => {
|
||||
if (state === VoiceBroadcastInfoState.Stopped) {
|
||||
this.clearCurrent();
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly cachedInstance = new VoiceBroadcastRecordingsStore();
|
||||
|
||||
/**
|
||||
|
|
56
src/voice-broadcast/utils/VoiceBroadcastResumer.ts
Normal file
56
src/voice-broadcast/utils/VoiceBroadcastResumer.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice";
|
||||
import { resumeVoiceBroadcastInRoom } from "./resumeVoiceBroadcastInRoom";
|
||||
|
||||
export class VoiceBroadcastResumer implements IDestroyable {
|
||||
private seenRooms = new Set<string>();
|
||||
private userId: string;
|
||||
private deviceId: string;
|
||||
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
) {
|
||||
this.client.on(ClientEvent.Room, this.onRoom);
|
||||
this.userId = this.client.getUserId();
|
||||
this.deviceId = this.client.getDeviceId();
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
if (this.seenRooms.has(room.roomId)) return;
|
||||
|
||||
this.seenRooms.add(room.roomId);
|
||||
|
||||
const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(
|
||||
room,
|
||||
this.userId,
|
||||
this.deviceId,
|
||||
);
|
||||
|
||||
if (infoEvent) {
|
||||
resumeVoiceBroadcastInRoom(infoEvent, room, this.client);
|
||||
}
|
||||
};
|
||||
|
||||
destroy(): void {
|
||||
this.client.off(ClientEvent.Room, this.onRoom);
|
||||
this.seenRooms = new Set<string>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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;
|
||||
};
|
29
src/voice-broadcast/utils/getChunkLength.ts
Normal file
29
src/voice-broadcast/utils/getChunkLength.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
||||
|
||||
/**
|
||||
* Returns the target chunk length for voice broadcasts:
|
||||
* - Tries 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 => {
|
||||
return SdkConfig.get("voice_broadcast")?.chunk_length
|
||||
|| DEFAULTS.voice_broadcast?.chunk_length
|
||||
|| 120;
|
||||
};
|
34
src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts
Normal file
34
src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "..";
|
||||
import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore";
|
||||
|
||||
export const resumeVoiceBroadcastInRoom = (latestInfoEvent: MatrixEvent, room: Room, client: MatrixClient) => {
|
||||
// voice broadcasts are based on their started event, try to find it
|
||||
const infoEvent = latestInfoEvent.getContent()?.state === VoiceBroadcastInfoState.Started
|
||||
? latestInfoEvent
|
||||
: room.findEventById(latestInfoEvent.getRelation()?.event_id);
|
||||
|
||||
if (!infoEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Paused);
|
||||
VoiceBroadcastRecordingsStore.instance().setCurrent(recording);
|
||||
};
|
|
@ -28,6 +28,7 @@ import {
|
|||
VoiceBroadcastRecordingsStore,
|
||||
VoiceBroadcastRecording,
|
||||
hasRoomLiveVoiceBroadcast,
|
||||
getChunkLength,
|
||||
} from "..";
|
||||
|
||||
const startBroadcast = async (
|
||||
|
@ -67,7 +68,7 @@ const startBroadcast = async (
|
|||
{
|
||||
device_id: client.getDeviceId(),
|
||||
state: VoiceBroadcastInfoState.Started,
|
||||
chunk_length: 300,
|
||||
chunk_length: getChunkLength(),
|
||||
} as VoiceBroadcastInfoEventContent,
|
||||
client.getUserId(),
|
||||
);
|
||||
|
@ -113,6 +114,11 @@ export const startNewVoiceBroadcastRecording = async (
|
|||
client: MatrixClient,
|
||||
recordingsStore: VoiceBroadcastRecordingsStore,
|
||||
): Promise<VoiceBroadcastRecording | null> => {
|
||||
if (recordingsStore.getCurrent()) {
|
||||
showAlreadyRecordingDialog();
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentUserId = client.getUserId();
|
||||
|
||||
if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue