import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react';
import type { ChatBskyConvoDefs } from '@atcute/bluesky';
/** How long a message stays highlighted after scrolling to it, before the flash fades out. */
export const MESSAGE_HIGHLIGHT_DURATION_MS = 1500;
type HighlightedMessage = {
id: string;
/**
* Bumped on every highlight so that re-tapping the same reply re-triggers the flash even while the previous
* highlight is still active.
*/
key: number;
};
type MessageRepliesContextType = {
/** The message currently staged for reply in the composer, or null. */
replyTo: ChatBskyConvoDefs.MessageView | null;
setReply: (message: ChatBskyConvoDefs.MessageView) => void;
clearReply: () => void;
/** Scroll the list to a message, if it's currently loaded, and flash it. No-op otherwise. */
scrollToMessage: (messageId: string) => void;
/** The message to flash, or null. Consumers compare against their own id. */
highlightedMessage: HighlightedMessage | null;
};
const Context = createContext<MessageRepliesContextType | null>(null);
export function useMessageReplies() {
const ctx = useContext(Context);
if (!ctx) {
throw new Error('useMessageReplies must be used within a MessageRepliesProvider');
}
return ctx;
}
export function MessageRepliesProvider({
children,
scrollToMessage: scrollToMessageRaw,
}: {
children: ReactNode;
/**
* scrolls to the message if loaded.
*
* @returns true if the message was found and scrolled to, false otherwise.
*/
scrollToMessage: (messageId: string) => boolean;
}) {
const [replyTo, setReplyTo] = useState<ChatBskyConvoDefs.MessageView | null>(null);
const [highlightedMessage, setHighlightedMessage] = useState<HighlightedMessage | null>(null);
const highlightKey = useRef(0);
const clearHighlightTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const setReply = (message: ChatBskyConvoDefs.MessageView) => {
setReplyTo(message);
};
const clearReply = () => {
setReplyTo(null);
};
const scrollToMessage = (messageId: string) => {
const didScroll = scrollToMessageRaw(messageId);
if (!didScroll) {
return;
}
highlightKey.current += 1;
setHighlightedMessage({ id: messageId, key: highlightKey.current });
if (clearHighlightTimeout.current) {
clearTimeout(clearHighlightTimeout.current);
}
clearHighlightTimeout.current = setTimeout(() => {
setHighlightedMessage(null);
}, MESSAGE_HIGHLIGHT_DURATION_MS);
};
useEffect(() => {
return () => {
if (clearHighlightTimeout.current) {
clearTimeout(clearHighlightTimeout.current);
}
};
}, []);
const ctx: MessageRepliesContextType = {
replyTo,
setReply,
clearReply,
scrollToMessage,
highlightedMessage,
};
return <Context.Provider value={ctx}>{children}</Context.Provider>;
}