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
+464
View File
@@ -0,0 +1,464 @@
/*
Copyright 2024 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, { type JSX } from "react";
import {
type MatrixEvent,
EventType,
MsgType,
RelationType,
type MatrixClient,
GroupCallIntent,
M_POLL_END,
M_POLL_START,
} from "matrix-js-sdk/src/matrix";
import {
EncryptionEventView,
TextualEventView,
useCreateAutoDisposedViewModel,
} from "@element-hq/web-shared-components";
import SettingsStore from "../settings/SettingsStore";
import type LegacyCallEventGrouper from "../components/structures/LegacyCallEventGrouper";
import { type IEventTileType, type EventTileProps } from "../components/views/rooms/EventTile";
import { TimelineRenderingType } from "../contexts/RoomContext";
import MessageEvent from "../components/views/messages/MessageEvent";
import LegacyCallEvent from "../components/views/messages/LegacyCallEvent";
import { CallEvent } from "../components/views/messages/CallEvent";
import { RoomPredecessorTile } from "../components/views/messages/RoomPredecessorTile";
import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent";
import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore";
import { ALL_RULE_TYPES } from "../mjolnir/BanList";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import MKeyVerificationRequest from "../components/views/messages/MKeyVerificationRequest";
import { WidgetType } from "../widgets/WidgetType";
import MJitsiWidgetEvent from "../components/views/messages/MJitsiWidgetEvent";
import { hasText } from "../TextForEvent";
import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils";
import HiddenBody from "../components/views/messages/HiddenBody";
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
import { type IBodyProps } from "../components/views/messages/IBodyProps";
import { ModuleApi } from "../modules/Api";
import { EncryptionEventViewModel } from "../viewmodels/event-tiles/EncryptionEventViewModel";
import { TextualEventViewModel } from "../viewmodels/event-tiles/TextualEventViewModel";
import { ElementCallEventType } from "../call-types";
// Subset of EventTile's IProps plus some mixins
export interface EventTileTypeProps extends Pick<
EventTileProps,
| "mxEvent"
| "highlights"
| "highlightLink"
| "showUrlPreview"
| "forExport"
| "getRelationsForEvent"
| "editState"
| "replacingEventId"
| "permalinkCreator"
| "callEventGrouper"
| "isSeeingThroughMessageHiddenForModeration"
| "inhibitInteraction"
> {
ref?: React.RefObject<IEventTileType | null>;
maxImageHeight?: number; // pixels
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;
showHiddenEvents: boolean;
}
type FactoryProps = Omit<EventTileTypeProps, "ref">;
type Factory<X = FactoryProps> = (ref: React.RefObject<any> | undefined, props: X) => JSX.Element;
export const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyCallEventGrouper }> = (ref, props) => (
<LegacyCallEvent ref={ref} {...props} />
);
const CallEventFactory: Factory = (ref, props) => <CallEvent ref={ref} {...props} />;
export const TextualEventFactory: Factory = (ref, props) => {
const vm = new TextualEventViewModel(props);
return <TextualEventView vm={vm} />;
};
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const cli = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(() => new EncryptionEventViewModel({ mxEvent, cli }));
return <EncryptionEventView vm={vm} ref={ref} />;
}
const EncryptionEventFactory: Factory = (ref, props) => {
return <EncryptionEventWrappedView ref={ref} {...props} />;
};
const VerificationReqFactory: Factory = (_ref, props) => <MKeyVerificationRequest {...props} />;
const HiddenEventFactory: Factory = (ref, props) => <HiddenBody ref={ref} {...props} />;
// These factories are exported for reference comparison against pickFactory()
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEvent ref={ref} {...props} />;
export const JSONEventFactory: Factory = (ref, props) => <ViewSourceEvent ref={ref} {...props} />;
export const RoomCreateEventFactory: Factory = (_ref, props) => <RoomPredecessorTile {...props} />;
const EVENT_TILE_TYPES = new Map<string, Factory>([
[EventType.RoomMessage, MessageEventFactory], // note that verification requests are handled in pickFactory()
[EventType.Sticker, MessageEventFactory],
[M_POLL_START.name, MessageEventFactory],
[M_POLL_START.altName, MessageEventFactory],
[M_POLL_END.name, MessageEventFactory],
[M_POLL_END.altName, MessageEventFactory],
[EventType.CallInvite, LegacyCallEventFactory as Factory], // note that this requires a special factory type
]);
const STATE_EVENT_TILE_TYPES = new Map<string, Factory>([
[EventType.RoomEncryption, EncryptionEventFactory],
[EventType.RoomCanonicalAlias, TextualEventFactory],
[EventType.RoomCreate, RoomCreateEventFactory],
[EventType.RoomMember, TextualEventFactory],
[EventType.RoomName, TextualEventFactory],
[EventType.RoomAvatar, (ref, props) => <RoomAvatarEvent ref={ref} {...props} />],
[EventType.RoomThirdPartyInvite, TextualEventFactory],
[EventType.RoomHistoryVisibility, TextualEventFactory],
[EventType.RoomTopic, TextualEventFactory],
[EventType.RoomPowerLevels, TextualEventFactory],
[EventType.RoomPinnedEvents, TextualEventFactory],
[EventType.RoomServerAcl, TextualEventFactory],
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
["im.vector.modular.widgets", TextualEventFactory], // note that Jitsi widgets are special in pickFactory()
[WIDGET_LAYOUT_EVENT_TYPE, TextualEventFactory],
[EventType.RoomTombstone, TextualEventFactory],
[EventType.RoomJoinRules, TextualEventFactory],
[EventType.RoomGuestAccess, TextualEventFactory],
]);
for (const evType of ElementCallEventType.names) {
STATE_EVENT_TILE_TYPES.set(evType, CallEventFactory);
}
// Add all the Mjolnir stuff to the renderer too
for (const evType of ALL_RULE_TYPES) {
STATE_EVENT_TILE_TYPES.set(evType, TextualEventFactory);
}
// These events should be recorded in the STATE_EVENT_TILE_TYPES
const SINGULAR_STATE_EVENTS = new Set([
EventType.RoomEncryption,
EventType.RoomCanonicalAlias,
EventType.RoomCreate,
EventType.RoomName,
EventType.RoomAvatar,
EventType.RoomHistoryVisibility,
EventType.RoomTopic,
EventType.RoomPowerLevels,
EventType.RoomPinnedEvents,
EventType.RoomServerAcl,
WIDGET_LAYOUT_EVENT_TYPE,
EventType.RoomTombstone,
EventType.RoomJoinRules,
EventType.RoomGuestAccess,
]);
/**
* Find an event tile factory for the given conditions.
* @param mxEvent The event.
* @param cli The matrix client to reference when needed.
* @param showHiddenEvents Whether hidden events should be shown.
* @param asHiddenEv When true, treat the event as always hidden.
* @returns The factory, or undefined if not possible.
*/
export function pickFactory(
mxEvent: MatrixEvent,
cli: MatrixClient,
showHiddenEvents: boolean,
asHiddenEv?: boolean,
): Factory | undefined {
const evType = mxEvent.getType(); // cache this to reduce call stack execution hits
// Note: we avoid calling SettingsStore unless absolutely necessary - this code is on the critical path.
if (asHiddenEv && showHiddenEvents) {
return JSONEventFactory;
}
const noEventFactoryFactory: () => Factory | undefined = () => (showHiddenEvents ? JSONEventFactory : undefined); // just don't render things that we shouldn't render
// We run all the event type checks first as they might override the factory entirely.
const moderationState = getMessageModerationState(mxEvent, cli);
if (moderationState === MessageModerationState.HIDDEN_TO_CURRENT_USER) {
return HiddenEventFactory;
}
if (evType === EventType.RoomMessage) {
// don't show verification requests we're not involved in,
// not even when showing hidden events
const content = mxEvent.getContent();
if (content?.msgtype === MsgType.KeyVerificationRequest) {
const me = cli.getUserId();
if (mxEvent.getSender() !== me && content["to"] !== me) {
return noEventFactoryFactory(); // not for/from us
} else {
// override the factory
return VerificationReqFactory;
}
}
}
if (evType === EventType.RoomCreate) {
const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors");
const predecessor = cli.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled);
if (!predecessor) {
return noEventFactoryFactory();
}
}
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (evType === "im.vector.modular.widgets") {
let type = mxEvent.getContent()["type"];
if (!type) {
// deleted/invalid widget - try the past widget type
type = mxEvent.getPrevContent()["type"];
}
if (WidgetType.JITSI.matches(type)) {
// override the factory
return JitsiEventFactory;
}
}
// Try and pick a state event factory, if we can.
if (mxEvent.isState()) {
if (shouldDisplayAsBeaconTile(mxEvent)) {
return MessageEventFactory;
}
if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== "") {
return noEventFactoryFactory(); // improper event type to render
}
if (STATE_EVENT_TILE_TYPES.get(evType) === TextualEventFactory && !hasText(mxEvent, cli, showHiddenEvents)) {
return noEventFactoryFactory();
}
return STATE_EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory();
}
// Blanket override for all events. The MessageEvent component handles redacted states for us.
if (mxEvent.isRedacted()) {
return MessageEventFactory;
}
if (mxEvent.isRelation(RelationType.Replace)) {
return noEventFactoryFactory();
}
return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory();
}
/**
* Render an event as a tile
* @param renderType The render type. Used to inform properties given to the eventual component.
* @param props The properties to provide to the eventual component.
* @param cli Optional client instance to use, otherwise the default MatrixClientPeg will be used.
* @returns The tile as JSX, or null if unable to render.
*/
export function renderTile(
renderType: TimelineRenderingType,
props: EventTileTypeProps,
cli?: MatrixClient,
): JSX.Element | null {
cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing
const factory = pickFactory(props.mxEvent, cli, props.showHiddenEvents);
if (!factory) {
// If we don't have a factory for this event, attempt
// to find a custom component that can render it.
// Will return null if no custom component can render it.
return ModuleApi.instance.customComponents.renderMessage({
mxEvent: props.mxEvent,
});
}
// Note that we split off the ones we actually care about here just to be sure that we're
// not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's
// lazy calls of `renderTile(..., this.props)` will have a lot more than we actually care
// about.
const {
ref,
mxEvent,
forExport,
replacingEventId,
editState,
highlights,
highlightLink,
showUrlPreview,
permalinkCreator,
callEventGrouper,
getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration,
inhibitInteraction,
showHiddenEvents,
} = props;
switch (renderType) {
case TimelineRenderingType.File:
case TimelineRenderingType.Notification:
case TimelineRenderingType.Thread:
return ModuleApi.instance.customComponents.renderMessage(
{
mxEvent: props.mxEvent,
},
(origProps) =>
factory(props.ref, {
// We only want a subset of props, so we don't end up causing issues for downstream components.
mxEvent,
highlights,
highlightLink,
showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview,
editState,
replacingEventId,
getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration,
permalinkCreator,
inhibitInteraction,
showHiddenEvents,
}),
);
default:
return ModuleApi.instance.customComponents.renderMessage(
{
mxEvent: props.mxEvent,
},
(origProps) =>
factory(ref, {
// NEARLY ALL THE OPTIONS!
mxEvent,
forExport,
replacingEventId,
editState,
highlights,
highlightLink,
showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview,
permalinkCreator,
callEventGrouper,
getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration,
inhibitInteraction,
showHiddenEvents,
}),
);
}
}
/**
* A version of renderTile() specifically for replies.
* @param props The properties to specify on the eventual object.
* @param showHiddenEvents Whether hidden events should be shown.
* @param cli Optional client instance to use, otherwise the default MatrixClientPeg will be used.
* @returns The tile as JSX, or falsy if unable to render.
*/
export function renderReplyTile(
props: EventTileTypeProps,
showHiddenEvents: boolean,
cli?: MatrixClient,
): JSX.Element | null {
cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing
const factory = pickFactory(props.mxEvent, cli, showHiddenEvents);
if (!factory) {
// If we don't have a factory for this event, attempt
// to find a custom component that can render it.
// Will return null if no custom component can render it.
return ModuleApi.instance.customComponents.renderMessage({
mxEvent: props.mxEvent,
});
}
// See renderTile() for why we split off so much
const {
ref,
mxEvent,
highlights,
highlightLink,
showUrlPreview,
overrideBodyTypes,
overrideEventTypes,
replacingEventId,
maxImageHeight,
getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration,
permalinkCreator,
} = props;
return ModuleApi.instance.customComponents.renderMessage(
{
mxEvent: props.mxEvent,
},
(origProps) =>
factory(ref, {
mxEvent,
highlights,
highlightLink,
showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview,
overrideBodyTypes,
overrideEventTypes,
replacingEventId,
maxImageHeight,
getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration,
permalinkCreator,
showHiddenEvents,
}),
);
}
// XXX: this'll eventually be dynamic based on the fields once we have extensible event types
const messageTypes = [EventType.RoomMessage, EventType.Sticker];
export function isMessageEvent(ev: MatrixEvent): boolean {
return (
messageTypes.includes(ev.getType() as EventType) ||
M_POLL_START.matches(ev.getType()) ||
M_POLL_END.matches(ev.getType())
);
}
export function haveRendererForEvent(
mxEvent: MatrixEvent,
matrixClient: MatrixClient,
showHiddenEvents: boolean,
): boolean {
// Only show "Message deleted" tile for plain message events, encrypted events,
// and state events as they'll likely still contain enough keys to be relevant.
if (mxEvent.isRedacted() && !mxEvent.isEncrypted() && !isMessageEvent(mxEvent) && !mxEvent.isState()) {
return false;
}
// Check to see if we have any hints for this message, which indicates
// there is a custom renderer for the event.
if (ModuleApi.instance.customComponents.getHintsForMessage(mxEvent)) {
return true;
}
// No tile for replacement events since they update the original tile
if (mxEvent.isRelation(RelationType.Replace)) return false;
const handler = pickFactory(mxEvent, matrixClient, showHiddenEvents);
if (!handler) return false;
if (handler === TextualEventFactory) {
return hasText(mxEvent, matrixClient, showHiddenEvents);
} else if (handler === STATE_EVENT_TILE_TYPES.get(EventType.RoomCreate)) {
const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors");
const predecessor = matrixClient.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled);
return Boolean(predecessor);
} else if (ElementCallEventType.names.some((eventType) => handler === STATE_EVENT_TILE_TYPES.get(eventType))) {
const intent = mxEvent.getContent()["m.intent"];
const newlyStarted = Object.keys(mxEvent.getPrevContent()).length === 0;
// Only interested in events that mark the start of a non-room call
return newlyStarted && typeof intent === "string" && intent !== GroupCallIntent.Room;
} else if (handler === JSONEventFactory) {
return false;
} else {
return true;
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
Copyright 2024 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 {
type MatrixClient,
type MatrixEvent,
MatrixEventEvent,
type RelationType,
TypedEventEmitter,
type Relations,
RelationsEvent,
} from "matrix-js-sdk/src/matrix";
import { type IDestroyable } from "../utils/IDestroyable";
export enum RelationsHelperEvent {
Add = "add",
}
interface EventMap {
[RelationsHelperEvent.Add]: (event: MatrixEvent) => void;
}
/**
* Helper class that manages a specific event type relation for an event.
* Just create an instance and listen for new events for that relation.
* Optionally receive the current events by calling emitCurrent().
* Clean up everything by calling destroy().
*/
export class RelationsHelper extends TypedEventEmitter<RelationsHelperEvent, EventMap> implements IDestroyable {
private relations?: Relations;
private eventId: string;
private roomId: string;
public constructor(
private event: MatrixEvent,
private relationType: RelationType,
private relationEventType: string,
private client: MatrixClient,
) {
super();
const eventId = event.getId();
if (!eventId) {
throw new Error("unable to create RelationsHelper: missing event ID");
}
const roomId = event.getRoomId();
if (!roomId) {
throw new Error("unable to create RelationsHelper: missing room ID");
}
this.eventId = eventId;
this.roomId = roomId;
this.setUpRelations();
}
private setUpRelations = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private onRelationsCreated = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
this.emitCurrent();
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private setRelations(): void {
const room = this.client.getRoom(this.event.getRoomId());
this.relations = room
?.getUnfilteredTimelineSet()
?.relations?.getChildEventsForEvent(this.eventId, this.relationType, this.relationEventType);
}
private onRelationsAdd = (event: MatrixEvent): void => {
this.emit(RelationsHelperEvent.Add, event);
};
public emitCurrent(): void {
this.relations?.getRelations()?.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
}
public getCurrent(): MatrixEvent[] {
return this.relations?.getRelations() || [];
}
/**
* Fetches all related events from the server and emits them.
*/
public async emitFetchCurrent(): Promise<void> {
let nextBatch: string | undefined = undefined;
do {
const response = await this.client.relations(
this.roomId,
this.eventId,
this.relationType,
this.relationEventType,
{
from: nextBatch,
limit: 50,
},
);
nextBatch = response?.nextBatch ?? undefined;
response?.events.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
} while (nextBatch);
}
public destroy(): void {
this.removeAllListeners();
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
if (this.relations) {
this.relations.off(RelationsEvent.Add, this.onRelationsAdd);
}
}
}
@@ -0,0 +1,28 @@
/*
Copyright 2024 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 { M_POLL_END, M_POLL_START, M_BEACON_INFO, type MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
/**
* Get forwardable event for a given event
* If an event is not forwardable return null
*/
export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): MatrixEvent | null => {
if (M_POLL_START.matches(event.getType()) || M_POLL_END.matches(event.getType())) {
return null;
}
// Live location beacons should forward their latest location as a static pin location
// If the beacon is not live, or doesn't have a location forwarding is not allowed
if (M_BEACON_INFO.matches(event.getType())) {
return getShareableLocationEventForBeacon(event, cli);
}
return event;
};
+10
View File
@@ -0,0 +1,10 @@
/*
Copyright 2024 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.
*/
export { getForwardableEvent } from "./forward/getForwardableEvent";
export { getShareableLocationEvent } from "./location/getShareableLocationEvent";
@@ -0,0 +1,27 @@
/*
Copyright 2024 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 { M_BEACON_INFO, type MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
import { isLocationEvent } from "../../utils/EventUtils";
/**
* Get event that is shareable as a location
* If an event does not have a shareable location, return null
*/
export const getShareableLocationEvent = (event: MatrixEvent, cli: MatrixClient): MatrixEvent | null => {
if (isLocationEvent(event)) {
return event;
}
if (M_BEACON_INFO.matches(event.getType())) {
return getShareableLocationEventForBeacon(event, cli);
}
return null;
};