Skip to main content

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

  1. Starting a conversation: Click the Message button on a user's HomeRoom, or use the "New Conversation" modal from the messaging page.
  2. Sending messages: Type in the input area and send. Messages appear at the bottom of the thread (newest at bottom).
  3. Replying to a message: Use the context menu on a message to reply, creating a threaded reference.
  4. Reacting to a message: Use the reaction picker to add emoji reactions.
  5. Editing a message: Open the edit modal from the context menu to modify a sent message.
  6. Deleting a message: Soft-delete via the context menu; the message disappears from the thread.
  7. Setting conversation state: Change the conversation between open, paused, resting, or closed.
  8. Setting a presence note: Leave a note on the conversation to communicate availability.
  9. Pinning a conversation: Toggle the pinned status for quick access.
  10. Loading older messages: Scroll to top to load more messages (50 per page).

Conversation States

StateMeaning
openActive conversation, both participants engaged
pausedTemporarily paused by one participant
restingOn a longer break; low-priority
closedConversation concluded

Technical Implementation

Key Files

FilePurpose
src/hooks/useConversation.jsMain hook for managing a single conversation's messages, participants, state, and real-time subscription (591 lines)
src/hooks/useSteppedAway.jsHook for the "stepped away" feature
src/hooks/useMessageReactions.jsHook for managing message reactions
src/components/messaging/ConversationView.jsxFull conversation thread view
src/components/messaging/ConversationPreview.jsxConversation list item preview
src/components/messaging/NewConversationModal.jsxModal for starting a new conversation
src/components/messaging/MessageContextMenu.jsxRight-click/long-press context menu for messages
src/components/messaging/EditMessageModal.jsxModal for editing a message
src/components/messaging/ReactionPicker.jsxEmoji reaction picker
src/components/messaging/MessageReactions.jsxDisplay component for reactions on a message
src/components/messaging/ReplyPreview.jsxPreview of the message being replied to
src/components/messaging/MessageReplyBadge.jsxBadge showing a message is a reply
src/components/messaging/PresenceNoteBadge.jsxDisplays a participant's presence note
src/components/messaging/PresenceNoteEditor.jsxEditor for setting a presence note
src/components/messaging/SteppedAwayPrompt.jsxPrompt for the stepped-away feature
src/components/messaging/SteppedAwayModal.jsxModal for managing stepped-away status
src/components/messaging/ScrollToBottomButton.jsxFloating button to scroll to latest messages
src/components/common/MessageButton.jsxButton 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 data
  • messages -- Array of messages in chronological order (oldest first)
  • participants -- Active participants with profile data
  • loading, loadingMore, sending, hasMore, error

Actions:

  • sendMessage(content, type, replyToId) -- Sends via send_message RPC (SECURITY DEFINER). Returns full message with sender info.
  • editMessage(messageId, newContent) -- Updates content, sets edited_at, preserves original_content.
  • deleteMessage(messageId) -- Soft delete by setting deleted_at.
  • loadMore() -- Fetches older messages before the oldest loaded message.
  • markAsRead() -- Calls mark_conversation_read RPC.
  • 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 member
  • removeParticipant(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_at is 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

TablePurpose
conversationsConversation records: user_a_id, user_b_id, state, state_changed_by
messagesMessage records: conversation_id, sender_id, content, type, reply_to_id, edited_at, original_content, deleted_at
conversation_participantsParticipants: conversation_id, user_id, role, status (active/left/removed), presence_note, presence_updated_at, pinned

Edge Cases

ScenarioBehavior
Duplicate real-time messageChecked by ID before adding to local state; duplicates are skipped
Message editededited_at timestamp set; original_content preserved for audit
Message deletedSoft delete via deleted_at; removed from local state immediately
Conversation state changedUpdated locally and in database; state_changed_by tracks who changed it
No messages in conversationEmpty thread displayed; no "start chatting" nudge
Presence note clearedSet to null in database and local state
Participant leavesStatus set to "left" with left_at timestamp
Invalid conversation stateupdateState validates against allowed states before updating
  • Friends -- Conversations are typically between friends
  • HomeRoom -- Message button on user profiles

Last updated: 2026-02-07