Docyrus

TeamChatChannel

Team chat channel feed for React Native with an inverted list, day separators, an unread marker, a keyboard-docked composer with @ mentions, emoji, attachments and entity links, long-press reactions and thread sheets.

iOSAndroid
Preview TeamChatChannel on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-team-chat-channel
Required Packages(3 packages)
pnpm add @shopify/flash-list expo-document-picker (optional) expo-image-picker (optional)

The attach button offers Files when expo-document-picker is installed and Photo library / Camera when expo-image-picker is installed. With neither, the button is hidden. Everything else is pure JS and runs in Expo Go.

Usage

import { TeamChatChannel } from '@/components/docyrus-native/team-chat-channel';

<TeamChatChannel
  channelName="# product-team"
  posts={posts}
  users={users}
  currentUser={currentUser}
  lastReadAt={membership.last_read_at}
  isCreatePending={createPost.isPending}
  isDeletePending={deletePost.isPending}
  onCreatePost={payload => createPost.mutateAsync(payload)}
  onUpdatePost={(id, content, options) => updatePost.mutateAsync({ id, content, ...options })}
  onDeletePost={id => deletePost.mutateAsync(id)}
  onToggleReaction={(id, emoji) => toggleReaction.mutateAsync({ id, emoji })}
  onLoadReplies={id => fetchReplies(id)}
  dataSources={dataSources}
  onSearchEntity={(dataSourceId, query) => searchRecords(dataSourceId, query)}
  onLoadMore={fetchOlderPosts}
  hasMore={hasOlderPosts}
  isLoadingMore={isFetchingOlder} />

Without maxHeight the channel is flex-1 and fills its parent — give the parent a bounded height (a screen or a sized View) and do not nest it in a vertical ScrollView. When the channel sits below a navigation header, pass that header's height as keyboardVerticalOffset so the composer clears the keyboard.

Feed

  • The feed is an inverted FlashList: the newest post is anchored at the bottom, like every mobile chat. Only top-level posts (parent_id === null) are listed.
  • Day separators (Today, Yesterday, then the date — through DateFormatProvider when mounted) split the feed.
  • lastReadAt inserts a "New messages (n)" divider above the oldest post by someone else that is newer than that timestamp.
  • onLoadMore fires when the user scrolls to the top while hasMore is true; isLoadingMore shows a spinner there.

Composer

The composer is docked at the bottom inside a KeyboardAvoidingView:

  • @ mentions — typing @ opens a user list above the input. The input shows @Name; on submit the draft becomes the stored [@Name](mention://<userId>) markdown (the format web renders) and mentioned_user_ids is filled. The @ button inserts the trigger.
  • #hashtags are collected into hashtags.
  • Emoji — the smiley opens the EmojiPicker sheet and inserts at the caret.
  • Attachments — the paperclip picks files (ChatFile = { uri, name, type, size? }) that arrive in CreatePostPayload.attachments.
  • Entity links — with dataSources + onSearchEntity, the link button opens a two-step sheet (data source → debounced record search). Picked records arrive in linked_entities.

Web fills neither mentioned_user_ids nor hashtags; native does.

Reactions, actions and editing

  • Reaction chips toggle on tap. The + / smiley buttons open the reaction sheet: the preset emoji row plus More for the full EmojiPicker.
  • Long-press a post (or tap "⋯" on your own post) for the action sheet: reactions, Reply, Edit post, Delete post.
  • Edit turns the bottom composer into the edit bar: the text is loaded (mentions included), existing attachments can be removed and new files added. Save calls onUpdatePost(id, content, { removedAttachmentIds, newFiles }).
  • Delete asks for confirmation, then calls onDeletePost(id).

Threads

Tapping "N replies" opens the thread in a full-height sheet: the root post, the replies from onLoadReplies(postId) (oldest first) and a reply composer. Reply opens the same sheet with the composer focused. Replies can be reacted to, edited and deleted inside the sheet; replies are reloaded after each change. Reply payloads carry parent_id = postId.

API Reference

TeamChatChannelProps

PropTypeDefaultDescription
postsChatPost[]-Required. Posts of the channel (top level; replies are loaded per thread)
currentUserChatUser-Signed-in user. Own posts get Edit / Delete
usersChatUser[]-Users for names, avatars and @ mentions
channelNamestring-Header title with the post count. The header is hidden when omitted
isLoadingbooleanfalseShow the loading skeleton
isCreatePendingbooleanfalsePending state of the composers
isDeletePendingbooleanfalsePending state of the delete confirmation
onCreatePost(payload: CreatePostPayload) => void | Promise<void>-Create a post or a reply. The composer is shown only when set
onUpdatePost(postId: string, content: string, options?: UpdatePostOptions) => void | Promise<void>-Save an edited post. Enables Edit
onDeletePost(postId: string) => void | Promise<void>-Delete a post after confirmation. Enables Delete
onToggleReaction(postId: string, emoji: string) => void | Promise<void>-Toggle a reaction. Enables the reaction chips and sheet
onUploadFile(file: ChatFile) => Promise<PostAttachment>-Accepted for API parity. Picked files arrive through onCreatePost / onUpdatePost
onFetchLinkPreview(url: string) => Promise<LinkPreview | null>-Accepted for API parity (unused, as on web). Render ChatPost.link_previews instead
dataSourcesDataSourceOption[]-Data sources offered by the composer's link button
onSearchEntity(dataSourceId: string, query: string) => Promise<EntitySearchResult[]>-Record search for entity linking (300 ms debounce)
onLoadReplies(postId: string) => Promise<ChatPost[]>-Load a thread's replies when its sheet opens
maxHeightnumber-Fixed height in dp. Otherwise the channel is flex-1
classNamestring-Container classes
lastReadAtstring | null-Native. ISO timestamp of the last read — newer posts by others get a "New messages" divider
onOpenAttachment(attachment: PostAttachment) => voidLinking.openURL(signed_url)Native. Open an attachment
onOpenEntity(entity: LinkedEntity) => void-Native. Tap on a linked-entity chip
onMentionPress(userId: string) => void-Native. Tap on a mention chip in a post
onLoadMore() => void | Promise<void>-Native. Load older posts when the user reaches the top
hasMorebooleanfalseNative. Older posts exist (gates onLoadMore)
isLoadingMorebooleanfalseNative. Spinner at the top while older posts load
enableMentionsbooleantrueNative. @ mentions in the composer (needs users)
enableEmojibooleantrueNative. Emoji button in the composer
enableAttachmentsbooleantrueNative. Attach button in the composer
keyboardVerticalOffsetnumber0Native. KeyboardAvoidingView offset (height of headers above the channel)

ChatPost

FieldTypeDescription
idstringPost id
contentstringMarkdown body ([@Name](mention://<id>) tokens render as chips)
attachmentsPostAttachment[] | nullFiles — images render as thumbnails, others as chips
reactionsPostReaction[]Reaction chips
link_previewsLinkPreview[]Link preview cards
linked_entitiesLinkedEntity[]Linked record chips
hashtagsstring[]Hashtags
mentioned_user_idsstring[]Mentioned users
reply_countnumberReplies count ("N replies" link)
created_onstringISO timestamp
created_bystringAuthor id (resolved against users)
last_modified_onstringShows "(edited)" when it differs from created_on
parent_idstring | nullnull for top-level posts

Other types

TypeFields
ChatUserid, firstname?, lastname?, avatar_url?
ChatFileuri, name, type, size? (same as NativeFile)
CreatePostPayloadcontent, parent_id?, attachments?: ChatFile[], linked_entities?, hashtags?, mentioned_user_ids?
UpdatePostOptionsremovedAttachmentIds?: string[], newFiles?: ChatFile[]
PostAttachmentid, file_name, file_type, file_size, signed_url
PostReactionemoji, user_ids, count
LinkPreviewurl, title?, description?, image_url?, site_name?
LinkedEntityid, data_source_id, data_source_name, record_id, display_value, icon?
EntitySearchResultrecord_id, display_value, icon?
DataSourceOptionid, name, icon?
PresetEmoji'👍' | '❤️' | '😂' | '😮' | '🎉' | '🔥' | '👀' | '💡'

Components

ComponentDescription
TeamChatChannelThe channel: header, feed, composer, thread sheet, delete confirmation

Internal building blocks (PostCard, PostComposer, PostThread, TeamChatProvider, …) are importable from their files — ContactActivityPanel uses PostCard + PostComposer for its reply thread, exactly like web.

Exports

ExportDescription
TeamChatChannelComponent
PRESET_EMOJISThe eight preset reaction emoji

Type Exports

TypeDescription
TeamChatChannelPropsProps of TeamChatChannel
ChatUser, ChatPost, ChatFile, PostAttachment, PostReaction, LinkPreview, LinkedEntity, EntitySearchResult, DataSourceOptionData types
CreatePostPayload, UpdatePostOptionsCallback payloads
PresetEmojiPreset reaction emoji union

Translation Keys

KeyFallback
ui.teamChat.writePostWrite a post...
ui.teamChat.editededited
ui.teamChat.editPostEdit post
ui.teamChat.deletePostDelete post
ui.teamChat.replyReply
ui.teamChat.repliesreplies
ui.teamChat.reactReact
ui.teamChat.noPostsYetNo posts yet
ui.teamChat.startConversationStart a conversation
ui.teamChat.selectDataSourceSelect data source
ui.teamChat.searchEntitySearch entity...
ui.teamChat.searchingSearching...
ui.teamChat.noResultsNo results
ui.teamChat.sendSend (native)
ui.teamChat.mentionMention someone (native)
ui.teamChat.linkEntityLink a record (native)
ui.teamChat.moreReactionsMore reactions (native)
ui.teamChat.moreActionsMore actions (native)
ui.teamChat.longPressHintLong-press for reactions and actions (native)
ui.teamChat.threadThread (native)
ui.teamChat.loadingRepliesLoading replies… (native)
ui.teamChat.todayToday (native)
ui.teamChat.yesterdayYesterday (native)
ui.teamChat.newMessagesNew messages (native)

Shared keys: ui.common.cancel, ui.common.save, ui.common.back, ui.emailComposer.insertEmoji, ui.commentsPanel.attachFile, ui.commentsPanel.sourceFiles, ui.commentsPanel.sourcePhotos, ui.commentsPanel.sourceCamera, ui.commentsPanel.removeFile.

Differences from web

  • File → ChatFile ({ uri, name, type, size? }) in CreatePostPayload.attachments, UpdatePostOptions.newFiles and onUploadFile. maxHeight is a number.
  • Newest post at the bottom (inverted list) and the composer below the feed; web lists newest first under a top composer.
  • Threads open in a sheet instead of expanding inline; editing happens in the bottom composer instead of inside the card.
  • Hover toolbars / dropdowns / popovers are replaced by long-press and bottom sheets.
  • Native-only: lastReadAt, day separators, onLoadMore paging, onOpenAttachment, onOpenEntity, onMentionPress, the enable* flags, keyboardVerticalOffset, and filled mentioned_user_ids / hashtags.

On this page