mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
+20
View File
@@ -0,0 +1,20 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type RendererMap } from "./utils.tsx";
import CodeBlock from "../components/views/messages/CodeBlock.tsx";
/**
* Replaces `pre` elements with a CodeBlock component
*/
export const codeBlockRenderer: RendererMap = {
pre: (pre) => {
return <CodeBlock preNode={pre} />;
},
};
+12
View File
@@ -0,0 +1,12 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
export { ambiguousLinkTooltipRenderer } from "./link-tooltip";
export { keywordPillRenderer, mentionPillRenderer } from "./pill";
export { spoilerRenderer } from "./spoiler";
export { codeBlockRenderer } from "./code-block";
export { applyReplacerOnString, combineRenderers, type RendererMap, type Replacer } from "./utils";
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2024-2025 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { domToReact } from "html-react-parser";
import LinkWithTooltip from "../components/views/elements/LinkWithTooltip";
import { getSingleTextContentNode, type RendererMap } from "./utils.tsx";
/**
* Wraps ambiguous links in a tooltip trigger that shows the full URL.
*/
export const ambiguousLinkTooltipRenderer: RendererMap = {
a: (anchor, { isHtml }) => {
// Ambiguous URLs are only possible in HTML content
if (!isHtml) return;
const href = anchor.attribs["href"];
if (href && href !== getSingleTextContentNode(anchor)) {
let tooltip = href as string;
try {
tooltip = new URL(href, window.location.href).toString();
} catch {
// Not all hrefs will be valid URLs
}
return <LinkWithTooltip tooltip={tooltip}>{domToReact([anchor])}</LinkWithTooltip>;
}
},
};
+102
View File
@@ -0,0 +1,102 @@
/*
Copyright 2024-2025 New Vector Ltd.
Copyright 2019-2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { RuleId } from "matrix-js-sdk/src/matrix";
import { type Element } from "html-react-parser";
import { textContent } from "domutils";
import reactStringReplace from "react-string-replace";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { Pill, PillType } from "../components/views/elements/Pill";
import { parsePermalink } from "../utils/permalinks/Permalinks";
import { type PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
import { hasParentMatching, type RendererMap, type ParentNode } from "./utils.tsx";
const AT_ROOM_REGEX = PushProcessor.getPushRuleGlobRegex("@room", true, "gmi");
/**
* A node here is an A element with a href attribute tag.
*
* It should be pillified if the permalink parser returns a result and one of the following conditions match:
* - Text content equals href. This is the case when sending a plain permalink inside a message.
* - The link is not from linkify (isHtml=true).
* Composer completions already create an A tag.
*/
const shouldBePillified = (node: Element, href: string, parts: PermalinkParts | null, isHtml: boolean): boolean => {
// permalink parser didn't return any parts
if (!parts) return false;
const text = textContent(node);
// event permalink with custom label
if (parts.eventId && href !== text) return false;
return href === text || isHtml;
};
const isPreCode = (domNode: ParentNode | null): boolean =>
(domNode as Element)?.tagName === "PRE" || (domNode as Element)?.tagName === "CODE";
/**
* Marks the text that activated a push-notification mention pattern.
*/
export const mentionPillRenderer: RendererMap = {
a: (anchor, { room, shouldShowPillAvatar, isHtml }) => {
if (!room) return;
const href = anchor.attribs["href"];
if (
href &&
!hasParentMatching(anchor, isPreCode) &&
shouldBePillified(anchor, href, parsePermalink(href), isHtml)
) {
return <Pill url={href} inMessage={true} room={room} shouldShowPillAvatar={shouldShowPillAvatar} />;
}
},
[Node.TEXT_NODE]: (text, { room, mxEvent, shouldShowPillAvatar }) => {
if (!room || !mxEvent) return;
const atRoomRule = room.client.pushProcessor.getPushRuleById(
mxEvent.getContent()["m.mentions"] !== undefined ? RuleId.IsRoomMention : RuleId.AtRoomNotification,
);
if (atRoomRule && room.client.pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) {
const parts = reactStringReplace(text.data, AT_ROOM_REGEX, (_match, i) => (
<Pill
key={i}
type={PillType.AtRoomMention}
inMessage={true}
room={room}
shouldShowPillAvatar={shouldShowPillAvatar}
/>
));
if (parts.length <= 1) return; // no matches, skip replacing
return <>{parts}</>;
}
},
};
/**
* Marks the text that activated a push-notification keyword pattern.
*/
export const keywordPillRenderer: RendererMap = {
[Node.TEXT_NODE]: (text, { keywordRegexpPattern }) => {
if (!keywordRegexpPattern) return;
const parts = reactStringReplace(text.data, keywordRegexpPattern, (match, i) => (
<Pill key={i} text={match} type={PillType.Keyword} />
));
if (parts.length <= 1) return; // no matches, skip replacing
return <>{parts}</>;
},
};
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { domToReact, type DOMNode } from "html-react-parser";
import { type RendererMap } from "./utils.tsx";
import Spoiler from "../components/views/elements/Spoiler.tsx";
/**
* Replaces spans with `data-mx-spoiler` with a Spoiler component.
*/
export const spoilerRenderer: RendererMap = {
span: (span, params) => {
const reason = span.attribs["data-mx-spoiler"];
if (typeof reason === "string") {
return (
<Spoiler reason={reason}>{domToReact(span.children as DOMNode[], { replace: params.replace })}</Spoiler>
);
}
},
};
+122
View File
@@ -0,0 +1,122 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { type DOMNode, Element, type HTMLReactParserOptions, Text } from "html-react-parser";
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
/**
* The type of a parent node of an element, normally exported by domhandler but that is not a direct dependency of ours
*/
export type ParentNode = NonNullable<Element["parentNode"]>;
/**
* Returns the text content of a node if it is the only child and that child is a text node
* @param node - the node to check
*/
export const getSingleTextContentNode = (node: Element): string | null => {
if (node.childNodes.length === 1 && node.childNodes[0].type === "text") {
return node.childNodes[0].data;
}
return null;
};
/**
* Returns true if the node has a parent that matches the given matcher
* @param node - the node to check
* @param matcher - a function that returns true if the node matches
*/
export const hasParentMatching = (node: Element, matcher: (node: ParentNode | null) => boolean): boolean => {
let parent = node.parentNode;
while (parent) {
if (matcher(parent)) return true;
parent = parent.parentNode;
}
return false;
};
/**
* A replacer function that can be used with html-react-parser
*/
export type Replacer = HTMLReactParserOptions["replace"];
/**
* Passes through any non-string inputs verbatim, as such they should only be used for emoji bodies
*/
export function applyReplacerOnString(
input: string | JSX.Element[],
replacer: Replacer,
): JSX.Element | JSX.Element[] | string {
if (!replacer) return input;
const arr = Array.isArray(input) ? input : [input];
return arr.map((input, index): JSX.Element => {
if (typeof input === "string") {
return (
<React.Fragment key={index}>{(replacer(new Text(input), 0) as JSX.Element) || input}</React.Fragment>
);
}
return input;
});
}
interface Parameters {
isHtml: boolean;
replace: Replacer;
// Required for keywordPillRenderer
keywordRegexpPattern?: RegExp;
// Required for mentionPillRenderer
mxEvent?: MatrixEvent;
room?: Room;
shouldShowPillAvatar?: boolean;
}
type SpecialisedReplacer<T extends DOMNode> = (
node: T,
parameters: Parameters,
index: number,
) => JSX.Element | string | void;
/**
* A map of replacer functions for different types of nodes/tags.
* When a function returns a JSX element, the element will be rendered in place of the node.
*/
export type RendererMap = Partial<
{
[tagName in keyof HTMLElementTagNameMap]: SpecialisedReplacer<Element>;
} & {
[Node.TEXT_NODE]: SpecialisedReplacer<Text>;
}
>;
type PreparedRenderer = (parameters: Omit<Parameters, "replace">) => Replacer;
/**
* Combines multiple renderers into a single Replacer function
* @param renderers - the list of renderers to combine
*/
export const combineRenderers =
(...renderers: RendererMap[]): PreparedRenderer =>
(parameters) => {
const replace: Replacer = (node, index) => {
if (node.type === "text") {
for (const replacer of renderers) {
const result = replacer[Node.TEXT_NODE]?.(node, parametersWithReplace, index);
if (result) return result;
}
}
if (node instanceof Element) {
const tagName = node.tagName.toLowerCase() as keyof HTMLElementTagNameMap;
for (const replacer of renderers) {
const result = replacer[tagName]?.(node, parametersWithReplace, index);
if (result) return result;
}
}
};
const parametersWithReplace: Parameters = { ...parameters, replace };
return replace;
};