Files
ThreadNet-Web/src/components/views/messages/ReactionsRow.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

233 lines
8.7 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
2023-02-13 11:39:16 +00:00
import React, { SyntheticEvent } from "react";
import classNames from "classnames";
import { MatrixEvent, MatrixEventEvent, Relations, RelationsEvent } from "matrix-js-sdk/src/matrix";
import { uniqBy } from "lodash";
2023-09-01 04:16:24 -06:00
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
2019-06-27 13:05:04 +01:00
import { _t } from "../../../languageHandler";
import { isContentActionable } from "../../../utils/EventUtils";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import ContextMenu, { aboveLeftOf, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton";
import RoomContext from "../../../contexts/RoomContext";
2022-01-07 10:40:53 +01:00
import AccessibleButton from "../elements/AccessibleButton";
2023-09-01 04:16:24 -06:00
import SettingsStore from "../../../settings/SettingsStore";
2019-06-27 13:05:04 +01:00
// The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8;
2023-09-01 04:16:24 -06:00
export const REACTION_SHORTCODE_KEY = new UnstableValue("shortcode", "com.beeper.reaction.shortcode");
const ReactButton: React.FC<IProps> = ({ mxEvent, reactions }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
let contextMenu: JSX.Element | undefined;
if (menuDisplayed && button.current) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("timeline|reactions|add_reaction_prompt")}
onClick={openMenu}
2023-02-13 11:39:16 +00:00
onContextMenu={(e: SyntheticEvent): void => {
e.preventDefault();
openMenu();
}}
isExpanded={menuDisplayed}
ref={button}
/>
{contextMenu}
</React.Fragment>
);
};
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations | null | undefined;
}
interface IState {
myReactions: MatrixEvent[] | null;
showAll: boolean;
}
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext;
2024-08-01 17:14:28 +01:00
public declare context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
2019-05-09 15:14:58 +01:00
2019-05-09 19:11:40 +01:00
this.state = {
myReactions: this.getMyReactions(),
2019-06-27 13:05:04 +01:00
showAll: false,
2019-05-09 19:11:40 +01:00
};
2019-05-09 15:14:58 +01:00
}
public componentDidMount(): void {
2021-05-26 16:47:21 +01:00
const { mxEvent, reactions } = this.props;
if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) {
mxEvent.once(MatrixEventEvent.Decrypted, this.onDecrypted);
2021-05-26 16:47:21 +01:00
}
if (reactions) {
reactions.on(RelationsEvent.Add, this.onReactionsChange);
reactions.on(RelationsEvent.Remove, this.onReactionsChange);
reactions.on(RelationsEvent.Redaction, this.onReactionsChange);
2021-05-26 16:47:21 +01:00
}
}
public componentWillUnmount(): void {
2021-05-26 16:47:21 +01:00
const { mxEvent, reactions } = this.props;
mxEvent.off(MatrixEventEvent.Decrypted, this.onDecrypted);
2021-05-26 16:47:21 +01:00
if (reactions) {
reactions.off(RelationsEvent.Add, this.onReactionsChange);
reactions.off(RelationsEvent.Remove, this.onReactionsChange);
reactions.off(RelationsEvent.Redaction, this.onReactionsChange);
2021-05-26 16:47:21 +01:00
}
}
public componentDidUpdate(prevProps: IProps): void {
if (this.props.reactions && prevProps.reactions !== this.props.reactions) {
this.props.reactions.on(RelationsEvent.Add, this.onReactionsChange);
this.props.reactions.on(RelationsEvent.Remove, this.onReactionsChange);
this.props.reactions.on(RelationsEvent.Redaction, this.onReactionsChange);
this.onReactionsChange();
2019-05-09 15:14:58 +01:00
}
}
private onDecrypted = (): void => {
2021-05-26 16:47:21 +01:00
// Decryption changes whether the event is actionable
this.forceUpdate();
2021-06-29 13:11:58 +01:00
};
2019-05-09 15:14:58 +01:00
private onReactionsChange = (): void => {
2019-05-09 15:14:58 +01:00
// TODO: Call `onHeightChanged` as needed
2019-05-09 19:11:40 +01:00
this.setState({
myReactions: this.getMyReactions(),
});
2019-05-13 15:00:43 +01:00
// Using `forceUpdate` for the moment, since we know the overall set of reactions
// has changed (this is triggered by events for that purpose only) and
// `PureComponent`s shallow state / props compare would otherwise filter this out.
2019-05-09 15:14:58 +01:00
this.forceUpdate();
2021-06-29 13:11:58 +01:00
};
2019-05-09 15:14:58 +01:00
private getMyReactions(): MatrixEvent[] | null {
2019-05-09 19:11:40 +01:00
const reactions = this.props.reactions;
if (!reactions) {
return null;
}
const userId = this.context.room?.client.getUserId();
if (!userId) return null;
const myReactions = reactions.getAnnotationsBySender()?.[userId];
2019-05-16 13:57:02 +01:00
if (!myReactions) {
return null;
}
return [...myReactions.values()];
2019-05-09 19:11:40 +01:00
}
private onShowAllClick = (): void => {
2019-06-27 13:05:04 +01:00
this.setState({
showAll: true,
});
2021-06-29 13:11:58 +01:00
};
2019-06-27 13:05:04 +01:00
public render(): React.ReactNode {
const { mxEvent, reactions } = this.props;
2019-06-27 13:05:04 +01:00
const { myReactions, showAll } = this.state;
if (!reactions || !isContentActionable(mxEvent)) {
return null;
}
2023-09-01 04:16:24 -06:00
const customReactionImagesEnabled = SettingsStore.getValue("feature_render_reaction_images");
2019-06-27 13:05:04 +01:00
let items = reactions
.getSortedAnnotationsByKey()
?.map(([content, events]) => {
2019-05-09 15:14:58 +01:00
const count = events.size;
if (!count) {
return null;
2019-05-09 19:11:40 +01:00
}
// Deduplicate the events as per the spec https://spec.matrix.org/v1.7/client-server-api/#annotations-client-behaviour
// This isn't done by the underlying data model as applications may still need access to the whole list of events
// for moderation purposes.
const deduplicatedEvents = uniqBy([...events], (e) => e.getSender());
const myReactionEvent = myReactions?.find((mxEvent) => {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getRelation()?.key === content;
});
return (
<ReactionsRowButton
key={content}
content={content}
count={deduplicatedEvents.length}
2019-05-09 19:11:40 +01:00
mxEvent={mxEvent}
reactionEvents={deduplicatedEvents}
2019-05-09 19:11:40 +01:00
myReactionEvent={myReactionEvent}
2023-09-01 04:16:24 -06:00
customReactionImagesEnabled={customReactionImagesEnabled}
disabled={
!this.context.canReact ||
(myReactionEvent && !myReactionEvent.isRedacted() && !this.context.canSelfRedact)
}
/>
);
2019-06-27 13:05:04 +01:00
})
.filter((item) => !!item);
if (!items?.length) return null;
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
// The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself.
let showAllButton: JSX.Element | undefined;
if (items.length > MAX_ITEMS_WHEN_LIMITED + 1 && !showAll) {
2019-06-27 13:05:04 +01:00
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
2022-01-07 10:40:53 +01:00
showAllButton = (
2019-06-27 13:05:04 +01:00
<AccessibleButton kind="link_inline" className="mx_ReactionsRow_showAll" onClick={this.onShowAllClick}>
{_t("action|show_all")}
2022-01-07 10:40:53 +01:00
</AccessibleButton>
);
2019-06-27 13:05:04 +01:00
}
let addReactionButton: JSX.Element | undefined;
if (this.context.canReact) {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
2019-12-06 15:48:34 +01:00
return (
<div className="mx_ReactionsRow" role="toolbar" aria-label={_t("common|reactions")}>
{items}
{showAllButton}
{addReactionButton}
</div>
);
}
}