-
-
-
-
-
-
-
-
-
-
-
- Browse
-
-
-
-
-
- Upload
-
-
-
-
-
-
- {conversation.canCreateMessage ? (
-
- ) : (
-
- You don't have permission to create messages in this
- conversation
-
- )}
-
-
- {isPending ? (
-
- ) : (
-
- )}
-
+
+ {conversation.canCreateMessage && replyTo && (
+
setReplyTo(null)}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Browse
+
+
+
+
+
+ Upload
+
+
+
+
+
+
+ {conversation.canCreateMessage ? (
+
+ ) : (
+
+ You don't have permission to create messages in this
+ conversation
+
+ )}
+
+
+ {isPending ? (
+
+ ) : (
+
+ )}
-
);
});
diff --git a/packages/ui/src/components/messages/message-delete-button.tsx b/packages/ui/src/components/messages/message-delete-button.tsx
deleted file mode 100644
index ba0e7493..00000000
--- a/packages/ui/src/components/messages/message-delete-button.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Trash2 } from 'lucide-react';
-import { Fragment, useState } from 'react';
-import { toast } from 'sonner';
-
-import {
- AlertDialog,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from '@colanode/ui/components/ui/alert-dialog';
-import { Button } from '@colanode/ui/components/ui/button';
-import { Spinner } from '@colanode/ui/components/ui/spinner';
-import { useWorkspace } from '@colanode/ui/contexts/workspace';
-import { useMutation } from '@colanode/ui/hooks/use-mutation';
-
-interface MessageDeleteButtonProps {
- id: string;
-}
-
-export const MessageDeleteButton = ({ id }: MessageDeleteButtonProps) => {
- const workspace = useWorkspace();
- const { mutate, isPending } = useMutation();
- const [showDeleteModal, setShowDeleteModal] = useState(false);
-
- return (
-
- setShowDeleteModal(true)}
- />
-
-
-
-
- Are you sure you want delete this message?
-
-
- This action cannot be undone. This message will no longer be
- accessible by you or others you've shared it with.
-
-
-
- Cancel
-
-
-
-
-
- );
-};
diff --git a/packages/ui/src/components/messages/message-delete-dialog.tsx b/packages/ui/src/components/messages/message-delete-dialog.tsx
new file mode 100644
index 00000000..7ef2ecad
--- /dev/null
+++ b/packages/ui/src/components/messages/message-delete-dialog.tsx
@@ -0,0 +1,72 @@
+import { toast } from 'sonner';
+
+import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@colanode/ui/components/ui/alert-dialog';
+import { Button } from '@colanode/ui/components/ui/button';
+import { Spinner } from '@colanode/ui/components/ui/spinner';
+import { useWorkspace } from '@colanode/ui/contexts/workspace';
+import { useMutation } from '@colanode/ui/hooks/use-mutation';
+
+interface MessageDeleteDialogProps {
+ id: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export const MessageDeleteDialog = ({
+ id,
+ open,
+ onOpenChange,
+}: MessageDeleteDialogProps) => {
+ const workspace = useWorkspace();
+ const { mutate, isPending } = useMutation();
+
+ return (
+
+
+
+
+ Are you sure you want delete this message?
+
+
+ This action cannot be undone. This message will no longer be
+ accessible by you or others you've shared it with.
+
+
+
+ Cancel
+
+
+
+
+ );
+};
diff --git a/packages/ui/src/components/messages/message-menu-mobile.tsx b/packages/ui/src/components/messages/message-menu-mobile.tsx
new file mode 100644
index 00000000..427076b3
--- /dev/null
+++ b/packages/ui/src/components/messages/message-menu-mobile.tsx
@@ -0,0 +1,178 @@
+import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
+import { MessagesSquare, Reply, Trash2 } from 'lucide-react';
+import { useCallback } from 'react';
+import { toast } from 'sonner';
+
+import { LocalMessageNode } from '@colanode/client/types';
+import { MessageQuickReaction } from '@colanode/ui/components/messages/message-quick-reaction';
+import { MessageReactionCreatePopover } from '@colanode/ui/components/messages/message-reaction-create-popover';
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetTitle,
+} from '@colanode/ui/components/ui/sheet';
+import { useConversation } from '@colanode/ui/contexts/conversation';
+import { useMessage } from '@colanode/ui/contexts/message';
+import { useWorkspace } from '@colanode/ui/contexts/workspace';
+import { useMutation } from '@colanode/ui/hooks/use-mutation';
+import { defaultEmojis } from '@colanode/ui/lib/assets';
+import { cn } from '@colanode/ui/lib/utils';
+
+interface MessageMenuMobileProps {
+ message: LocalMessageNode;
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+const MenuAction = ({
+ children,
+ onClick,
+ className,
+}: {
+ children: React.ReactNode;
+ onClick?: () => void;
+ className?: string;
+}) => {
+ return (
+
+ );
+};
+
+export const MessageMenuMobile = ({
+ isOpen,
+ onOpenChange,
+}: MessageMenuMobileProps) => {
+ const workspace = useWorkspace();
+ const conversation = useConversation();
+ const message = useMessage();
+
+ const { mutate, isPending } = useMutation();
+
+ const canReplyInThread = false;
+
+ const handleReactionClick = useCallback(
+ (reaction: string) => {
+ if (isPending) {
+ return;
+ }
+
+ mutate({
+ input: {
+ type: 'node.reaction.create',
+ nodeId: message.id,
+ accountId: workspace.accountId,
+ workspaceId: workspace.id,
+ reaction,
+ rootId: conversation.rootId,
+ },
+ onError(error) {
+ toast.error(error.message);
+ },
+ });
+
+ onOpenChange(false);
+ },
+ [
+ isPending,
+ mutate,
+ workspace.accountId,
+ message.id,
+ conversation.rootId,
+ onOpenChange,
+ ]
+ );
+
+ const handleReply = () => {
+ conversation.onReply(message);
+ onOpenChange(false);
+ };
+
+ return (
+
+
+ Message Actions
+ Actions for the selected message
+
+
+
+
+
+ Quick Reactions
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!isPending) {
+ handleReactionClick(reaction);
+ }
+ }}
+ />
+
+
+
+
+ {/* Actions Section */}
+
+ {canReplyInThread && (
+
+
+ Reply in thread
+
+ )}
+
+ {conversation.canCreateMessage && (
+
+
+ Reply
+
+ )}
+
+ {message.canDelete && (
+ {
+ message.openDelete();
+ }}
+ className="text-destructive hover:bg-destructive/10"
+ >
+
+ Delete message
+
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/ui/src/components/messages/message.tsx b/packages/ui/src/components/messages/message.tsx
index f64dc31b..ca1d9a5b 100644
--- a/packages/ui/src/components/messages/message.tsx
+++ b/packages/ui/src/components/messages/message.tsx
@@ -1,3 +1,4 @@
+import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { LocalMessageNode } from '@colanode/client/types';
@@ -5,11 +6,18 @@ import { MessageActions } from '@colanode/ui/components/messages/message-actions
import { MessageAuthorAvatar } from '@colanode/ui/components/messages/message-author-avatar';
import { MessageAuthorName } from '@colanode/ui/components/messages/message-author-name';
import { MessageContent } from '@colanode/ui/components/messages/message-content';
+import { MessageDeleteDialog } from '@colanode/ui/components/messages/message-delete-dialog';
+import { MessageMenuMobile } from '@colanode/ui/components/messages/message-menu-mobile';
import { MessageReactionCounts } from '@colanode/ui/components/messages/message-reaction-counts';
import { MessageReference } from '@colanode/ui/components/messages/message-reference';
import { MessageTime } from '@colanode/ui/components/messages/message-time';
+import { useConversation } from '@colanode/ui/contexts/conversation';
+import { MessageContext } from '@colanode/ui/contexts/message';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
+import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
+import { useLongPress } from '@colanode/ui/hooks/use-long-press';
+import { cn } from '@colanode/ui/lib/utils';
interface MessageProps {
message: LocalMessageNode;
@@ -36,48 +44,106 @@ const shouldDisplayAuthor = (
export const Message = ({ message, previousMessage }: MessageProps) => {
const workspace = useWorkspace();
+ const conversation = useConversation();
+
const radar = useRadar();
+ const isMobile = useIsMobile();
+
+ const [isLongPressing, setIsLongPressing] = useState(false);
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
+
const displayAuthor = shouldDisplayAuthor(message, previousMessage);
- return (
-
-
- {displayAuthor && }
-
+ const longPressHandlers = isMobile
+ ? useLongPress(
+ () => {
+ setIsMobileMenuOpen(true);
+ },
+ {
+ onStart: () => {
+ setIsLongPressing(true);
+ },
+ onFinish: () => {
+ setIsLongPressing(false);
+ },
+ onCancel: () => {
+ setIsLongPressing(false);
+ },
+ }
+ )
+ : {};
-
- {displayAuthor && (
-
-
-
-
+ return (
+
{
+ setOpenDeleteDialog(true);
+ },
+ }}
+ >
+ {
- if (inView) {
- radar.markNodeAsSeen(
- workspace.accountId,
- workspace.id,
- message.id
- );
- }
- }}
- >
-
- {message.attributes.referenceId && (
-
+ {...longPressHandlers}
+ >
+
+ {displayAuthor && }
+
+
+
+ {displayAuthor && (
+
+
+
+
)}
-
-
-
+
{
+ if (inView) {
+ radar.markNodeAsSeen(
+ workspace.accountId,
+ workspace.id,
+ message.id
+ );
+ }
+ }}
+ >
+ {!isMobile && }
+ {message.attributes.referenceId && (
+
+ )}
+
+
+
+
+
+ {isMobile && (
+
+ )}
+ {openDeleteDialog && (
+
+ )}
-
+
);
};
diff --git a/packages/ui/src/components/spaces/space-create-dialog.tsx b/packages/ui/src/components/spaces/space-create-dialog.tsx
index c525609d..fd92c131 100644
--- a/packages/ui/src/components/spaces/space-create-dialog.tsx
+++ b/packages/ui/src/components/spaces/space-create-dialog.tsx
@@ -25,7 +25,7 @@ export const SpaceCreateDialog = ({
return (