Skip to main content

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

  1. Architecture Overview
  2. File Structure
  3. TypeScript Types
  4. GraphQL Layer
  5. Hooks
  6. Components
  7. Pages
  8. Navigation Integration
  9. Real-Time (WebSocket)
  10. Features
  11. Patterns & Conventions
  12. Frontend Limitations
  13. 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 deleteConversation mutation — "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)

TypeValues
ConversationType"DIRECT", "GROUP"
ConversationParticipantRole"OWNER", "ADMIN", "MEMBER"
MessageStatus"SENT", "DELIVERED", "DELETED"

Core Interfaces

InterfaceKey Fields
ParticipantuserId, role, isMuted, isPinned, isArchived, isFlagged, isMarkedUnread, firstName, lastName, profileImageUrl
Conversationid, type, title?, companyId?, lastMessageAt, lastMessageText, unreadCount, participants[]
Messageid, senderId, content?, status, isEdited, replyTo?, attachments?, isSelf?, sender info fields
MessageAttachmentid, filename, contentType, sizeBytes, objectPath, downloadUrl?
ConversationListOutputconversations[], hasMore, nextCursor?
MessageListOutputmessages[], hasMore, nextCursor?

Input Types

InterfacePurpose
CreateDirectConversationInputrecipientId, optional companyId
CreateGroupConversationInputtitle, participantIds[], optional companyId
SendMessageInputconversationId, content?, replyToId?, attachments?[]
MessageAttachmentInputfilename, contentType, sizeBytes, objectPath
GetConversationsInputcursor?, limit?, companyId?, type?, search?
GetMessagesInputcursor?, limit?
UpdateGroupConversationInputconversationId, title
AddGroupParticipantsInputconversationId, userIds[]
RemoveGroupParticipantInputconversationId, userId

GraphQL Layer

Fragments (graphql/operations/schema/messaging/fragments.ts)

FragmentOn TypeNotes
PARTICIPANT_FIELDSParticipantOutput17 fields incl. roles, flags, names
CONVERSATION_FIELDSConversationOutputEmbeds ParticipantFields
MESSAGE_FIELDSMessageOutput15 fields + attachments array
MESSAGE_WITH_REPLY_FIELDSMessageOutputExtends MessageFields with nested replyTo

Re-exported from graphql/operations/schema/index.ts.

Queries (graphql/operations/messaging/queries.ts)

QueryVariablesReturns
MY_CONVERSATIONSGetConversationsInputConversationListOutput
GET_CONVERSATIONid: ID!ConversationOutput
GET_MESSAGESconversationId: ID!, GetMessagesInputMessageListOutput
UNREAD_MESSAGE_COUNTcompanyId?: IDUnreadMessageCount

Mutations (graphql/operations/messaging/mutations.ts)

CategoryMutations
Conversation CreationCREATE_DIRECT_CONVERSATION, CREATE_GROUP_CONVERSATION
MessagesSEND_MESSAGE, EDIT_MESSAGE, DELETE_MESSAGE
Read StateMARK_CONVERSATION_AS_READ, MARK_CONVERSATION_AS_UNREAD
Conversation SettingsMUTE_CONVERSATION, PIN_CONVERSATION, ARCHIVE_CONVERSATION, FLAG_CONVERSATION
Group ManagementUPDATE_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:

SubscriptionFieldsNotes
MESSAGE_RECEIVED_SUBSCRIPTIONFull message fields + isSelf, attachments, replyToReal-time message delivery
UNREAD_MESSAGE_COUNT_CHANGED_SUBSCRIPTIONcount, companyIdBadge 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
  • isSelf flag from subscription prevents duplicate message rendering
  • Conversation creation returns the new conversationId and 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-network fetch 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 === true are 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 isMarkedUnread without 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_SHARING query for member search.
    • Media: 3-column grid of shared images from loaded messages
    • Files: List with file icons, names, sizes, dates
  • 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
  • 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.lastReadAt to 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 LastName into the text
  • Placeholder changes to "Type a message... (@ to mention)" when participants are provided

File upload:

  • Uses S3 presigned URLs via GENERATE_UPLOAD_URL mutation
  • Shows upload progress and file previews
  • Files can be removed before sending

Keyboard:

  • Enter sends the message
  • Shift+Enter inserts 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 companyId from user.selectedCompanyId to 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 mobileShowThread state
  • 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:

  • TWSetPageTitle with "Messages" title
  • Accent strip gradient at top
  • NewConversationModal for creating conversations
  • Empty state when no conversation is selected

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 companyId parameter for company-scoped counts
  • Uses Apollo polling (30s) + WebSocket subscription for real-time updates

Real-Time (WebSocket)

Connection

  • Protocol: graphql-ws (not legacy subscriptions-transport-ws)
  • Endpoint: NEXT_PUBLIC_GRAPHQL_WS_URL (default: wss://api.aiqlick.com/graphql)
  • Auth: Bearer token from localStorage.getItem("token") via connectionParams
  • 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 isSelf field 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 useRef to 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

PatternDetails
Fragment organizationgraphql/operations/schema/messaging/fragments.ts, re-exported from schema/index.ts
Subscription formatPlain strings (not gql tagged) for graphql-ws client
PaginationCursor-based via fetchMore + updateQuery
Error handlingMutations extract graphQLErrors[0].message, rethrow for caller
Ref patternWebSocket client and subscription cleanup stored in useRef
Sort patternPinned conversations sorted to top via useMemo
Message orderingAPI returns newest-first, hook reverses for display
File uploadsGENERATE_UPLOAD_URL mutation then S3 PUT then MessageAttachmentInput
Responsivehidden lg:flex / lg:hidden for mobile vs desktop panels
Barrel exportscomponents/messaging/index.ts and graphql/operations/messaging/index.ts

Frontend Limitations

These are current frontend-side constraints that do not require backend changes:

  1. No sidebar navigation — Messages are accessible via direct URL only (/company/messages, /jobseeker/messages). The useUnreadMessageCount hook is available if sidebar integration is desired later.
  2. No message content search — Only conversation-level search (by participant name / group title). Full-text message search would require a dedicated backend query.
  3. No deleteConversation API — "Delete chat" uses archiveConversation under the hood. The conversation still exists in the archived folder.
  4. 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.
  5. @ mentions are frontend-only — Backend stores plain text; mention highlighting is done via client-side regex. Mentioned users do not receive special notifications.
  6. Read receipts are approximate — Uses participant.lastReadAt timestamp compared against message.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 messageReceived subscription updates
  • Include reactions in the MESSAGE_FIELDS fragment

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

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 type filter: "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 LastName patterns 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 to SendMessageInput so 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: userPresenceChanged for 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 isPinned in the MESSAGE_FIELDS fragment and messageReceived subscription
  • 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 isFlaggedByMe in the MESSAGE_FIELDS fragment
  • Flagged messages should be accessible from a dedicated "Flagged" or "Saved" section in the messaging UI