Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@
@import "./views/rooms/_EditMessageComposer.pcss";
@import "./views/rooms/_EmojiButton.pcss";
@import "./views/rooms/_EventBubbleTile.pcss";
@import "./views/rooms/_EventPreview.pcss";
@import "./views/rooms/_EventTile.pcss";
@import "./views/rooms/_HistoryTile.pcss";
@import "./views/rooms/_IRCLayout.pcss";
Expand Down
138 changes: 0 additions & 138 deletions apps/web/src/components/views/rooms/EventPreview.tsx

This file was deleted.

24 changes: 22 additions & 2 deletions apps/web/src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
useCreateAutoDisposedViewModel,
ActionBarView,
E2eMessageSharedIconView,
EventPreviewView,
MessageTimestampView,
PinnedMessageBadge,
ReactionsRowButtonView,
Expand Down Expand Up @@ -89,7 +90,6 @@ import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationB
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
import { Icon as LateIcon } from "../../../../res/img/sensor.svg";
import PinningUtils from "../../../utils/PinningUtils";
import { EventPreview } from "./EventPreview";
import { E2eStandardPadlockIcon } from "./EventTile/E2eStandardPadlockIcon";
import SettingsStore from "../../../settings/SettingsStore";
import { CardContext } from "../right_panel/context";
Expand Down Expand Up @@ -146,6 +146,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useSettingValue } from "../../../hooks/useSettings";
import { DecryptionFailureBodyFactory, RedactedBodyFactory } from "../messages/MBodyFactory";
import { EventTileE2eViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileE2eViewModel";
import { EventPreviewViewModel } from "../../../viewmodels/room/timeline/event-tile/EventPreviewViewModel";

/** Relation lookup type retained for EventTile consumers. */
export type { GetRelationsForEvent } from "../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";
Expand Down Expand Up @@ -1308,7 +1309,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
) : this.props.mxEvent.isDecryptionFailure() ? (
<DecryptionFailureBodyFactory mxEvent={this.props.mxEvent} />
) : (
<EventPreview mxEvent={this.props.mxEvent} />
<EventPreviewWrapper mxEvent={this.props.mxEvent} />
)}
</div>
{this.renderThreadPanelSummary(threadState)}
Expand Down Expand Up @@ -1571,6 +1572,25 @@ function MessageTimestampAdapter({ vm, timestampProps }: Readonly<MessageTimesta
);
}

type EventPreviewWrapperProps = Omit<React.ComponentPropsWithoutRef<"span">, "children" | "title"> & {
mxEvent: MatrixEvent;
};

function EventPreviewWrapper({ mxEvent, ...props }: Readonly<EventPreviewWrapperProps>): JSX.Element {
const cli = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(() => new EventPreviewViewModel({ cli, mxEvent }));

useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);

useEffect(() => {
vm.setClient(cli);
}, [cli, vm]);

return <EventPreviewView {...props} vm={vm} />;
}

interface ThreadMessagePreviewWrapperProps {
thread: Thread;
showDisplayName?: boolean;
Expand Down
25 changes: 23 additions & 2 deletions apps/web/src/components/views/rooms/PinnedMessageBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-soli
import { Button } from "@vector-im/compound-web";
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { EventPreviewView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";

import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
import { _t } from "../../../languageHandler";
Expand All @@ -24,8 +25,9 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo
import { Action } from "../../../dispatcher/actions";
import MessageEvent from "../messages/MessageEvent";
import PosthogTrackers from "../../../PosthogTrackers.ts";
import { EventPreview } from "./EventPreview.tsx";
import { SDKContext } from "../../../contexts/SDKContext.ts";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { EventPreviewViewModel } from "../../../viewmodels/room/timeline/event-tile/EventPreviewViewModel";

/**
* The props for the {@link PinnedMessageBanner} component.
Expand Down Expand Up @@ -118,7 +120,7 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
)}
</div>
)}
<EventPreview
<EventPreviewWrapper
mxEvent={pinnedEvent}
className="mx_PinnedMessageBanner_message"
data-testid="banner-message"
Expand All @@ -141,6 +143,25 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
);
}

type EventPreviewWrapperProps = Omit<React.ComponentPropsWithoutRef<"span">, "children" | "title"> & {
mxEvent: MatrixEvent;
};

function EventPreviewWrapper({ mxEvent, ...props }: Readonly<EventPreviewWrapperProps>): JSX.Element {
const cli = useContext(MatrixClientContext);
const vm = useCreateAutoDisposedViewModel(() => new EventPreviewViewModel({ cli, mxEvent }));

useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);

useEffect(() => {
vm.setClient(cli);
}, [cli, vm]);

return <EventPreviewView {...props} vm={vm} />;
}

/**
* When the banner is displayed or hidden, we want to notify the timeline to resize itself.
* @param pinnedEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2026 Element Creations 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 ReactNode } from "react";
import { M_POLL_START, type MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";

import { _t } from "../../../../languageHandler";
import { MessagePreviewStore } from "../../../../stores/message-preview";

export interface EventPreviewContent {
previewContent: ReactNode;
previewTooltip?: string;
}

export class EventPreviewContentCache {
private key?: string;
private content?: ReactNode;

public get(preview: string, prefix: string | null): ReactNode {
const key = `${prefix ?? ""}\u0000${preview}`;
if (this.key === key && this.content !== undefined) {
return this.content;
}

this.key = key;
this.content = prefix
? _t(
"event_preview|preview",
{
prefix,
preview,
},
{
bold: (sub) => <strong>{sub}</strong>,
},
)
: preview;

return this.content;
}
}

export class MatrixEventContentChangeListener {
private mxEvent?: MatrixEvent;
private callback?: () => void;

public setEvent(mxEvent: MatrixEvent | undefined, callback: () => void): void {
if (this.mxEvent === mxEvent && this.callback === callback) return;

this.teardown();
this.mxEvent = mxEvent;
this.callback = callback;

if (!mxEvent) return;

mxEvent.on(MatrixEventEvent.Replaced, callback);
mxEvent.on(MatrixEventEvent.Decrypted, callback);
}

public teardown(): void {
if (!this.mxEvent || !this.callback) {
this.mxEvent = undefined;
this.callback = undefined;
return;
}

this.mxEvent.off(MatrixEventEvent.Replaced, this.callback);
this.mxEvent.off(MatrixEventEvent.Decrypted, this.callback);
this.mxEvent = undefined;
this.callback = undefined;
}
}

export function getEventPreviewContent(
mxEvent: MatrixEvent,
cache: EventPreviewContentCache,
): EventPreviewContent | null {
const preview = MessagePreviewStore.instance.generatePreviewForEvent(mxEvent);
if (!preview) {
return null;
}

const prefix = getEventPreviewPrefix(mxEvent.getType(), mxEvent.getContent().msgtype as MsgType | undefined);

return {
previewContent: cache.get(preview, prefix),
previewTooltip: prefix ? undefined : preview,
};
}

function getEventPreviewPrefix(type: string, msgType?: MsgType): string | null {
switch (type) {

Check warning on line 96 in apps/web/src/viewmodels/room/timeline/event-tile/EventPreviewUtils.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this "switch" statement by "if" statements to increase readability.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ5y033y3qigiBTjgvwu&open=AZ5y033y3qigiBTjgvwu&pullRequest=33646
case M_POLL_START.name:
return _t("event_preview|prefix|poll");
default:
}

switch (msgType) {
case MsgType.Audio:
return _t("event_preview|prefix|audio");
case MsgType.Image:
return _t("event_preview|prefix|image");
case MsgType.Video:
return _t("event_preview|prefix|video");
case MsgType.File:
return _t("event_preview|prefix|file");
default:
return null;
}
}
Loading
Loading