import type { Logger, MessagingUser, StreamChatServiceConfig, } from '@linktr.ee/messaging-core' import type { ComponentType, Dispatch, SetStateAction } from 'react' import type { Channel, ChannelFilters, ChannelMemberResponse, ChannelSort, Event, LocalMessage, SendMessageAPIResponse, StreamChat, } from 'stream-chat' import type { ChannelPreviewUIComponentProps, EmptyStateIndicatorProps, InfiniteScrollProps, } from 'stream-chat-react' import type { MessageMetadata } from './stream-custom-data' export type { LockedAttachmentSource } from './components/CustomMessage/LockedAttachment' /** * Callback invoked when a user clicks a link inside the channel view. * Receives the clicked URL and, when resolvable, the message the link * was rendered in (so consumers can filter on sender or metadata). * `message` is `undefined` when the click happens on a link outside a * resolvable message wrapper. Fires alongside the native navigation, * not in place of it. */ export type MessageLinkClickHandler = ( url: string, message: LocalMessage | undefined ) => void export type ResolvedOutbound = { /** Automation id referenced by the outbound record. */ id: string name?: string } export type OutboundClickHandler = (outbound: ResolvedOutbound) => void export type OutboundResolver = ( outboundRecordId: string ) => ResolvedOutbound | undefined export type ChannelPreviewProps = ChannelPreviewUIComponentProps & { selectedChannel?: Channel | null onChannelSelect: (channel: Channel) => void viewerLanguage?: string } /** * Generic participant interface for different host environments */ export interface Participant { id: string name: string image?: string username?: string phone?: string metadata?: Record } /** * Messaging capabilities configuration */ export interface MessagingCapabilities { /** * Show the "Delete Conversation" button in channel info dialog. * Defaults to true for backward compatibility. */ showDeleteConversation?: boolean } /** * ChannelList component props */ export interface ChannelListProps { onChannelSelect: (channel: Channel) => void selectedChannel?: Channel className?: string filters: ChannelFilters /** * Controls whether new-message events can promote channels that are not * already present in the current list. * * Defaults to `false` for backward compatibility with existing filtered * ChannelList consumers. */ allowNewMessagesFromUnfilteredChannels?: boolean /** * Client-side filter applied before rendering the channel list. * Websocket events can add channels to the list that bypass server-side * query filters. Use this to enforce visibility rules that can't be * automatically derived from the filters prop (e.g. $or conditions). * * @example * // Hide channels where the visitor hasn't sent a message yet, * // but keep legacy channels that predate the has_visitor_message field * channelRenderFilterFn={(channels) => * channels.filter(ch => ch.data?.has_visitor_message !== false) * } */ channelRenderFilterFn?: (channels: Channel[]) => Channel[] /** * Sort order for the channel list query. * Defaults to `{ last_message_at: -1 }` (most recent first). */ sort?: ChannelSort customEmptyStateIndicator?: React.ComponentType /** * Custom paginator for the channel list. The default is Stream's * `InfiniteScroll`; hosts that render multiple ChannelLists in one stacked * layout can provide a paginator that listens to the host-owned scroll * container or renders an explicit load-more control. */ channelListPaginator?: ComponentType /** * Custom component rendered for each channel row in the conversation list. * Receives Stream's channel preview props plus this package's selection * state and `onChannelSelect` handler. */ channelPreview?: ComponentType renderMessagePreview?: ( message: LocalMessage | undefined, defaultPreview?: string ) => React.ReactNode /** * Language code used to pick translated message text from Stream Chat i18n. * Falls back to message.text when no matching translation exists. */ viewerLanguage?: string /** * Lock the visible channel order so live events don't promote a channel to * the top. Forwarded to Stream's ``. Note `lockChannelOrder` * alone doesn't cover `notification.message_new` or `channel.visible`; pair it * with the event handlers below to fully neutralize promotion. */ lockChannelOrder?: boolean /** * Live channel-list event handlers forwarded verbatim to Stream's * ``. Left undefined, Stream applies its default "move the * changed channel to the top" behaviour. * * Hosts that order by a custom channel field (`last_conversation_at` for the * inbox, `priority_score` for Spotlight) use these to neutralize promotion on * new-message / visibility events and re-sort on `channel.updated` instead. * Each receives Stream's `setChannels` dispatcher and the triggering event. * See MES-1214 / MES-1219. */ onMessageNewHandler?: ChannelListReorderHandler onMessageNew?: ChannelListReorderHandler onChannelVisible?: ChannelListReorderHandler onAddedToChannel?: ChannelListReorderHandler onChannelUpdated?: ChannelListReorderHandler } /** * Signature of Stream's `` live-event handlers: mutate the loaded * channel array via `setChannels` in response to a websocket `event`. */ export type ChannelListReorderHandler = ( setChannels: Dispatch>, event: Event ) => void /** * ChannelView component props */ export interface ChannelViewProps { channel: Channel onBack?: () => void showBackButton?: boolean renderMessageInputActions?: (channel: Channel) => React.ReactNode renderMessageInputFooter?: (channel: Channel) => React.ReactNode renderConversationFooter?: (channel: Channel) => React.ReactNode onLeaveConversation?: (channel: Channel) => void onBlockParticipant?: (participantId?: string) => void className?: string CustomChannelEmptyState?: React.ComponentType /** * Show the "Delete Conversation" button in channel info dialog. * Defaults to true for backward compatibility. */ showDeleteConversation?: boolean /** * Analytics callback fired when "Delete Conversation" is clicked. */ onDeleteConversationClick?: () => void /** * Analytics callback fired when "Block" or "Unblock" is clicked. */ onBlockParticipantClick?: () => void /** * Analytics callback fired when "Report" is clicked. */ onReportParticipantClick?: () => void /** * Show the "Block"/"Unblock" action in the channel info dialog. * Defaults to true. Set false for restricted surfaces such as the * Linktree official channel, where blocking is not offered. */ showBlockParticipant?: boolean /** * Show the "Report" action in the channel info dialog. * Defaults to true. Set false for restricted surfaces such as the * Linktree official channel, where reporting is not offered. */ showReportParticipant?: boolean /** * @deprecated Not currently rendered. The channel info sidebar was removed * in favour of the actions popover; this prop is retained for API * compatibility and will be wired up again when the replacement profile * surface lands. Previously toggled the subscription/follower-status label * in the channel info dialog profile card. */ showFollowerStatus?: boolean /** * Replace the message composer with a non-interactive locked panel showing * `composerDisabledReason`. Defaults to false. Used by the Linktree official * channel, where the linker cannot message Linktree from the inbox (they * message Linktree from its public profile instead). * * Distinct from the channel's `frozen` flag, which keeps the composer * rendered but read-only/dimmed. */ composerDisabled?: boolean /** * Explanatory text shown inside the locked panel. Only rendered when * `composerDisabled` is true. */ composerDisabledReason?: string /** * When true and DM agent is active on the channel (not paused), * messages will be sent with skip_push and silent flags to suppress * notifications to the creator until the agent responds. * The library reads chatbot_paused from channel.data internally. * * In header UI, this treatment is shown only for visitor view. */ dmAgentEnabled?: boolean /** * Custom metadata to attach to all outgoing messages. * Used for paid messaging flows where messages need custom_type and listing_id. * * @example * messageMetadata={{ custom_type: 'MESSAGE_PAID', listing_id: '...' }} */ messageMetadata?: Partial /** * Callback fired after a message is successfully sent. * Receives the full API response including message.id. * Useful for triggering payment flows with the message ID. * * @example * onMessageSent={(response) => { * openPaymentModal(response.message.id) * }} */ onMessageSent?: (response: SendMessageAPIResponse) => void /** * Show the star/pin button in channel header. * Defaults to false. Only useful when users can view multiple channels * and filter by starred/pinned status. */ showStarButton?: boolean /** * @deprecated No longer rendered. The redesigned header shows built-in paid * and starred badges instead. Kept temporarily so existing call sites type-check. */ renderHeaderTitleBadges?: (context: { channel: Channel participant?: ChannelMemberResponse }) => React.ReactNode /** * Show the channel actions menu (the `...` popover) in the header, which * exposes the block/report/delete moderation actions. Defaults to true. * Set false for surfaces that should not expose those actions — e.g. * anonymous visitor chat, where the visitor has no authenticated identity * to act on. * * Note: the channel info sidebar was removed in favour of the actions * popover, so this no longer mounts a profile dialog or renders a clickable * participant name; it now solely gates the actions menu. */ showChannelInfo?: boolean /** * Language code used to pick translated message text from Stream Chat i18n. * Falls back to message.text when no matching translation exists. */ viewerLanguage?: string /** * Custom render function for a banner/card component that renders * between the channel header and message list. * Useful for showing summaries, alerts, or contextual information. */ renderChannelBanner?: () => React.ReactNode /** * @deprecated Not currently rendered. The channel info sidebar was removed * in favour of the actions popover; this prop is retained for API * compatibility and will be wired up again when the replacement profile * surface lands. Previously rendered custom content (badges, metadata) * below the participant name in the channel info dialog profile card. */ customProfileContent?: React.ReactNode /** * Custom actions rendered at the bottom of the channel actions popover * (below Delete Conversation, Block/Unblock, Report). * Pass one or more
  • elements so they match the list styling. * Use the exported ActionButton for consistent styling. */ customChannelActions?: React.ReactNode /** * Override the entire channel-header actions slot. When provided, this * replaces the default `(...)` overflow menu (Delete/Block/Report + * `customChannelActions`) — use it to render a host-owned trigger that does * something other than open the popover. * * @example * renderChannelActions={} */ renderChannelActions?: React.ReactNode /** * Custom render function for decorating each message in the message list. * Receives the default message node and the message object. * * @example * renderMessage={(messageNode, message) => ( * * {messageNode} * * )} */ renderMessage?: ( messageNode: React.ReactElement, message: LocalMessage ) => React.ReactNode /** * Fired when a user clicks any link inside the channel view (message * bubbles, attachment cards, etc.). Receives the clicked URL. Fires * alongside the native navigation, not in place of it. * * @example * onMessageLinkClick={(url) => trackChannelLinkClicked(url)} */ onMessageLinkClick?: MessageLinkClickHandler /** * Fired when an outbound source label is clicked on an outgoing message. * Hosts own navigation so this shared package never assumes an admin URL. */ onOutboundClick?: OutboundClickHandler /** * Resolves `metadata.outbound_id` (an outbound record id) to the current * automation id and name. The host must provide a new resolver reference * when a returned outbound changes so ChannelView re-renders and publishes * the new value to memoized rows. Missing records stay unresolved and render * no label. */ resolveOutbound?: OutboundResolver /** * Passed to Stream `Channel` as `SendButton`. Required for hosts that replace * the send control: `Channel` merges this into `ComponentContext` and an * explicit `SendButton: undefined` would otherwise override outer * `WithComponents` overrides. * * @example * sendButton={MediaSendButton} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any sendButton?: ComponentType /** * Custom component rendered inside the message composer bubble (above the * textarea). Passed to Stream `Channel` as `AttachmentPreviewList`. * Use this to render staged paid or media attachments inside the input. */ attachmentPreviewList?: ComponentType /** * Replace the entire inner composer — the textarea + send arrangement — with * a host-owned component. Passed to Stream's `` as the `Input` * component, so it runs inside the message-input context and reuses the * library's send pipeline (`doSendMessageRequest`, message metadata, DM-agent * suppression), attachments, and previews via the Stream hooks * (`useMessageInputContext`, `useMessageComposerHasSendableData`, * `TextareaComposer`, …). Use it when a surface needs a different composer * layout (e.g. a full-width textarea above a toolbar row) than the default. * * The outer wrapper — frozen handling and the `renderMessageInputActions` / * `renderMessageInputFooter` slots — is unchanged. Read the exported * `useComposerLocked()` hook to honour the frozen state. Defaults to the * built-in composer when omitted. */ composerInput?: ComponentType /** * Fired when the participant identity in the channel header is clicked. When * provided, the avatar + name pill render as a button with a trailing caret; * when omitted, they render as non-interactive chrome. * * @example * onParticipantNameClick={() => openThreadContentDialog()} */ onParticipantNameClick?: () => void } /** * Props that MessagingShell passes through to ChannelView. * ChannelViewProps is the source of truth for these props. */ export type ChannelViewPassthroughProps = Pick< ChannelViewProps, | 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onOutboundClick' | 'resolveOutbound' | 'showChannelInfo' | 'composerInput' | 'attachmentPreviewList' > /** * Main MessagingShell component props. * * MessagingShell renders a single direct conversation between the connected * user and `initialParticipantFilter`. It does not show a channel list — for * inbox-style surfaces, compose `` and `` directly. */ export interface MessagingShellProps extends ChannelViewPassthroughProps { capabilities?: MessagingCapabilities onChannelSelect?: (channel: Channel) => void /** * Fired when the user leaves the conversation or blocks the participant. * The consumer is responsible for what happens next — typically * unmounting MessagingShell, navigating away, or re-rendering with new * participant data. When omitted, the shell renders a "Conversation * ended" state rather than an indefinite spinner. */ onExitConversation?: () => void /** * UUID of the other participant for the direct conversation to render. * * If a channel with this participant already exists, it is auto-selected. * If no channel exists, supply `initialParticipantData` to auto-create one * via the configured StreamChatService channel creator. */ initialParticipantFilter: string /** * Participant data for auto-creating a channel when no channel exists yet * for `initialParticipantFilter`. When omitted, the shell renders an error * state ("No conversation found with this account") if the channel is * missing. * * This reuses the existing ChannelCreator from StreamChatServiceConfig. */ initialParticipantData?: Participant } interface MessagingProviderBaseProps { children: React.ReactNode user: MessagingUser | null capabilities?: MessagingCapabilities debug?: boolean /** * Optional sink for the package's own log output. Levels left undefined fall * back to console, which is also the default when the prop is omitted. In * apiKey mode it is forwarded to the underlying StreamChatService unless * `serviceConfig.logger` is set, which wins. */ logger?: Logger } /** * Injected-client mode: the provider renders `` against an * already-constructed Stream client and does NOT create a StreamChatService or * call `connectUser` — the caller owns the client's connection lifecycle. This * is the seam for offline/dev (`createMockMessagingClient` from * `@linktr.ee/messaging-react/testing`) and for tests, so consumers render the * real UI instead of forking it. `apiKey`/`serviceConfig` are not used here. */ interface MessagingProviderInjectedClientProps extends MessagingProviderBaseProps { client: StreamChat apiKey?: never serviceConfig?: never } /** * apiKey mode: the provider builds its own Stream client from `apiKey` and owns * the connect/disconnect lifecycle. Both fields are required so a non-injected * consumer cannot compile a provider that never connects. */ interface MessagingProviderApiKeyProps extends MessagingProviderBaseProps { client?: undefined apiKey: string serviceConfig: Omit } /** * MessagingProvider component props. Either an injected `client` (caller owns * the lifecycle) or an `apiKey` + `serviceConfig` (provider owns it) — never * neither. */ export type MessagingProviderProps = MessagingProviderInjectedClientProps | MessagingProviderApiKeyProps