Message Actions Usage - nself-org/nchat GitHub Wiki
Complete guide for using the message actions and context menu system in nself-chat.
The message actions system provides three main components:
- MessageActions - Hover action bar with quick actions
- MessageContextMenu - Right-click context menu with full action list
- useMessageActions - Hook for handling all message actions
- ✅ Quick reactions (emoji picker)
- ✅ Reply
- ✅ Thread
- ✅ Share
- ✅ More menu (...)
- ✅ Copy text
- ✅ Copy link
- ✅ Edit message (if own)
- ✅ Delete message (if own/admin)
- ✅ Pin message
- ✅ Star/bookmark
- ✅ Forward message
- ✅ Report message
- ✅ Mark unread
- ✅ Remind me
- ✅ Start thread
- ✅ View message details
- ✅ View edit history
- ✅ View reactions
- ✅ Select multiple messages
- ✅ Bulk delete
- ✅ Bulk forward
- ✅ Copy multiple
import { useState } from 'react'
import { MessageItem, MessageActions, MessageContextMenu } from '@/components/chat'
import { useMessageActions } from '@/hooks'
function MessageList({ channelId, messages }) {
const { handlers, bulkHandlers, selection, handleAction, getPermissions } = useMessageActions({
channelId,
onReplyMessage: (message) => {
// Handle reply
console.log('Reply to:', message)
},
onOpenThread: (message) => {
// Handle thread
console.log('Open thread:', message)
},
onEditMessage: (message) => {
// Handle edit
console.log('Edit:', message)
},
onDeleteMessage: async (messageId) => {
// Handle delete
await deleteMessageMutation({ variables: { id: messageId } })
},
})
return (
<div>
{messages.map((message) => {
const permissions = getPermissions(message)
return (
<MessageContextMenu
key={message.id}
message={message}
permissions={permissions}
onAction={(action, data) => handleAction(action, message, data)}
>
<div className="relative">
<MessageItem message={message} />
{/* Show on hover */}
<MessageActions
message={message}
permissions={permissions}
onAction={(action, data) => handleAction(action, message, data)}
/>
</div>
</MessageContextMenu>
)
})}
</div>
)
}Hover action bar that appears on message hover.
<MessageActions
message={message}
permissions={permissions}
onAction={(action, data) => handleAction(action, message, data)}
position="right" // 'left' | 'right'
variant="default" // 'default' | 'compact' | 'mobile'
/>Variants:
-
default- Full hover bar with all actions -
compact- Smaller inline actions -
mobile- Mobile floating action sheet
Actions:
- React (opens emoji picker)
- Reply
- Thread
- Share
- More menu
Right-click context menu with comprehensive actions.
<MessageContextMenu
message={message}
permissions={permissions}
onAction={(action, data) => handleAction(action, message, data)}
showAdvanced={true} // Show advanced options
onEnterSelectionMode={() => selection.enterSelectionMode()}
isSelected={selection.selectedMessages.has(message.id)}
>
{children}
</MessageContextMenu>Menu Structure:
- Quick reactions (row of emoji buttons)
- Copy text/link
- Reply/Thread
- Add reaction (submenu with categories)
- Edit/Pin/Bookmark/Forward
- Share submenu
- Mark unread/Remind me
- Message info submenu
- Report/Delete
Simplified context menu for compact views.
<SimpleMessageContextMenu
message={message}
permissions={permissions}
onAction={(action, data) => handleAction(action, message, data)}
>
{children}
</SimpleMessageContextMenu>Main hook for managing message actions.
const {
handlers, // Individual action handlers
bulkHandlers, // Bulk operation handlers
selection, // Selection state
handleAction, // Main action dispatcher
canPerformAction, // Permission checker
getPermissions, // Get message permissions
isLoading, // Loading state
} = useMessageActions({
channelId: 'channel-123',
onReplyMessage: (message) => {},
onOpenThread: (message) => {},
onEditMessage: (message) => {},
onDeleteMessage: async (id) => {},
onForwardMessage: (message) => {},
onReportMessage: (message) => {},
onViewMessageDetails: (message) => {},
enableBulkOperations: true,
})Individual action handlers:
// React/unreact
await handlers.onReact(messageId, '👍')
await handlers.onRemoveReaction(messageId, '👍')
// Navigation
handlers.onReply(message)
handlers.onThread(message)
handlers.onEdit(message)
// State changes
await handlers.onPin(messageId)
await handlers.onUnpin(messageId)
await handlers.onBookmark(messageId)
await handlers.onUnbookmark(messageId)
// Actions
handlers.onForward(message)
handlers.onCopy(message)
handlers.onCopyLink(message)
handlers.onReport(message)
// Utilities
await handlers.onDelete(messageId)
await handlers.onMarkUnread(messageId)
handlers.onViewDetails(message)
handlers.onViewEditHistory(message)
handlers.onViewReactions(message)Bulk operation handlers:
// Bulk delete
await bulkHandlers.onBulkDelete(['msg-1', 'msg-2', 'msg-3'])
// Bulk forward
bulkHandlers.onBulkForward([message1, message2])
// Bulk copy
bulkHandlers.onBulkCopy([message1, message2])Message selection for bulk operations:
// Enter/exit selection mode
selection.enterSelectionMode()
selection.exitSelectionMode()
// Toggle individual message
selection.toggleSelection(messageId)
// Select all
selection.selectAll(allMessageIds)
// Clear selection
selection.clearSelection()
// Check state
const isInSelectionMode = selection.isSelectionMode
const selectedIds = selection.selectedMessages // Set<string>const { handleAction } = useMessageActions({
channelId,
onReplyMessage: (message) => {
// Custom reply logic
setReplyTo(message)
focusMessageInput()
},
onDeleteMessage: async (messageId) => {
// Confirm before delete
const confirmed = await showConfirmDialog({
title: 'Delete message?',
description: 'This cannot be undone.',
})
if (confirmed) {
await deleteMessage(messageId)
}
},
})const permissions = getPermissions(message)
// Check individual permissions
if (permissions.canEdit) {
// Show edit UI
}
if (permissions.canDelete) {
// Show delete UI
}
// Or check action availability
if (canPerformAction('pin', message)) {
// Pin action is available
}import { BulkMessageActions } from '@/components/chat'
function MessageListWithBulk() {
const { selection, bulkHandlers } = useMessageActions({
channelId,
enableBulkOperations: true,
})
return (
<>
{selection.isSelectionMode && (
<BulkMessageActions
selectedCount={selection.selectedMessages.size}
onDelete={() => bulkHandlers.onBulkDelete(Array.from(selection.selectedMessages))}
onForward={() => {
const selected = messages.filter((m) => selection.selectedMessages.has(m.id))
bulkHandlers.onBulkForward(selected)
}}
onCopy={() => {
const selected = messages.filter((m) => selection.selectedMessages.has(m.id))
bulkHandlers.onBulkCopy(selected)
}}
onClearSelection={selection.clearSelection}
/>
)}
{/* Messages */}
</>
)
}import { MobileMessageActions } from '@/components/chat'
function MobileMessage({ message }) {
const [showActions, setShowActions] = useState(false)
const { handleAction, getPermissions } = useMessageActions({ channelId })
return (
<>
<div onClick={() => setShowActions(true)}>
<MessageItem message={message} />
</div>
{showActions && (
<MobileMessageActions
message={message}
permissions={getPermissions(message)}
onAction={(action, data) => handleAction(action, message, data)}
onClose={() => setShowActions(false)}
/>
)}
</>
)
}The context menu supports keyboard shortcuts:
- ⌘C - Copy text
- ⌘⇧C - Copy link
- R - Reply
- T - Reply in thread
- E - Edit (if own message)
- P - Pin/Unpin (if moderator)
- S - Save/Bookmark
- ⌘F - Forward
- U - Mark as unread
- ⌘⌫ - Delete
All components use Tailwind CSS and respect the theme system:
// Custom styling
<MessageActions
className="custom-class"
// ... other props
/>
<MessageContextMenu
className="custom-menu-class"
// ... other props
>
{children}
</MessageContextMenu>import { useMutation } from '@apollo/client'
import {
PIN_MESSAGE,
UNPIN_MESSAGE,
STAR_MESSAGE,
DELETE_MESSAGE,
} from '@/graphql/mutations/messages'
function MessageWithActions({ message }) {
const [pinMessage] = useMutation(PIN_MESSAGE)
const [deleteMessage] = useMutation(DELETE_MESSAGE)
const { handleAction, getPermissions } = useMessageActions({
channelId: message.channelId,
onDeleteMessage: async (messageId) => {
await deleteMessage({
variables: { id: messageId },
update(cache) {
// Update cache after delete
},
})
},
})
// ... render with actions
}- Permission Checking: Always check permissions before showing actions
- Optimistic Updates: Use optimistic updates for instant feedback
- Error Handling: Handle errors gracefully with toast notifications
- Accessibility: Ensure keyboard navigation works
- Mobile: Use mobile-specific components on mobile devices
- Performance: Memoize callbacks and use useCallback
- Selection: Clear selection after bulk operations
- Check that permissions are correctly set
- Verify the user is authenticated
- Ensure the message is not deleted
- Make sure the MessageContextMenu wraps the entire message
- Check browser console for errors
- Verify Radix UI context menu is installed
- Check user role is correctly set
- Verify the isOwnMessage check
- Ensure message.userId matches current user
See the following files for complete examples:
/Users/admin/Sites/nself-chat/src/components/chat/message-item.tsx/Users/admin/Sites/nself-chat/src/components/chat/message-list.tsx/Users/admin/Sites/nself-chat/src/app/chat/page.tsx