Overview
Compare
Inspect
The 메시지 inbox is a DetailShell + SegmentedTabs (주요/요청) over DmThreadRows, opening a bottom-up FeedList thread with asymmetric mine/theirs bubbles under a ComposeBar (a RequestGateBar when pending).
route · apps/mobile/app/messages/{index,[threadId],new-group,add-members}.tsx → messaging/src/dm-{inbox,thread,create-group,add-members}-screen.tsx
grounded in 5 source file(s)
packages/features/messaging/src/dm-inbox-screen.tsxpackages/features/messaging/src/dm-thread-screen.tsxpackages/features/messaging/src/dm-create-group-screen.tsxpackages/features/messaging/src/shared/friend-picker-row.tsxpackages/app/src/presentation/hooks/queries/use-dm.ts
Spec
1
Foundations · 디자인 시스템
Spacing · 4px grid
$1 – $4
4 · 8 · 12 · 16 px
$5 – $10
20 · 24 · 32 · 40 · 48 · 64 px
Radius
$2 8 · $3 12 · $4 16 · $7 9999 (pill)
Color · Classic Grass (light)
Surface
$background #F8FBF9 · $card #FFFFFF · $surfaceTertiary #E5FAEA
Brand
$primary #006D37 · $primarySubtle #E5FAEA
Text
$text #0D100E · $textSecondary #4F5851 · $textTertiary #626C64
Border
$border #CAD4CC · $borderSubtle #DFE7E1
Status
$success #14804A · $warning #946200 · $error #B8332E
Badge intents
accent #E0F8F3/#007B6B · warning #FFF4D6/#946200 · error #FDECEC/#B8332E
Court
$courtClay #B84A2E · $courtHard #247BA0 · $courtGreen #006633
Typography · Pretendard
cardTitle · navTitle
16 / 24 · 600 · 17 / 22 · 600
cardBody · cardMeta
14 / 22 · 400 · 13 / 18 · 500
badge · micro
12 / 18 · 600 · 10 / 14 · 600
display Sm/Md/Lg/Xl
20/28 · 28/36 · 36/44 · 48/56 · 700
Primitives · @twomore/ui
Layout
YStack · XStack · Card (default/flat) · Well · Divider
Controls
Pressable · Badge · Icon · PillNav · MainTabShell · BottomCtaBand
Data viz
DateTile · CapacityRing · ProgressBar · AvatarStack · SegmentedDonutChart
Domain cards
SessionCard · ClubCard · VenueHero · SessionCompactRow · member row
Box sizes
AVATAR XS 28 · SM 32 · MD 40 · LG 44 · 2XL 88 · TOUCH_TARGET 44
Rationale
Every part is built from these — one 4px grid, the Pretendard type scale, the Classic-Grass palette, and the shared @twomore/ui primitives + domain cards.
2
받은함 · 스레드 목록
Structure
Shell
DetailShell title=메시지, headerRight MessageCircle HeaderIconButton → new-group
Row
DmThreadRow — 1:1 AvatarBubble MD / group AvatarStack max-3; name + unread dot + relative time; group "N명" prefixes the PREVIEW line, not the name line (dm-thread-row.tsx:144-150); request rows swap chevron for inline 수락/거절
Requests tab
pending count is a small unlabeled LIVE dot on the PillNav tab, not a number in the label (dm-inbox-screen.tsx:221-231)
3
대화 · 비대칭 말풍선
Structure
Header
DetailShell titleLeading = avatar/AvatarStack + name; headerRight ⋯ → block/report ModalPanel (1:1) or ParticipantsModal (group)
Bubble
DmMessageRow — mine $primary/white right-tail, theirs $surfaceSecondary left-tail; avatar only on run-first; timestamp + 전송됨/읽음 on run-last
4
그룹 대화 · 발신자 + 시스템
Structure
Sender
group bubbles show the sender name above the run-first message
System
a centered system line for membership events ("…님을 초대했어요")
5
메시지 없음 · 신규 대화
Structure
Empty
Usability audit 2026-09-16 #5 — an accepted thread with zero messages used to render a blank canvas below ThreadInfoHeader; now FeedList emptyState shows EmptyState icon={MessageSquare} title={strings.threadEmpty} ("첫 메시지를 보내 보세요"), variant="compact"
Header + compose
ThreadInfoHeader (avatar + name + start-date caption) still renders — it is the FeedList's own header, independent of message count; ComposeBar still shows (the thread IS accepted)
6
찾을 수 없음 · 잘못된 threadId
Structure
Guard
bug-class fix 2026-09-16 — settled query, no thread row (bad/inaccessible id) → threadNotFound = !isLoadingThread && !thread; body renders NotFoundState title={strings.threadNotFound} ("대화방을 찾을 수 없어요") with a 뒤로 가기 action, replacing what used to be indistinguishable from the empty-thread state above
Header + compose
title falls back to the generic t().common.messages ("메시지") only once loading has settled (a blank title during load, never the generic word as a false-positive); NO avatar/AvatarStack (needs otherName, unresolved) and NO trailing ⋯ menu (gated on otherUserId, unresolved); the bottom bar (ComposeBar/RequestGateBar) is suppressed entirely — isAccepted defaults true when thread is null, which would otherwise still float a live compose box over this body
7
새 그룹 · 멤버 피커
Structure
Form
StyledInput group name + FriendPickerRow list (seeded candidates pre-checked) → BottomCtaBand 만들기 (N명)
Row
FriendPickerRow — AvatarBubble MD + name + 24px checkbox (filled $primary+✓ / outlined)