diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 2d87e9bd..c8393137 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -79,6 +79,7 @@ import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag'; +import { AddReactionButton } from '../../../../owl/components/AddReactionButton'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; @@ -813,6 +814,15 @@ export const Message = as<'div', MessageProps>( ); + const visibleReactions = relations?.getSortedAnnotationsByKey(); + const hasVisibleReactions = !!visibleReactions && visibleReactions.some( + ([key, events]) => typeof key === 'string' && Array.from(events).length > 0 + ); + + const handleInlineAddReaction: MouseEventHandler = (evt) => { + setEmojiBoardAnchor(evt.currentTarget.getBoundingClientRect()); + }; + const msgContentJSX = ( {reply} @@ -831,7 +841,11 @@ export const Message = as<'div', MessageProps>( ) : ( children )} - {reactions} + {reactions && React.isValidElement(reactions) + ? React.cloneElement(reactions as React.ReactElement, { + onAddReaction: handleInlineAddReaction, + }) + : reactions} ); @@ -1129,6 +1143,11 @@ export const Message = as<'div', MessageProps>( )} + {!edit && collapse && canSendReaction && !hasVisibleReactions + && messageLayout !== MessageLayout.Compact + && (hover || !!emojiBoardAnchor) && ( + + )} {messageLayout === MessageLayout.Compact && ( {msgContentJSX} diff --git a/src/app/features/room/message/Reactions.tsx b/src/app/features/room/message/Reactions.tsx index f0f308bb..8c15f030 100644 --- a/src/app/features/room/message/Reactions.tsx +++ b/src/app/features/room/message/Reactions.tsx @@ -23,6 +23,7 @@ import * as css from './styles.css'; import { ReactionViewer } from '../reaction-viewer'; import { stopPropagation } from '../../../utils/keyboard'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { AddReactionButton } from '../../../../owl/components/AddReactionButton'; export type ReactionsProps = { room: Room; @@ -30,9 +31,10 @@ export type ReactionsProps = { canSendReaction?: boolean; relations: Relations; onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void; + onAddReaction?: MouseEventHandler; }; 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 useAuthentication = useMediaAuthentication(); const [viewer, setViewer] = useState(false); @@ -41,6 +43,9 @@ export const Reactions = as<'div', ReactionsProps>( relations, useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], []) ); + const visibleCount = reactions.filter( + ([key, events]) => typeof key === 'string' && Array.from(events).length > 0 + ).length; const handleViewReaction: MouseEventHandler = (evt) => { evt.stopPropagation(); @@ -94,7 +99,10 @@ export const Reactions = as<'div', ReactionsProps>( ); })} - {reactions.length > 0 && ( + {visibleCount > 0 && canSendReaction && onAddReaction && ( + + )} + {visibleCount > 0 && ( { evt.stopPropagation(); diff --git a/src/owl/components/AddReactionButton.tsx b/src/owl/components/AddReactionButton.tsx new file mode 100644 index 00000000..bd112be5 --- /dev/null +++ b/src/owl/components/AddReactionButton.tsx @@ -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; + ghost?: boolean; +}; + +export function AddReactionButton({ onClick, ghost }: AddReactionButtonProps) { + return ( + + + + ); +} diff --git a/src/owl/styles/reactions.css.ts b/src/owl/styles/reactions.css.ts new file mode 100644 index 00000000..c4448c3a --- /dev/null +++ b/src/owl/styles/reactions.css.ts @@ -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, + }, +]);