Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode

This commit is contained in:
Florian Duros 2022-12-05 17:40:46 +01:00
commit 54e12d265b
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
139 changed files with 2830 additions and 3202 deletions

View file

@ -149,14 +149,10 @@ declare global {
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
interface OffscreenCanvas {
height: number;
width: number;
getContext: HTMLCanvasElement["getContext"];
convertToBlob(opts?: {
type?: string;
quality?: number;
}): Promise<Blob>;
transferToImageBitmap(): ImageBitmap;
}
interface HTMLAudioElement {

View file

@ -174,12 +174,12 @@ export class DecryptionFailureTracker {
* Start checking for and tracking failures.
*/
public start(): void {
this.checkInterval = setInterval(
this.checkInterval = window.setInterval(
() => this.checkFailures(Date.now()),
DecryptionFailureTracker.CHECK_INTERVAL_MS,
);
this.trackInterval = setInterval(
this.trackInterval = window.setInterval(
() => this.trackFailures(),
DecryptionFailureTracker.TRACK_INTERVAL_MS,
);

View file

@ -47,6 +47,7 @@ import {
removeClientInformation,
} from "./utils/device/clientInformation";
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
import { UIFeature } from "./settings/UIFeature";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
@ -68,6 +69,7 @@ export default class DeviceListener {
private displayingToastsForDeviceIds = new Set<string>();
private running = false;
private shouldRecordClientInformation = false;
private enableBulkUnverifiedSessionsReminder = true;
private deviceClientInformationSettingWatcherRef: string | undefined;
public static sharedInstance() {
@ -86,6 +88,8 @@ export default class DeviceListener {
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn');
// only configurable in config, so we don't need to watch the value
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting(
'deviceClientInformationOptIn',
null,
@ -306,6 +310,9 @@ export default class DeviceListener {
// Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set<string>();
const isCurrentDeviceTrusted = crossSigningReady &&
await (cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!)).isCrossSigningVerified();
// as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts
if (crossSigningReady) {
@ -313,7 +320,7 @@ export default class DeviceListener {
for (const device of devices) {
if (device.deviceId === cli.deviceId) continue;
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!);
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId);
@ -329,7 +336,12 @@ export default class DeviceListener {
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
// Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) {
// don't show the toast if the current device is unverified
if (
oldUnverifiedDeviceIds.size > 0
&& isCurrentDeviceTrusted
&& this.enableBulkUnverifiedSessionsReminder
) {
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
} else {
hideBulkUnverifiedSessionsToast();

View file

@ -24,7 +24,7 @@ import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex';
import { split } from 'lodash';
import katex from 'katex';
import { AllHtmlEntities } from 'html-entities';
import { decode } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event';
import { Optional } from 'matrix-events-sdk';
@ -518,7 +518,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
return katex.renderToString(
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
decode(phtml(e).attr('data-mx-maths')),
{
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node

View file

@ -71,13 +71,52 @@ export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
const CHECK_PROTOCOLS_ATTEMPTS = 3;
enum AudioID {
type MediaEventType = keyof HTMLMediaElementEventMap;
const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [
'error',
// The media has become empty; for example, this event is sent if the media has
// already been loaded (or partially loaded), and the HTMLMediaElement.load method
// is called to reload it.
'emptied',
// The user agent is trying to fetch media data, but data is unexpectedly not
// forthcoming.
'stalled',
// Media data loading has been suspended.
'suspend',
// Playback has stopped because of a temporary lack of data
'waiting',
];
const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [
'play',
'pause',
'playing',
'ended',
'loadeddata',
'loadedmetadata',
'canplay',
'canplaythrough',
'volumechange',
];
const MEDIA_EVENT_TYPES = [
...MEDIA_ERROR_EVENT_TYPES,
...MEDIA_DEBUG_EVENT_TYPES,
];
export enum AudioID {
Ring = 'ringAudio',
Ringback = 'ringbackAudio',
CallEnd = 'callendAudio',
Busy = 'busyAudio',
}
/* istanbul ignore next */
const debuglog = (...args: any[]): void => {
if (SettingsStore.getValue("debug_legacy_call_handler")) {
logger.log.call(console, "LegacyCallHandler debuglog:", ...args);
}
};
interface ThirdpartyLookupResponseFields {
/* eslint-disable camelcase */
@ -119,6 +158,7 @@ export default class LegacyCallHandler extends EventEmitter {
// call with a different party to this one.
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
private audioPromises = new Map<AudioID, Promise<void>>();
private audioElementsWithListeners = new Map<HTMLMediaElement, boolean>();
private supportsPstnProtocol = null;
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
@ -176,6 +216,16 @@ export default class LegacyCallHandler extends EventEmitter {
}
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
// Add event listeners for the <audio> elements
Object.values(AudioID).forEach((audioId) => {
const audioElement = document.getElementById(audioId) as HTMLMediaElement;
if (audioElement) {
this.addEventListenersForAudioElement(audioElement);
} else {
logger.warn(`LegacyCallHandler: missing <audio id="${audioId}"> from page`);
}
});
}
public stop(): void {
@ -183,6 +233,39 @@ export default class LegacyCallHandler extends EventEmitter {
if (cli) {
cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming);
}
// Remove event listeners for the <audio> elements
Array.from(this.audioElementsWithListeners.keys()).forEach((audioElement) => {
this.removeEventListenersForAudioElement(audioElement);
});
}
private addEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
// Only need to setup the listeners once
if (!this.audioElementsWithListeners.get(audioElement)) {
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
audioElement.addEventListener(errorEventType, this);
this.audioElementsWithListeners.set(audioElement, true);
});
}
}
private removeEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
audioElement.removeEventListener(errorEventType, this);
});
}
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
public handleEvent(e: Event): void {
const target = e.target as HTMLElement;
const audioId = target?.id;
if (MEDIA_ERROR_EVENT_TYPES.includes(e.type as MediaEventType)) {
logger.error(`LegacyCallHandler: encountered "${e.type}" event with <audio id="${audioId}">`, e);
} else if (MEDIA_EVENT_TYPES.includes(e.type as MediaEventType)) {
debuglog(`encountered "${e.type}" event with <audio id="${audioId}">`, e);
}
}
public isForcedSilent(): boolean {
@ -254,7 +337,7 @@ export default class LegacyCallHandler extends EventEmitter {
logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
} else {
logger.log("Failed to check for protocol support: will retry", e);
setTimeout(() => {
window.setTimeout(() => {
this.checkProtocols(maxTries - 1);
}, 10000);
}
@ -402,11 +485,21 @@ export default class LegacyCallHandler extends EventEmitter {
// which listens?
const audio = document.getElementById(audioId) as HTMLMediaElement;
if (audio) {
this.addEventListenersForAudioElement(audio);
const playAudio = async () => {
try {
if (audio.muted) {
logger.error(
`${logPrefix} <audio> element was unexpectedly muted but we recovered ` +
`gracefully by unmuting it`,
);
// Recover gracefully
audio.muted = false;
}
// This still causes the chrome debugger to break on promise rejection if
// the promise is rejected, even though we're catching the exception.
logger.debug(`${logPrefix} attempting to play audio`);
logger.debug(`${logPrefix} attempting to play audio at volume=${audio.volume}`);
await audio.play();
logger.debug(`${logPrefix} playing audio successfully`);
} catch (e) {

View file

@ -584,7 +584,7 @@ async function doSetLoggedIn(
// later than MatrixChat might assume.
//
// we fire it *synchronously* to make sure it fires before on_logged_in.
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
// (dis.dispatch uses `window.setTimeout`, which does not guarantee ordering.)
dis.dispatch({ action: 'on_logging_in' }, true);
if (clearStorageEnabled) {
@ -865,7 +865,7 @@ export async function onLoggedOut(): Promise<void> {
if (SdkConfig.get().logout_redirect_url) {
logger.log("Redirecting to external provider to finish logout");
// XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login
setTimeout(() => {
window.setTimeout(() => {
window.location.href = SdkConfig.get().logout_redirect_url;
}, 100);
}

View file

@ -119,7 +119,7 @@ export default class NodeAnimator extends React.Component<IProps> {
}
// and then we animate to the resting state
setTimeout(() => {
window.setTimeout(() => {
this.applyStyles(domNode as HTMLElement, restingStyle);
}, 0);
}

View file

@ -27,6 +27,7 @@ import {
PermissionChanged as PermissionChangedEvent,
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { IRoomTimelineData } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from './MatrixClientPeg';
import { PosthogAnalytics } from "./PosthogAnalytics";
@ -217,7 +218,7 @@ export const Notifier = {
this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this);
this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this);
MatrixClientPeg.get().on(ClientEvent.Event, this.boundOnEvent);
MatrixClientPeg.get().on(RoomEvent.Timeline, this.boundOnEvent);
MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt);
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange);
@ -227,7 +228,7 @@ export const Notifier = {
stop: function(this: typeof Notifier) {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener(ClientEvent.Event, this.boundOnEvent);
MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.boundOnEvent);
MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange);
@ -368,7 +369,15 @@ export const Notifier = {
}
},
onEvent: function(this: typeof Notifier, ev: MatrixEvent) {
onEvent: function(
this: typeof Notifier,
ev: MatrixEvent,
room: Room | undefined,
toStartOfTimeline: boolean | undefined,
removed: boolean,
data: IRoomTimelineData,
) {
if (!data.liveEvent) return; // only notify for new things, not old.
if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
@ -428,6 +437,11 @@ export const Notifier = {
}
}
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
// e.g we are in the process of joining a room.
// Seen in the cypress lazy-loading test.
return;
}
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);

View file

@ -119,7 +119,7 @@ export default class PasswordReset {
this.checkEmailLinkClicked()
.then(() => resolve())
.catch(() => {
setTimeout(
window.setTimeout(
() => this.tryCheckEmailLinkClicked(resolve),
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
);

View file

@ -127,7 +127,7 @@ export class PlaybackClock implements IDestroyable {
// cast to number because the types are wrong
// 100ms interval to make sure the time is as accurate as possible without
// being overly insane
this.timerId = <number><any>setInterval(this.checkTime, 100);
this.timerId = <number><any>window.setInterval(this.checkTime, 100);
}
}

View file

@ -85,4 +85,4 @@ class MxVoiceWorklet extends AudioWorkletProcessor {
registerProcessor(WORKLET_NAME, MxVoiceWorklet);
export default null; // to appease module loaders (we never use the export)
export default ""; // to appease module loaders (we never use the export)

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Recorder from 'opus-recorder';
// @ts-ignore
import Recorder from 'opus-recorder/dist/recorder.min.js';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
import { SimpleObservable } from "matrix-widget-api";
import EventEmitter from "events";
@ -32,12 +33,26 @@ import mxRecorderWorkletPath from "./RecorderWorklet";
const CHANNELS = 1; // stereo isn't important
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus.
const TARGET_MAX_LENGTH = 900; // 15 minutes in seconds. Somewhat arbitrary, though longer == larger files.
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
export const RECORDING_PLAYBACK_SAMPLES = 44;
interface RecorderOptions {
bitrate: number;
encoderApplication: number;
}
export const voiceRecorderOptions: RecorderOptions = {
bitrate: 24000, // recommended Opus bitrate for high-quality VoIP
encoderApplication: 2048, // voice
};
export const highQualityRecorderOptions: RecorderOptions = {
bitrate: 96000, // recommended Opus bitrate for high-quality music/audio streaming
encoderApplication: 2049, // full band audio
};
export interface IRecordingUpdate {
waveform: number[]; // floating points between 0 (low) and 1 (high).
timeSeconds: number; // float
@ -88,13 +103,22 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.targetMaxLength = null;
}
private shouldRecordInHighQuality(): boolean {
// Non-voice use case is suspected when noise suppression is disabled by the user.
// When recording complex audio, higher quality is required to avoid audio artifacts.
// This is a really arbitrary decision, but it can be refined/replaced at any time.
return !MediaDeviceHandler.getAudioNoiseSuppression();
}
private async makeRecorder() {
try {
this.recorderStream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: CHANNELS,
noiseSuppression: true, // browsers ignore constraints they can't honour
deviceId: MediaDeviceHandler.getAudioInput(),
autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() },
echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() },
noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() },
},
});
this.recorderContext = createAudioContext({
@ -135,15 +159,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
}
const recorderOptions = this.shouldRecordInHighQuality() ?
highQualityRecorderOptions : voiceRecorderOptions;
const { encoderApplication, bitrate } = recorderOptions;
this.recorder = new Recorder({
encoderPath, // magic from webpack
encoderSampleRate: SAMPLE_RATE,
encoderApplication: 2048, // voice (default is "audio")
encoderApplication: encoderApplication,
streamPages: true, // this speeds up the encoding process by using CPU over time
encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder
numberOfChannels: CHANNELS,
sourceNode: this.recorderSource,
encoderBitRate: BITRATE,
encoderBitRate: bitrate,
// We use low values for the following to ease CPU usage - the resulting waveform
// is indistinguishable for a voice message. Note that the underlying library will

View file

@ -35,7 +35,7 @@ export interface ISelectionRange {
}
export interface ICompletion {
type: "at-room" | "command" | "community" | "room" | "user";
type?: "at-room" | "command" | "community" | "room" | "user";
completion: string;
completionId?: string;
component?: ReactElement;

View file

@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
return []; // don't give any suggestions if the user doesn't want them
}
let completions = [];
let completions: ISortedEmoji[] = [];
const { command, range } = this.getCurrentCommand(query, selection);
if (command && command[0].length > 2) {
@ -132,7 +132,7 @@ export default class EmojiProvider extends AutocompleteProvider {
}
// Finally, sort by original ordering
sorters.push(c => c._orderBy);
completions = sortBy(uniq(completions), sorters);
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
completions = completions.slice(0, LIMIT);
@ -141,9 +141,9 @@ export default class EmojiProvider extends AutocompleteProvider {
this.recentlyUsed.forEach(emoji => {
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
});
completions = sortBy(uniq(completions), sorters);
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
completions = completions.map(c => ({
return completions.map(c => ({
completion: c.emoji.unicode,
component: (
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
@ -153,7 +153,7 @@ export default class EmojiProvider extends AutocompleteProvider {
range,
}));
}
return completions;
return [];
}
getName() {

View file

@ -127,7 +127,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
});
if (this.props.poll) {
this.intervalId = setInterval(() => {
this.intervalId = window.setInterval(() => {
this.authLogic.poll();
}, 2000);
}

View file

@ -1965,7 +1965,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.accountPassword = password;
// self-destruct the password after 5mins
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
this.accountPasswordTimer = setTimeout(() => {
this.accountPasswordTimer = window.setTimeout(() => {
this.accountPassword = null;
this.accountPasswordTimer = null;
}, 60 * 5 * 1000);

View file

@ -459,7 +459,7 @@ export default class ScrollPanel extends React.Component<IProps> {
if (this.unfillDebouncer) {
clearTimeout(this.unfillDebouncer);
}
this.unfillDebouncer = setTimeout(() => {
this.unfillDebouncer = window.setTimeout(() => {
this.unfillDebouncer = null;
debuglog("unfilling now", { backwards, origExcessHeight });
this.props.onUnfillRequest?.(backwards, markerScrollToken!);
@ -485,7 +485,7 @@ export default class ScrollPanel extends React.Component<IProps> {
// this will block the scroll event handler for +700ms
// if messages are already cached in memory,
// This would cause jumping to happen on Chrome/macOS.
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return new Promise(resolve => window.setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
this.pendingFillRequests[dir] = false;

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import filesize from "filesize";
import { filesize } from "filesize";
import { IEventRelation } from 'matrix-js-sdk/src/matrix';
import { Optional } from "matrix-events-sdk";

View file

@ -96,8 +96,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
private async updateMode(mode: Mode) {
this.setState({ phase: Phase.Loading });
if (this.state.rendezvous) {
this.state.rendezvous.onFailure = undefined;
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
const rendezvous = this.state.rendezvous;
rendezvous.onFailure = undefined;
await rendezvous.cancel(RendezvousFailureReason.UserCancelled);
this.setState({ rendezvous: undefined });
}
if (mode === Mode.Show) {

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { _t } from '../../../languageHandler';
@ -27,11 +26,6 @@ interface IProps extends IContextMenuProps {
}
export default class LegacyCallContextMenu extends React.Component<IProps> {
static propTypes = {
// js-sdk User object. Not required because it might not exist.
user: PropTypes.object,
};
constructor(props) {
super(props);
}

View file

@ -697,7 +697,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.debounceTimer = window.setTimeout(() => {
this.updateSuggestions(term);
}, 150); // 150ms debounce (human reaction time + some)
};

View file

@ -48,7 +48,7 @@ async function syncHealthCheck(cli: MatrixClient): Promise<void> {
*/
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s
const res = await fetch(endpoint + "/client/server.json", {
signal: controller.signal,
});

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import filesize from "filesize";
import { filesize } from "filesize";
import { Icon as FileIcon } from '../../../../res/img/feather-customised/files.svg';
import { _t } from '../../../languageHandler';

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import filesize from 'filesize';
import { filesize } from 'filesize';
import React from 'react';
import { _t } from '../../../languageHandler';

View file

@ -51,7 +51,7 @@ const VerificationRequestExplorer: React.FC<{
if (request.timeout == 0) return;
/* Note that request.timeout is a getter, so its value changes */
const id = setInterval(() => {
const id = window.setInterval(() => {
setRequestTimeout(request.timeout);
}, 500);

View file

@ -228,7 +228,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
if (!queryLength) return;
// send metrics after a 1s debounce
const timeoutId = setTimeout(() => {
const timeoutId = window.setTimeout(() => {
PosthogAnalytics.instance.trackEvent<WebSearchEvent>({
eventName: "WebSearch",
viaSpotlight,

View file

@ -106,7 +106,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
}
async componentDidMount() {
// setInterval() first waits and then executes, therefore
// window.setInterval() first waits and then executes, therefore
// we call getDesktopCapturerSources() here without any delay.
// Otherwise the dialog would be left empty for some time.
this.setState({
@ -114,7 +114,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
});
// We update the sources every 500ms to get newer thumbnails
this.interval = setInterval(async () => {
this.interval = window.setInterval(async () => {
this.setState({
sources: await getDesktopCapturerSources(),
});

View file

@ -240,7 +240,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
{ _t("In reply to <a>this message</a>",
{},
{ a: (sub) => (
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
<a className="mx_reply_anchor" href={`#${eventId}`} data-scroll-to={eventId}> { sub } </a>
),
})
}

View file

@ -80,12 +80,13 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
if (!canChange && this.props.hideIfCannotSet) return null;
const label = this.props.label
const label = (this.props.label
? _t(this.props.label)
: SettingsStore.getDisplayName(this.props.name, this.props.level);
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
const description = SettingsStore.getDescription(this.props.name);
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
let disabledDescription: JSX.Element;
let disabledDescription: JSX.Element | null = null;
if (this.props.disabled && this.props.disabledDescription) {
disabledDescription = <div className="mx_SettingsFlag_microcopy">
{ this.props.disabledDescription }
@ -106,7 +107,20 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
<label className="mx_SettingsFlag_label">
<span className="mx_SettingsFlag_labelText">{ label }</span>
{ description && <div className="mx_SettingsFlag_microcopy">
{ description }
{ shouldWarn
? _t(
"<w>WARNING:</w> <description/>", {},
{
"w": (sub) => (
<span className="mx_SettingsTab_microcopy_warning">
{ sub }
</span>
),
"description": description,
},
)
: description
}
</div> }
{ disabledDescription }
</label>

View file

@ -35,7 +35,7 @@ export function UseCaseSelection({ onFinished }: Props) {
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
useEffect(() => {
if (selection) {
let handler: number | null = setTimeout(() => {
let handler: number | null = window.setTimeout(() => {
handler = null;
onFinished(selection);
}, TIMEOUT);

View file

@ -191,7 +191,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
this.setState({ filter });
// Header underlines need to be updated, but updating requires knowing
// where the categories are, so we wait for a tick.
setTimeout(this.updateVisibility, 0);
window.setTimeout(this.updateVisibility, 0);
};
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => {

View file

@ -31,8 +31,8 @@ class Search extends React.PureComponent<IProps> {
private inputRef = React.createRef<HTMLInputElement>();
componentDidMount() {
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
setTimeout(() => this.inputRef.current.focus(), 0);
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout
window.setTimeout(() => this.inputRef.current.focus(), 0);
}
private onKeyDown = (ev: React.KeyboardEvent) => {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import filesize from 'filesize';
import { filesize } from 'filesize';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';

View file

@ -270,6 +270,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Set a placeholder image when we can't decrypt the image.
this.setState({ error });
return;
}
} else {
thumbUrl = this.getThumbUrl();
@ -291,16 +292,27 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
img.crossOrigin = "Anonymous"; // CORS allow canvas access
img.src = contentUrl;
await loadPromise;
const blob = await this.props.mediaEventHelper.sourceBlob.value;
if (!await blobIsAnimated(content.info.mimetype, blob)) {
isAnimated = false;
try {
await loadPromise;
} catch (error) {
logger.error("Unable to download attachment: ", error);
this.setState({ error: error as Error });
return;
}
if (isAnimated) {
const thumb = await createThumbnail(img, img.width, img.height, content.info.mimetype, false);
thumbUrl = URL.createObjectURL(thumb.thumbnail);
try {
const blob = await this.props.mediaEventHelper.sourceBlob.value;
if (!await blobIsAnimated(content.info?.mimetype, blob)) {
isAnimated = false;
}
if (isAnimated) {
const thumb = await createThumbnail(img, img.width, img.height, content.info!.mimetype, false);
thumbUrl = URL.createObjectURL(thumb.thumbnail);
}
} catch (error) {
// This is a non-critical failure, do not surface the error or bail the method here
logger.warn("Unable to generate thumbnail for animated image: ", error);
}
}
}
@ -335,7 +347,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Add a 150ms timer for blurhash to first appear.
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
this.clearBlurhashTimeout();
this.timeout = setTimeout(() => {
this.timeout = window.setTimeout(() => {
if (!this.state.imgLoaded || !this.state.imgError) {
this.setState({
placeholder: Placeholder.Blurhash,

View file

@ -130,7 +130,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (codes.length > 0) {
// Do this asynchronously: parsing code takes time and we don't
// need to block the DOM update on it.
setTimeout(() => {
window.setTimeout(() => {
if (this.unmounted) return;
for (let i = 0; i < codes.length; i++) {
this.highlightCode(codes[i]);

View file

@ -111,8 +111,21 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
const onStartVerification = useCallback(async () => {
setRequesting(true);
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
let verificationRequest_: VerificationRequest;
try {
const roomId = await ensureDMExists(cli, member.userId);
verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
} catch (e) {
console.error("Error starting verification", e);
setRequesting(false);
Modal.createDialog(ErrorDialog, {
headerImage: require("../../../../res/img/e2e/warning.svg").default,
title: _t("Error starting verification"),
description: _t("We were unable to start a chat with the other user."),
});
return;
}
setRequest(verificationRequest_);
setPhase(verificationRequest_.phase);
// Notify the RightPanelStore about this

View file

@ -127,7 +127,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
}
return new Promise((resolve) => {
this.debounceCompletionsRequest = setTimeout(() => {
this.debounceCompletionsRequest = window.setTimeout(() => {
resolve(this.processQuery(query, selection));
}, autocompleteDelay);
});

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React, { ComponentProps, createRef } from 'react';
import { AllHtmlEntities } from 'html-entities';
import { decode } from 'html-entities';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
@ -124,7 +124,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
// opaque string. This does not allow any HTML to be injected into the DOM.
const description = AllHtmlEntities.decode(p["og:description"] || "");
const description = decode(p["og:description"] || "");
const title = p["og:title"]?.trim() ?? "";
const anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;

View file

@ -199,7 +199,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
// that the ScrollPanel listening to the resizeNotifier can
// correctly measure it's new height and scroll down to keep
// at the bottom if it already is
setTimeout(() => {
window.setTimeout(() => {
this.props.resizeNotifier.notifyTimelineHeightChanged();
}, 100);
}
@ -395,7 +395,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
private onRecordingEndingSoon = ({ secondsLeft }) => {
this.setState({ recordingTimeLeftSeconds: secondsLeft });
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
};
private setStickerPickerOpen = (isStickerPickerOpen: boolean) => {
@ -584,6 +584,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.get(),
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
VoiceBroadcastRecordingsStore.instance(),
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);

View file

@ -123,6 +123,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
}
const classes = classNames("mx_ReplyTile", {
mx_ReplyTile_inline: msgType === MsgType.Emote,
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
mx_ReplyTile_audio: msgType === MsgType.Audio,
mx_ReplyTile_video: msgType === MsgType.Video,

View file

@ -99,7 +99,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
// again and this time we want to show the newest breadcrumb because it'll be hidden
// off screen for the animation.
this.setState({ doAnimation: false, skipFirst: true });
setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0);
window.setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0);
};
private viewRoom = (room: Room, index: number, viaKeyboard = false) => {

View file

@ -385,7 +385,7 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("Layout type")}
title={_t("Change layout")}
alignment={Alignment.Bottom}
key="layout"
/>

View file

@ -747,13 +747,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
public render(): React.ReactElement {
const visibleTiles = this.renderVisibleTiles();
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
const classes = classNames({
'mx_RoomSublist': true,
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist_minimized': this.props.isMinimized,
'mx_RoomSublist_hidden': (
!this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true
),
'mx_RoomSublist_hidden': hidden,
});
let content = null;
@ -898,6 +897,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
ref={this.sublistRef}
className={classes}
role="group"
aria-hidden={hidden}
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>

View file

@ -28,7 +28,7 @@ export function useIsFocused() {
} else {
// To avoid a blink when we switch mode between plain text and rich text mode
// We delay the unfocused action
timeoutIDRef.current = setTimeout(() => setIsFocused(false), 100);
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
}
}, [setIsFocused, timeoutIDRef]);

View file

@ -37,7 +37,7 @@ export function focusComposer(
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = setTimeout(
timeoutId.current = window.setTimeout(
() => composerElement.current?.focus(),
200,
);

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
@ -66,11 +65,6 @@ interface IBridgeStateEvent {
}
export default class BridgeTile extends React.PureComponent<IProps> {
static propTypes = {
ev: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
};
render() {
const content: IBridgeStateEvent = this.props.ev.getContent();
// Validate

View file

@ -150,7 +150,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } });
this.themeTimer = setTimeout(() => {
this.themeTimer = window.setTimeout(() => {
this.setState({ customThemeMessage: { text: "", isError: false } });
}, 3000);
};

View file

@ -64,12 +64,13 @@ const isDeviceSelected = (
) => selectedDeviceIds.includes(deviceId);
// devices without timestamp metadata should be sorted last
const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right: ExtendedDevice) =>
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
.sort(sortDevicesByLatestActivity);
.sort(sortDevicesByLatestActivityThenDisplayName);
const ALL_FILTER_ID = 'ALL';
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;

View file

@ -324,12 +324,13 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
const privilegedUsers = [];
const mutedUsers = [];
const privilegedUsers: JSX.Element[] = [];
const mutedUsers: JSX.Element[] = [];
Object.keys(userLevels).forEach((user) => {
if (!Number.isInteger(userLevels[user])) { return; }
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
if (!Number.isInteger(userLevels[user])) return;
const isMe = user === client.getUserId();
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(
<PowerSelector

View file

@ -19,7 +19,6 @@ import { sortBy } from "lodash";
import { _t } from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
@ -28,24 +27,6 @@ import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { EnhancedMap } from "../../../../../utils/maps";
interface ILabsSettingToggleProps {
featureId: string;
}
export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps> {
private onChange = async (checked: boolean): Promise<void> => {
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
this.forceUpdate();
};
public render(): JSX.Element {
const label = SettingsStore.getDisplayName(this.props.featureId);
const value = SettingsStore.getValue(this.props.featureId);
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
return <LabelledToggleSwitch value={value} label={label} onChange={this.onChange} disabled={!canChange} />;
}
}
interface IState {
showJumpToDate: boolean;
showExploringPublicSpaces: boolean;
@ -93,7 +74,7 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
labs.forEach(f => {
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
<LabsSettingToggle featureId={f} key={f} />,
<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />,
);
});
@ -154,24 +135,42 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
return (
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Labs") }</div>
<div className="mx_SettingsTab_heading">{ _t("Upcoming features") }</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t('Feeling experimental? Labs are the best way to get things early, ' +
'test out new features and help shape them before they actually launch. ' +
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener'
target='_blank'
>{ sub }</a>;
},
})
_t(
"What's next for %(brand)s? "
+ "Labs are the best way to get things early, "
+ "test out new features and help shape them before they actually launch.",
{ brand: SdkConfig.get("brand") },
)
}
</div>
{ betaSection }
{ labsSections }
{ labsSections && <>
<div className="mx_SettingsTab_heading">{ _t("Early previews") }</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t(
"Feeling experimental? "
+ "Try out our latest ideas in development. "
+ "These features are not finalised; "
+ "they may be unstable, may change, or may be dropped altogether. "
+ "<a>Learn more</a>.",
{},
{
'a': (sub) => {
return <a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener'
target='_blank'
>{ sub }</a>;
},
})
}
</div>
{ labsSections }
</> }
</div>
);
}

View file

@ -127,7 +127,7 @@ const SessionManagerTab: React.FC = () => {
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const scrollIntoViewTimeoutRef = useRef<number>();
const matrixClient = useContext(MatrixClientContext);
const userId = matrixClient.getUserId();

View file

@ -57,7 +57,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
async componentDidMount() {
const { request } = this.props;
if (request.timeout && request.timeout > 0) {
this.intervalHandle = setInterval(() => {
this.intervalHandle = window.setInterval(() => {
let { counter } = this.state;
counter = Math.max(0, counter - 1);
this.setState({ counter });

View file

@ -55,7 +55,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) {
const [showList, setShowList] = useState<boolean>(false);
useEffect(() => {
if (initialSyncComplete) {
let handler: number | null = setTimeout(() => {
let handler: number | null = window.setTimeout(() => {
handler = null;
setShowList(true);
}, ANIMATION_DURATION);

View file

@ -43,7 +43,7 @@ interface GroupCallDurationProps {
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);

View file

@ -367,14 +367,14 @@ class PipView extends React.Component<IProps, IState> {
const pipMode = true;
let pipContent: CreatePipChildren | null = null;
if (this.props.voiceBroadcastPreRecording) {
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
}
if (this.props.voiceBroadcastPlayback) {
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
}
if (this.props.voiceBroadcastPreRecording) {
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
}
if (this.props.voiceBroadcastRecording) {
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
}

View file

@ -49,7 +49,7 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
// if you dispatch from within a dispatch, so rather than action
// handlers having to worry about not calling anything that might
// then dispatch, we just do dispatches asynchronously.
setTimeout(super.dispatch.bind(this, payload), 0);
window.setTimeout(super.dispatch.bind(this, payload), 0);
}
}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { AllHtmlEntities } from 'html-entities';
import { encode } from 'html-entities';
import cheerio from 'cheerio';
import escapeHtml from "escape-html";
@ -117,7 +117,7 @@ export function htmlSerializeFromMdIfNeeded(md: string, { forceHTML = false } =
patternDefaults[patternName][patternType];
md = md.replace(RegExp(pattern, "gms"), function(m, p1, p2) {
const p2e = AllHtmlEntities.encode(p2);
const p2e = encode(p2);
switch (patternType) {
case "display":
return `${p1}<div data-mx-maths="${p2e}">\n\n</div>\n\n`;

View file

@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { M_POLL_START, Optional } from "matrix-events-sdk";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall";
import EditorStateTransfer from "../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
@ -412,13 +413,9 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo
return Boolean(mxEvent.getContent()['predecessor']);
} else if (ElementCall.CALL_EVENT_TYPE.names.some(eventType => handler === STATE_EVENT_TILE_TYPES.get(eventType))) {
const intent = mxEvent.getContent()['m.intent'];
const prevContent = mxEvent.getPrevContent();
// If the call became unterminated or previously had invalid contents,
// then this event marks the start of the call
const newlyStarted = 'm.terminated' in prevContent
|| !('m.intent' in prevContent) || !('m.type' in prevContent);
const newlyStarted = Object.keys(mxEvent.getPrevContent()).length === 0;
// Only interested in events that mark the start of a non-room call
return typeof intent === 'string' && intent !== 'm.room' && newlyStarted;
return newlyStarted && typeof intent === 'string' && intent !== GroupCallIntent.Room;
} else if (handler === JSONEventFactory) {
return false;
} else {

View file

@ -30,7 +30,7 @@ export function useDebouncedCallback<T extends any[]>(
callback(...params);
};
if (enabled !== false) {
handle = setTimeout(doSearch, DEBOUNCE_TIMEOUT);
handle = window.setTimeout(doSearch, DEBOUNCE_TIMEOUT);
return () => {
if (handle) {
clearTimeout(handle);

View file

@ -30,7 +30,7 @@ export const useTimeout = (handler: Handler, timeoutMs: number) => {
// Set up timer
useEffect(() => {
const timeoutID = setTimeout(() => {
const timeoutID = window.setTimeout(() => {
savedHandler.current();
}, timeoutMs);
return () => clearTimeout(timeoutID);
@ -49,7 +49,7 @@ export const useInterval = (handler: Handler, intervalMs: number) => {
// Set up timer
useEffect(() => {
const intervalID = setInterval(() => {
const intervalID = window.setInterval(() => {
savedHandler.current();
}, intervalMs);
return () => clearInterval(intervalID);

View file

@ -28,7 +28,7 @@ export const useTimeoutToggle = (defaultValue: boolean, timeoutMs: number) => {
const toggle = () => {
setValue(!defaultValue);
timeoutId.current = setTimeout(() => setValue(defaultValue), timeoutMs);
timeoutId.current = window.setTimeout(() => setValue(defaultValue), timeoutMs);
};
useEffect(() => {

View file

@ -68,7 +68,7 @@ function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: Matri
}
setValue(await handler(cli));
if (enabled) {
handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
handle = window.setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
}
};
repeater().catch(err => logger.warn("could not update user onboarding context", err));

View file

@ -660,6 +660,7 @@
"Change input device": "Change input device",
"Live": "Live",
"Voice broadcast": "Voice broadcast",
"Buffering…": "Buffering…",
"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",
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
@ -808,7 +809,7 @@
"Yes": "Yes",
"No": "No",
"Help improve %(analyticsOwner)s": "Help improve %(analyticsOwner)s",
"You have unverified logins": "You have unverified logins",
"You have unverified sessions": "You have unverified sessions",
"Review to ensure your account is safe": "Review to ensure your account is safe",
"Review": "Review",
"Later": "Later",
@ -908,7 +909,8 @@
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Report to moderators": "Report to moderators",
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Message Pinning": "Message Pinning",
"Threaded messaging": "Threaded messaging",
@ -920,9 +922,11 @@
"How can I leave the beta?": "How can I leave the beta?",
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
"Leave the beta": "Leave the beta",
"Try out the rich text editor (plain text mode coming soon)": "Try out the rich text editor (plain text mode coming soon)",
"Rich text editor": "Rich text editor",
"Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.",
"Render simple counters in room header": "Render simple counters in room header",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"New ways to ignore people": "New ways to ignore people",
"Currently experimental.": "Currently experimental.",
"Support adding custom themes": "Support adding custom themes",
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
@ -932,15 +936,19 @@
"Show HTML representation of room topics": "Show HTML representation of room topics",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Use new room breadcrumbs": "Use new room breadcrumbs",
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
"Right panel stays open": "Right panel stays open",
"Defaults to room member list.": "Defaults to room member list.",
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
"Send read receipts": "Send read receipts",
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
"Sliding Sync mode": "Sliding Sync mode",
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
"Element Call video rooms": "Element Call video rooms",
"New group call experience": "New group call experience",
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
"Voice broadcast (under active development)": "Voice broadcast (under active development)",
"Live Location Sharing": "Live Location Sharing",
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
"Favourite Messages": "Favourite Messages",
"Under active development.": "Under active development.",
"Under active development": "Under active development",
"Use new session manager": "Use new session manager",
"New session manager": "New session manager",
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
@ -1001,7 +1009,8 @@
"Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list",
"Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list",
"Show hidden events in timeline": "Show hidden events in timeline",
"Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)",
"Low bandwidth mode": "Low bandwidth mode",
"Requires compatible homeserver.": "Requires compatible homeserver.",
"Allow fallback call assist server (turn.matrix.org)": "Allow fallback call assist server (turn.matrix.org)",
"Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.",
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
@ -1539,8 +1548,10 @@
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
"Clear cache and reload": "Clear cache and reload",
"Keyboard": "Keyboard",
"Labs": "Labs",
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
"Upcoming features": "Upcoming features",
"What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.",
"Early previews": "Early previews",
"Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
"Ignored/Blocked": "Ignored/Blocked",
"Error adding ignored user/server": "Error adding ignored user/server",
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
@ -1950,7 +1961,7 @@
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
"Freedom": "Freedom",
"Spotlight": "Spotlight",
"Layout type": "Layout type",
"Change layout": "Change layout",
"Forget room": "Forget room",
"Hide Widgets": "Hide Widgets",
"Show Widgets": "Show Widgets",
@ -2151,6 +2162,8 @@
"The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to",
"Yours, or the other users' internet connection": "Yours, or the other users' internet connection",
"Yours, or the other users' session": "Yours, or the other users' session",
"Error starting verification": "Error starting verification",
"We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.",
"Nothing pinned, yet": "Nothing pinned, yet",
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages",
@ -2560,6 +2573,7 @@
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
"Homeserver": "Homeserver",
"Help": "Help",
"<w>WARNING:</w> <description/>": "<w>WARNING:</w> <description/>",
"Choose a locale": "Choose a locale",
"Continue with %(provider)s": "Continue with %(provider)s",
"Sign in with single sign-on": "Sign in with single sign-on",
@ -2992,6 +3006,7 @@
"Upload %(count)s other files|one": "Upload %(count)s other file",
"Cancel All": "Cancel All",
"Upload Error": "Upload Error",
"Labs": "Labs",
"Verify other device": "Verify other device",
"Verification Request": "Verification Request",
"Approve widget permissions": "Approve widget permissions",

View file

@ -377,7 +377,7 @@ export class JitsiCall extends Call {
this.participants = participants;
if (allExpireAt < Infinity) {
this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), allExpireAt - now);
this.participantsExpirationTimer = window.setTimeout(() => this.updateParticipants(), allExpireAt - now);
}
}
@ -553,7 +553,7 @@ export class JitsiCall extends Call {
// Tell others that we're connected, by adding our device to room state
await this.addOurDevice();
// Re-add this device every so often so our video member event doesn't become stale
this.resendDevicesTimer = setInterval(async () => {
this.resendDevicesTimer = window.setInterval(async () => {
logger.log(`Resending video member event for ${this.roomId}`);
await this.addOurDevice();
}, (this.STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
@ -647,7 +647,6 @@ export class ElementCall extends Call {
client,
);
this.on(CallEvent.ConnectionState, this.onConnectionState);
this.on(CallEvent.Participants, this.onParticipants);
groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
@ -704,6 +703,7 @@ export class ElementCall extends Call {
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
}
this.groupCall.enteredViaAnotherSession = true;
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
@ -724,11 +724,11 @@ export class ElementCall extends Call {
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
super.setDisconnected();
this.groupCall.enteredViaAnotherSession = false;
}
public destroy() {
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId);
this.off(CallEvent.ConnectionState, this.onConnectionState);
this.off(CallEvent.Participants, this.onParticipants);
this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
@ -760,20 +760,6 @@ export class ElementCall extends Call {
participants.set(member, new Set(deviceMap.keys()));
}
// We never enter group calls natively, so the GroupCall will think it's
// disconnected regardless of what our call member state says. Thus we
// have to insert our own device manually when connected via the widget.
if (this.connected) {
const localMember = this.room.getMember(this.client.getUserId()!)!;
let devices = participants.get(localMember);
if (devices === undefined) {
devices = new Set();
participants.set(localMember, devices);
}
devices.add(this.client.getDeviceId()!);
}
this.participants = participants;
}
@ -782,15 +768,6 @@ export class ElementCall extends Call {
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
}
private onConnectionState = (state: ConnectionState, prevState: ConnectionState) => {
if (
(state === ConnectionState.Connected && !isConnected(prevState))
|| (state === ConnectionState.Disconnected && isConnected(prevState))
) {
this.updateParticipants(); // Local echo
}
};
private onParticipants = async (
participants: Map<RoomMember, Set<string>>,
prevParticipants: Map<RoomMember, Set<string>>,
@ -814,7 +791,7 @@ export class ElementCall extends Call {
// randomly between 2 and 8 seconds before terminating the call, to
// probabilistically reduce event spam. If someone else beats us to it,
// this timer will be automatically cleared upon the call's destruction.
this.terminationTimer = setTimeout(
this.terminationTimer = window.setTimeout(
() => this.groupCall.terminate(),
Math.random() * 6000 + 2000,
);

View file

@ -154,7 +154,7 @@ export class IndexedDBLogStore {
// @ts-ignore
this.db = event.target.result;
// Periodically flush logs to local storage / indexeddb
setInterval(this.flush.bind(this), FLUSH_RATE_MS);
window.setInterval(this.flush.bind(this), FLUSH_RATE_MS);
resolve();
};

View file

@ -206,7 +206,7 @@ export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promis
new Sentry.Integrations.InboundFilters(),
new Sentry.Integrations.FunctionToString(),
new Sentry.Integrations.Breadcrumbs(),
new Sentry.Integrations.UserAgent(),
new Sentry.Integrations.HttpContext(),
new Sentry.Integrations.Dedupe(),
];

View file

@ -122,13 +122,13 @@ export const labGroupNames: Record<LabGroup, string> = {
[LabGroup.Developer]: _td("Developer"),
};
export type SettingValueType = boolean |
number |
string |
number[] |
string[] |
Record<string, unknown> |
null;
export type SettingValueType = boolean
| number
| string
| number[]
| string[]
| Record<string, unknown>
| null;
export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
isFeature?: false | undefined;
@ -180,6 +180,9 @@ export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
extraSettings?: string[];
requiresRefresh?: boolean;
};
// Whether the setting should have a warning sign in the microcopy
shouldWarn?: boolean;
}
export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
@ -245,8 +248,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"feature_report_to_moderators": {
isFeature: true,
labsGroup: LabGroup.Moderation,
displayName: _td("Report to moderators prototype. " +
"In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
displayName: _td("Report to moderators"),
description: _td(
"In rooms that support moderation, "
+"the “Report” button will let you report abuse to room moderators.",
),
supportedLevels: LEVELS_FEATURE,
default: false,
},
@ -307,7 +313,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"feature_wysiwyg_composer": {
isFeature: true,
labsGroup: LabGroup.Messaging,
displayName: _td("Try out the rich text editor (plain text mode coming soon)"),
displayName: _td("Rich text editor"),
description: _td("Use rich text instead of Markdown in the message composer. Plain text mode coming soon."),
supportedLevels: LEVELS_FEATURE,
default: false,
},
@ -321,7 +328,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"feature_mjolnir": {
isFeature: true,
labsGroup: LabGroup.Moderation,
displayName: _td("Try out new ways to ignore people (experimental)"),
displayName: _td("New ways to ignore people"),
description: _td("Currently experimental."),
supportedLevels: LEVELS_FEATURE,
default: false,
},
@ -400,7 +408,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
isFeature: true,
labsGroup: LabGroup.Rooms,
supportedLevels: LEVELS_FEATURE,
displayName: _td("Right panel stays open (defaults to room member list)"),
displayName: _td("Right panel stays open"),
description: _td("Defaults to room member list."),
default: false,
},
"feature_jump_to_date": {
@ -425,7 +434,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
isFeature: true,
labsGroup: LabGroup.Developer,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Sliding Sync mode (under active development, cannot be disabled)'),
displayName: _td('Sliding Sync mode'),
description: _td("Under active development, cannot be disabled."),
shouldWarn: true,
default: false,
controller: new SlidingSyncController(),
},
@ -453,23 +464,25 @@ export const SETTINGS: {[setting: string]: ISetting} = {
isFeature: true,
labsGroup: LabGroup.Messaging,
supportedLevels: LEVELS_FEATURE,
displayName: _td(
"Live Location Sharing (temporary implementation: locations persist in room history)",
),
displayName: _td("Live Location Sharing"),
description: _td("Temporary implementation. Locations persist in room history."),
shouldWarn: true,
default: false,
},
"feature_favourite_messages": {
isFeature: true,
labsGroup: LabGroup.Messaging,
supportedLevels: LEVELS_FEATURE,
displayName: _td("Favourite Messages (under active development)"),
displayName: _td("Favourite Messages"),
description: _td("Under active development."),
default: false,
},
[Features.VoiceBroadcast]: {
isFeature: true,
labsGroup: LabGroup.Messaging,
supportedLevels: LEVELS_FEATURE,
displayName: _td("Voice broadcast (under active development)"),
displayName: _td("Voice broadcast"),
description: _td("Under active development"),
default: false,
},
"feature_new_device_manager": {
@ -910,9 +923,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
"lowBandwidth": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Low bandwidth mode (requires compatible homeserver)'),
displayName: _td('Low bandwidth mode'),
description: _td("Requires compatible homeserver."),
default: false,
controller: new ReloadOnChangeController(),
shouldWarn: true,
},
"fallbackICEServerAllowed": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
@ -1056,6 +1071,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"debug_legacy_call_handler": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"audioInputMuted": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
@ -1130,6 +1149,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.BulkUnverifiedSessionsReminder]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
// Electron-specific settings, they are stored by Electron and set/read over an IPC.
// We store them over there are they are necessary to know before the renderer process launches.

View file

@ -295,6 +295,16 @@ export default class SettingsStore {
return SETTINGS[settingName].isFeature;
}
/**
* Determines if a setting should have a warning sign in the microcopy
* @param {string} settingName The setting to look up.
* @return {boolean} True if the setting should have a warning sign.
*/
public static shouldHaveWarning(settingName: string): boolean {
if (!SETTINGS[settingName]) return false;
return SETTINGS[settingName].shouldWarn ?? false;
}
public static getBetaInfo(settingName: string): ISetting["betaInfo"] {
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
if (SettingsStore.isFeature(settingName)
@ -355,7 +365,7 @@ export default class SettingsStore {
public static getValueAt(
level: SettingLevel,
settingName: string,
roomId: string = null,
roomId: string | null = null,
explicit = false,
excludeDefault = false,
): any {
@ -420,7 +430,7 @@ export default class SettingsStore {
private static getFinalValue(
setting: ISetting,
level: SettingLevel,
roomId: string,
roomId: string | null,
calculatedValue: any,
calculatedAtLevel: SettingLevel,
): any {

View file

@ -31,6 +31,7 @@ export enum UIFeature {
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
BulkUnverifiedSessionsReminder = "UIFeature.BulkUnverifiedSessionsReminder",
}
export enum UIComponent {

View file

@ -39,7 +39,7 @@ export default abstract class SettingController {
*/
public getValueOverride(
level: SettingLevel,
roomId: string,
roomId: string | null,
calculatedValue: any,
calculatedAtLevel: SettingLevel,
): any {

View file

@ -437,7 +437,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return;
}
this.locationInterval = setInterval(() => {
this.locationInterval = window.setInterval(() => {
if (!this.lastPublishedPositionTimestamp) {
return;
}

View file

@ -228,7 +228,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
if (!room) {
logger.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
logger.warn(`Queuing failed room update for retry as a result.`);
setTimeout(async () => {
window.setTimeout(async () => {
const updatedRoom = this.matrixClient.getRoom(roomId);
await tryUpdate(updatedRoom);
}, 100); // 100ms should be enough for the room to show up

View file

@ -122,9 +122,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
this.allowedCapabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
);
this.allowedCapabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Send, "org.matrix.msc3401.call").raw,
);
this.allowedCapabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call").raw,
);

View file

@ -298,7 +298,7 @@ export async function setTheme(theme?: string): Promise<void> {
// In case of theme toggling (white => black => white)
// Chrome doesn't fire the `load` event when the white theme is selected the second times
const intervalId = setInterval(() => {
const intervalId = window.setInterval(() => {
if (isStyleSheetLoaded()) {
clearInterval(intervalId);
styleSheet.onload = undefined;

View file

@ -38,7 +38,7 @@ export const showToast = (deviceIds: Set<string>) => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("You have unverified logins"),
title: _t("You have unverified sessions"),
icon: "verification_warning",
props: {
description: _t("Review to ensure your account is safe"),

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import filesize from 'filesize';
import { filesize } from 'filesize';
import { IMediaEventContent } from '../customisations/models/IMediaEventContent';
import { _t } from '../languageHandler';

View file

@ -241,7 +241,7 @@ export default class MultiInviter {
break;
case "M_LIMIT_EXCEEDED":
// we're being throttled so wait a bit & try again
setTimeout(() => {
window.setTimeout(() => {
this.doInvite(address, ignoreProfile).then(resolve, reject);
}, 5000);
return;

View file

@ -55,7 +55,7 @@ export default class Timer {
this.setNotStarted();
} else {
const delta = this.timeout - elapsed;
this.timerHandle = setTimeout(this.onTimeout, delta);
this.timerHandle = window.setTimeout(this.onTimeout, delta);
}
};
@ -78,7 +78,7 @@ export default class Timer {
start() {
if (!this.isRunning()) {
this.startTs = Date.now();
this.timerHandle = setTimeout(this.onTimeout, this.timeout);
this.timerHandle = window.setTimeout(this.onTimeout, this.timeout);
}
return this;
}

View file

@ -166,7 +166,7 @@ export default class WidgetUtils {
resolve();
}
}
const timerId = setTimeout(() => {
const timerId = window.setTimeout(() => {
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, onAccountData);
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
}, WIDGET_WAIT_TIME);
@ -221,7 +221,7 @@ export default class WidgetUtils {
resolve();
}
}
const timerId = setTimeout(() => {
const timerId = window.setTimeout(() => {
MatrixClientPeg.get().removeListener(RoomStateEvent.Events, onRoomStateEvents);
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
}, WIDGET_WAIT_TIME);

View file

@ -27,7 +27,7 @@ function showToast(text) {
const el = document.getElementById("snackbar");
el.innerHTML = text;
el.className = "mx_show";
setTimeout(() => {
window.setTimeout(() => {
el.className = el.className.replace("mx_show", "");
}, 2000);
}
@ -35,7 +35,7 @@ function showToast(text) {
window.onload = () => {
document.querySelectorAll('.mx_reply_anchor').forEach(element => {
element.addEventListener('click', event => {
showToastIfNeeded(event.target.getAttribute("scroll-to"));
showToastIfNeeded(event.target.dataset.scrollTo);
});
});
};

View file

@ -77,10 +77,10 @@ export async function createThumbnail(
}
let canvas: HTMLCanvasElement | OffscreenCanvas;
let context: CanvasRenderingContext2D;
let context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
try {
canvas = new window.OffscreenCanvas(targetWidth, targetHeight);
context = canvas.getContext("2d");
context = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D;
} catch (e) {
// Fallback support for other browsers (Safari and Firefox for now)
canvas = document.createElement("canvas");
@ -92,7 +92,7 @@ export async function createThumbnail(
context.drawImage(element, 0, 0, targetWidth, targetHeight);
let thumbnailPromise: Promise<Blob>;
if (window.OffscreenCanvas && canvas instanceof window.OffscreenCanvas) {
if (window.OffscreenCanvas && canvas instanceof OffscreenCanvas) {
thumbnailPromise = canvas.convertToBlob({ type: mimeType });
} else {
thumbnailPromise = new Promise<Blob>(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));

View file

@ -102,10 +102,10 @@ export async function waitForRoomReadyAndApplyAfterCreateCallbacks(
finish();
};
const checkRoomStateIntervalHandle = setInterval(() => {
const checkRoomStateIntervalHandle = window.setInterval(() => {
if (isRoomReady(client, localRoom)) finish();
}, 500);
const stopgapTimeoutHandle = setTimeout(stopgapFinish, 5000);
const stopgapTimeoutHandle = window.setTimeout(stopgapFinish, 5000);
});
}

View file

@ -97,7 +97,7 @@ export async function waitForMember(client: MatrixClient, roomId: string, userId
/* We don't want to hang if this goes wrong, so we proceed and hope the other
user is already in the megolm session */
setTimeout(resolve, timeout, false);
window.setTimeout(resolve, timeout, false);
}).finally(() => {
client.removeListener(RoomStateEvent.NewMember, handler);
});

View file

@ -18,7 +18,7 @@ limitations under the License.
// or when the timeout of ms is reached with the value of given timeoutValue
export async function timeout<T, Y>(promise: Promise<T>, timeoutValue: Y, ms: number): Promise<T | Y> {
const timeoutPromise = new Promise<T | Y>((resolve) => {
const timeoutId = setTimeout(resolve, ms, timeoutValue);
const timeoutId = window.setTimeout(resolve, ms, timeoutValue);
promise.then(() => {
clearTimeout(timeoutId);
});

View file

@ -25,6 +25,7 @@ import AccessibleButton from "../../../components/views/elements/AccessibleButto
import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
import Clock from "../../../components/views/audio_messages/Clock";
import { formatTimeLeft } from "../../../DateUtils";
import Spinner from "../../../components/views/elements/Spinner";
interface VoiceBroadcastHeaderProps {
live?: VoiceBroadcastLiveness;
@ -33,6 +34,7 @@ interface VoiceBroadcastHeaderProps {
room: Room;
microphoneLabel?: string;
showBroadcast?: boolean;
showBuffering?: boolean;
timeLeft?: number;
showClose?: boolean;
}
@ -44,47 +46,55 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
room,
microphoneLabel,
showBroadcast = false,
showBuffering = false,
showClose = false,
timeLeft,
}) => {
const broadcast = showBroadcast
? <div className="mx_VoiceBroadcastHeader_line">
const broadcast = showBroadcast && (
<div className="mx_VoiceBroadcastHeader_line">
<LiveIcon className="mx_Icon mx_Icon_16" />
{ _t("Voice broadcast") }
</div>
: null;
);
const liveBadge = live === "not-live"
? null
: <LiveBadge grey={live === "grey"} />;
const liveBadge = live !== "not-live" && (
<LiveBadge grey={live === "grey"} />
);
const closeButton = showClose
? <AccessibleButton onClick={onCloseClick}>
const closeButton = showClose && (
<AccessibleButton onClick={onCloseClick}>
<XIcon className="mx_Icon mx_Icon_16" />
</AccessibleButton>
: null;
);
const timeLeftLine = timeLeft
? <div className="mx_VoiceBroadcastHeader_line">
const timeLeftLine = timeLeft && (
<div className="mx_VoiceBroadcastHeader_line">
<TimerIcon className="mx_Icon mx_Icon_16" />
<Clock formatFn={formatTimeLeft} seconds={timeLeft} />
</div>
: null;
);
const buffering = showBuffering && (
<div className="mx_VoiceBroadcastHeader_line">
<Spinner w={14} h={14} />
{ _t("Buffering…") }
</div>
);
const microphoneLineClasses = classNames({
mx_VoiceBroadcastHeader_line: true,
["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick,
});
const microphoneLine = microphoneLabel
? <div
const microphoneLine = microphoneLabel && (
<div
className={microphoneLineClasses}
onClick={onMicrophoneLineClick}
>
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
<span>{ microphoneLabel }</span>
</div>
: null;
);
return <div className="mx_VoiceBroadcastHeader">
<RoomAvatar room={room} width={32} height={32} />
@ -95,6 +105,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
{ microphoneLine }
{ timeLeftLine }
{ broadcast }
{ buffering }
</div>
{ liveBadge }
{ closeButton }

View file

@ -23,7 +23,6 @@ import {
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";
@ -54,40 +53,35 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
toggle,
} = useVoiceBroadcastPlayback(playback);
let control: React.ReactNode;
let controlIcon: React.FC<React.SVGProps<SVGSVGElement>>;
let controlLabel: string;
let className = "";
if (playbackState === VoiceBroadcastPlaybackState.Buffering) {
control = <Spinner />;
} else {
let controlIcon: React.FC<React.SVGProps<SVGSVGElement>>;
let controlLabel: string;
let className = "";
switch (playbackState) {
case VoiceBroadcastPlaybackState.Stopped:
controlIcon = PlayIcon;
className = "mx_VoiceBroadcastControl-play";
controlLabel = _t("play voice broadcast");
break;
case VoiceBroadcastPlaybackState.Paused:
controlIcon = PlayIcon;
className = "mx_VoiceBroadcastControl-play";
controlLabel = _t("resume voice broadcast");
break;
case VoiceBroadcastPlaybackState.Playing:
controlIcon = PauseIcon;
controlLabel = _t("pause voice broadcast");
break;
}
control = <VoiceBroadcastControl
className={className}
label={controlLabel}
icon={controlIcon}
onClick={toggle}
/>;
switch (playbackState) {
case VoiceBroadcastPlaybackState.Stopped:
controlIcon = PlayIcon;
className = "mx_VoiceBroadcastControl-play";
controlLabel = _t("play voice broadcast");
break;
case VoiceBroadcastPlaybackState.Paused:
controlIcon = PlayIcon;
className = "mx_VoiceBroadcastControl-play";
controlLabel = _t("resume voice broadcast");
break;
case VoiceBroadcastPlaybackState.Buffering:
case VoiceBroadcastPlaybackState.Playing:
controlIcon = PauseIcon;
controlLabel = _t("pause voice broadcast");
break;
}
const control = <VoiceBroadcastControl
className={className}
label={controlLabel}
icon={controlIcon}
onClick={toggle}
/>;
let seekBackwardButton: ReactElement | null = null;
let seekForwardButton: ReactElement | null = null;
@ -124,7 +118,8 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
live={liveness}
microphoneLabel={sender?.name}
room={room}
showBroadcast={true}
showBroadcast={playbackState !== VoiceBroadcastPlaybackState.Buffering}
showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering}
/>
<div className="mx_VoiceBroadcastBody_controls">
{ seekBackwardButton }

View file

@ -27,6 +27,13 @@ import {
export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(playback.infoEvent.getRoomId());
if (!room) {
throw new Error(
`Voice Broadcast room not found (event ${playback.infoEvent.getId()})`,
);
}
const playbackToggle = () => {
playback.toggle();
};

View file

@ -387,7 +387,7 @@ export class VoiceBroadcastPlayback
const offsetInChunk = time - this.chunkEvents.getLengthTo(event);
await skipToPlayback.skipTo(offsetInChunk / 1000);
if (currentPlayback !== skipToPlayback) {
if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) {
await skipToPlayback.play();
}

View file

@ -18,6 +18,7 @@ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import { IDestroyable } from "../../utils/IDestroyable";
import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore";
import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore";
import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording";
@ -34,6 +35,7 @@ export class VoiceBroadcastPreRecording
public room: Room,
public sender: RoomMember,
private client: MatrixClient,
private playbacksStore: VoiceBroadcastPlaybacksStore,
private recordingsStore: VoiceBroadcastRecordingsStore,
) {
super();
@ -43,6 +45,7 @@ export class VoiceBroadcastPreRecording
await startNewVoiceBroadcastRecording(
this.room,
this.client,
this.playbacksStore,
this.recordingsStore,
);
this.emit("dismiss", this);

View file

@ -18,6 +18,7 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import {
checkVoiceBroadcastPreConditions,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
@ -26,6 +27,7 @@ import {
export const setUpVoiceBroadcastPreRecording = (
room: Room,
client: MatrixClient,
playbacksStore: VoiceBroadcastPlaybacksStore,
recordingsStore: VoiceBroadcastRecordingsStore,
preRecordingStore: VoiceBroadcastPreRecordingStore,
): VoiceBroadcastPreRecording | null => {
@ -39,7 +41,11 @@ export const setUpVoiceBroadcastPreRecording = (
const sender = room.getMember(userId);
if (!sender) return null;
const preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
// pause and clear current playback (if any)
playbacksStore.getCurrent()?.pause();
playbacksStore.clearCurrent();
const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
preRecordingStore.setCurrent(preRecording);
return preRecording;
};

View file

@ -24,6 +24,7 @@ import {
VoiceBroadcastRecordingsStore,
VoiceBroadcastRecording,
getChunkLength,
VoiceBroadcastPlaybacksStore,
} from "..";
import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions";
@ -80,17 +81,23 @@ const startBroadcast = async (
/**
* Starts a new Voice Broadcast Recording, if
* - the user has the permissions to do so in the room
* - the user is not already recording a voice broadcast
* - there is no other broadcast being recorded in the room, yet
* Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state.
*/
export const startNewVoiceBroadcastRecording = async (
room: Room,
client: MatrixClient,
playbacksStore: VoiceBroadcastPlaybacksStore,
recordingsStore: VoiceBroadcastRecordingsStore,
): Promise<VoiceBroadcastRecording | null> => {
if (!checkVoiceBroadcastPreConditions(room, client, recordingsStore)) {
return null;
}
// pause and clear current playback (if any)
playbacksStore.getCurrent()?.pause();
playbacksStore.clearCurrent();
return startBroadcast(room, client, recordingsStore);
};