channels
Key capabilities:
- Per‑channel message feeds (polling today, socket‑friendly design).
- Public and private channels scoped to an organisation.
- Threaded replies on each message.
- Reactions, media attachments and mentions on messages and replies.
Channels are exposed in the UI inside the main Noticeboard experience (NoticeBoardModule) under the Channels group.
System Overview Diagram
Section titled “System Overview Diagram”NoticeBoardModule │ ├── VerticalTabber │ ├── Noticeboard tabs (boards) │ ├── Channel tabs (one per Channel) │ └── Celebrations tab │ └── ChannelView (for active channel)
ChannelView │ ├── useQuery(getChannelById, getChannelMessages, getChannelMembers) │ │ │ └── channels.ts │ │ │ └── /channels API │ ├── ChannelMessageComponent │ ├── useChannelMessageReplies() │ ├── useChannelMessageSender() │ └── useChannelMessagePermissions() │ └── ChannelMembersPanel (optional members view)Core Types & Data Model
Section titled “Core Types & Data Model”Defined primarily in src/services/channels.ts.
Channel
Section titled “Channel”-
Interface
channelId: numbername: stringdescription?: string | nullisPrivate: booleancreatedAt: stringupdatedAt?: stringmemberCount?: numbercreatedBy?: number | nullrole?: string | null(membership role, when relevant)
-
Mapping
- Implemented in
mapChannel(raw: any): Channel. - Accepts multiple raw shapes:
channelId/id/channel_id/channelIDcreatedBy/created_byrole/membership_roleisPrivate/is_private/private
- Ensures
channelIdis numeric and filters invalid entries ingetMyChannels.
- Implemented in
ArchivedChannel
Section titled “ArchivedChannel”- Extends
Channel:archivedAt?: stringarchivedByName?: string | null
- Normalised via
mapArchivedChannel:- Reads
archivedAt/archived_at/deletedAt/deleted_at. - Reads
archivedByName/archived_by_name/archivedBy/archived_by/deletedBy/deleted_by.
- Reads
ChannelMember
Section titled “ChannelMember”-
Interface:
userId: numberfirstName: stringlastName: stringemail: stringprofileImgUrl?: string | nulljoinedAt?: string
-
Enrichment:
getChannelMembers(channelId)fetches:- channel members from
/channels/{id}/members - all org users via
getAllOrgUsers()
- channel members from
- Joins by user id and prefers org‑user data for names, email, and profile image.
Messages
Section titled “Messages”-
ChannelMessage
messageId: numberchannelId: numbersenderId: numbersenderFirstName?: stringsenderLastName?: stringcontentText?: stringcontentJson?: any(rich‑text content)createdAt: stringupdatedAt?: stringmediaUrl?: string | nullmedia?: ChannelReplyMedia[]replyToMessageId?: number | nullreactions?: ChannelMessageReaction[] | Record<string, number>userReaction?: string | nullisPinned?: booleanpinnedAt?: string | null
-
ChannelReplyMedia
media_id?: numbercategory?: stringfile_url?: stringfileUrl?: string
-
ChannelMessageReaction
reaction_type: stringcount?: numberuser_ids?: number[]
-
Normalisation
mapChannelMessage(raw: any): ChannelMessage:- Normalises ids, sender fields and timestamps (
normalizeDate). - Normalises
mediatoChannelReplyMedia[]. - Normalises
reactionswithnormalizeReactions, which:- Accepts either an object map or an array of reaction rows.
- Returns
Record<string, number> | undefined.
- Extracts current user’s reaction via
extractUserReaction.
- Normalises ids, sender fields and timestamps (
Replies
Section titled “Replies”-
ChannelReply
replyId: numbermessageId: numbersenderId: numbersenderFirstName?,senderLastName?contentText?: stringcontentJson?: anycreatedAt: stringupdatedAt?: stringmediaUrl?: string | nullmedia?: ChannelReplyMedia[]reactions?: ChannelReplyReaction[] | Record<string, number>userReaction?: string | nullparentReplyId?: number | null(reply‑to‑reply support)
-
ChannelReplyReaction
- Same structure as
ChannelMessageReaction.
- Same structure as
-
ChannelMessageThread
message: ChannelMessagereplies: ChannelReply[]- Built by
getMessageThread(channelId, messageId).
API Surface (src/services/channels.ts)
Section titled “API Surface (src/services/channels.ts)”Channel Management
Section titled “Channel Management”-
Create channel
- Function:
createChannel(payload: CreateChannelPayload) - Endpoint:
POST /channels/ - Payload:
name: stringdescription?: stringis_private: booleaninvite_user_ids?: number[]
- Function:
-
Get my channels
- Function:
getMyChannels(): Promise<Channel[]> - Endpoint:
GET /channels/ - Behaviour:
- Reads
response.data.bodyorresponse.dataviaextractArrayData. - Maps via
mapChanneland removes invalidchannelIds.
- Reads
- Function:
-
Get channel by id
- Function:
getChannelById(channelId: number) - Endpoint:
GET /channels/{channelId} - Returns original response with
.datareplaced by a mappedChannel.
- Function:
-
Update channel
- Function:
updateChannel(channelId: number, payload: UpdateChannelPayload) - Endpoint:
PATCH /channels/{channelId} - Payload:
name?,description?,is_private?.
- Function:
-
Soft delete (archive) channel
- Function:
deleteChannel(channelId: number) - Endpoint:
DELETE /channels/{channelId}
- Function:
-
List archived channels
- Function:
getArchivedChannels(): Promise<ArchivedChannel[]> - Endpoint:
GET /channels/deleted - Behaviour:
- Accepts response in multiple shapes:
- raw array
- object with
.channels - object with
.data/.bodyarrays
- Maps via
mapArchivedChannel.
- Accepts response in multiple shapes:
- Function:
-
Restore archived channel
- Function:
restoreChannel(channelId: number) - Endpoint:
POST /channels/{channelId}/recover
- Function:
-
Permanently delete archived channel
- Function:
permanentlyDeleteChannel(channelId: number) - Endpoint:
DELETE /channels/{channelId}/permanent
- Function:
Members
Section titled “Members”-
Get channel members
- Function:
getChannelMembers(channelId: number): Promise<ChannelMember[]> - Endpoint:
GET /channels/{channelId}/members - Also calls
getAllOrgUsers()to build a user lookup map by id.
- Function:
-
Invite users to channel
- Function:
inviteUsersToChannel(channelId: number, userIds: number[]) - Endpoint:
POST /channels/{channelId}/invite - Body:
{ user_ids: number[] }
- Function:
-
Remove member from channel
- Function:
removeMemberFromChannel(channelId: number, userId: number) - Endpoint:
DELETE /channels/{channelId}/members/{userId}
- Function:
Messages
Section titled “Messages”-
Get messages
- Function:
getChannelMessages(channelId: number, params?: { limit?: number; before?: string }) - Endpoint:
GET /channels/{channelId}/messages?limit=…&before=… - Returns:
ChannelMessage[](mapped).
- Function:
-
Post message
- Function:
postChannelMessage(channelId: number, payload: PostMessagePayload) - Endpoint:
POST /channels/{channelId}/messages - Payload:
content_text?: stringcontent_json?: any
- Function:
-
Edit message
- Function:
patchChannelMessage(channelId, messageId, payload) - Endpoint:
PATCH /channels/{channelId}/messages/{messageId}
- Function:
-
Delete message
- Function:
deleteChannelMessage(channelId, messageId) - Endpoint:
DELETE /channels/{channelId}/messages/{messageId}
- Function:
-
Upload message media
- Function:
uploadMessageMedia(channelId, messageId, imageFile) - Endpoint:
POST /channels/{channelId}/messages/{messageId}/media - Body:
FormDatawithimagekey.
- Function:
Replies
Section titled “Replies”-
Get replies for a message
- Function:
getMessageReplies(channelId, messageId): Promise<ChannelReply[]> - Endpoint:
GET /channels/{channelId}/messages/{messageId}/replies
- Function:
-
Post reply
- Function:
postMessageReply(channelId, messageId, payload: PostReplyPayload) - Endpoint:
POST /channels/{channelId}/messages/{messageId}/replies - Payload:
content_text?: stringcontent_json?: anyparent_reply_id?: number | null
- Function:
-
Delete reply
- Function:
deleteMessageReply(channelId, messageId, replyId) - Endpoint:
DELETE /channels/{channelId}/messages/{messageId}/replies/{replyId}
- Function:
-
Edit reply
- Function:
patchMessageReply(channelId, messageId, replyId, payload) - Endpoint:
PATCH /channels/{channelId}/messages/{messageId}/replies/{replyId}
- Function:
-
Upload reply media
- Function:
uploadReplyMedia(channelId, messageId, replyId, imageFile) - Endpoint:
POST /channels/{channelId}/messages/{messageId}/replies/{replyId}/media
- Function:
-
Get full message thread
- Function:
getMessageThread(channelId, messageId): Promise<ChannelMessageThread | null> - Endpoint:
GET /channels/{channelId}/messages/{messageId}/thread - Returns: a single message plus its replies.
- Function:
Reactions
Section titled “Reactions”-
React to a message
- Function:
reactToMessage(channelId, messageId, payload: ReactionPayload) - Endpoint:
POST /channels/{channelId}/messages/{messageId}/reactions
- Function:
-
Update own message reaction
- Function:
patchMessageReaction(channelId, messageId, payload) - Endpoint:
PATCH /channels/{channelId}/messages/{messageId}/reaction
- Function:
-
React to a reply
- Function:
reactToReply(channelId, messageId, replyId, payload, hasExistingReaction = false) - Endpoints:
- When
hasExistingReaction === true, attemptsPATCH /channels/{channelId}/replies/{replyId}/reaction. - If API responds with “Reaction not found”, falls back to:
POST /channels/{channelId}/messages/{messageId}/replies/{replyId}/reactions.
- Otherwise uses
POSTdirectly for first reaction.
- When
- Function:
-
Reaction users model
MessageReactionUser:userId,reactionType,firstName,lastName,email,profileImgUrl,reactedAt.
MessageReactionsUsersResponse:summary: Record<string, { count: number; mine?: boolean }>usersByReaction: Record<string, MessageReactionUser[]>totalReactions: number
normalizeReactionsUsersBody(raw)handles multiple body shapes and returns this structure.
-
Fetch message reactions + users
getMessageReactionsUsersSummary(channelId, messageId)GET /channels/{channelId}/messages/{messageId}/reactions/users
getReplyReactionsUsersSummary(channelId, messageId, replyId)GET /channels/{channelId}/messages/{messageId}/replies/{replyId}/reactions/users
- Convenience lists:
getMessageReactionUsers(channelId, messageId)getReplyReactionUsers(channelId, messageId, replyId)
When to Use Channels APIs
Section titled “When to Use Channels APIs”Use src/services/channels.ts when:
- Working inside the Noticeboard/Channels UI.
- Fetching or mutating channels, members, messages, replies, or reactions.
- You need normalised models (consistent ids, dates, and reaction shapes).
Use other services when:
- You need high‑level org layout (e.g. noticeboard layout, tab groups) – see
src/services/org.tsforgetOrgNoticeBoardLayout/saveOrgNoticeBoardLayout. - You are working with celebrations or other modules that only link into channels UI indirectly.
Frontend Usage
Section titled “Frontend Usage”Noticeboard & Channels Container (src/modules/NoticeBoard/index.tsx)
Section titled “Noticeboard & Channels Container (src/modules/NoticeBoard/index.tsx)”- Uses React Query to load:
boards(noticeboard).channelsviagetMyChannels.- Org‑level layout via
getOrgNoticeBoardLayout.
- Combines boards + channels into a nested
VerticalTabber:- Tab groups:
"noticeboard"and"channels"(plus"celebrations"). - Channels group is always visible and labelled Channels.
- Tab groups:
- Channel tabs:
- Built from
sortedChannels, which applies persistedchannelOrder. - Each tab key:
channel-{channelId}. - Channel owner (creator) gets:
- Channel details action.
- Delete channel (archive) action.
- Tab content is a
ChannelViewwith:channelIdisAdmincanManageMembers(true for channel owner).
- Built from
Channel Creation (NewChannelForm)
Section titled “Channel Creation (NewChannelForm)”File: src/modules/NoticeBoard/components/NewChannelForm.tsx
- Fetches all org users (
getAllOrgUsers) via React Query. - Captures:
nameisPrivateflag- Optional
selectedMembersfor private channels.
- Submit behaviour:
- Builds a
CreateChannelPayload:- If public:
invite_user_ids= all org user ids.
- If private:
invite_user_ids= selected member ids.
- If public:
- Calls
props.onSubmit(payload).
- Builds a
- The parent (
NoticeBoardModule) wires this tocreateChannelMutation:createChannel(payload)→ invalidates["channels"]query and shows toasts.
Channel View (ChannelView)
Section titled “Channel View (ChannelView)”File: src/modules/NoticeBoard/components/ChannelView.tsx
-
Props:
channelId: numberisAdmin: booleancanManageMembers?: boolean
-
Data fetching:
getChannelById(channelId)→ channel header info.getChannelMessages(channelId, { limit: 50 }):- Polls every 30 seconds (
refetchInterval).
- Polls every 30 seconds (
getChannelMembers(channelId)to power mentions.
-
Mentions:
- Builds
channelMentionsfromchannelMembersand passes intoRichTextEditor.
- Builds
-
Input behaviour:
- Collapsed “Write a message…” CTA by default.
- Expands to rich‑text input + attachment bar on click.
- Uses
postMessageMutation:- First
postChannelMessagewithcontent_text. - Then, if there is
imageFile, callsuploadMessageMedia. - On success: invalidates
["channel-messages", channelId], clears input, and toasts.
- First
-
Messages list:
- Renders
ChannelMessageComponentfor eachChannelMessage.
- Renders
-
Members view:
- Toggles between Messages and Members (via
showMembers). - Members panel uses
ChannelMembersPanel.
- Toggles between Messages and Members (via
Message Replies (useChannelMessageReplies)
Section titled “Message Replies (useChannelMessageReplies)”File: src/modules/NoticeBoard/hooks/useChannelMessageReplies.ts
- Fetches:
- Org users (for author info).
ChannelReply[]viagetMessageReplies.- Polls every 30 seconds.
- Maps backend replies into UI
Replyobjects:- Computes
posterName,posterProfileImgUrl,reactions(as a map),userReaction, andisEdited.
- Computes
- Exposes:
repliespostReplyMutation(simple text reply)updateReplyMutationdeleteReplyMutationpostReplyWithMedia(contentText, imageFiles[]):- Creates reply, extracts
replyId, uploads all images viauploadReplyMedia.
- Creates reply, extracts
getReplyAuthorLabel(reply)(resolves “You” vs name based on session user).
Message Sender Info (useChannelMessageSender)
Section titled “Message Sender Info (useChannelMessageSender)”File: src/modules/NoticeBoard/hooks/useChannelMessageSender.ts
- Given a
ChannelMessage, fetches org users and returnssenderInfo:firstName,lastName,profileImgUrl, and anamefallback.
Message Permissions (useChannelMessagePermissions)
Section titled “Message Permissions (useChannelMessagePermissions)”File: src/modules/NoticeBoard/hooks/useChannelMessagePermissions.ts
- Reads the current user from
next-authsession. - Allows editing if:
- Role is
super_adminoradmin, or rolesarray contains either of those.
- Role is
Layout & Sorting
Section titled “Layout & Sorting”- Channel ordering is managed by
NoticeBoardModule:- State:
NoticeBoardLayoutState:channelOrder: number[]tabGroupOverrides: Record<string, "noticeboard" | "channels">
- Persisted per org via:
getOrgNoticeBoardLayout(orgId)saveOrgNoticeBoardLayout(orgId, { channelOrder, tabGroupOverrides }).
- State:
- Sorting UI:
ChannelsSettingsreceiveschannelSortItemsand calls:onSaveChannelSort(channelIds)→saveChannelSortOrder.onResetChannelSort()→ clears persisted order.
Implementation Notes & Gotchas
Section titled “Implementation Notes & Gotchas”-
API shape tolerance:
- The service layer is defensive:
- Accepts multiple id and field names for channels, messages, replies, and reactions.
- When adding new endpoints or fields, prefer reusing existing normalisers (
mapChannel,mapChannelMessage,mapChannelReply,normalizeReactions,normalizeReactionsUsersBody).
- The service layer is defensive:
-
Dates & timezones:
normalizeDateensures date strings contain a"T"and appends"Z"when needed.- When adding new timestamp fields, run them through
normalizeDatefor consistency.
-
Reactions:
- Callers should treat
reactionsas a plainRecord<string, number>in the UI. - Use
userReactionfrom the normalised model to highlight the current user’s reaction.
- Callers should treat
-
Real‑time:
- Both messages and replies use React Query polling (30s) for now.
- Any future websocket/subscription implementation should update or invalidate the same query keys:
["channel-messages", channelId]["channel-message-replies", channelId, messageId]
Typical Flows
Section titled “Typical Flows”Example: Posting a Channel Message
Section titled “Example: Posting a Channel Message”User types message and clicks "Send" in ChannelView ↓ChannelView.postMessageMutation ↓postChannelMessage(channelId, { content_text }) ↓POST /channels/{channelId}/messages ↓onSuccess → React Query invalidates: ["channel-messages", channelId] ↓ChannelMessageComponent list re-renders with new messageExample: Replying to a Message
Section titled “Example: Replying to a Message”User opens replies for a message and submits a reply ↓useChannelMessageReplies.postReplyMutation / postReplyWithMedia ↓postMessageReply(channelId, messageId, payload) ↓POST /channels/{channelId}/messages/{messageId}/replies ↓React Query invalidates: ["channel-message-replies", channelId, messageId] ↓Replies list re-renders with new replyExample: Reacting to a Message
Section titled “Example: Reacting to a Message”User clicks an emoji reaction on a message ↓ChannelMessageComponent → reactToMessage(channelId, messageId, { reaction_type }) ↓POST /channels/{channelId}/messages/{messageId}/reactions ↓Service normalises reactions via normalizeReactions / extractUserReaction ↓UI treats reactions as Record<string, number> + userReaction for highlighting```---
### File Structure (Channels)
```textsrc/services│└── channels.ts // All channel, member, message, reply, reaction APIs + normalisers
src/modules/NoticeBoard│├── index.tsx // NoticeBoardModule: boards + channels + celebrations├── components│ ├── ChannelView.tsx // Per-channel view (header, input, messages, members panel)│ ├── ChannelMessage.tsx // Single message + replies + reactions UI│ ├── ChannelMembersPanel.tsx // Members list for a channel│ ├── ChannelMembersModal.tsx // (If present) modal wrapper for members│ ├── ChannelsSettings.tsx // Channel sort / layout settings│ ├── ArchivedChannels.tsx // Archived (soft-deleted) channels│ ├── NewChannelForm.tsx // Create channel form│ └── ChannelDetailsModal.tsx // Channel metadata/settings modal│├── hooks│ ├── useChannelMessageReplies.ts // Fetch + mutate replies for a message│ ├── useChannelMessageSender.ts // Resolve sender info for a message│ ├── useChannelMessagePermissions.ts // Admin/super-admin edit permissions│ └── useChannelMessageReactions.ts // (If present) reaction-related helpers│└── styles ├── channelView.module.css // Layout for ChannelView ├── channelMessage.module.css // Styles for messages/replies └── noticeBoard.module.css // Shared Noticeboard + Channels tab layout