The contact management system tracks user profiles with real-time online status, favorite contacts, and detailed profile information.
Contacts store comprehensive user information:
export interface Contact {
id: string
name: string
phoneNumber: string
about: string
avatarUrl: string
isOnline: boolean
lastSeenAt: string
favorite?: boolean
pinned?: boolean
}
Field descriptions
id - Unique identifier for the contact
name - Display name
phoneNumber - Phone number with country code
about - Status message or bio
avatarUrl - Profile picture URL
isOnline - Real-time online status
lastSeenAt - ISO timestamp of last activity
favorite - Whether contact is favorited
pinned - Whether contact chats are pinned to top
Online status tracking
Contacts maintain real-time online status:
The ChatHeader component displays online status with an indicator:
components/chat/chat-header.tsx
<div className="relative">
<Avatar className="h-11 w-11 border border-border/60">
<AvatarImage src={contact.avatarUrl} alt={contact.name} />
<AvatarFallback>{initials(contact.name)}</AvatarFallback>
</Avatar>
<span
className={cn(
"absolute -right-0.5 -bottom-0.5 h-2.5 w-2.5 rounded-full border-2 border-background",
isOnline ? "bg-accent" : "bg-muted"
)}
/>
</div>
<div>
<p className="text-sm font-semibold text-foreground">{contact.name}</p>
<p className="text-xs text-muted-foreground">
{isOnline ? "Online" : `last seen ${formatDistanceToNow(new Date(contact.lastSeenAt), { addSuffix: true })}`}
</p>
</div>
The online indicator is a small colored dot: green for online, gray for offline.
Presence tracking
The system maintains a presence map for all contacts:
export interface PresenceMap {
[contactId: string]: {
isOnline: boolean
lastSeenAt: string
}
}
Contacts are stored in a normalized structure:
export interface ChatStateSnapshot {
chats: Record<string, Chat>
messages: Record<string, Message>
contacts: Record<string, Contact>
drafts: Record<string, DraftMessage>
typingIndicators: TypingIndicator[]
searchHistory: ChatSearchHistoryItem[]
activeChatId?: string
}
Use the contacts selector to retrieve all contacts:
export const chatSelectors = {
chatList: (state: ChatStore) => Object.values(state.chats),
activeChat: (state: ChatStore) =>
state.activeChatId ? state.chats[state.activeChatId] : undefined,
activeMessages: (state: ChatStore) => {
if (!state.activeChatId) return []
const chat = state.chats[state.activeChatId]
return chat.messageIds.map((id) => state.messages[id]).filter(Boolean)
},
contacts: (state: ChatStore) => Object.values(state.contacts),
}
Usage:
const contacts = useChatStore(chatSelectors.contacts)
Avatar display
Contact avatars use the shadcn/ui Avatar component:
components/chat/sidebar.tsx
<Avatar className="h-12 w-12 border border-border/60">
<AvatarImage src={contact.avatarUrl} alt={contact.name} />
<AvatarFallback>{initials(contact.name)}</AvatarFallback>
</Avatar>
Generating initials
Fallback initials are generated from the contact name:
components/chat/sidebar.tsx
function initials(value: string) {
return value
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
}
Initials are generated from the first letter of the first two words in the name.
Mark contacts as favorites for quick access:
export interface Contact {
// ...
favorite?: boolean
pinned?: boolean
}
const contacts: Contact[] = [
{
id: "carlos",
name: "Carlos Mendes",
phoneNumber: "+55 11 98888-1122",
about: "Product designer • Rio",
avatarUrl: "https://i.pravatar.cc/120?img=12",
isOnline: true,
lastSeenAt: iso(subMinutes(baseTimestamp, 2)),
favorite: true,
},
// ...
]
Favorite contacts can be filtered or displayed in a dedicated section of the UI.
Pinned contacts keep their chats at the top of the chat list:
{
id: "sofia",
name: "Sofia Patel",
phoneNumber: "+44 7444 222333",
about: "Mobile engineer • London",
avatarUrl: "https://i.pravatar.cc/120?img=33",
isOnline: false,
lastSeenAt: iso(subMinutes(baseTimestamp, 12)),
pinned: true,
}
User profile
The current user’s profile uses the Profile interface:
export interface Profile {
id: string
name: string
phoneNumber: string
avatarUrl: string
about: string
}
The user’s profile is displayed at the top of the sidebar:
components/chat/sidebar.tsx
<header className="flex items-center justify-between px-5 pb-4 pt-5">
<div className="flex items-center gap-3">
<Avatar className="h-11 w-11 border border-border/60">
<AvatarImage src={profile.avatarUrl} alt={profile.name} />
<AvatarFallback>{initials(profile.name)}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-semibold text-sidebar-foreground">{profile.name}</p>
<p className="text-xs text-muted-foreground">{profile.about}</p>
</div>
</div>
<Button
onClick={onStartNewChat}
size="icon"
variant="outline"
className="h-10 w-10 rounded-full border-sidebar-border/80 bg-sidebar"
>
<Plus className="h-5 w-5" />
<span className="sr-only">Start new chat</span>
</Button>
</header>
Retrieve contact information from chat data:
components/chat/sidebar.tsx
{filtered.map((chat) => {
const contact = contacts[chat.contactId]
if (!contact) return null
// Render chat item
})}
Always check if a contact exists before rendering to handle edge cases where chat data may reference deleted contacts.
Example contacts from the mock data:
const contacts: Contact[] = [
{
id: "carlos",
name: "Carlos Mendes",
phoneNumber: "+55 11 98888-1122",
about: "Product designer • Rio",
avatarUrl: "https://i.pravatar.cc/120?img=12",
isOnline: true,
lastSeenAt: iso(subMinutes(baseTimestamp, 2)),
favorite: true,
},
{
id: "sofia",
name: "Sofia Patel",
phoneNumber: "+44 7444 222333",
about: "Mobile engineer • London",
avatarUrl: "https://i.pravatar.cc/120?img=33",
isOnline: false,
lastSeenAt: iso(subMinutes(baseTimestamp, 12)),
pinned: true,
},
{
id: "li",
name: "Li Wei",
phoneNumber: "+86 139 8888 1212",
about: "Traveling in Kyoto 🇯🇵",
avatarUrl: "https://i.pravatar.cc/120?img=52",
isOnline: true,
lastSeenAt: iso(subMinutes(baseTimestamp, 1)),
},
]
Filter contacts by name in the chat list:
components/chat/sidebar.tsx
const filtered = useMemo(() => {
const query = searchQuery.trim().toLowerCase()
if (!query) {
return chats
}
return chats.filter((chat) => {
const contact = contacts[chat.contactId]
return (
contact?.name.toLowerCase().includes(query) ||
chat.lastMessagePreview?.toLowerCase().includes(query)
)
})
}, [chats, contacts, searchQuery])
Display relative time for when a contact was last online:
components/chat/chat-header.tsx
<p className="text-xs text-muted-foreground">
{isOnline ? "Online" : `last seen ${formatDistanceToNow(new Date(contact.lastSeenAt), { addSuffix: true })}`}
</p>
Examples:
- “last seen 5 minutes ago”
- “last seen 2 hours ago”
- “last seen yesterday”
Normalized storage
Contacts are stored separately from chats and referenced by ID:
const initialState: ChatStateSnapshot = {
chats: Object.fromEntries(
Object.entries(chats).sort(([, a], [, b]) => (a.lastActivityAt > b.lastActivityAt ? -1 : 1))
),
messages,
contacts: contacts.reduce<Record<string, Contact>>((acc, contact) => {
acc[contact.id] = contact
return acc
}, {}),
drafts: {},
typingIndicators: [],
searchHistory: [],
activeChatId: "chat-carlos",
}
This normalized structure allows efficient lookup and prevents data duplication across multiple chats with the same contact.