mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-02 21:32:46 +00:00
4bbcb8bb5d
* Very first pass at shared component views Turn the trivial TextualEvent into a shared component with a separate view model for element web. Args to view model will probably change to be more specific and VM typer needs abstracting out into an interface, but should give the general idea. * Remove old TextualEvent * Pass showHiddenEvents Because we used it anyway, we just cheated by getting it from the context * Factor out common view model stuff * Move ViewModel interface into the shared components * Add tiny wrapper hook * Move showHiddenEvents into props fully * Fill in stories / test * chore: setup storybook cherry pick edc5e8705674b8708d986910b02b5d2545777fb3 from florianduros/storybook * Add TextualEvent component to storybook * Add mock view model & snapshot * Remove old style stories entry * Change import * Change import * Prettier * Add paxckage patch to @types/mdx for React 19 compat * Pass getSnapshot as getServerSnapshot too * Maybe make sonar regognise tests as tests * Typo * Use storybook reacvt-vite There's no reason to use the react-webpack plugin just because our app is stuck on webpack, it just means we have vite as a dependency too. * Change here too * Workaround for incomatible types in rollup https://github.com/rollup/rollup/issues/5199 * Remove webpack styling addon Not necessary now we're using vite * Hopefully do screenshot testing... * need newer node * quote issues * Make it an npm script * colons * use right port * Install playwright browsers * Try without the if * Oh right, we need the headless shell * Pass flag to store received screenshots and upload diffs too * Update snapshot from received * Include platform in snapshot / received dir because font rendering differs between platforms * Suffix snapshots with platform instead like we do for playwright * Remove unnecessary env vars and better name * Add some comments * Prettier * Fix yarn.lock * Memoise vm creation Co-authored-by: Florian Duros <florianduros@element.io> * Add implements Co-authored-by: Florian Duros <florianduros@element.io> * Fix listener interface * Add implements Co-authored-by: Florian Duros <florianduros@element.io> * Fix types * Fix more types * Revert useMemo as this isn't a hook * Unused import * Add missing playwright step * Add return type annotation * Change to add / remove subscription callback * Change to 'add' rather than 'subs.subscribe' * Add cache specifier for only shell playwright browsers * Add copyright headers --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Florian Duros <florianduros@element.io>
175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2020, 2021 Tulir Asokan <tulir@maunium.net>
|
|
|
|
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, { createRef } from "react";
|
|
import classNames from "classnames";
|
|
import { type MatrixEvent, MatrixEventEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
|
|
import { _t } from "../../../languageHandler";
|
|
import dis from "../../../dispatcher/dispatcher";
|
|
import { Action } from "../../../dispatcher/actions";
|
|
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
|
import SenderProfile from "../messages/SenderProfile";
|
|
import MImageReplyBody from "../messages/MImageReplyBody";
|
|
import { isVoiceMessage } from "../../../utils/EventUtils";
|
|
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
|
|
import MFileBody from "../messages/MFileBody";
|
|
import MemberAvatar from "../avatars/MemberAvatar";
|
|
import MVoiceMessageBody from "../messages/MVoiceMessageBody";
|
|
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|
import { renderReplyTile } from "../../../events/EventTileFactory";
|
|
import { type GetRelationsForEvent } from "../rooms/EventTile";
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
import { type IBodyProps } from "../messages/IBodyProps";
|
|
|
|
interface IProps {
|
|
mxEvent: MatrixEvent;
|
|
permalinkCreator?: RoomPermalinkCreator;
|
|
highlights?: string[];
|
|
highlightLink?: string;
|
|
toggleExpandedQuote?: () => void;
|
|
getRelationsForEvent?: GetRelationsForEvent;
|
|
}
|
|
|
|
export default class ReplyTile extends React.PureComponent<IProps> {
|
|
private anchorElement = createRef<HTMLAnchorElement>();
|
|
|
|
public componentDidMount(): void {
|
|
this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted);
|
|
this.props.mxEvent.on(MatrixEventEvent.BeforeRedaction, this.onEventRequiresUpdate);
|
|
this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onEventRequiresUpdate);
|
|
}
|
|
|
|
public componentWillUnmount(): void {
|
|
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
|
this.props.mxEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onEventRequiresUpdate);
|
|
this.props.mxEvent.removeListener(MatrixEventEvent.Replaced, this.onEventRequiresUpdate);
|
|
}
|
|
|
|
private onDecrypted = (): void => {
|
|
this.forceUpdate();
|
|
};
|
|
|
|
private onEventRequiresUpdate = (): void => {
|
|
// Force update when necessary - redactions and edits
|
|
this.forceUpdate();
|
|
};
|
|
|
|
private onClick = (e: React.MouseEvent): void => {
|
|
const clickTarget = e.target as HTMLElement;
|
|
// Following a link within a reply should not dispatch the `view_room` action
|
|
// so that the browser can direct the user to the correct location
|
|
// The exception being the link wrapping the reply
|
|
if (
|
|
clickTarget.tagName.toLowerCase() !== "a" ||
|
|
clickTarget.closest("a") === null ||
|
|
clickTarget === this.anchorElement.current
|
|
) {
|
|
// This allows the permalink to be opened in a new tab/window or copied as
|
|
// matrix.to, but also for it to enable routing within Riot when clicked.
|
|
e.preventDefault();
|
|
// Expand thread on shift key
|
|
if (this.props.toggleExpandedQuote && e.shiftKey) {
|
|
this.props.toggleExpandedQuote();
|
|
} else {
|
|
dis.dispatch<ViewRoomPayload>({
|
|
action: Action.ViewRoom,
|
|
event_id: this.props.mxEvent.getId(),
|
|
highlighted: true,
|
|
room_id: this.props.mxEvent.getRoomId(),
|
|
metricsTrigger: undefined, // room doesn't change
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
public render(): React.ReactNode {
|
|
const mxEvent = this.props.mxEvent;
|
|
const msgType = mxEvent.getContent().msgtype;
|
|
const evType = mxEvent.getType();
|
|
|
|
const { hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(
|
|
MatrixClientPeg.safeGet(),
|
|
mxEvent,
|
|
false /* Replies are never hidden, so this should be fine */,
|
|
);
|
|
// This shouldn't happen: the caller should check we support this type
|
|
// before trying to instantiate us
|
|
if (!hasRenderer) {
|
|
const { mxEvent } = this.props;
|
|
logger.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
|
return (
|
|
<div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">{_t("timeline|error_no_renderer")}</div>
|
|
);
|
|
}
|
|
|
|
const classes = classNames("mx_ReplyTile", {
|
|
mx_ReplyTile_inline: msgType === MsgType.Emote,
|
|
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
|
|
mx_ReplyTile_audio: msgType === MsgType.Audio,
|
|
mx_ReplyTile_video: msgType === MsgType.Video,
|
|
});
|
|
|
|
let permalink = "#";
|
|
if (this.props.permalinkCreator) {
|
|
permalink = this.props.permalinkCreator.forEvent(mxEvent.getId()!);
|
|
}
|
|
|
|
let sender;
|
|
const hasOwnSender = isInfoMessage || evType === EventType.RoomCreate;
|
|
if (!hasOwnSender) {
|
|
sender = (
|
|
<div className="mx_ReplyTile_sender">
|
|
<MemberAvatar member={mxEvent.sender} fallbackUserId={mxEvent.getSender()} size="16px" />
|
|
<SenderProfile mxEvent={mxEvent} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const msgtypeOverrides: Record<string, React.ComponentType<IBodyProps>> = {
|
|
[MsgType.Image]: MImageReplyBody,
|
|
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
|
|
[MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody,
|
|
[MsgType.Video]: MFileBody,
|
|
};
|
|
const evOverrides: Record<string, React.ComponentType<IBodyProps>> = {
|
|
// Use MImageReplyBody so that the sticker isn't taking up a lot of space
|
|
[EventType.Sticker]: MImageReplyBody,
|
|
};
|
|
|
|
return (
|
|
<div className={classes}>
|
|
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
|
{sender}
|
|
{renderReplyTile(
|
|
{
|
|
...this.props,
|
|
|
|
// overrides
|
|
ref: undefined,
|
|
showUrlPreview: false,
|
|
overrideBodyTypes: msgtypeOverrides,
|
|
overrideEventTypes: evOverrides,
|
|
maxImageHeight: 96,
|
|
isSeeingThroughMessageHiddenForModeration,
|
|
|
|
// appease TS
|
|
highlights: this.props.highlights,
|
|
highlightLink: this.props.highlightLink,
|
|
permalinkCreator: this.props.permalinkCreator,
|
|
showHiddenEvents: false,
|
|
},
|
|
false /* showHiddenEvents shouldn't be relevant */,
|
|
)}
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
}
|