Skip to content

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.


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)

Defined primarily in src/services/channels.ts.

  • Interface

    • channelId: number
    • name: string
    • description?: string | null
    • isPrivate: boolean
    • createdAt: string
    • updatedAt?: string
    • memberCount?: number
    • createdBy?: number | null
    • role?: string | null (membership role, when relevant)
  • Mapping

    • Implemented in mapChannel(raw: any): Channel.
    • Accepts multiple raw shapes:
      • channelId / id / channel_id / channelID
      • createdBy / created_by
      • role / membership_role
      • isPrivate / is_private / private
    • Ensures channelId is numeric and filters invalid entries in getMyChannels.
  • Extends Channel:
    • archivedAt?: string
    • archivedByName?: string | null
  • Normalised via mapArchivedChannel:
    • Reads archivedAt / archived_at / deletedAt / deleted_at.
    • Reads archivedByName / archived_by_name / archivedBy / archived_by / deletedBy / deleted_by.
  • Interface:

    • userId: number
    • firstName: string
    • lastName: string
    • email: string
    • profileImgUrl?: string | null
    • joinedAt?: string
  • Enrichment:

    • getChannelMembers(channelId) fetches:
      • channel members from /channels/{id}/members
      • all org users via getAllOrgUsers()
    • Joins by user id and prefers org‑user data for names, email, and profile image.
  • ChannelMessage

    • messageId: number
    • channelId: number
    • senderId: number
    • senderFirstName?: string
    • senderLastName?: string
    • contentText?: string
    • contentJson?: any (rich‑text content)
    • createdAt: string
    • updatedAt?: string
    • mediaUrl?: string | null
    • media?: ChannelReplyMedia[]
    • replyToMessageId?: number | null
    • reactions?: ChannelMessageReaction[] | Record<string, number>
    • userReaction?: string | null
    • isPinned?: boolean
    • pinnedAt?: string | null
  • ChannelReplyMedia

    • media_id?: number
    • category?: string
    • file_url?: string
    • fileUrl?: string
  • ChannelMessageReaction

    • reaction_type: string
    • count?: number
    • user_ids?: number[]
  • Normalisation

    • mapChannelMessage(raw: any): ChannelMessage:
      • Normalises ids, sender fields and timestamps (normalizeDate).
      • Normalises media to ChannelReplyMedia[].
      • Normalises reactions with normalizeReactions, which:
        • Accepts either an object map or an array of reaction rows.
        • Returns Record<string, number> | undefined.
      • Extracts current user’s reaction via extractUserReaction.
  • ChannelReply

    • replyId: number
    • messageId: number
    • senderId: number
    • senderFirstName?, senderLastName?
    • contentText?: string
    • contentJson?: any
    • createdAt: string
    • updatedAt?: string
    • mediaUrl?: string | null
    • media?: ChannelReplyMedia[]
    • reactions?: ChannelReplyReaction[] | Record<string, number>
    • userReaction?: string | null
    • parentReplyId?: number | null (reply‑to‑reply support)
  • ChannelReplyReaction

    • Same structure as ChannelMessageReaction.
  • ChannelMessageThread

    • message: ChannelMessage
    • replies: ChannelReply[]
    • Built by getMessageThread(channelId, messageId).

  • Create channel

    • Function: createChannel(payload: CreateChannelPayload)
    • Endpoint: POST /channels/
    • Payload:
      • name: string
      • description?: string
      • is_private: boolean
      • invite_user_ids?: number[]
  • Get my channels

    • Function: getMyChannels(): Promise<Channel[]>
    • Endpoint: GET /channels/
    • Behaviour:
      • Reads response.data.body or response.data via extractArrayData.
      • Maps via mapChannel and removes invalid channelIds.
  • Get channel by id

    • Function: getChannelById(channelId: number)
    • Endpoint: GET /channels/{channelId}
    • Returns original response with .data replaced by a mapped Channel.
  • Update channel

    • Function: updateChannel(channelId: number, payload: UpdateChannelPayload)
    • Endpoint: PATCH /channels/{channelId}
    • Payload:
      • name?, description?, is_private?.
  • Soft delete (archive) channel

    • Function: deleteChannel(channelId: number)
    • Endpoint: DELETE /channels/{channelId}
  • 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/.body arrays
      • Maps via mapArchivedChannel.
  • Restore archived channel

    • Function: restoreChannel(channelId: number)
    • Endpoint: POST /channels/{channelId}/recover
  • Permanently delete archived channel

    • Function: permanentlyDeleteChannel(channelId: number)
    • Endpoint: DELETE /channels/{channelId}/permanent
  • 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.
  • Invite users to channel

    • Function: inviteUsersToChannel(channelId: number, userIds: number[])
    • Endpoint: POST /channels/{channelId}/invite
    • Body: { user_ids: number[] }
  • Remove member from channel

    • Function: removeMemberFromChannel(channelId: number, userId: number)
    • Endpoint: DELETE /channels/{channelId}/members/{userId}
  • Get messages

    • Function: getChannelMessages(channelId: number, params?: { limit?: number; before?: string })
    • Endpoint: GET /channels/{channelId}/messages?limit=…&before=…
    • Returns: ChannelMessage[] (mapped).
  • Post message

    • Function: postChannelMessage(channelId: number, payload: PostMessagePayload)
    • Endpoint: POST /channels/{channelId}/messages
    • Payload:
      • content_text?: string
      • content_json?: any
  • Edit message

    • Function: patchChannelMessage(channelId, messageId, payload)
    • Endpoint: PATCH /channels/{channelId}/messages/{messageId}
  • Delete message

    • Function: deleteChannelMessage(channelId, messageId)
    • Endpoint: DELETE /channels/{channelId}/messages/{messageId}
  • Upload message media

    • Function: uploadMessageMedia(channelId, messageId, imageFile)
    • Endpoint: POST /channels/{channelId}/messages/{messageId}/media
    • Body: FormData with image key.
  • Get replies for a message

    • Function: getMessageReplies(channelId, messageId): Promise<ChannelReply[]>
    • Endpoint: GET /channels/{channelId}/messages/{messageId}/replies
  • Post reply

    • Function: postMessageReply(channelId, messageId, payload: PostReplyPayload)
    • Endpoint: POST /channels/{channelId}/messages/{messageId}/replies
    • Payload:
      • content_text?: string
      • content_json?: any
      • parent_reply_id?: number | null
  • Delete reply

    • Function: deleteMessageReply(channelId, messageId, replyId)
    • Endpoint: DELETE /channels/{channelId}/messages/{messageId}/replies/{replyId}
  • Edit reply

    • Function: patchMessageReply(channelId, messageId, replyId, payload)
    • Endpoint: PATCH /channels/{channelId}/messages/{messageId}/replies/{replyId}
  • Upload reply media

    • Function: uploadReplyMedia(channelId, messageId, replyId, imageFile)
    • Endpoint: POST /channels/{channelId}/messages/{messageId}/replies/{replyId}/media
  • 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.
  • React to a message

    • Function: reactToMessage(channelId, messageId, payload: ReactionPayload)
    • Endpoint: POST /channels/{channelId}/messages/{messageId}/reactions
  • Update own message reaction

    • Function: patchMessageReaction(channelId, messageId, payload)
    • Endpoint: PATCH /channels/{channelId}/messages/{messageId}/reaction
  • React to a reply

    • Function: reactToReply(channelId, messageId, replyId, payload, hasExistingReaction = false)
    • Endpoints:
      • When hasExistingReaction === true, attempts PATCH /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 POST directly for first reaction.
  • 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)

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.ts for getOrgNoticeBoardLayout / saveOrgNoticeBoardLayout.
  • You are working with celebrations or other modules that only link into channels UI indirectly.

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).
    • channels via getMyChannels.
    • 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.
  • Channel tabs:
    • Built from sortedChannels, which applies persisted channelOrder.
    • Each tab key: channel-{channelId}.
    • Channel owner (creator) gets:
      • Channel details action.
      • Delete channel (archive) action.
    • Tab content is a ChannelView with:
      • channelId
      • isAdmin
      • canManageMembers (true for channel owner).

File: src/modules/NoticeBoard/components/NewChannelForm.tsx

  • Fetches all org users (getAllOrgUsers) via React Query.
  • Captures:
    • name
    • isPrivate flag
    • Optional selectedMembers for private channels.
  • Submit behaviour:
    • Builds a CreateChannelPayload:
      • If public:
        • invite_user_ids = all org user ids.
      • If private:
        • invite_user_ids = selected member ids.
    • Calls props.onSubmit(payload).
  • The parent (NoticeBoardModule) wires this to createChannelMutation:
    • createChannel(payload) → invalidates ["channels"] query and shows toasts.

File: src/modules/NoticeBoard/components/ChannelView.tsx

  • Props:

    • channelId: number
    • isAdmin: boolean
    • canManageMembers?: boolean
  • Data fetching:

    • getChannelById(channelId) → channel header info.
    • getChannelMessages(channelId, { limit: 50 }):
      • Polls every 30 seconds (refetchInterval).
    • getChannelMembers(channelId) to power mentions.
  • Mentions:

    • Builds channelMentions from channelMembers and passes into RichTextEditor.
  • Input behaviour:

    • Collapsed “Write a message…” CTA by default.
    • Expands to rich‑text input + attachment bar on click.
    • Uses postMessageMutation:
      • First postChannelMessage with content_text.
      • Then, if there is imageFile, calls uploadMessageMedia.
      • On success: invalidates ["channel-messages", channelId], clears input, and toasts.
  • Messages list:

    • Renders ChannelMessageComponent for each ChannelMessage.
  • Members view:

    • Toggles between Messages and Members (via showMembers).
    • Members panel uses ChannelMembersPanel.

Message Replies (useChannelMessageReplies)

Section titled “Message Replies (useChannelMessageReplies)”

File: src/modules/NoticeBoard/hooks/useChannelMessageReplies.ts

  • Fetches:
    • Org users (for author info).
    • ChannelReply[] via getMessageReplies.
    • Polls every 30 seconds.
  • Maps backend replies into UI Reply objects:
    • Computes posterName, posterProfileImgUrl, reactions (as a map), userReaction, and isEdited.
  • Exposes:
    • replies
    • postReplyMutation (simple text reply)
    • updateReplyMutation
    • deleteReplyMutation
    • postReplyWithMedia(contentText, imageFiles[]):
      • Creates reply, extracts replyId, uploads all images via uploadReplyMedia.
    • 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 returns senderInfo:
    • firstName, lastName, profileImgUrl, and a name fallback.

Message Permissions (useChannelMessagePermissions)

Section titled “Message Permissions (useChannelMessagePermissions)”

File: src/modules/NoticeBoard/hooks/useChannelMessagePermissions.ts

  • Reads the current user from next-auth session.
  • Allows editing if:
    • Role is super_admin or admin, or
    • roles array contains either of those.

  • 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 }).
  • Sorting UI:
    • ChannelsSettings receives channelSortItems and calls:
      • onSaveChannelSort(channelIds)saveChannelSortOrder.
      • onResetChannelSort() → clears persisted order.

  • 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).
  • Dates & timezones:

    • normalizeDate ensures date strings contain a "T" and appends "Z" when needed.
    • When adding new timestamp fields, run them through normalizeDate for consistency.
  • Reactions:

    • Callers should treat reactions as a plain Record<string, number> in the UI.
    • Use userReaction from the normalised model to highlight the current user’s reaction.
  • 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]

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 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 reply
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)
```text
src/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