Skip to content

Commit c84cf3c

Browse files
authored
Remove legacy Safari/Firefox/IE compatibility aids (#29010)
* Remove legacy Safari prefix compatibility for AudioContext Signed-off-by: Michael Telatynski <[email protected]> * Remove more legacy webkit/ms/moz support Signed-off-by: Michael Telatynski <[email protected]> * Fix tests Signed-off-by: Michael Telatynski <[email protected]> * Improve coverage, cull dead code Signed-off-by: Michael Telatynski <[email protected]> * Simplify Signed-off-by: Michael Telatynski <[email protected]> * Improve coverage Signed-off-by: Michael Telatynski <[email protected]> * Improve coverage Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent e235100 commit c84cf3c

File tree

12 files changed

+110
-125
lines changed

12 files changed

+110
-125
lines changed

src/@types/global.d.ts

-37
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,10 @@ declare global {
8282
mxMatrixClientPeg: IMatrixClientPeg;
8383
mxReactSdkConfig: DeepReadonly<IConfigOptions>;
8484

85-
// Needed for Safari, unknown to TypeScript
86-
webkitAudioContext: typeof AudioContext;
87-
8885
// https://docs.microsoft.com/en-us/previous-versions/hh772328(v=vs.85)
8986
// we only ever check for its existence, so we can ignore its actual type
9087
MSStream?: unknown;
9188

92-
// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029#issuecomment-869224737
93-
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
94-
OffscreenCanvas?: {
95-
new (width: number, height: number): OffscreenCanvas;
96-
};
97-
9889
mxContentMessages: ContentMessages;
9990
mxToastStore: ToastStore;
10091
mxDeviceListener: DeviceListener;
@@ -156,31 +147,10 @@ declare global {
156147
fetchWindowIcons?: boolean;
157148
}
158149

159-
interface Document {
160-
// Safari & IE11 only have this prefixed: we used prefixed versions
161-
// previously so let's continue to support them for now
162-
webkitExitFullscreen(): Promise<void>;
163-
msExitFullscreen(): Promise<void>;
164-
readonly webkitFullscreenElement: Element | null;
165-
readonly msFullscreenElement: Element | null;
166-
}
167-
168-
interface Navigator {
169-
userLanguage?: string;
170-
}
171-
172150
interface StorageEstimate {
173151
usageDetails?: { [key: string]: number };
174152
}
175153

176-
interface Element {
177-
// Safari & IE11 only have this prefixed: we used prefixed versions
178-
// previously so let's continue to support them for now
179-
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
180-
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
181-
// scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
182-
}
183-
184154
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
185155
interface AudioWorkletProcessor {
186156
readonly port: MessagePort;
@@ -239,11 +209,4 @@ declare global {
239209
var mx_rage_store: IndexedDBLogStore;
240210
}
241211

242-
// add method which is missing from the node typing
243-
declare module "url" {
244-
interface Url {
245-
format(): string;
246-
}
247-
}
248-
249212
/* eslint-enable @typescript-eslint/naming-convention */

src/audio/Playback.ts

+15-30
Original file line numberDiff line numberDiff line change
@@ -163,36 +163,21 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
163163
this.element.src = URL.createObjectURL(new Blob([this.buf]));
164164
await deferred.promise; // make sure the audio element is ready for us
165165
} else {
166-
// Safari compat: promise API not supported on this function
167-
this.audioBuf = await new Promise((resolve, reject) => {
168-
this.context.decodeAudioData(
169-
this.buf,
170-
(b) => resolve(b),
171-
async (e): Promise<void> => {
172-
try {
173-
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
174-
// very well.
175-
logger.error("Error decoding recording: ", e);
176-
logger.warn("Trying to re-encode to WAV instead...");
177-
178-
const wav = await decodeOgg(this.buf);
179-
180-
// noinspection ES6MissingAwait - not needed when using callbacks
181-
this.context.decodeAudioData(
182-
wav,
183-
(b) => resolve(b),
184-
(e) => {
185-
logger.error("Still failed to decode recording: ", e);
186-
reject(e);
187-
},
188-
);
189-
} catch (e) {
190-
logger.error("Caught decoding error:", e);
191-
reject(e);
192-
}
193-
},
194-
);
195-
});
166+
try {
167+
this.audioBuf = await this.context.decodeAudioData(this.buf);
168+
} catch (e) {
169+
logger.error("Error decoding recording:", e);
170+
logger.warn("Trying to re-encode to WAV instead...");
171+
172+
try {
173+
// This error handler is largely for Safari, which doesn't support Opus/Ogg very well.
174+
const wav = await decodeOgg(this.buf);
175+
this.audioBuf = await this.context.decodeAudioData(wav);
176+
} catch (e) {
177+
logger.error("Error decoding recording:", e);
178+
throw e;
179+
}
180+
}
196181

197182
// Update the waveform to the real waveform once we have channel data to use. We don't
198183
// exactly trust the user-provided waveform to be accurate...

src/audio/compat.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,15 @@ import { logger } from "matrix-js-sdk/src/logger";
1515
import { SAMPLE_RATE } from "./VoiceRecording";
1616

1717
export function createAudioContext(opts?: AudioContextOptions): AudioContext {
18-
let ctx: AudioContext;
1918
if (window.AudioContext) {
20-
ctx = new AudioContext(opts);
21-
} else if (window.webkitAudioContext) {
22-
// While the linter is correct that "a constructor name should not start with
23-
// a lowercase letter", it's also wrong to think that we have control over this.
24-
// eslint-disable-next-line new-cap
25-
ctx = new window.webkitAudioContext(opts);
19+
const ctx = new AudioContext(opts);
20+
// Initialize in suspended state, as Firefox starts using
21+
// CPU/battery right away, even if we don't play any sound yet.
22+
void ctx.suspend();
23+
return ctx;
2624
} else {
2725
throw new Error("Unsupported browser");
2826
}
29-
// Initialize in suspended state, as Firefox starts using
30-
// CPU/battery right away, even if we don't play any sound yet.
31-
void ctx.suspend();
32-
return ctx;
3327
}
3428

3529
export function decodeOgg(audioBuffer: ArrayBuffer): Promise<ArrayBuffer> {

src/components/views/elements/LanguageDropdown.tsx

+2-14
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import React, { type ReactElement } from "react";
1111
import classNames from "classnames";
1212

1313
import * as languageHandler from "../../../languageHandler";
14-
import SettingsStore from "../../../settings/SettingsStore";
1514
import { _t } from "../../../languageHandler";
1615
import Spinner from "./Spinner";
1716
import Dropdown from "./Dropdown";
@@ -29,7 +28,7 @@ function languageMatchesSearchQuery(query: string, language: Languages[0]): bool
2928
interface IProps {
3029
className?: string;
3130
onOptionChange: (language: string) => void;
32-
value?: string;
31+
value: string;
3332
disabled?: boolean;
3433
}
3534

@@ -103,25 +102,14 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
103102
return <div key={language.value}>{language.labelInTargetLanguage}</div>;
104103
}) as NonEmptyArray<ReactElement & { key: string }>;
105104

106-
// default value here too, otherwise we need to handle null / undefined
107-
// values between mounting and the initial value propagating
108-
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
109-
let value: string | undefined;
110-
if (language) {
111-
value = this.props.value || language;
112-
} else {
113-
language = navigator.language || navigator.userLanguage;
114-
value = this.props.value || language;
115-
}
116-
117105
return (
118106
<Dropdown
119107
id="mx_LanguageDropdown"
120108
className={classNames("mx_LanguageDropdown", this.props.className)}
121109
onOptionChange={this.props.onOptionChange}
122110
onSearchChange={this.onSearchChange}
123111
searchEnabled={true}
124-
value={value}
112+
value={this.props.value}
125113
label={_t("language_dropdown_label")}
126114
disabled={this.props.disabled}
127115
>

src/components/views/elements/SpellCheckLanguagesDropdown.tsx

+1-13
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import React, { type ReactElement } from "react";
1010

1111
import Dropdown from "../../views/elements/Dropdown";
1212
import PlatformPeg from "../../../PlatformPeg";
13-
import SettingsStore from "../../../settings/SettingsStore";
1413
import { _t, getUserLanguage } from "../../../languageHandler";
1514
import Spinner from "./Spinner";
1615
import { type NonEmptyArray } from "../../../@types/common";
@@ -105,25 +104,14 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
105104
return <div key={language.value}>{language.label}</div>;
106105
}) as NonEmptyArray<ReactElement & { key: string }>;
107106

108-
// default value here too, otherwise we need to handle null / undefined;
109-
// values between mounting and the initial value propagating
110-
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
111-
let value: string | undefined;
112-
if (language) {
113-
value = this.props.value || language;
114-
} else {
115-
language = navigator.language || navigator.userLanguage;
116-
value = this.props.value || language;
117-
}
118-
119107
return (
120108
<Dropdown
121109
id="mx_LanguageDropdown"
122110
className={this.props.className}
123111
onOptionChange={this.props.onOptionChange}
124112
onSearchChange={this.onSearchChange}
125113
searchEnabled={true}
126-
value={value}
114+
value={this.props.value}
127115
label={_t("language_dropdown_label")}
128116
placeholder={_t("settings|general|spell_check_locale_placeholder")}
129117
>

src/components/views/voip/LegacyCallView.tsx

+3-14
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,15 @@ interface IState {
6666
}
6767

6868
function getFullScreenElement(): Element | null {
69-
return (
70-
document.fullscreenElement ||
71-
// moz omitted because firefox supports this unprefixed now (webkit here for safari)
72-
document.webkitFullscreenElement ||
73-
document.msFullscreenElement
74-
);
69+
return document.fullscreenElement;
7570
}
7671

7772
function requestFullscreen(element: Element): void {
78-
const method =
79-
element.requestFullscreen ||
80-
// moz omitted since firefox supports unprefixed now
81-
element.webkitRequestFullScreen ||
82-
element.msRequestFullscreen;
83-
if (method) method.call(element);
73+
element.requestFullscreen();
8474
}
8575

8676
function exitFullscreen(): void {
87-
const exitMethod = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
88-
if (exitMethod) exitMethod.call(document);
77+
document.exitFullscreen();
8978
}
9079

9180
export default class LegacyCallView extends React.Component<IProps, IState> {

src/languageHandler.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,7 @@ export async function getAllLanguagesWithLabels(): Promise<Language[]> {
529529

530530
export function getLanguagesFromBrowser(): readonly string[] {
531531
if (navigator.languages && navigator.languages.length) return navigator.languages;
532-
if (navigator.language) return [navigator.language];
533-
return [navigator.userLanguage || "en"];
532+
return [navigator.language ?? "en"];
534533
}
535534

536535
export function getLanguageFromBrowser(): string {

test/unit-tests/audio/Playback-test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe("Playback", () => {
4747
beforeEach(() => {
4848
jest.spyOn(logger, "error").mockRestore();
4949
mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData);
50-
mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer));
50+
mockAudioContext.decodeAudioData.mockReset().mockResolvedValue(mockAudioBuffer);
5151
mockAudioContext.resume.mockClear().mockResolvedValue(undefined);
5252
mockAudioContext.suspend.mockClear().mockResolvedValue(undefined);
5353
mocked(decodeOgg).mockClear().mockResolvedValue(new ArrayBuffer(1));
@@ -131,8 +131,8 @@ describe("Playback", () => {
131131
const buffer = new ArrayBuffer(8);
132132
const decodingError = new Error("test");
133133
mockAudioContext.decodeAudioData
134-
.mockImplementationOnce((_b, _callback, error) => error(decodingError))
135-
.mockImplementationOnce((_b, callback) => callback(mockAudioBuffer));
134+
.mockRejectedValueOnce(decodingError)
135+
.mockResolvedValueOnce(mockAudioBuffer);
136136

137137
const playback = new Playback(buffer);
138138

test/unit-tests/audio/compat-test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { createAudioContext } from "../../../src/audio/compat";
9+
10+
describe("createAudioContext", () => {
11+
it("should throw if AudioContext is not supported", () => {
12+
window.AudioContext = undefined as any;
13+
expect(createAudioContext).toThrow("Unsupported browser");
14+
});
15+
});

test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe("<RecordingPlayback />", () => {
6565
beforeEach(() => {
6666
jest.spyOn(logger, "error").mockRestore();
6767
mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData);
68-
mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer));
68+
mockAudioContext.decodeAudioData.mockReset().mockResolvedValue(mockAudioBuffer);
6969
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
7070
});
7171

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { render } from "jest-matrix-react";
10+
import { type MatrixCall } from "matrix-js-sdk/src/matrix";
11+
12+
import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView";
13+
import { stubClient } from "../../../../test-utils";
14+
15+
describe("LegacyCallView", () => {
16+
it("should exit full screen on unmount", () => {
17+
const element = document.createElement("div");
18+
// @ts-expect-error
19+
document.fullscreenElement = element;
20+
document.exitFullscreen = jest.fn();
21+
22+
stubClient();
23+
24+
const call = {
25+
on: jest.fn(),
26+
removeListener: jest.fn(),
27+
getFeeds: jest.fn().mockReturnValue([]),
28+
isLocalOnHold: jest.fn().mockReturnValue(false),
29+
isRemoteOnHold: jest.fn().mockReturnValue(false),
30+
isMicrophoneMuted: jest.fn().mockReturnValue(false),
31+
isLocalVideoMuted: jest.fn().mockReturnValue(false),
32+
isScreensharing: jest.fn().mockReturnValue(false),
33+
} as unknown as MatrixCall;
34+
35+
const { unmount } = render(<LegacyCallView call={call} />);
36+
expect(document.exitFullscreen).not.toHaveBeenCalled();
37+
unmount();
38+
expect(document.exitFullscreen).toHaveBeenCalled();
39+
});
40+
});

test/unit-tests/languageHandler-test.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type TranslationKey,
2727
type IVariables,
2828
type Tags,
29+
getLanguagesFromBrowser,
2930
} from "../../src/languageHandler";
3031
import { stubClient } from "../test-utils";
3132
import { setupLanguageMock } from "../setup/setupLanguage";
@@ -198,6 +199,29 @@ describe("languageHandler", () => {
198199
setupLanguageMock(); // restore language mock
199200
});
200201
});
202+
203+
describe("getLanguagesFromBrowser", () => {
204+
beforeEach(() => {
205+
jest.restoreAllMocks();
206+
});
207+
208+
it("should return navigator.languages if available", () => {
209+
jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["en", "de"]);
210+
expect(getLanguagesFromBrowser()).toEqual(["en", "de"]);
211+
});
212+
213+
it("should return navigator.language if available", () => {
214+
jest.spyOn(window.navigator, "languages", "get").mockReturnValue([]);
215+
jest.spyOn(window.navigator, "language", "get").mockReturnValue("de");
216+
expect(getLanguagesFromBrowser()).toEqual(["de"]);
217+
});
218+
219+
it("should return 'en' otherwise", () => {
220+
jest.spyOn(window.navigator, "languages", "get").mockReturnValue([]);
221+
jest.spyOn(window.navigator, "language", "get").mockReturnValue(undefined as any);
222+
expect(getLanguagesFromBrowser()).toEqual(["en"]);
223+
});
224+
});
201225
});
202226

203227
describe("languageHandler JSX", function () {

0 commit comments

Comments
 (0)