Messaging System — Frontend Documentation
Status: Phase 1 complete (core messaging, real-time, file sharing, group management) Backend API Docs: https://docs-dev.aiqlick.com/docs/api/graphql/messaging-api
Table of Contents
- Architecture Overview
- File Structure
- TypeScript Types
- GraphQL Layer
- Hooks
- Components
- Pages
- Navigation Integration
- Real-Time (WebSocket)
- Features
- Patterns & Conventions
- Frontend Limitations
- Backend Requirements (Not Yet Implemented)
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Pages (company/messages, jobseeker/messages) │
│ └─ useMessaging hook │
│ ├─ Apollo queries/mutations (HTTP) │
│ └─ graphql-ws subscription (WebSocket) │
├─────────────────────────────────────────────────────────┤
│ Components │
│ ConversationList ← ConversationItem │
│ MessageThread ← MessageBubble + MessageInput │
│ NewConversationModal │
├─────────────────────────────────────────────────────────┤
│ GraphQL Operations │
│ queries.ts │ mutations.ts │ subscriptions.ts │
│ fragments.ts (shared) │
├─────────────────────────────────────────────────────────┤
│ Types (lib/types/messaging.ts) │
└─────────────────────────────────────────────────────────┘
Key design decisions:
- Apollo Client for queries/mutations (HTTP), graphql-ws for subscriptions (WebSocket)
- Cursor-based pagination (the messaging API uses cursors, unlike the rest of the app which uses offset)
- Client-side archived conversation filtering via
participant.isArchived - No
deleteConversationmutation — "Delete chat" archives the conversation - @ mentions are frontend-only — backend stores plain text, frontend renders highlights
File Structure
lib/
types/
messaging.ts # All TS types & interfaces (150 lines)
hooks/
useMessaging.ts # Core messaging hook (657 lines)
useUnreadMessageCount.ts # Sidebar badge hook (88 lines)
graphql/operations/
schema/messaging/
fragments.ts # 4 GraphQL fragments (113 lines)
messaging/
queries.ts # 4 queries (53 lines)
mutations.ts # 15 mutations (157 lines)
subscriptions.ts # 2 subscriptions (62 lines)
index.ts # Barrel re-exports
components/messaging/
ConversationList.tsx # Left panel with search & archive (226 lines)
ConversationItem.tsx # Single conversation row (317 lines)
MessageThread.tsx # Chat area + info panel (621 lines)
MessageBubble.tsx # Individual message (282 lines)
MessageInput.tsx # Composer with attachments & mentions (460 lines)
NewConversationModal.tsx # Create direct/group conversation (371 lines)
index.ts # Barrel exports
app/
company/messages/page.tsx # Company messages page (142 lines)
jobseeker/messages/page.tsx # Job seeker messages page (141 lines)
Total: ~3,500+ lines of messaging-specific code.
TypeScript Types
File: lib/types/messaging.ts
Enums (string literals)
| Type | Values |
|---|---|
ConversationType | "DIRECT", "GROUP" |
ConversationParticipantRole | "OWNER", "ADMIN", "MEMBER" |
MessageStatus | "SENT", "DELIVERED", "DELETED" |
Core Interfaces
| Interface | Key Fields |
|---|---|
Participant | userId, role, isMuted, isPinned, isArchived, isFlagged, isMarkedUnread, firstName, lastName, profileImageUrl |
Conversation | id, type, title?, companyId?, lastMessageAt, lastMessageText, unreadCount, participants[] |
Message | id, senderId, content?, status, isEdited, replyTo?, attachments?, isSelf?, sender info fields |
MessageAttachment | id, filename, contentType, sizeBytes, objectPath, downloadUrl? |
ConversationListOutput | conversations[], hasMore, nextCursor? |
MessageListOutput | messages[], hasMore, nextCursor? |
Input Types
| Interface | Purpose |
|---|---|
CreateDirectConversationInput | recipientId, optional companyId |
CreateGroupConversationInput | title, participantIds[], optional companyId |
SendMessageInput | conversationId, content?, replyToId?, attachments?[] |
MessageAttachmentInput | filename, contentType, sizeBytes, objectPath |
GetConversationsInput | cursor?, limit?, companyId?, type?, search? |
GetMessagesInput | cursor?, limit? |
UpdateGroupConversationInput | conversationId, title |
AddGroupParticipantsInput | conversationId, userIds[] |
RemoveGroupParticipantInput | conversationId, userId |
GraphQL Layer
Fragments (graphql/operations/schema/messaging/fragments.ts)
| Fragment | On Type | Notes |
|---|---|---|
PARTICIPANT_FIELDS | ParticipantOutput | 17 fields incl. roles, flags, names |
CONVERSATION_FIELDS | ConversationOutput | Embeds ParticipantFields |
MESSAGE_FIELDS | MessageOutput | 15 fields + attachments array |
MESSAGE_WITH_REPLY_FIELDS | MessageOutput | Extends MessageFields with nested replyTo |
Re-exported from graphql/operations/schema/index.ts.
Queries (graphql/operations/messaging/queries.ts)
| Query | Variables | Returns |
|---|---|---|
MY_CONVERSATIONS | GetConversationsInput | ConversationListOutput |
GET_CONVERSATION | id: ID! | ConversationOutput |
GET_MESSAGES | conversationId: ID!, GetMessagesInput | MessageListOutput |
UNREAD_MESSAGE_COUNT | companyId?: ID | UnreadMessageCount |
Mutations (graphql/operations/messaging/mutations.ts)
| Category | Mutations |
|---|---|
| Conversation Creation | CREATE_DIRECT_CONVERSATION, CREATE_GROUP_CONVERSATION |
| Messages | SEND_MESSAGE, EDIT_MESSAGE, DELETE_MESSAGE |
| Read State | MARK_CONVERSATION_AS_READ, MARK_CONVERSATION_AS_UNREAD |
| Conversation Settings | MUTE_CONVERSATION, PIN_CONVERSATION, ARCHIVE_CONVERSATION, FLAG_CONVERSATION |
| Group Management | UPDATE_GROUP_CONVERSATION, ADD_GROUP_PARTICIPANTS, REMOVE_GROUP_PARTICIPANT, LEAVE_GROUP_CONVERSATION |
Subscriptions (graphql/operations/messaging/subscriptions.ts)
Plain strings (not gql tagged) for use with graphql-ws:
| Subscription | Fields | Notes |
|---|---|---|
MESSAGE_RECEIVED_SUBSCRIPTION | Full message fields + isSelf, attachments, replyTo | Real-time message delivery |
UNREAD_MESSAGE_COUNT_CHANGED_SUBSCRIPTION | count, companyId | Badge count updates |
Hooks
useMessaging(options?: { companyId?: string })
File: lib/hooks/useMessaging.ts (657 lines)
The central orchestration hook. Manages all messaging state and operations.
Return type — UseMessagingReturn:
{
// Conversations
conversations: Conversation[]
conversationsLoading: boolean
conversationsHasMore: boolean
loadMoreConversations: () => void
refetchConversations: () => void
searchConversations: (query: string) => void
searchQuery: string
// Active conversation
activeConversation: Conversation | null
activeConversationId: string | null
selectConversation: (id: string | null) => void
// Messages
messages: Message[]
messagesLoading: boolean
messagesHasMore: boolean
loadMoreMessages: () => void
// Message actions
sendMessage: (content: string, replyToId?: string, attachments?: MessageAttachmentInput[]) => Promise<void>
editMessage: (messageId: string, content: string) => Promise<void>
deleteMessage: (messageId: string) => Promise<void>
// Conversation creation
createDirectConversation: (recipientId: string) => Promise<string>
createGroupConversation: (title: string, participantIds: string[]) => Promise<string>
// Conversation management
markAsRead: (conversationId: string) => void
markAsUnread: (conversationId: string) => void
muteConversation: (conversationId: string, muted: boolean) => void
pinConversation: (conversationId: string, pinned: boolean) => void
archiveConversation: (conversationId: string, archived: boolean) => void
flagConversation: (conversationId: string, flagged: boolean) => void
// Group management
updateGroupTitle: (conversationId: string, title: string) => Promise<void>
addGroupParticipants: (conversationId: string, userIds: string[]) => Promise<void>
removeGroupParticipant: (conversationId: string, userId: string) => Promise<void>
leaveGroupConversation: (conversationId: string) => Promise<void>
// State
sendingMessage: boolean
isConnected: boolean
error: string | null
}
Key behaviors:
- Pinned conversations are sorted to the top via
useMemo - Messages are reversed for display (API returns newest-first, UI needs oldest-first)
- Auto-marks active conversation as read when selected or when new messages arrive
- WebSocket subscription auto-prepends new messages and updates conversation ordering
isSelfflag from subscription prevents duplicate message rendering- Conversation creation returns the new
conversationIdand auto-selects it
useUnreadMessageCount(companyId?: string)
File: lib/hooks/useUnreadMessageCount.ts (88 lines)
Lightweight hook for sidebar badge integration.
Return: { count: number, refetch: () => void }
Behavior:
- Primary: Apollo polling every 30 seconds (
pollInterval: 30000) - Secondary: graphql-ws subscription for instant updates (takes precedence)
- Skips both query and subscription when not logged in
- Uses
cache-and-networkfetch policy
Components
ConversationList
File: components/messaging/ConversationList.tsx
Left panel displaying all conversations with search, archived folder, and infinite scroll.
Props:
{
conversations: Conversation[]
loading: boolean
hasMore: boolean
currentUserId: string
activeConversationId: string | null
searchQuery: string
onSelect: (id: string) => void
onSearch: (query: string) => void
onLoadMore: () => void
onNewConversation: () => void
onPin?: (conversationId: string, pinned: boolean) => void
onMute?: (conversationId: string, muted: boolean) => void
onArchive?: (conversationId: string, archived: boolean) => void
onFlag?: (conversationId: string, flagged: boolean) => void
onMarkUnread?: (conversationId: string) => void
}
Features:
- Archived folder (WhatsApp-style): Conversations where
participant.isArchived === trueare separated into an archived section accessible via a folder button at the bottom of the list - Search bar with clear button (hidden in archived view)
- Infinite scroll via scroll event listener (loads more when within 100px of bottom)
- Empty states for both active and archived views
ConversationItem
File: components/messaging/ConversationItem.tsx
Individual conversation row in the list.
Props:
{
conversation: Conversation
currentUserId: string
isActive: boolean
onClick: () => void
onPin?: onMute?: onArchive?: onFlag?: onMarkUnread?
}
Features:
- Avatar: TWAvatar for direct messages, gradient initial for groups
- Unread indicator: Primary badge with count, or small dot for
isMarkedUnreadwithout actual unread count. Muted conversations show gray badge. - Status icons: Pin (rotated 45deg), flag (amber), mute icon in last message preview
- Context menu (right-click or chevron hover): Pin, Mute, Flag, Mark as unread, Archive — each with colored icon backgrounds (blue, gray, amber, purple, green)
- Time formatting: Today shows time, yesterday shows "Yesterday", within a week shows weekday, older shows month+day
MessageThread
File: components/messaging/MessageThread.tsx
Main chat area with header, messages, input, and a WhatsApp-style info side panel.
Props:
{
conversation: Conversation
messages: Message[]
messagesLoading: boolean
messagesHasMore: boolean
currentUserId: string
sendingMessage: boolean
onSendMessage: (content: string, replyToId?: string, attachments?: MessageAttachmentInput[]) => Promise<void>
onEditMessage: (messageId: string, content: string) => Promise<void>
onDeleteMessage: (messageId: string) => Promise<void>
onLoadMoreMessages: () => void
onBack?: () => void
onPin?: onMute?: onArchive?: onFlag?: onMarkUnread?
onUpdateGroupTitle?: (conversationId: string, title: string) => Promise<void>
onAddGroupParticipants?: (conversationId: string, userIds: string[]) => Promise<void>
onRemoveGroupParticipant?: (conversationId: string, userId: string) => Promise<void>
onLeaveGroup?: (conversationId: string) => Promise<void>
onDeleteChat?: (conversationId: string) => void
}
Layout:
┌──────────────────────┬────────────────────┐
│ Header (clickable) │ │
├──────────────────────┤ Info Panel │
│ │ (w-80/w-96) │
│ Messages (scroll) │ │
│ │ Profile │
│ │ Quick actions │
│ │ Tabs: │
│ │ Members|Media| │
│ │ Files │
├──────────────────────┤ Danger zone │
│ MessageInput │ │
└──────────────────────┴────────────────────┘
Info Panel features:
- Toggle: Click avatar/name in header to open/close
- Profile section: Large avatar, editable group name (pencil icon for OWNER/ADMIN)
- Quick actions: Mute, Pin, Flag — circular toggle buttons
- Tabs:
- Members (groups only): Add/remove participants, role display. Uses
GET_USERS_FOR_SHARINGquery for member search. - Media: 3-column grid of shared images from loaded messages
- Files: List with file icons, names, sizes, dates
- Members (groups only): Add/remove participants, role display. Uses
- Default tab: "members" for groups, "media" for direct messages
- Danger zone: Leave group, Delete chat
- Responsive: On mobile (
< lg), info panel replaces the chat area
Header menu (three-dot): Pin, Mute, Flag, Mark unread, Archive, Delete chat, Leave group (groups only)
MessageBubble
File: components/messaging/MessageBubble.tsx
Individual message rendering.
Props:
{
message: Message
currentUserId: string
isGroupConversation?: boolean
participants?: Participant[]
onReply?: (message: Message) => void
onEdit?: (message: Message) => void
onSaveEdit?: (messageId: string, content: string) => Promise<void>
onCancelEdit?: () => void
onDelete?: (message: Message) => void
isEditing?: boolean
}
Features:
- Alignment: Own messages right-aligned (primary bg, white text), others left-aligned (white bg, gray border)
- Avatar: Shown for other users' messages (not for own)
- Sender name: Shown in group conversations
- Reply preview: Border-left accent with quoted content
- Attachment cards: Inline images with preview, file cards with icon and size
- @ mention highlighting: Regex
/@([A-Z][a-zA-Za-y'-]*(?:\s[A-Z][a-zA-Za-y'-]*){0,2})/g- Own messages:
text-white/90 bg-white/15 - Others' messages:
text-primary bg-primary/10
- Own messages:
- Inline editing: When
isEditing=true, the bubble content becomes an editable textarea with Save/Cancel buttons. Enter saves, Escape cancels, Shift+Enter for newline. - Read receipts: Single tick (sent) or double blue tick (read) shown on own messages. Uses
participant.lastReadAtto determine if the message has been read by at least one other participant. - Hover actions: Reply, Edit (own only), Delete (own only) — hidden during edit mode
- Deleted messages: Italic "This message was deleted" placeholder
- Edited indicator: "edited" text next to timestamp
MessageInput
File: components/messaging/MessageInput.tsx
Message composer with file attachments and @ mention support.
Props:
{
onSend: (content: string, replyToId?: string, attachments?: MessageAttachmentInput[]) => Promise<void>
disabled?: boolean
sending?: boolean
replyTo?: Message | null
onCancelReply?: () => void
participants?: Participant[] // enables @ mentions
currentUserId?: string // excludes self from mention list
}
Constraints:
- Max 10,000 characters
- Max 10 attachments per message
- Max 25 MB per file
- Allowed MIME types: jpeg, png, gif, webp, svg+xml, pdf, doc/docx, xls/xlsx, ppt/pptx, txt, csv, zip
@ Mention system:
- Triggers when
@is typed preceded by space, newline, or start of text - Shows a dropdown above the input with participant avatars, names, and roles
- Keyboard navigation: Arrow Up/Down to select, Enter/Tab to insert, Escape to close
- Inserts
@FirstName LastNameinto the text - Placeholder changes to
"Type a message... (@ to mention)"when participants are provided
File upload:
- Uses S3 presigned URLs via
GENERATE_UPLOAD_URLmutation - Shows upload progress and file previews
- Files can be removed before sending
Keyboard:
Entersends the messageShift+Enterinserts a newline- Textarea auto-resizes up to 120px height
NewConversationModal
File: components/messaging/NewConversationModal.tsx
Two-tab modal for creating conversations.
Props:
{
isOpen: boolean
onClose: () => void
onCreateDirect: (recipientId: string) => Promise<string>
onCreateGroup: (title: string, participantIds: string[]) => Promise<string>
currentUserId: string
}
Tabs:
- Direct Message: Search and select a single user
- Group Chat: Enter group name + select multiple users via checkboxes
Uses GET_USERS_FOR_SHARING query to populate user list. Filters by active status and search term (name/email).
Pages
Company Messages (app/company/messages/page.tsx)
const messaging = useMessaging({ companyId })
- Passes
companyIdfromuser.selectedCompanyIdto scope conversations - Two-panel layout: ConversationList (left, w-80/xl:w-96) | MessageThread (right, flex-1)
- Mobile responsive: toggles between list and thread view via
mobileShowThreadstate handleDeleteChat: Archives + deselects conversation, returns to list on mobile
Job Seeker Messages (app/jobseeker/messages/page.tsx)
const messaging = useMessaging()
- No
companyId— sees all personal conversations - Same layout and behavior as company page
Both pages include:
TWSetPageTitlewith "Messages" title- Accent strip gradient at top
NewConversationModalfor creating conversations- Empty state when no conversation is selected
Navigation Integration
Messages are accessible via direct URL only (/company/messages and /jobseeker/messages). They are intentionally not added to the sidebar navigation.
Unread Badge Hook (Available)
The useUnreadMessageCount hook is ready for use if sidebar integration is desired later:
- Import from
@hooks/useUnreadMessageCount - Returns
{ count: number, refetch: () => void } - Supports optional
companyIdparameter for company-scoped counts - Uses Apollo polling (30s) + WebSocket subscription for real-time updates
Real-Time (WebSocket)
Connection
- Protocol:
graphql-ws(not legacysubscriptions-transport-ws) - Endpoint:
NEXT_PUBLIC_GRAPHQL_WS_URL(default:wss://api.aiqlick.com/graphql) - Auth: Bearer token from
localStorage.getItem("token")viaconnectionParams - Keep-alive: 10,000ms
- Retry: 3 attempts
- Lazy: Connection established only when first subscription is created
Subscriptions
messageReceived (in useMessaging):
- Fires when any message is sent to a conversation the user is a participant in
- The
isSelffield distinguishes own echoed messages from others' messages - On receive: prepends message to list, refetches conversations for updated ordering, auto-marks as read if conversation is active
unreadMessageCountChanged (in useUnreadMessageCount):
- Fires when unread count changes (new message, mark-as-read, etc.)
- WebSocket count takes precedence over polled count for instant badge updates
Client Management
- WebSocket client is created once and stored in a
useRefto prevent re-creation on re-renders - Cleanup: unsubscribes and disposes client on component unmount
- Auth check: subscription only starts when user is logged in
Features
Conversation Management
- Create direct (1:1) and group conversations
- Search conversations by participant name or group title
- Pin conversations to top of list
- Mute conversations (gray badge, mute icon on preview)
- Flag conversations (amber flag icon)
- Archive conversations (moved to archived folder)
- Mark as unread (shows unread dot indicator)
- Delete chat (archives the conversation — no backend delete API)
Group Management (OWNER/ADMIN only)
- Edit group title (inline editing with save)
- Add participants (user search, click to add)
- Remove participants (with confirmation)
- Leave group (with confirmation, deselects conversation)
Messaging
- Send text messages with Enter key
- Reply to messages (shows quoted preview)
- Inline edit messages (own only): Click "Edit" on hover, bubble becomes editable textarea with Save/Cancel. Enter saves, Escape cancels.
- Delete messages (own only, with confirmation)
- File attachments via S3 presigned URL upload
- @ mentions in group conversations (frontend highlighting)
- Inline images in message bubbles with preview
- File cards with icon, filename, and size
- Read receipts: Single tick (sent) / double blue tick (read by others) on own messages
UI/UX
- WhatsApp-style context menus with colored icon backgrounds
- WhatsApp-style info panel sliding in from right
- WhatsApp-style read receipts (tick/double-tick)
- Archived conversations folder at bottom of list
- Responsive design: Mobile shows either list or thread, desktop shows both
- Infinite scroll for both conversations and messages
- Auto-scroll to bottom on new messages
- Hover actions on message bubbles
Patterns & Conventions
| Pattern | Details |
|---|---|
| Fragment organization | graphql/operations/schema/messaging/fragments.ts, re-exported from schema/index.ts |
| Subscription format | Plain strings (not gql tagged) for graphql-ws client |
| Pagination | Cursor-based via fetchMore + updateQuery |
| Error handling | Mutations extract graphQLErrors[0].message, rethrow for caller |
| Ref pattern | WebSocket client and subscription cleanup stored in useRef |
| Sort pattern | Pinned conversations sorted to top via useMemo |
| Message ordering | API returns newest-first, hook reverses for display |
| File uploads | GENERATE_UPLOAD_URL mutation then S3 PUT then MessageAttachmentInput |
| Responsive | hidden lg:flex / lg:hidden for mobile vs desktop panels |
| Barrel exports | components/messaging/index.ts and graphql/operations/messaging/index.ts |
Frontend Limitations
These are current frontend-side constraints that do not require backend changes:
- No sidebar navigation — Messages are accessible via direct URL only (
/company/messages,/jobseeker/messages). TheuseUnreadMessageCounthook is available if sidebar integration is desired later. - No message content search — Only conversation-level search (by participant name / group title). Full-text message search would require a dedicated backend query.
- No
deleteConversationAPI — "Delete chat" usesarchiveConversationunder the hood. The conversation still exists in the archived folder. - Shared media/files limited to loaded messages — The info panel only shows attachments from messages currently in memory, not a full history. A dedicated backend query for conversation attachments would improve this.
- @ mentions are frontend-only — Backend stores plain text; mention highlighting is done via client-side regex. Mentioned users do not receive special notifications.
- Read receipts are approximate — Uses
participant.lastReadAttimestamp compared againstmessage.createdAt. This shows read status but does not indicate which specific message was last read.
Backend Requirements (Not Yet Implemented)
The following features require new backend API support before they can be built on the frontend:
1. Typing Indicators
What: Show "User is typing..." in real-time when another participant is composing a message. Backend needs:
- New mutation:
sendTypingIndicator(conversationId: ID!)— called by frontend when user starts/stops typing - New subscription:
typingIndicator— broadcasts{ conversationId, userId, isTyping }to other participants - Should auto-expire after ~5 seconds of inactivity
2. Message Reactions
What: Allow users to react to messages with emoji (like WhatsApp/Slack). Backend needs:
- New mutation:
addMessageReaction(messageId: ID!, emoji: String!)/removeMessageReaction(messageId: ID!, emoji: String!) - New field on
MessageOutput:reactions: [{ emoji: String, users: [{ userId, firstName }], count: Int }] - Include reactions in
messageReceivedsubscription updates - Include reactions in the
MESSAGE_FIELDSfragment
3. Delete Conversation (Hard Delete)
What: Permanently remove a conversation from the user's view instead of just archiving. Backend needs:
- New mutation:
deleteConversation(conversationId: ID!)— marks the conversation as permanently hidden for the calling user - Different from archive: should not appear in any query results for that user
4. Message Search
What: Full-text search within message content across all conversations. Backend needs:
- New query:
searchMessages(query: String!, companyId: ID, limit: Int, cursor: String): MessageSearchOutput MessageSearchOutput:{ results: [{ message: MessageOutput, conversationId, conversationTitle }], hasMore, nextCursor }- Should search message content with basic text matching or full-text search
5. Conversation Attachments Query
What: Fetch all shared media/files for a conversation (not limited to loaded messages). Backend needs:
- New query:
getConversationAttachments(conversationId: ID!, type: String, limit: Int, cursor: String): AttachmentListOutput AttachmentListOutput:{ attachments: [MessageAttachmentOutput], hasMore, nextCursor }- Optional
typefilter:"image","file", or all - Used by the info panel's Media and Files tabs
6. Mention Notifications
What: When a user is @mentioned in a group message, they receive a push notification or in-app notification. Backend needs:
- Parse message content for
@FirstName LastNamepatterns on send - Match mentioned names to participant user IDs
- Create notification entries for mentioned users (or a special push notification)
- Optional: Add
mentionedUserIds: [ID]field toSendMessageInputso frontend can pass resolved user IDs
7. Voice/Video Calls
What: In-app calling between conversation participants. Backend needs:
- WebRTC signaling server or integration with a third-party service (Twilio, Agora, etc.)
- New mutations:
initiateCall(conversationId: ID!, type: VOICE|VIDEO),acceptCall,rejectCall,endCall - New subscription:
callStateChanged— ring, accept, reject, end events - ICE candidate exchange via WebSocket
8. Online/Last Seen Status
What: Show whether a user is currently online or their last active timestamp. Backend needs:
- Track user online status (heartbeat-based or WebSocket connection tracking)
- New query field on
ParticipantOutput:isOnline: Boolean,lastSeenAt: DateTime - Optional subscription:
userPresenceChangedfor real-time online/offline indicators
9. Message Pinning (Star Message)
What: Allow users to pin/star individual messages within a conversation (like WhatsApp starred messages). Currently only conversation-level pinning exists (pinConversation); this adds message-level pinning.
Backend needs:
- New mutations:
pinMessage(messageId: ID!)/unpinMessage(messageId: ID!) - New field on
MessageOutput:isPinned: Boolean(or per-user:isPinnedByMe: Boolean) - New query:
getPinnedMessages(conversationId: ID!, limit: Int, cursor: String): MessageListOutput— fetch all pinned messages in a conversation - Include
isPinnedin theMESSAGE_FIELDSfragment andmessageReceivedsubscription - Decision: pinning can be per-user (only visible to the user who pinned, like WhatsApp starred) or global (visible to all participants, like Slack pinned messages)
10. Message Flagging (Bookmark Message)
What: Allow users to flag/bookmark individual messages for later reference. Currently only conversation-level flagging exists (flagConversation); this adds message-level flagging.
Backend needs:
- New mutations:
flagMessage(messageId: ID!)/unflagMessage(messageId: ID!) - New field on
MessageOutput:isFlaggedByMe: Boolean(per-user — each user has their own flagged messages) - New query:
getFlaggedMessages(companyId: ID, limit: Int, cursor: String): MessageListOutput— fetch all flagged messages across conversations for the current user - Include
isFlaggedByMein theMESSAGE_FIELDSfragment - Flagged messages should be accessible from a dedicated "Flagged" or "Saved" section in the messaging UI