Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 1x 30x 30x 30x 30x 30x 9x 9x 30x 9x 9x 30x 10x 10x 10x 10x 10x 10x 3x 2x 2x 10x 9x 10x 10x 10x 30x | import { RefObject, useCallback, useRef, useState } from "react";
import { Easing, useSharedValue, withSequence, withTiming } from "react-native-reanimated";
import { Keyboard, TextInput } from "react-native";
interface UseComposerArgs {
inputRef: RefObject<TextInput | null>;
hasImage: boolean;
/** Resolves false when the provider rejected the send. */
send: (text: string) => Promise<boolean>;
}
/**
* Owns the composer's draft text and its send animation.
*
* The field clears optimistically so the send feels instant, which means the
* draft has to be restored when the provider rejects it (no thread yet, a
* stream already running) — otherwise the user silently loses what they typed.
*/
export const useComposer = ({ inputRef, hasImage, send }: UseComposerArgs) => {
const [text, setText] = useState("");
const textRef = useRef("");
const sendArrowY = useSharedValue(0);
const sendArrowOpacity = useSharedValue(1);
const onChangeText = useCallback((value: string) => {
textRef.current = value;
setText(value);
}, []);
const playSendAnimation = useCallback(() => {
sendArrowY.value = withSequence(
withTiming(-20, { duration: 180, easing: Easing.out(Easing.quad) }),
withTiming(0, { duration: 0 }),
);
sendArrowOpacity.value = withSequence(
withTiming(0, { duration: 160 }),
withTiming(1, { duration: 220, easing: Easing.out(Easing.ease) }),
);
}, [sendArrowY, sendArrowOpacity]);
const onSend = useCallback(
(value?: string) => {
const textToSend = (value ?? textRef.current).trim();
if (textToSend || hasImage) playSendAnimation();
const previousText = textRef.current;
setText("");
textRef.current = "";
const restoreDraft = () => {
// A newer draft typed during the round trip always wins.
if (textRef.current) return;
setText(previousText);
textRef.current = previousText;
};
send(textToSend)
.then((accepted) => {
if (!accepted) restoreDraft();
})
.catch(restoreDraft);
setTimeout(() => {
inputRef.current?.blur();
Keyboard.dismiss();
}, 150);
},
[send, inputRef, hasImage, playSendAnimation],
);
return { text, onChangeText, onSend, sendArrowY, sendArrowOpacity };
};
|