Goal
Merge the two separate desktop apps — dm ("Messages", NIP-04 via sphere.communications) and group-chat (NIP-29 via sphere.groupChat) — into one unified Messages app with improved UI/UX: a single conversation list mixing DMs and groups, one chat window, one unread badge.
Current state (~5,400 lines under src/components/chat/, two near mirror-image stacks)
Registered as two apps in src/config/activities.ts, mounted by DesktopLayout.renderTabContent() as DMChatSection / GroupChatSection. Duplication between the stacks:
- Section shells
dm/DMChatSection.tsx (278 ln) vs group/GroupChatSection.tsx (391 ln) — ~70% identical (layout, sidebar collapse, URL params, autofocus, mention handler).
- Sidebars
DMConversationList vs GroupList — line-for-line the same markup; rows DMConversationItem vs GroupItem — same anatomy.
- Message panes
DMMessageList vs GroupMessageList — date grouping, "Load earlier", scroll-restoration and the orbit loader are copy-pasted.
- Bubbles
DMMessageBubble vs GroupMessageBubble — same styling base.
- Data hooks
hooks/useChat.ts (382 ln) vs hooks/useGroupChat.ts (691 ln) — mirrored structure (address-scoped keys, localStorage selection, 20/+20 pagination, mark-as-read hardcoded to its own tab id).
useDmUnreadCount / useGroupUnreadCount + three duplicated dual-badge call sites (DesktopShortcuts, Sidebar, TabBar).
- Display helpers duplicated wholesale:
data/chatTypes.ts vs utils/groupChatHelpers.ts.
- Mini-chat (
mini/) re-implements DM querying/sending/mark-as-read a third time outside useChat.
- Legacy
ChatSection.tsx (dm/global mode toggle) is effectively dead code.
Feature asymmetry a unified UI must respect: groups add roles/moderation/write-restrictions (canWriteToGroup), invites + ?join= deep link, discovery, reply threading, member list, relay connection lifecycle (ServicesProvider auto-connect + pinned groups); DMs add E2E, read receipts (SENT/DELIVERED/READ), typing indicators, welcome-DM filtering, mini-chat.
Proposed architecture
- One app: replace
dm + group-chat in activities.ts with a single messages app; keep /agents/dm and /agents/group-chat as aliases opening the right thread (preserves @mention navigation, ?nametag=/?join= deep links, Connect dm intent, marketplace links).
- Thread abstraction in
data/: ChatThread = { kind: 'dm' | 'group', id, … } + two adapters (over sphere.communications / sphere.groupChat) normalizing to one DisplayMessage shape.
- Capability flags instead of component forks: adapters expose
canWrite, canReply, canDeleteMessage, canKick, showReadReceipts, showTypingIndicator, isConnected; single ChatWindow/MessageList/MessageBubble render conditionally (DMChatInput is already shared by both — proof it works).
- Single conversation list: DMs + groups sorted by last message, pinned groups on top, kind icons, All/DMs/Groups filter chips, one "+" menu (New DM / Join group / Create group).
- Hooks: merge into
useThreads() + useThreadMessages(threadRef); unified query keys; one useTotalUnreadCount → single desktop badge (removes the triple badge wiring).
- Events: normalize
message:dm and groupchat:* into one thread-message event in useSphereEvents.
- Mini-chat: key windows by thread id (
dm:<pubkey> / group:<groupId>) so groups can pop out too; refactor MiniChatWindow onto useThreadMessages.
- Cleanup: delete legacy
ChatSection + CHAT_MODE storage key; merge duplicated display helpers; adopt SDK getMessagesPage for group pagination; consider exposing communications.deleteConversation in the thread menu.
UX improvements to fold in
- Unified search across DMs and groups.
- Consistent reply UX (groups have it, DMs don't — decide whether to add for DMs or keep per-capability).
- Clickable nametag in group bubbles currently cross-navigates to the DM app — becomes an in-app thread switch.
- Surface relay connection state only for group threads.
Acceptance criteria
Goal
Merge the two separate desktop apps —
dm("Messages", NIP-04 viasphere.communications) andgroup-chat(NIP-29 viasphere.groupChat) — into one unified Messages app with improved UI/UX: a single conversation list mixing DMs and groups, one chat window, one unread badge.Current state (~5,400 lines under
src/components/chat/, two near mirror-image stacks)Registered as two apps in
src/config/activities.ts, mounted byDesktopLayout.renderTabContent()asDMChatSection/GroupChatSection. Duplication between the stacks:dm/DMChatSection.tsx(278 ln) vsgroup/GroupChatSection.tsx(391 ln) — ~70% identical (layout, sidebar collapse, URL params, autofocus, mention handler).DMConversationListvsGroupList— line-for-line the same markup; rowsDMConversationItemvsGroupItem— same anatomy.DMMessageListvsGroupMessageList— date grouping, "Load earlier", scroll-restoration and the orbit loader are copy-pasted.DMMessageBubblevsGroupMessageBubble— same styling base.hooks/useChat.ts(382 ln) vshooks/useGroupChat.ts(691 ln) — mirrored structure (address-scoped keys, localStorage selection, 20/+20 pagination, mark-as-read hardcoded to its own tab id).useDmUnreadCount/useGroupUnreadCount+ three duplicated dual-badge call sites (DesktopShortcuts,Sidebar,TabBar).data/chatTypes.tsvsutils/groupChatHelpers.ts.mini/) re-implements DM querying/sending/mark-as-read a third time outsideuseChat.ChatSection.tsx(dm/global mode toggle) is effectively dead code.Feature asymmetry a unified UI must respect: groups add roles/moderation/write-restrictions (
canWriteToGroup), invites +?join=deep link, discovery, reply threading, member list, relay connection lifecycle (ServicesProviderauto-connect + pinned groups); DMs add E2E, read receipts (SENT/DELIVERED/READ), typing indicators, welcome-DM filtering, mini-chat.Proposed architecture
dm+group-chatinactivities.tswith a singlemessagesapp; keep/agents/dmand/agents/group-chatas aliases opening the right thread (preserves@mentionnavigation,?nametag=/?join=deep links, Connectdmintent, marketplace links).data/:ChatThread = { kind: 'dm' | 'group', id, … }+ two adapters (oversphere.communications/sphere.groupChat) normalizing to oneDisplayMessageshape.canWrite,canReply,canDeleteMessage,canKick,showReadReceipts,showTypingIndicator,isConnected; single ChatWindow/MessageList/MessageBubble render conditionally (DMChatInputis already shared by both — proof it works).useThreads()+useThreadMessages(threadRef); unified query keys; oneuseTotalUnreadCount→ single desktop badge (removes the triple badge wiring).message:dmandgroupchat:*into one thread-message event inuseSphereEvents.dm:<pubkey>/group:<groupId>) so groups can pop out too; refactorMiniChatWindowontouseThreadMessages.ChatSection+CHAT_MODEstorage key; merge duplicated display helpers; adopt SDKgetMessagesPagefor group pagination; consider exposingcommunications.deleteConversationin the thread menu.UX improvements to fold in
Acceptance criteria
/agents/dm?nametag=…,/agents/group-chat?join=…, Connectdmintent.buildAddressId) unchanged (no lost local state).ChatSectionremoved.