Messaging
Overview
CommonPlace's messaging system provides private, one-on-one and group conversations between users. Unlike typical messaging apps that create urgency through read receipts, typing indicators, and online status, CommonPlace messaging is designed around calm, intentional communication. Conversations have explicit states (open, paused, resting, closed) that participants can set to communicate their availability without pressure.
The messaging system supports core features like sending, editing, and deleting messages (soft delete), threaded replies, emoji reactions, and presence notes. Presence notes let users leave a short status on a conversation (like "stepping away for a bit") without the urgency of online/offline indicators. Messages are paginated (50 per page) and receive real-time updates via Supabase Realtime subscriptions.
The system also includes a "stepped away" feature that allows users to temporarily reduce interaction with a specific friend, integrating with the broader de-escalation philosophy of the platform.
Relevant Invariants
- Invariant #9: "Time Is Not Weaponized" -- No typing indicators, no read receipts, no "last seen" timestamps. Presence notes are set voluntarily by the user.
- Invariant #5: "Repair Over Punishment" -- Messages can be edited (with original preserved) and soft-deleted rather than permanently destroyed.
- Invariant #15: "Non-Interaction Is Valid" -- Conversation states like "paused" and "resting" are first-class; not responding is a valid choice.
User Experience
User Flow
- Starting a conversation: Click the Message button on a user's HomeRoom, or use the "New Conversation" modal from the messaging page.
- Sending messages: Type in the input area and send. Messages appear at the bottom of the thread (newest at bottom).
- Replying to a message: Use the context menu on a message to reply, creating a threaded reference.
- Reacting to a message: Use the reaction picker to add emoji reactions.
- Editing a message: Open the edit modal from the context menu to modify a sent message.
- Deleting a message: Soft-delete via the context menu; the message disappears from the thread.
- Setting conversation state: Change the conversation between open, paused, resting, or closed.
- Setting a presence note: Leave a note on the conversation to communicate availability.
- Pinning a conversation: Toggle the pinned status for quick access.
- Loading older messages: Scroll to top to load more messages (50 per page).
Conversation States
| State | Meaning |
|---|---|
open | Active conversation, both participants engaged |
paused | Temporarily paused by one participant |
resting | On a longer break; low-priority |
closed | Conversation concluded |
Technical Implementation
Key Files
| File | Purpose |
|---|---|
src/hooks/useConversation.js | Main hook for managing a single conversation's messages, participants, state, and real-time subscription (591 lines) |
src/hooks/useSteppedAway.js | Hook for the "stepped away" feature |
src/hooks/useMessageReactions.js | Hook for managing message reactions |
src/components/messaging/ConversationView.jsx | Full conversation thread view |
src/components/messaging/ConversationPreview.jsx | Conversation list item preview |
src/components/messaging/NewConversationModal.jsx | Modal for starting a new conversation |
src/components/messaging/MessageContextMenu.jsx | Right-click/long-press context menu for messages |
src/components/messaging/EditMessageModal.jsx | Modal for editing a message |
src/components/messaging/ReactionPicker.jsx | Emoji reaction picker |
src/components/messaging/MessageReactions.jsx | Display component for reactions on a message |
src/components/messaging/ReplyPreview.jsx | Preview of the message being replied to |
src/components/messaging/MessageReplyBadge.jsx | Badge showing a message is a reply |
src/components/messaging/PresenceNoteBadge.jsx | Displays a participant's presence note |
src/components/messaging/PresenceNoteEditor.jsx | Editor for setting a presence note |
src/components/messaging/SteppedAwayPrompt.jsx | Prompt for the stepped-away feature |
src/components/messaging/SteppedAwayModal.jsx | Modal for managing stepped-away status |
src/components/messaging/ScrollToBottomButton.jsx | Floating button to scroll to latest messages |
src/components/common/MessageButton.jsx | Button component for starting a conversation from profiles |
useConversation(conversationId) Hook
Manages the full lifecycle of a conversation.
State:
conversation-- Conversation record with user_a and user_b profile datamessages-- Array of messages in chronological order (oldest first)participants-- Active participants with profile dataloading,loadingMore,sending,hasMore,error
Actions:
sendMessage(content, type, replyToId)-- Sends viasend_messageRPC (SECURITY DEFINER). Returns full message with sender info.editMessage(messageId, newContent)-- Updates content, setsedited_at, preservesoriginal_content.deleteMessage(messageId)-- Soft delete by settingdeleted_at.loadMore()-- Fetches older messages before the oldest loaded message.markAsRead()-- Callsmark_conversation_readRPC.updateState(newState)-- Changes conversation state (open/paused/resting/closed).updatePresenceNote(note)-- Sets or clears the user's presence note.togglePinned()-- Toggles pinned status.
useConversationParticipants(conversationId) Hook
Manages participants in group conversations:
addParticipant(userId)-- Add a new memberremoveParticipant(userId)-- Remove a member (sets status to "removed")leaveConversation()-- Current user leaves (sets status to "left")updateRole(userId, newRole)-- Change a participant's role
Real-time Subscription
Each conversation subscribes to a Supabase Realtime channel for the messages table filtered by conversation_id:
- INSERT events: New messages are fetched with full sender and reply data, then appended to the local array (with duplicate detection).
- UPDATE events: If
deleted_atis set, the message is removed from local state. Otherwise, the message is updated in place (for edits).
The subscription is created when conversationId changes and cleaned up on unmount.
Message Query Structure
Messages are fetched with joins:
sender:sender_id-- Sender profile (display_name, username, avatar_url)reply_to:reply_to_id-- Referenced message with its sender info (for reply threads)- Soft-deleted messages are excluded via
.is('deleted_at', null)
Database Tables
| Table | Purpose |
|---|---|
conversations | Conversation records: user_a_id, user_b_id, state, state_changed_by |
messages | Message records: conversation_id, sender_id, content, type, reply_to_id, edited_at, original_content, deleted_at |
conversation_participants | Participants: conversation_id, user_id, role, status (active/left/removed), presence_note, presence_updated_at, pinned |
Edge Cases
| Scenario | Behavior |
|---|---|
| Duplicate real-time message | Checked by ID before adding to local state; duplicates are skipped |
| Message edited | edited_at timestamp set; original_content preserved for audit |
| Message deleted | Soft delete via deleted_at; removed from local state immediately |
| Conversation state changed | Updated locally and in database; state_changed_by tracks who changed it |
| No messages in conversation | Empty thread displayed; no "start chatting" nudge |
| Presence note cleared | Set to null in database and local state |
| Participant leaves | Status set to "left" with left_at timestamp |
| Invalid conversation state | updateState validates against allowed states before updating |
Related
Last updated: 2026-02-07