Merge branch 'develop' of https://github.com/vector-im/element-web into dbkr/stateafter
# Conflicts: # test/unit-tests/components/structures/RoomView-test.tsx # test/unit-tests/components/structures/TimelinePanel-test.tsx
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Based on...
|
||||
* ChromaCheck 1.16
|
||||
* author Roel Nieskens, https://pixelambacht.nl
|
||||
* MIT license
|
||||
*/
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
function safariVersionCheck(ua: string): boolean {
|
||||
logger.log("Browser is Safari - checking version for COLR support");
|
||||
try {
|
||||
const safariVersionMatch = ua.match(/Mac OS X ([\d|_]+).*Version\/([\d|.]+).*Safari/);
|
||||
if (safariVersionMatch) {
|
||||
const macOSVersionStr = safariVersionMatch[1];
|
||||
const safariVersionStr = safariVersionMatch[2];
|
||||
const macOSVersion = macOSVersionStr.split("_").map((n) => parseInt(n, 10));
|
||||
const safariVersion = safariVersionStr.split(".").map((n) => parseInt(n, 10));
|
||||
const colrFontSupported =
|
||||
macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12 && safariVersion[0] < 17;
|
||||
// https://www.colorfonts.wtf/ states Safari supports COLR fonts from this version on but Safari 17 breaks it
|
||||
logger.log(
|
||||
`COLR support on Safari requires macOS 10.14 and Safari 12-16, ` +
|
||||
`detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` +
|
||||
`COLR supported: ${colrFontSupported}`,
|
||||
);
|
||||
return colrFontSupported;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error in Safari COLR version check", err);
|
||||
}
|
||||
logger.warn("Couldn't determine Safari version to check COLR font support, assuming no.");
|
||||
return false;
|
||||
}
|
||||
|
||||
async function isColrFontSupported(): Promise<boolean> {
|
||||
logger.log("Checking for COLR support");
|
||||
|
||||
const { userAgent } = navigator;
|
||||
// Firefox has supported COLR fonts since version 26
|
||||
// but doesn't support the check below without
|
||||
// "Extract canvas data" permissions
|
||||
// when content blocking is enabled.
|
||||
if (userAgent.includes("Firefox")) {
|
||||
logger.log("Browser is Firefox - assuming COLR is supported");
|
||||
return true;
|
||||
}
|
||||
// Safari doesn't wait for the font to load (if it doesn't have it in cache)
|
||||
// to emit the load event on the image, so there is no way to not make the check
|
||||
// reliable. Instead sniff the version.
|
||||
// Excluding "Chrome", as it's user agent unhelpfully also contains Safari...
|
||||
if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) {
|
||||
return safariVersionCheck(userAgent);
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
const img = new Image();
|
||||
// eslint-disable-next-line
|
||||
const fontCOLR =
|
||||
"d09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA==";
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="100" style="background:#fff;fill:#000;">
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: "chromacheck-colr";
|
||||
src: url(data:application/x-font-woff;base64,${fontCOLR}) format("woff");
|
||||
}
|
||||
</style>
|
||||
<text x="0" y="0" font-size="20">
|
||||
<tspan font-family="chromacheck-colr" x="0" dy="20"></tspan>
|
||||
</text>
|
||||
</svg>`;
|
||||
canvas.width = 20;
|
||||
canvas.height = 100;
|
||||
|
||||
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
||||
|
||||
logger.log("Waiting for COLR SVG to load");
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
logger.log("Drawing canvas to detect COLR support");
|
||||
context.drawImage(img, 0, 0);
|
||||
const colrFontSupported = context.getImageData(10, 10, 1, 1).data[0] === 200;
|
||||
logger.log("Canvas check revealed COLR is supported? " + colrFontSupported);
|
||||
return colrFontSupported;
|
||||
} catch (e) {
|
||||
logger.error("Couldn't load COLR font", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let colrFontCheckStarted = false;
|
||||
export async function fixupColorFonts(): Promise<void> {
|
||||
if (colrFontCheckStarted) {
|
||||
return;
|
||||
}
|
||||
colrFontCheckStarted = true;
|
||||
|
||||
if (await isColrFontSupported()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2")}')`;
|
||||
document.fonts.add(new FontFace("Twemoji", path, {}));
|
||||
// For at least Chrome on Windows 10, we have to explictly add extra
|
||||
// weights for the emoji to appear in bold messages, etc.
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "600" }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "700" }));
|
||||
} else {
|
||||
// fall back to SBIX, generated via https://github.com/matrix-org/twemoji-colr/tree/matthew/sbix
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2")}')`;
|
||||
document.fonts.add(new FontFace("Twemoji", path, {}));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "600" }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "700" }));
|
||||
}
|
||||
// ...and if SBIX is not supported, the browser will fall back to one of the native fonts specified.
|
||||
}
|
||||
@@ -7,23 +7,10 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
IContent,
|
||||
IEventRelation,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
THREAD_RELATION_TYPE,
|
||||
M_BEACON_INFO,
|
||||
M_POLL_END,
|
||||
M_POLL_START,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { IContent, IEventRelation, MatrixEvent, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import escapeHtml from "escape-html";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
|
||||
import { PERMITTED_URL_SCHEMES } from "./UrlUtils";
|
||||
import { makeUserPermalink, RoomPermalinkCreator } from "./permalinks/Permalinks";
|
||||
import { isSelfLocation } from "./location";
|
||||
|
||||
export function getParentEventId(ev?: MatrixEvent): string | undefined {
|
||||
if (!ev || ev.isRedacted()) return;
|
||||
@@ -62,137 +49,6 @@ export function stripHTMLReply(html: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
export function getNestedReplyText(
|
||||
ev: MatrixEvent,
|
||||
permalinkCreator?: RoomPermalinkCreator,
|
||||
): { body: string; html: string } | null {
|
||||
if (!ev) return null;
|
||||
|
||||
let {
|
||||
body,
|
||||
formatted_body: html,
|
||||
msgtype,
|
||||
} = ev.getContent<{
|
||||
body: string;
|
||||
msgtype?: string;
|
||||
formatted_body?: string;
|
||||
}>();
|
||||
if (getParentEventId(ev)) {
|
||||
if (body) body = stripPlainReply(body);
|
||||
}
|
||||
|
||||
if (!body) body = ""; // Always ensure we have a body, for reasons.
|
||||
|
||||
if (html) {
|
||||
// sanitize the HTML before we put it in an <mx-reply>
|
||||
html = stripHTMLReply(html);
|
||||
} else {
|
||||
// Escape the body to use as HTML below.
|
||||
// We also run a nl2br over the result to fix the fallback representation. We do this
|
||||
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
|
||||
html = escapeHtml(body).replace(/\n/g, "<br/>");
|
||||
}
|
||||
|
||||
// dev note: do not rely on `body` being safe for HTML usage below.
|
||||
|
||||
const evLink = permalinkCreator?.forEvent(ev.getId()!);
|
||||
const userLink = makeUserPermalink(ev.getSender()!);
|
||||
const mxid = ev.getSender();
|
||||
|
||||
if (M_BEACON_INFO.matches(ev.getType())) {
|
||||
const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a";
|
||||
return {
|
||||
html:
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>shared ${aTheir} live location.</blockquote></mx-reply>`,
|
||||
body: `> <${mxid}> shared ${aTheir} live location.\n\n`,
|
||||
};
|
||||
}
|
||||
|
||||
if (M_POLL_START.matches(ev.getType())) {
|
||||
const extensibleEvent = ev.unstableExtensibleEvent as PollStartEvent;
|
||||
const question = extensibleEvent?.question?.text;
|
||||
return {
|
||||
html:
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>Poll: ${question}</blockquote></mx-reply>`,
|
||||
body: `> <${mxid}> started poll: ${question}\n\n`,
|
||||
};
|
||||
}
|
||||
if (M_POLL_END.matches(ev.getType())) {
|
||||
return {
|
||||
html:
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>Ended poll</blockquote></mx-reply>`,
|
||||
body: `> <${mxid}>Ended poll\n\n`,
|
||||
};
|
||||
}
|
||||
|
||||
// This fallback contains text that is explicitly EN.
|
||||
switch (msgtype) {
|
||||
case MsgType.Text:
|
||||
case MsgType.Notice: {
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split("\n");
|
||||
if (lines.length > 0) {
|
||||
lines[0] = `<${mxid}> ${lines[0]}`;
|
||||
body = lines.map((line) => `> ${line}`).join("\n") + "\n\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MsgType.Image:
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent an image.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent an image.\n\n`;
|
||||
break;
|
||||
case MsgType.Video:
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent a video.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent a video.\n\n`;
|
||||
break;
|
||||
case MsgType.Audio:
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent an audio file.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent an audio file.\n\n`;
|
||||
break;
|
||||
case MsgType.File:
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent a file.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent a file.\n\n`;
|
||||
break;
|
||||
case MsgType.Location: {
|
||||
const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a";
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>shared ${aTheir} location.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> shared ${aTheir} location.\n\n`;
|
||||
break;
|
||||
}
|
||||
case MsgType.Emote: {
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> * ` +
|
||||
`<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split("\n");
|
||||
if (lines.length > 0) {
|
||||
lines[0] = `* <${mxid}> ${lines[0]}`;
|
||||
body = lines.map((line) => `> ${line}`).join("\n") + "\n\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return { body, html };
|
||||
}
|
||||
|
||||
export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation {
|
||||
if (!ev) return {};
|
||||
|
||||
@@ -227,34 +83,9 @@ export function shouldDisplayReply(event: MatrixEvent): boolean {
|
||||
return !!inReplyTo.event_id;
|
||||
}
|
||||
|
||||
interface AddReplyOpts {
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
includeLegacyFallback: false;
|
||||
}
|
||||
|
||||
interface IncludeLegacyFeedbackOpts {
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
includeLegacyFallback: true;
|
||||
}
|
||||
|
||||
export function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
replyToEvent: MatrixEvent,
|
||||
opts: AddReplyOpts | IncludeLegacyFeedbackOpts,
|
||||
): void {
|
||||
export function addReplyToMessageContent(content: IContent, replyToEvent: MatrixEvent): void {
|
||||
content["m.relates_to"] = {
|
||||
...(content["m.relates_to"] || {}),
|
||||
...makeReplyMixIn(replyToEvent),
|
||||
};
|
||||
|
||||
if (opts.includeLegacyFallback) {
|
||||
// Part of Replies fallback support - prepend the text we're sending with the text we're replying to
|
||||
const nestedReply = getNestedReplyText(replyToEvent, opts.permalinkCreator);
|
||||
if (nestedReply) {
|
||||
if (content.formatted_body) {
|
||||
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||
}
|
||||
content.body = nestedReply.body + content.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,39 @@ export async function asyncSome<T>(values: Iterable<T>, predicate: (value: T) =>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of Array.some that runs all promises in parallel.
|
||||
* @param values
|
||||
* @param predicate
|
||||
*/
|
||||
export async function asyncSomeParallel<T>(
|
||||
values: Array<T>,
|
||||
predicate: (value: T) => Promise<boolean>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await Promise.any<boolean>(
|
||||
values.map((value) =>
|
||||
predicate(value).then((result) => (result ? Promise.resolve(true) : Promise.reject(false))),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// If the array is empty or all the promises are false, Promise.any will reject an AggregateError
|
||||
if (e instanceof AggregateError) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of Array.filter.
|
||||
* If one of the promises rejects, the whole operation will reject.
|
||||
* @param values
|
||||
* @param predicate
|
||||
*/
|
||||
export async function asyncFilter<T>(values: Array<T>, predicate: (value: T) => Promise<boolean>): Promise<Array<T>> {
|
||||
const results = await Promise.all(values.map(predicate));
|
||||
return values.filter((_, i) => results[i]);
|
||||
}
|
||||
|
||||
export function filterBoolean<T>(values: Array<T | null | undefined>): T[] {
|
||||
return values.filter(Boolean) as T[];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption";
|
||||
import { asyncSomeParallel } from "../arrays.ts";
|
||||
|
||||
/**
|
||||
* If encryption is force disabled AND the user is not in any encrypted rooms
|
||||
@@ -16,7 +17,13 @@ import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption";
|
||||
* @param client
|
||||
* @returns {boolean} true when we can skip settings up encryption
|
||||
*/
|
||||
export const shouldSkipSetupEncryption = (client: MatrixClient): boolean => {
|
||||
export const shouldSkipSetupEncryption = async (client: MatrixClient): Promise<boolean> => {
|
||||
const isEncryptionForceDisabled = shouldForceDisableEncryption(client);
|
||||
return isEncryptionForceDisabled && !client.getRooms().some((r) => client.isRoomEncrypted(r.roomId));
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) return true;
|
||||
|
||||
return (
|
||||
isEncryptionForceDisabled &&
|
||||
!(await asyncSomeParallel(client.getRooms(), ({ roomId }) => crypto.isEncryptionEnabledInRoom(roomId)))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
import { ImageInfo } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { BlurhashEncoder } from "../BlurhashEncoder";
|
||||
|
||||
@@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||
|
||||
interface IThumbnail {
|
||||
info: {
|
||||
thumbnail_info?: {
|
||||
w: number;
|
||||
h: number;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
};
|
||||
w: number;
|
||||
h: number;
|
||||
[BLURHASH_FIELD]?: string;
|
||||
thumbnail_url?: string;
|
||||
thumbnail_file?: EncryptedFile;
|
||||
};
|
||||
info: ImageInfo;
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type MatrixChat from "../components/structures/MatrixChat";
|
||||
import Views from "../Views";
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
@@ -14,6 +13,5 @@ export function isLoggedIn(): boolean {
|
||||
// `element-web` and into this file? Better yet, we should probably create a
|
||||
// store to hold this state.
|
||||
// See also https://github.com/vector-im/element-web/issues/15034.
|
||||
const app = window.matrixChat;
|
||||
return (app as MatrixChat)?.state.view === Views.LOGGED_IN;
|
||||
return window.matrixChat?.state.view === Views.LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -151,10 +151,7 @@ export async function setMarkedUnreadState(room: Room, client: MatrixClient, unr
|
||||
const currentState = getMarkedUnreadState(room);
|
||||
|
||||
if (Boolean(currentState) !== unread) {
|
||||
// Assuming MSC2867 passes FCP with no changes, we should update to start writing
|
||||
// the flag to the stable prefix (or both) and then ultimately use only the
|
||||
// stable prefix.
|
||||
await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_UNSTABLE, { unread });
|
||||
await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_STABLE, { unread });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user