Move ReactionRow To Shared Components MVVM (#32634)
* Init shared component structure * Storybook implementation * Add snapshots of storybook examples * ViewModel Creation + Implementation In EventTile.tsx * Prettier * Update HTML snapshot * Add onhover pointer on bottons * Added compound web tooltip * Removed possible of undefined on label * Update snapshots * Update setters to use merge instead of updating full snapshot * adapt view model test for setters change * Actions should be passed to viewmodel fix * replace ReactionsRowWrapper forceRender with explicit reaction state * Update snapshot
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
import { _t } from "../../languageHandler";
|
||||
import { formatList } from "../../utils/FormattingUtils";
|
||||
import { unicodeToShortcode } from "../../HtmlUtils";
|
||||
import { REACTION_SHORTCODE_KEY } from "../../components/views/messages/ReactionsRow";
|
||||
import { REACTION_SHORTCODE_KEY } from "./reactionShortcode";
|
||||
|
||||
export interface ReactionsRowButtonTooltipViewModelProps {
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,8 @@ import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { formatList } from "../../utils/FormattingUtils";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { REACTION_SHORTCODE_KEY } from "../../components/views/messages/ReactionsRow";
|
||||
import { ReactionsRowButtonTooltipViewModel } from "./ReactionsRowButtonTooltipViewModel";
|
||||
import { REACTION_SHORTCODE_KEY } from "./reactionShortcode";
|
||||
|
||||
export interface ReactionsRowButtonViewModelProps {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 { type MouseEvent, type MouseEventHandler, type ReactNode } from "react";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type ReactionsRowViewSnapshot,
|
||||
type ReactionsRowViewModel as ReactionsRowViewModelInterface,
|
||||
} from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
export const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
export interface ReactionsRowViewModelProps {
|
||||
/**
|
||||
* Whether the current event is actionable for reactions.
|
||||
*/
|
||||
isActionable: boolean;
|
||||
/**
|
||||
* Number of reaction keys with at least one event.
|
||||
*/
|
||||
reactionGroupCount: number;
|
||||
/**
|
||||
* Whether the current user can add reactions.
|
||||
*/
|
||||
canReact: boolean;
|
||||
/**
|
||||
* Whether the add-reaction context menu is currently open.
|
||||
*/
|
||||
addReactionButtonActive?: boolean;
|
||||
/**
|
||||
* Optional callback invoked when the add-reaction button is clicked.
|
||||
*/
|
||||
onAddReactionClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Optional callback invoked on add-reaction button context-menu.
|
||||
*/
|
||||
onAddReactionContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Reaction row children (typically reaction buttons).
|
||||
*/
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface InternalProps extends ReactionsRowViewModelProps {
|
||||
showAll: boolean;
|
||||
}
|
||||
|
||||
export class ReactionsRowViewModel
|
||||
extends BaseViewModel<ReactionsRowViewSnapshot, InternalProps>
|
||||
implements ReactionsRowViewModelInterface
|
||||
{
|
||||
private static readonly computeDerivedSnapshot = (
|
||||
props: InternalProps,
|
||||
): Pick<
|
||||
ReactionsRowViewSnapshot,
|
||||
"isVisible" | "showAllButtonVisible" | "showAddReactionButton" | "addReactionButtonActive" | "children"
|
||||
> => ({
|
||||
isVisible: props.isActionable && props.reactionGroupCount > 0,
|
||||
showAllButtonVisible: props.reactionGroupCount > MAX_ITEMS_WHEN_LIMITED + 1 && !props.showAll,
|
||||
showAddReactionButton: props.canReact,
|
||||
addReactionButtonActive: !!props.addReactionButtonActive,
|
||||
children: props.children,
|
||||
});
|
||||
|
||||
private static readonly computeSnapshot = (props: InternalProps): ReactionsRowViewSnapshot => ({
|
||||
ariaLabel: _t("common|reactions"),
|
||||
className: "mx_ReactionsRow",
|
||||
showAllButtonLabel: _t("action|show_all"),
|
||||
addReactionButtonLabel: _t("timeline|reactions|add_reaction_prompt"),
|
||||
addReactionButtonVisible: false,
|
||||
...ReactionsRowViewModel.computeDerivedSnapshot(props),
|
||||
});
|
||||
|
||||
public constructor(props: ReactionsRowViewModelProps) {
|
||||
const internalProps: InternalProps = {
|
||||
...props,
|
||||
showAll: false,
|
||||
};
|
||||
super(internalProps, ReactionsRowViewModel.computeSnapshot(internalProps));
|
||||
}
|
||||
|
||||
public setActionable(isActionable: boolean): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
isActionable,
|
||||
};
|
||||
|
||||
const isVisible = this.props.isActionable && this.props.reactionGroupCount > 0;
|
||||
|
||||
this.snapshot.merge({ isVisible });
|
||||
}
|
||||
|
||||
public setReactionGroupCount(reactionGroupCount: number): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
reactionGroupCount,
|
||||
};
|
||||
|
||||
const nextIsVisible = this.props.isActionable && this.props.reactionGroupCount > 0;
|
||||
const nextShowAllButtonVisible =
|
||||
this.props.reactionGroupCount > MAX_ITEMS_WHEN_LIMITED + 1 && !this.props.showAll;
|
||||
const updates: Partial<ReactionsRowViewSnapshot> = {};
|
||||
|
||||
if (this.snapshot.current.isVisible !== nextIsVisible) {
|
||||
updates.isVisible = nextIsVisible;
|
||||
}
|
||||
if (this.snapshot.current.showAllButtonVisible !== nextShowAllButtonVisible) {
|
||||
updates.showAllButtonVisible = nextShowAllButtonVisible;
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
this.snapshot.merge(updates);
|
||||
}
|
||||
}
|
||||
|
||||
public setCanReact(canReact: boolean): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
canReact,
|
||||
};
|
||||
|
||||
this.snapshot.merge({ showAddReactionButton: canReact });
|
||||
}
|
||||
|
||||
public setAddReactionButtonActive(addReactionButtonActive: boolean): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
addReactionButtonActive,
|
||||
};
|
||||
|
||||
this.snapshot.merge({ addReactionButtonActive });
|
||||
}
|
||||
|
||||
public setChildren(children?: ReactNode): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
children,
|
||||
};
|
||||
|
||||
this.snapshot.merge({ children });
|
||||
}
|
||||
|
||||
public setAddReactionHandlers({
|
||||
onAddReactionClick,
|
||||
onAddReactionContextMenu,
|
||||
}: Pick<ReactionsRowViewModelProps, "onAddReactionClick" | "onAddReactionContextMenu">): void {
|
||||
this.props = {
|
||||
...this.props,
|
||||
onAddReactionClick,
|
||||
onAddReactionContextMenu,
|
||||
};
|
||||
}
|
||||
|
||||
public onShowAllClick = (): void => {
|
||||
this.props = {
|
||||
...this.props,
|
||||
showAll: true,
|
||||
};
|
||||
this.snapshot.merge({ showAllButtonVisible: false });
|
||||
};
|
||||
|
||||
public onAddReactionClick = (event: MouseEvent<HTMLButtonElement>): void => {
|
||||
this.props.onAddReactionClick?.(event);
|
||||
};
|
||||
|
||||
public onAddReactionContextMenu = (event: MouseEvent<HTMLButtonElement>): void => {
|
||||
this.props.onAddReactionContextMenu?.(event);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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 { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||
|
||||
export const REACTION_SHORTCODE_KEY = new UnstableValue("shortcode", "com.beeper.reaction.shortcode");
|
||||
Reference in New Issue
Block a user