chore: merge v4.12.1 — security, calling, editor, media fixes
Key v4.12.1 changes merged: - Security: sanitize-html updated to v2.17.4 - Calling: video calls in DMs/rooms, user avatars during calls, right-click to start - Calling: IncomingCallListener with ring sound and answer/reject UI - Editor: list crash fixes (Firefox + empty headings), codeblock filename support - Media: URL preview hover state, keyboard nav, click-to-open, OGG audio support - Date: ISO 8601 (YYYY-MM-DD) date format option - Misc: stable mutual rooms endpoint, Android notification crash fix Lotus customisations preserved: - PiP drag/resize, DM call ring notification, PTT, GIF picker, noise suppression - Poll voting, message forwarding, image captions, location sharing - Lotus Terminal design theme
This commit is contained in:
@@ -51,12 +51,36 @@ export class CallEmbed {
|
||||
|
||||
private styleRetryObserver?: MutationObserver;
|
||||
|
||||
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent {
|
||||
if (ongoing) {
|
||||
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting;
|
||||
static getIntent(dm: boolean, ongoing: boolean, video?: boolean): ElementCallIntent {
|
||||
if (dm && ongoing) {
|
||||
return video ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExistingDMVoice;
|
||||
}
|
||||
if (dm) {
|
||||
return video ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCallDMVoice;
|
||||
}
|
||||
|
||||
return dm ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCall;
|
||||
if (ongoing) {
|
||||
return video ? ElementCallIntent.JoinExisting : ElementCallIntent.JoinExistingVoice;
|
||||
}
|
||||
return video ? ElementCallIntent.StartCall : ElementCallIntent.StartCallVoice;
|
||||
}
|
||||
|
||||
static dmCall(intent: ElementCallIntent): boolean {
|
||||
return (
|
||||
intent === ElementCallIntent.JoinExistingDM ||
|
||||
intent === ElementCallIntent.JoinExistingDMVoice ||
|
||||
intent === ElementCallIntent.StartCallDM ||
|
||||
intent === ElementCallIntent.StartCallDMVoice
|
||||
);
|
||||
}
|
||||
|
||||
static startingCall(intent: ElementCallIntent): boolean {
|
||||
return (
|
||||
intent === ElementCallIntent.StartCallDM ||
|
||||
intent === ElementCallIntent.StartCallDMVoice ||
|
||||
intent === ElementCallIntent.StartCall ||
|
||||
intent === ElementCallIntent.StartCallVoice
|
||||
);
|
||||
}
|
||||
|
||||
static getWidget(
|
||||
@@ -91,8 +115,13 @@ export class CallEmbed {
|
||||
noiseSuppression: noiseSuppression.toString(),
|
||||
audio: initialAudio.toString(),
|
||||
video: initialVideo.toString(),
|
||||
header: 'none',
|
||||
});
|
||||
|
||||
if (!room.isCallRoom() && CallEmbed.startingCall(intent)) {
|
||||
params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification');
|
||||
}
|
||||
|
||||
const widgetUrl = new URL(
|
||||
`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/index.html`,
|
||||
window.location.origin
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export enum ElementCallIntent {
|
||||
StartCall = 'start_call',
|
||||
JoinExisting = 'join_existing',
|
||||
StartCallVoice = 'start_call_voice',
|
||||
JoinExistingVoice = 'join_existing_voice',
|
||||
StartCallDM = 'start_call_dm',
|
||||
JoinExistingDM = 'join_existing_dm',
|
||||
StartCallDMVoice = 'start_call_dm_voice',
|
||||
|
||||
@@ -15,6 +15,8 @@ export function getCallCapabilities(
|
||||
|
||||
capabilities.add(MatrixCapabilities.Screenshots);
|
||||
capabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
||||
capabilities.add(MatrixCapabilities.MSC4039UploadFile);
|
||||
capabilities.add(MatrixCapabilities.MSC4039DownloadFile);
|
||||
capabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||
capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
@@ -78,19 +80,13 @@ export function getCallCapabilities(
|
||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw
|
||||
);
|
||||
|
||||
capabilities.add(
|
||||
WidgetEventCapability.forRoomEvent(
|
||||
EventDirection.Receive,
|
||||
'org.matrix.msc4075.rtc.notification'
|
||||
).raw
|
||||
);
|
||||
|
||||
[
|
||||
'io.element.call.encryption_keys',
|
||||
'org.matrix.rageshake_request',
|
||||
EventType.Reaction,
|
||||
EventType.RoomRedaction,
|
||||
'io.element.call.reaction',
|
||||
'org.matrix.msc4075.rtc.notification',
|
||||
'org.matrix.msc4310.rtc.decline',
|
||||
].forEach((type) => {
|
||||
capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, type).raw);
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { replaceMatch } from '../internal';
|
||||
import {
|
||||
BlockQuoteRule,
|
||||
CodeBlockRule,
|
||||
ESC_BLOCK_SEQ,
|
||||
HeadingRule,
|
||||
OrderedListRule,
|
||||
UnorderedListRule,
|
||||
} from './rules';
|
||||
import { BlockQuoteRule, CodeBlockRule, ESC_BLOCK_SEQ, HeadingRule, ListRule } from './rules';
|
||||
import { runBlockRule } from './runner';
|
||||
import { BlockMDParser } from './type';
|
||||
|
||||
@@ -23,8 +16,7 @@ export const parseBlockMD: BlockMDParser = (text, parseInline) => {
|
||||
|
||||
if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, OrderedListRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, UnorderedListRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, ListRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline);
|
||||
|
||||
// replace \n with <br/> because want to preserve empty lines
|
||||
|
||||
@@ -10,14 +10,22 @@ export const HeadingRule: BlockMDRule = {
|
||||
},
|
||||
};
|
||||
|
||||
const CODEBLOCK_MD_1 = '```';
|
||||
const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m;
|
||||
// opening fence: 3 or more backticks
|
||||
// capture the exact fence length in group 1
|
||||
// optional info string in group 2
|
||||
// code content in group 3
|
||||
// closing fence must match the exact same fence sequence via \1
|
||||
const CODEBLOCK_REG_1 = /^(`{3,})(?!`)(\S*)\n((?:.*\n)+?)\1 *(?!.)\n?/m;
|
||||
export const CodeBlockRule: BlockMDRule = {
|
||||
match: (text) => text.match(CODEBLOCK_REG_1),
|
||||
html: (match) => {
|
||||
const [, g1, g2] = match;
|
||||
const classNameAtt = g1 ? ` class="language-${g1}"` : '';
|
||||
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code></pre>`;
|
||||
const [, fence, g1, g2] = match;
|
||||
// use last identifier after dot, e.g. for "example.json" gets us "json" as language code.
|
||||
const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null;
|
||||
const filename = g1 !== langCode ? g1 : null;
|
||||
const classNameAtt = langCode ? ` class="language-${langCode}"` : '';
|
||||
const filenameAtt = filename ? ` data-label="${filename}"` : '';
|
||||
return `<pre data-md="${fence}"><code${classNameAtt}${filenameAtt}>${g2}</code></pre>`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,55 +52,146 @@ export const BlockQuoteRule: BlockMDRule = {
|
||||
};
|
||||
|
||||
const ORDERED_LIST_MD_1 = '-';
|
||||
const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
|
||||
const O_LIST_START = /^([\d])\./;
|
||||
const O_LIST_TYPE = /^([aAiI])\./;
|
||||
const O_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
|
||||
export const OrderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(ORDERED_LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
const [, listStart] = listText.match(O_LIST_START) ?? [];
|
||||
const [, listType] = listText.match(O_LIST_TYPE) ?? [];
|
||||
|
||||
const lines = listText
|
||||
.replace(O_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
|
||||
const startAtt = listStart ? ` start="${listStart}"` : '';
|
||||
const typeAtt = listType ? ` type="${listType}"` : '';
|
||||
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
|
||||
},
|
||||
};
|
||||
|
||||
const UNORDERED_LIST_MD_1 = '*';
|
||||
const U_LIST_ITEM_PREFIX = /^\* */;
|
||||
const U_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
|
||||
export const UnorderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(UNORDERED_LIST_REG_1),
|
||||
const LIST_ITEM_REG = /^( *)([-*]|[\da-zA-Z]\.) +(.+)$/;
|
||||
type ListType = 'ol' | 'ul';
|
||||
|
||||
function getListType(marker: string): ListType {
|
||||
return marker === '*' ? 'ul' : 'ol';
|
||||
}
|
||||
|
||||
function getOrderedMeta(marker: string) {
|
||||
const startMatch = marker.match(/^(\d)\./);
|
||||
const typeMatch = marker.match(/^([aAiI])\./);
|
||||
|
||||
return {
|
||||
start: startMatch?.[1],
|
||||
type: typeMatch?.[1],
|
||||
};
|
||||
}
|
||||
|
||||
interface ParsedLine {
|
||||
indent: number;
|
||||
marker: string;
|
||||
content: string;
|
||||
listType: ListType;
|
||||
}
|
||||
|
||||
function parseLines(text: string): ParsedLine[] {
|
||||
return text
|
||||
.replace(/\n$/, '')
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const match = line.match(LIST_ITEM_REG);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const [, spaces, marker, content] = match;
|
||||
|
||||
return {
|
||||
indent: spaces.length,
|
||||
marker,
|
||||
content,
|
||||
listType: getListType(marker),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ParsedLine[];
|
||||
}
|
||||
|
||||
function openList(line: ParsedLine) {
|
||||
if (line.listType === 'ul') {
|
||||
return `<ul data-md="${UNORDERED_LIST_MD_1}">`;
|
||||
}
|
||||
const { type, start } = getOrderedMeta(line.marker);
|
||||
const dataMdAtt = `data-md="${type || start || ORDERED_LIST_MD_1}"`;
|
||||
const startAtt = start ? ` start="${start}"` : '';
|
||||
const typeAtt = type ? ` type="${type}"` : '';
|
||||
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>`;
|
||||
}
|
||||
|
||||
function closeList(listType: ListType) {
|
||||
return listType === 'ul' ? '</ul>' : '</ol>';
|
||||
}
|
||||
|
||||
function buildList(lines: ParsedLine[], parseInline?: (s: string) => string): string {
|
||||
let html = '';
|
||||
|
||||
const stack: ('ul' | 'ol')[] = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const prev = lines[index - 1];
|
||||
const next = lines[index + 1];
|
||||
|
||||
const content = parseInline ? parseInline(line.content) : line.content;
|
||||
|
||||
// FIRST ITEM
|
||||
if (!prev) {
|
||||
html += openList(line);
|
||||
stack.push(line.listType);
|
||||
}
|
||||
|
||||
// DEEPER INDENT > open nested list
|
||||
else if (line.indent > prev.indent) {
|
||||
html += openList(line);
|
||||
stack.push(line.listType);
|
||||
}
|
||||
|
||||
// SAME LEVEL
|
||||
else if (line.indent === prev.indent) {
|
||||
html += '</li>';
|
||||
|
||||
// different list type
|
||||
if (line.listType !== prev.listType) {
|
||||
html += closeList(stack.pop()!);
|
||||
|
||||
html += openList(line);
|
||||
stack.push(line.listType);
|
||||
}
|
||||
}
|
||||
|
||||
// GOING BACK UP
|
||||
else if (line.indent < prev.indent) {
|
||||
html += '</li>';
|
||||
|
||||
while (stack.length > line.indent + 1) {
|
||||
html += closeList(stack.pop()!);
|
||||
html += '</li>';
|
||||
}
|
||||
|
||||
if (line.listType !== stack[stack.length - 1]) {
|
||||
html += closeList(stack.pop()!);
|
||||
|
||||
html += openList(line);
|
||||
stack.push(line.listType);
|
||||
}
|
||||
}
|
||||
|
||||
html += `<li><p>${content}</p>`;
|
||||
|
||||
// LAST ITEM cleanup
|
||||
if (!next) {
|
||||
html += '</li>';
|
||||
|
||||
while (stack.length) {
|
||||
html += closeList(stack.pop()!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
const LIST_REG_1 = /^(?: *(?:[-*]|[\da-zA-Z]\.) +.+\n?)+/m;
|
||||
export const ListRule: BlockMDRule = {
|
||||
match: (text) => text.match(LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
|
||||
const lines = listText
|
||||
.replace(U_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
const lines = parseLines(listText);
|
||||
|
||||
return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`;
|
||||
const html = buildList(lines, parseInline);
|
||||
|
||||
return html;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -232,8 +232,9 @@ export function CodeBlock({
|
||||
opts: HTMLReactParserOptions;
|
||||
}) {
|
||||
const code = children[0];
|
||||
const languageClass =
|
||||
code instanceof Element && code.name === 'code' ? code.attribs.class : undefined;
|
||||
const attribs = code instanceof Element && code.name === 'code' ? code.attribs : undefined;
|
||||
const languageClass = attribs?.class;
|
||||
const customLabel = attribs?.['data-label'];
|
||||
const language =
|
||||
languageClass && languageClass.startsWith('language-')
|
||||
? languageClass.replace('language-', '')
|
||||
@@ -262,7 +263,7 @@ export function CodeBlock({
|
||||
<Header variant="Surface" size="400" className={css.CodeBlockHeader}>
|
||||
<Box grow="Yes">
|
||||
<Text size="L400" truncate>
|
||||
{language ?? 'Code'}
|
||||
{customLabel ?? language ?? 'Code'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" gap="200">
|
||||
|
||||
Reference in New Issue
Block a user