# rn-team-chat-channel URL: /docs/native/docyrus/team-chat-channel 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. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-team-chat-channel ``` **Dependencies:** - [@shopify/flash-list](https://www.npmjs.com/package/@shopify/flash-list) - [expo-document-picker (optional)](https://www.npmjs.com/package/expo-document-picker) - [expo-image-picker (optional)](https://www.npmjs.com/package/expo-image-picker) 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 ```tsx import { TeamChatChannel } from '@/components/docyrus-native/team-chat-channel'; 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://)` 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 | Prop | Type | Default | Description | |------|------|---------|-------------| | `posts` | `ChatPost[]` | - | **Required.** Posts of the channel (top level; replies are loaded per thread) | | `currentUser` | `ChatUser` | - | Signed-in user. Own posts get Edit / Delete | | `users` | `ChatUser[]` | - | Users for names, avatars and `@` mentions | | `channelName` | `string` | - | Header title with the post count. The header is hidden when omitted | | `isLoading` | `boolean` | `false` | Show the loading skeleton | | `isCreatePending` | `boolean` | `false` | Pending state of the composers | | `isDeletePending` | `boolean` | `false` | Pending state of the delete confirmation | | `onCreatePost` | `(payload: CreatePostPayload) => void \| Promise` | - | Create a post or a reply. The composer is shown only when set | | `onUpdatePost` | `(postId: string, content: string, options?: UpdatePostOptions) => void \| Promise` | - | Save an edited post. Enables Edit | | `onDeletePost` | `(postId: string) => void \| Promise` | - | Delete a post after confirmation. Enables Delete | | `onToggleReaction` | `(postId: string, emoji: string) => void \| Promise` | - | Toggle a reaction. Enables the reaction chips and sheet | | `onUploadFile` | `(file: ChatFile) => Promise` | - | Accepted for API parity. Picked files arrive through `onCreatePost` / `onUpdatePost` | | `onFetchLinkPreview` | `(url: string) => Promise` | - | Accepted for API parity (unused, as on web). Render `ChatPost.link_previews` instead | | `dataSources` | `DataSourceOption[]` | - | Data sources offered by the composer's link button | | `onSearchEntity` | `(dataSourceId: string, query: string) => Promise` | - | Record search for entity linking (300 ms debounce) | | `onLoadReplies` | `(postId: string) => Promise` | - | Load a thread's replies when its sheet opens | | `maxHeight` | `number` | - | Fixed height in dp. Otherwise the channel is `flex-1` | | `className` | `string` | - | Container classes | | `lastReadAt` | `string \| null` | - | Native. ISO timestamp of the last read — newer posts by others get a "New messages" divider | | `onOpenAttachment` | `(attachment: PostAttachment) => void` | `Linking.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` | - | Native. Load older posts when the user reaches the top | | `hasMore` | `boolean` | `false` | Native. Older posts exist (gates `onLoadMore`) | | `isLoadingMore` | `boolean` | `false` | Native. Spinner at the top while older posts load | | `enableMentions` | `boolean` | `true` | Native. `@` mentions in the composer (needs `users`) | | `enableEmoji` | `boolean` | `true` | Native. Emoji button in the composer | | `enableAttachments` | `boolean` | `true` | Native. Attach button in the composer | | `keyboardVerticalOffset` | `number` | `0` | Native. `KeyboardAvoidingView` offset (height of headers above the channel) | ### ChatPost | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Post id | | `content` | `string` | Markdown body (`[@Name](mention://)` tokens render as chips) | | `attachments` | `PostAttachment[] \| null` | Files — images render as thumbnails, others as chips | | `reactions` | `PostReaction[]` | Reaction chips | | `link_previews` | `LinkPreview[]` | Link preview cards | | `linked_entities` | `LinkedEntity[]` | Linked record chips | | `hashtags` | `string[]` | Hashtags | | `mentioned_user_ids` | `string[]` | Mentioned users | | `reply_count` | `number` | Replies count ("N replies" link) | | `created_on` | `string` | ISO timestamp | | `created_by` | `string` | Author id (resolved against `users`) | | `last_modified_on` | `string` | Shows "(edited)" when it differs from `created_on` | | `parent_id` | `string \| null` | `null` for top-level posts | ### Other types | Type | Fields | |------|--------| | `ChatUser` | `id`, `firstname?`, `lastname?`, `avatar_url?` | | `ChatFile` | `uri`, `name`, `type`, `size?` (same as `NativeFile`) | | `CreatePostPayload` | `content`, `parent_id?`, `attachments?: ChatFile[]`, `linked_entities?`, `hashtags?`, `mentioned_user_ids?` | | `UpdatePostOptions` | `removedAttachmentIds?: string[]`, `newFiles?: ChatFile[]` | | `PostAttachment` | `id`, `file_name`, `file_type`, `file_size`, `signed_url` | | `PostReaction` | `emoji`, `user_ids`, `count` | | `LinkPreview` | `url`, `title?`, `description?`, `image_url?`, `site_name?` | | `LinkedEntity` | `id`, `data_source_id`, `data_source_name`, `record_id`, `display_value`, `icon?` | | `EntitySearchResult` | `record_id`, `display_value`, `icon?` | | `DataSourceOption` | `id`, `name`, `icon?` | | `PresetEmoji` | `'👍' \| '❤️' \| '😂' \| '😮' \| '🎉' \| '🔥' \| '👀' \| '💡'` | ## Components | Component | Description | |-----------|-------------| | `TeamChatChannel` | The 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 | Export | Description | |--------|-------------| | `TeamChatChannel` | Component | | `PRESET_EMOJIS` | The eight preset reaction emoji | ## Type Exports | Type | Description | |------|-------------| | `TeamChatChannelProps` | Props of `TeamChatChannel` | | `ChatUser`, `ChatPost`, `ChatFile`, `PostAttachment`, `PostReaction`, `LinkPreview`, `LinkedEntity`, `EntitySearchResult`, `DataSourceOption` | Data types | | `CreatePostPayload`, `UpdatePostOptions` | Callback payloads | | `PresetEmoji` | Preset reaction emoji union | ## Translation Keys | Key | Fallback | |-----|----------| | `ui.teamChat.writePost` | `Write a post...` | | `ui.teamChat.edited` | `edited` | | `ui.teamChat.editPost` | `Edit post` | | `ui.teamChat.deletePost` | `Delete post` | | `ui.teamChat.reply` | `Reply` | | `ui.teamChat.replies` | `replies` | | `ui.teamChat.react` | `React` | | `ui.teamChat.noPostsYet` | `No posts yet` | | `ui.teamChat.startConversation` | `Start a conversation` | | `ui.teamChat.selectDataSource` | `Select data source` | | `ui.teamChat.searchEntity` | `Search entity...` | | `ui.teamChat.searching` | `Searching...` | | `ui.teamChat.noResults` | `No results` | | `ui.teamChat.send` | `Send` (native) | | `ui.teamChat.mention` | `Mention someone` (native) | | `ui.teamChat.linkEntity` | `Link a record` (native) | | `ui.teamChat.moreReactions` | `More reactions` (native) | | `ui.teamChat.moreActions` | `More actions` (native) | | `ui.teamChat.longPressHint` | `Long-press for reactions and actions` (native) | | `ui.teamChat.thread` | `Thread` (native) | | `ui.teamChat.loadingReplies` | `Loading replies…` (native) | | `ui.teamChat.today` | `Today` (native) | | `ui.teamChat.yesterday` | `Yesterday` (native) | | `ui.teamChat.newMessages` | `New 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`.