/** Behaviour the message list should apply when new messages arrive. */ export type AutoScrollBehavior = 'smooth' | 'instant' | 'none'; export interface AutoScrollDecisionInput { /** Whether the message count grew since the last render. */ readonly newMessages: boolean; /** Forced jump for the local user's own just-sent message. */ readonly forceLocalSend: boolean; /** Pixels from the bottom of the scroll container. */ readonly distanceFromBottom: number; /** * True while the conversation is still settling after a channel/server * switch (initial scroll + async embed/image height changes). During this * window we jump instantly so we never animate to the wrong position. */ readonly withinInitialGrace: boolean; /** Distance threshold under which the list is considered stuck to bottom. */ readonly stickyThreshold?: number; } /** * Decides how the message list should react when the message count changes. * * - No new messages -> `none`. * - Local user's own send -> `instant` (never animate your own message). * - Near bottom during the post-switch settle window -> `instant` (so loading * a channel always lands at the bottom without a mid-chat smooth animation). * - Near bottom afterwards -> `smooth` (live messages animate into view). * - Far from bottom -> `none` (show the new-messages indicator instead). */ export function resolveAutoScrollBehavior(input: AutoScrollDecisionInput): AutoScrollBehavior { if (!input.newMessages) return 'none'; if (input.forceLocalSend) return 'instant'; const threshold = input.stickyThreshold ?? 300; if (input.distanceFromBottom > threshold) return 'none'; return input.withinInitialGrace ? 'instant' : 'smooth'; } /** Default pixel distance under which the list is considered stuck to bottom. */ export const STICKY_BOTTOM_THRESHOLD = 300; /** * Whether the scroll position is close enough to the bottom that the list * should keep auto-pinning to the latest message. Negative distances (rubber * band / overscroll) count as stuck. * * This is the predicate the message list uses to decide whether a content * height change (late image/embed/plugin render) should re-pin to bottom. */ export function isStuckToBottom(distanceFromBottom: number, threshold: number = STICKY_BOTTOM_THRESHOLD): boolean { return distanceFromBottom <= threshold; }