Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/common/BaseThreadMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const BaseThreadMessage = ({
went inert). Desktop keeps hover cards: hover never conflicts
with click navigation. Same rule as MessageResultBlock. */}
<div className="[&_p]:my-0 max-md:[&_.mention]:pointer-events-none">
<MessageContent message={thread as unknown as Message} showLinkPreview={false} />
<MessageContent message={thread as unknown as Message} showLinkPreview={false} showLinkedDocument={false} />
</div>
{/* List rows stay compact: the doctype + id line, not the full card.
Darker ink than the default — this line is part of the row's
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/components/common/EmojiFace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { QuickEmoji } from "@utils/preferences"

/**
* A QuickEmoji face: custom emojis are plain images; native ones render via
* em-emoji from the Apple set (initialized in App.tsx) so reactions look the
* same on every platform. Sized relative to the host's font size — put
* text-2xl (or similar) on the wrapping button to scale it.
*/
export const EmojiFace = ({ emoji }: { emoji: QuickEmoji }) =>
emoji.src ? (
<img src={emoji.src} alt={emoji.id} loading="lazy" className="h-4.5 w-4.5 object-contain" aria-hidden="true" />
) : (
<span className="flex h-4.5 w-4.5 items-center justify-center" aria-hidden="true">
<em-emoji native={emoji.native} set="native" size="1.1em" fallback={emoji.id} />
</span>
)
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const MessageResultBlockInner = ({ message, user, channel, dmChannel, peer, work
search list would fire get_poll per row and drop the highlight. */}
{message.message_type === "Poll"
? <MessageBody content={message.text} />
: <MessageContent message={message} showLinkPreview={false} />}
: <MessageContent message={message} showLinkPreview={false} showLinkedDocument={false} />}
</div>
</div>
</div>
Expand Down
14 changes: 2 additions & 12 deletions apps/web/src/components/features/message/ThreadRootMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,8 @@ export const ThreadRootMessage = ({ threadID, parentID }: { threadID: string; pa
)}
</div>
) : (
<>
<MessageContent message={message} />
{/* MessageContent doesn't know about linked documents —
MessageItem renders the card in the stream, so this
surface must too. */}
{linkedDocMember && (
<DocumentLinkRenderer
doctype={linkedDocMember.link_doctype!}
docname={linkedDocMember.link_document!}
/>
)}
</>
// MessageContent renders the linked-document card itself.
<MessageContent message={message} />
)
) : (
// The whole collapsed preview is a click target for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ export const MessageActionMenu = ({
const lastTapRef = useRef({ messageID: "", time: 0 })
const menuOpenedAtRef = useRef(0)
const wrapperRef = useRef<HTMLDivElement>(null)
/** Hovered message + its toolbar position; null hides the toolbar. */
const [hovered, setHovered] = useState<{ message: Message; top: number } | null>(null)
/** Hovered message + its toolbar position (top; one of left/right anchors
* it); null hides the toolbar. */
const [hovered, setHovered] = useState<{ message: Message; top: number; left?: number; right?: number } | null>(null)
/** While the toolbar's ellipsis menu is open, hover-clearing is suspended. */
const toolbarMenuOpenRef = useRef(false)

Expand Down Expand Up @@ -157,8 +158,38 @@ export const MessageActionMenu = ({

const showToolbarFor = (message: Message, element: HTMLElement) => {
if (!wrapperRef.current) return
const top = element.getBoundingClientRect().top - wrapperRef.current.getBoundingClientRect().top - 14
setHovered({ message, top: Math.max(top, 2) })
const wrapperRect = wrapperRef.current.getBoundingClientRect()
// Resolve the ROW SHELL — `element` can be an inner node (image tile)
// or an outer wrapper (batch root).
const row =
(element.closest("[data-message-row]") as HTMLElement | null) ??
(element.querySelector("[data-message-row]") as HTMLElement | null) ??
element
// The row says how it's aligned (data-message-row, set by MessageRow) —
// no class sniffing. Rows are full width in every mode, and the toolbar
// sits 24px above the row in the corner OPPOSITE the message's side, so
// its overlap always lands on empty row space:
// - "own" content hugs the right → toolbar top-LEFT.
// - everyone else's hugs the left → toolbar top-RIGHT, which is the
// same corner Simple mode has always used.
// The lower half overlaps the row, so the pointer reaches the toolbar
// without leaving the row — it can't flicker away en route.
const rect = row.getBoundingClientRect()
const top = Math.max(rect.top - wrapperRect.top - 24, 2)
const mode = row.getAttribute("data-message-row")
if (mode === "own") {
setHovered({
message,
top,
left: Math.max(rect.left - wrapperRect.left + 16, 16),
})
return
}
setHovered({
message,
top,
right: Math.max(wrapperRect.right - rect.right + 16, 16),
})
}

/** Desktop: tracks which message the pointer is over and positions the floating toolbar. */
Expand Down Expand Up @@ -578,6 +609,8 @@ export const MessageActionMenu = ({
<MessageHoverToolbar
message={hovered.message}
top={hovered.top}
left={hovered.left}
right={hovered.right}
canInteract={canInteract}
onMenuOpenChange={onToolbarMenuOpenChange}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ import { useIsMobile } from "@hooks/use-mobile"
export const MessageHoverToolbar = ({
message,
top,
left,
right,
canInteract = true,
onMenuOpenChange,
onOpenFullMenu,
}: {
message: Message
/** Offset from the stream wrapper's top, computed at hover time. */
/** The toolbar's TOP, offset from the stream wrapper's top. */
top: number
/** Horizontal anchor, offset from the wrapper's edge. One of the two is
* set: `left` for own Left-Right rows, `right` for everything else. */
left?: number
right?: number
/** From the host's composer gate — see MessageActionMenu. */
canInteract?: boolean
/** Lets the hover tracker keep the toolbar mounted while the menu is open. */
Expand Down Expand Up @@ -86,8 +92,8 @@ export const MessageHoverToolbar = ({
data-message-id={message.name}
data-hover-toolbar
// z-30: must float above the sticky date separators (z-20)
className="absolute right-4 z-40 flex items-center gap-0.5 rounded-md border border-outline-gray-2 bg-surface-base p-0.5 shadow-xs"
style={{ top }}
className="absolute z-40 flex items-center gap-0.5 rounded-md border border-outline-gray-2 bg-surface-base p-0.5 shadow-xs"
style={{ top, left, right }}
>
{/* One provider for the whole toolbar: after the first tooltip shows,
moving across the icons shows the next ones instantly (skip delay) —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { EditableMessageBody, MessageAttributes } from "./MessageContent"
import { MessageLinkPreview } from "./LinkPreview"
import { MessageReactionsRow } from "./MessageReactions"
import { MessageRow, MessageSenderLayout } from "./MessageRow"
import { MessageThreadPill } from "./ThreadMessage"
import { cn } from "@lib/utils"
import { MessageThreadPill, ThreadConnector } from "./ThreadMessage"
import ReplyMessage from "./ReplyMessage"
import { OptimisticStatus, optimisticRowClass } from "./OptimisticStatus"
import { FailedSendIndicator, OptimisticStatus, optimisticRowClass, sendingDimClass } from "./OptimisticStatus"
import { getAttachmentKind, messagesToAttachments } from "@utils/attachmentPreview"
import { isThreadParent, parseRepliedMessageDetails } from "@utils/messageUtils"
import { useMessageAlignment } from "@hooks/useChatStyle"
import type { RepliedMessageDetails } from "./RepliedMessagePreview"
import type { MessageBatchBlock } from "@stores/messages/types"
import type { Message } from "@raven/types/common/Message"
Expand Down Expand Up @@ -78,6 +80,10 @@ export const BatchMessageItem = ({
const head = block.messages[0]
const newest = block.messages[block.messages.length - 1]

const owner = head.is_bot_message ? head.bot || '' : head.owner
// See MessageItem — own rows have no avatar column for the connector.
const { isLeftRight, isOwn } = useMessageAlignment(owner)

// The selector keeps at most one thread parent in a batch (it splits 2+ into individual
// messages), so find that member wherever it sits and show its pill + connector. v3 batch
// actions create the thread on the NEWEST member (see blockFromEvent), but finding it by
Expand Down Expand Up @@ -129,7 +135,18 @@ export const BatchMessageItem = ({
))

const content = (
<div className="space-y-2">
// Bubble mode is a flex column: media, caption bubble and cards each
// keep their own width and align to the message's side.
<div
className={
isLeftRight
? cn(
"flex max-w-full flex-col gap-2 has-[[data-raven-editor]]:w-full",
isOwn ? "items-end" : "items-start",
)
: "space-y-2"
}
>
{/* Badges sit above everything, same as a single message — the flags
describe the whole block (a forwarded batch arrives all-forwarded). */}
<MessageAttributes message={attributeFlags} />
Expand All @@ -141,33 +158,44 @@ export const BatchMessageItem = ({
/>
)}
<BatchMediaGroups messages={block.messages} />
{captionMember && <EditableMessageBody message={captionMember} />}
{captionMember && <EditableMessageBody message={captionMember} bubble={isLeftRight} />}
{/* Links live on the caption member (the server extracts them from its
text), so that's where the first-link preview hangs off a batch too. */}
{captionMember && <MessageLinkPreview message={captionMember} />}
{linkedDocMember && (
<DocumentLinkRenderer
doctype={linkedDocMember.link_doctype!}
docname={linkedDocMember.link_document!}
// Fixed width in the fit-content bubble column — see MessageContent.
className={isLeftRight ? "w-96 max-w-full" : undefined}
/>
)}
<OptimisticStatus message={head} />
{memberReactions}
{!isLeftRight && memberReactions}
</div>
)

return (
<MessageRow ref={ref} className={optimisticRowClass(head)}>
{threadMember && <div className="absolute left-7 w-6 border-l-2 border-b-2 border-outline-gray-2 rounded-bl-2xl z-0 top-[48px] h-[calc(100%-66px)]" />}
<MessageRow
ref={ref}
alignment={isOwn ? "own" : isLeftRight ? "left-right" : "simple"}
className={isLeftRight ? sendingDimClass(head) : optimisticRowClass(head)}
>
{threadMember && <ThreadConnector side={isOwn ? "right" : "left"} />}
<MessageSenderLayout
owner={head.is_bot_message ? head.bot || '' : head.owner}
owner={owner}
creation={head.creation}
isContinuation={block.is_continuation === 1}
isLeftRight={isLeftRight}
isOwn={isOwn}
reactions={isLeftRight ? <>{memberReactions}</> : undefined}
footer={threadMember && isOwn ? <MessageThreadPill threadID={threadMember.name} channelID={threadMember.channel_id} align="end" /> : undefined}
statusIcon={isOwn ? <FailedSendIndicator message={head} /> : undefined}
>
{content}
</MessageSenderLayout>

{threadMember && <MessageThreadPill threadID={threadMember.name} channelID={threadMember.channel_id} />}
{threadMember && !isOwn && <MessageThreadPill threadID={threadMember.name} channelID={threadMember.channel_id} align="start" />}
</MessageRow>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,15 @@ export const documentPreviewSwrKey = (doctype: string, docname: string) =>
* image box). The preview DATA waits for visibility, poll-style, so a channel
* full of linked documents only fetches what the user actually sees.
*/
export const DocumentLinkRenderer = ({ doctype, docname }: { doctype: string; docname: string }) => {
export const DocumentLinkRenderer = ({ doctype, docname, className }: { doctype: string; docname: string; className?: string }) => {
const { meta, workflowDoc } = useDoctypeMeta(doctype)
const { ref, hasBeenInView } = useHasBeenInView()

// w-full stretches the card in the classic full-width row. Inside a
// fit-content column (Left-Right mode) w-full collapses to the content,
// so those callers pass a fixed width via className instead.
return (
<div ref={ref} className="w-full max-w-xl py-1">
<div ref={ref} className={cn("w-full max-w-xl py-1", className)}>
{hasBeenInView ? (
<LoadedDocumentLink doctype={doctype} docname={docname} meta={meta} workflowDoc={workflowDoc} />
) : (
Expand Down
Loading
Loading