🦉 additional reaction chip left

This commit is contained in:
2026-04-16 00:26:21 +02:00
parent 3b8d7fb026
commit 8e6b26477a
4 changed files with 94 additions and 3 deletions

View File

@@ -79,6 +79,7 @@ import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
import { PowerIcon } from '../../../components/power'; import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID'; import colorMXID from '../../../../util/colorMXID';
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag'; import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
import { AddReactionButton } from '../../../../owl/components/AddReactionButton';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@@ -813,6 +814,15 @@ export const Message = as<'div', MessageProps>(
</AvatarBase> </AvatarBase>
); );
const visibleReactions = relations?.getSortedAnnotationsByKey();
const hasVisibleReactions = !!visibleReactions && visibleReactions.some(
([key, events]) => typeof key === 'string' && Array.from(events).length > 0
);
const handleInlineAddReaction: MouseEventHandler<HTMLButtonElement> = (evt) => {
setEmojiBoardAnchor(evt.currentTarget.getBoundingClientRect());
};
const msgContentJSX = ( const msgContentJSX = (
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}> <Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
{reply} {reply}
@@ -831,7 +841,11 @@ export const Message = as<'div', MessageProps>(
) : ( ) : (
children children
)} )}
{reactions} {reactions && React.isValidElement(reactions)
? React.cloneElement(reactions as React.ReactElement<any>, {
onAddReaction: handleInlineAddReaction,
})
: reactions}
</Box> </Box>
); );
@@ -1129,6 +1143,11 @@ export const Message = as<'div', MessageProps>(
</Menu> </Menu>
</div> </div>
)} )}
{!edit && collapse && canSendReaction && !hasVisibleReactions
&& messageLayout !== MessageLayout.Compact
&& (hover || !!emojiBoardAnchor) && (
<AddReactionButton onClick={handleInlineAddReaction} ghost />
)}
{messageLayout === MessageLayout.Compact && ( {messageLayout === MessageLayout.Compact && (
<CompactLayout before={headerJSX} onContextMenu={handleContextMenu}> <CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
{msgContentJSX} {msgContentJSX}

View File

@@ -23,6 +23,7 @@ import * as css from './styles.css';
import { ReactionViewer } from '../reaction-viewer'; import { ReactionViewer } from '../reaction-viewer';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { AddReactionButton } from '../../../../owl/components/AddReactionButton';
export type ReactionsProps = { export type ReactionsProps = {
room: Room; room: Room;
@@ -30,9 +31,10 @@ export type ReactionsProps = {
canSendReaction?: boolean; canSendReaction?: boolean;
relations: Relations; relations: Relations;
onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void; onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
onAddReaction?: MouseEventHandler<HTMLButtonElement>;
}; };
export const Reactions = as<'div', ReactionsProps>( export const Reactions = as<'div', ReactionsProps>(
({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => { ({ className, room, relations, mEventId, canSendReaction, onReactionToggle, onAddReaction, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState<boolean | string>(false); const [viewer, setViewer] = useState<boolean | string>(false);
@@ -41,6 +43,9 @@ export const Reactions = as<'div', ReactionsProps>(
relations, relations,
useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], []) useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
); );
const visibleCount = reactions.filter(
([key, events]) => typeof key === 'string' && Array.from(events).length > 0
).length;
const handleViewReaction: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleViewReaction: MouseEventHandler<HTMLButtonElement> = (evt) => {
evt.stopPropagation(); evt.stopPropagation();
@@ -94,7 +99,10 @@ export const Reactions = as<'div', ReactionsProps>(
</TooltipProvider> </TooltipProvider>
); );
})} })}
{reactions.length > 0 && ( {visibleCount > 0 && canSendReaction && onAddReaction && (
<AddReactionButton onClick={onAddReaction} />
)}
{visibleCount > 0 && (
<Overlay <Overlay
onContextMenu={(evt: any) => { onContextMenu={(evt: any) => {
evt.stopPropagation(); evt.stopPropagation();

View File

@@ -0,0 +1,23 @@
import React, { MouseEventHandler } from 'react';
import { Box, Icon, Icons } from 'folds';
import * as css from '../styles/reactions.css';
type AddReactionButtonProps = {
onClick: MouseEventHandler<HTMLButtonElement>;
ghost?: boolean;
};
export function AddReactionButton({ onClick, ghost }: AddReactionButtonProps) {
return (
<Box
as="button"
className={ghost ? css.AddReactionGhost : css.AddReactionChip}
alignItems="Center"
justifyContent="Center"
shrink="No"
onClick={onClick}
>
<Icon src={Icons.SmilePlus} size="50" />
</Box>
);
}

View File

@@ -0,0 +1,41 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
const addReactionBase = {
padding: `${toRem(2)} ${config.space.S200}`,
backgroundColor: color.SurfaceVariant.Container,
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
borderRadius: config.radii.R300,
cursor: 'pointer',
selectors: {
'&:hover, &:focus-visible': {
backgroundColor: color.SurfaceVariant.ContainerHover,
opacity: 1,
},
'&:active': {
backgroundColor: color.SurfaceVariant.ContainerActive,
},
},
} as const;
export const AddReactionChip = style([
DefaultReset,
FocusOutline,
{
...addReactionBase,
opacity: 0.6,
},
]);
export const AddReactionGhost = style([
DefaultReset,
FocusOutline,
{
...addReactionBase,
position: 'absolute',
left: config.space.S400,
bottom: config.space.S100,
opacity: 0.5,
zIndex: 1,
},
]);