diff --git a/desktop/src/features/messages/lib/waveMessage.ts b/desktop/src/features/messages/lib/waveMessage.ts new file mode 100644 index 0000000000..7ef320dd31 --- /dev/null +++ b/desktop/src/features/messages/lib/waveMessage.ts @@ -0,0 +1,26 @@ +export const WAVE_MESSAGE_MARKER = ""; + +export type WaveMessageContent = { + fallbackText: string; +}; + +export function buildWaveMessageContent(senderName: string): string { + const trimmedName = senderName.trim() || "Someone"; + return `${WAVE_MESSAGE_MARKER}\n${trimmedName} waved at you.`; +} + +export function parseWaveMessageContent( + content: string, +): WaveMessageContent | null { + const trimmedContent = content.trimStart(); + + if (!trimmedContent.startsWith(WAVE_MESSAGE_MARKER)) { + return null; + } + + const fallbackText = trimmedContent.slice(WAVE_MESSAGE_MARKER.length).trim(); + + return { + fallbackText: fallbackText || "Someone waved at you.", + }; +} diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 16c3ceed72..30959aa24f 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -22,6 +22,7 @@ import { UserAvatar } from "@/shared/ui/UserAvatar"; import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"; import { parseImetaTags } from "@/features/messages/lib/parseImeta"; import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji"; +import { parseWaveMessageContent } from "@/features/messages/lib/waveMessage"; import { resolveMentionNames, resolveMentionPubkeysByName, @@ -31,6 +32,7 @@ import type { VideoReviewContext } from "@/shared/ui/VideoPlayer"; import { MessageActionBar } from "./MessageActionBar"; import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader"; import { MessageTimestamp } from "./MessageTimestamp"; +import { WaveMessageAttachment } from "./WaveMessageAttachment"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; const DiffMessage = React.lazy(() => import("./DiffMessage")); @@ -275,6 +277,18 @@ export const MessageRow = React.memo( ); default: + { + const waveMessage = parseWaveMessageContent(message.body); + if (waveMessage) { + return ( + + ); + } + } + return ( ) => { + event.preventDefault(); + event.stopPropagation(); + + if (!channelId || isStarting) { + return; + } + + try { + await startHuddle(channelId, []); + await queryClient.invalidateQueries({ queryKey: channelsQueryKey }); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to start huddle.", + ); + } + }, + [channelId, isStarting, queryClient, startHuddle], + ); + + return ( + + + + {fallbackText} + + Start a huddle to talk to them. + + + + + Start huddle + + + + ); +} diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index 6d8af80263..f4ce80c759 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -25,6 +25,7 @@ import { usePresenceQuery } from "@/features/presence/hooks"; import { useUserStatusQuery } from "@/features/user-status/hooks"; import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji"; import { ProfileAvatarWithStatus } from "@/features/profile/ui/ProfileAvatarWithStatus"; +import { buildWaveMessageContent } from "@/features/messages/lib/waveMessage"; import { useAgentSession } from "@/shared/context/AgentSessionContext"; import { useProfilePanel } from "@/shared/context/ProfilePanelContext"; import { sendChannelMessage } from "@/shared/api/tauri"; @@ -309,7 +310,7 @@ export function UserProfilePopover({ const senderName = selfProfileQuery.data?.displayName?.trim() || (currentPubkey ? truncatePubkey(currentPubkey) : "Someone"); - await sendChannelMessage(dm.id, `${senderName} waved at you.`); + await sendChannelMessage(dm.id, buildWaveMessageContent(senderName)); await queryClient.invalidateQueries({ queryKey: channelMessagesKey(dm.id), }); @@ -485,6 +486,31 @@ export function UserProfilePopover({ ) : null} {showProfileActions ? (
+
) : null} diff --git a/desktop/src/shared/ui/attachment.tsx b/desktop/src/shared/ui/attachment.tsx new file mode 100644 index 0000000000..b3765415d3 --- /dev/null +++ b/desktop/src/shared/ui/attachment.tsx @@ -0,0 +1,196 @@ +import type * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; + +import { cn } from "@/shared/lib/cn"; +import { Button, type ButtonProps } from "@/shared/ui/button"; + +type AttachmentProps = React.ComponentProps<"div"> & { + orientation?: "horizontal" | "vertical"; + size?: "default" | "sm" | "xs"; + state?: "idle" | "uploading" | "processing" | "error" | "done"; +}; + +function Attachment({ + className, + orientation = "horizontal", + size = "default", + state = "done", + ...props +}: AttachmentProps) { + return ( +
+ ); +} + +type AttachmentMediaProps = React.ComponentProps<"div"> & { + variant?: "icon" | "image"; +}; + +function AttachmentMedia({ + className, + variant = "icon", + ...props +}: AttachmentMediaProps) { + return ( +
+ ); +} + +function AttachmentContent({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AttachmentTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AttachmentDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AttachmentActions({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +type AttachmentActionProps = ButtonProps & { + asChild?: boolean; +}; + +function AttachmentAction({ + className, + size = "icon-xs", + variant = "ghost", + ...props +}: AttachmentActionProps) { + return ( +