Skip to content

Commit

Permalink
Merge pull request #4314 from tloncorp/po/clean-up-bare-chat-input
Browse files Browse the repository at this point in the history
clean up bare chat input
  • Loading branch information
patosullivan authored Jan 8, 2025
2 parents d28b923 + df107ff commit f31353f
Showing 1 changed file with 142 additions and 149 deletions.
291 changes: 142 additions & 149 deletions packages/ui/src/components/BareChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,132 @@ import { useMentions } from './useMentions';

const bareChatInputLogger = createDevLogger('bareChatInput', false);

const DEFAULT_KEYBOARD_HEIGHT = 300;

function useKeyboardHeight(maxInputHeightBasic: number) {
const [maxInputHeight, setMaxInputHeight] = useState(maxInputHeightBasic);

useEffect(() => {
const handleKeyboardShow = () => {
const keyboardHeight =
Keyboard.metrics()?.height || DEFAULT_KEYBOARD_HEIGHT;
setMaxInputHeight(maxInputHeightBasic - keyboardHeight);
};

const handleKeyboardHide = () => {
setMaxInputHeight(maxInputHeightBasic);
};

const showSubscription = Keyboard.addListener(
'keyboardDidShow',
handleKeyboardShow
);
const hideSubscription = Keyboard.addListener(
'keyboardDidHide',
handleKeyboardHide
);

return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, [maxInputHeightBasic]);

return maxInputHeight;
}

function usePasteHandler(addAttachment: (attachment: Attachment) => void) {
// For now, we only check to make sure we're on web,
// we don't check if the input is focused. This allows users to paste
// images before they select the input. We may want to change this behavior
// if this feels weird, but it feels like a nice quality of life improvement.
// We can do this because there is only ever one input on the screen at a time,
// unlike the old app where you could have both the main chat input and the
// thread input on screen at the same time.
useEffect(() => {
if (!isWeb) return;

const handlePaste = async (e: ClipboardEvent) => {
const items = Array.from(e.clipboardData?.items || []);
const image = items.find((item) => item.type.includes('image'));

if (!image) return;

const file = image.getAsFile();
if (!file) return;

const uri = URL.createObjectURL(file);
const img = new Image();

img.onload = () => {
addAttachment({
type: 'image',
file: {
uri,
height: img.height,
width: img.width,
},
});
};

img.src = uri;
};

document.addEventListener('paste', handlePaste);
return () => document.removeEventListener('paste', handlePaste);
}, [addAttachment]);
}

interface TextWithMentionsProps {
text: string;
mentions: Array<{ start: number; end: number; display: string; id: string }>;
textColor: string;
}

function TextWithMentions({
text,
mentions,
textColor,
}: TextWithMentionsProps) {
if (!text || mentions.length === 0) {
return <RawText color={textColor}>{text}</RawText>;
}

const sortedMentions = [...mentions].sort((a, b) => a.start - b.start);
const textParts: JSX.Element[] = [];

if (sortedMentions[0].start > 0) {
textParts.push(
<RawText key="text-start" color={textColor}>
{text.slice(0, sortedMentions[0].start)}
</RawText>
);
}

sortedMentions.forEach((mention, index) => {
textParts.push(
<Text
key={`mention-${mention.id}-${index}`}
color="$positiveActionText"
backgroundColor="$positiveBackground"
>
{mention.display}
</Text>
);

const nextStart = sortedMentions[index + 1]?.start ?? text.length;
if (mention.end < nextStart) {
textParts.push(
<RawText key={`text-${index}`} color={textColor}>
{text.slice(mention.end, nextStart)}
</RawText>
);
}
});

return <>{textParts}</>;
}

export default function BareChatInput({
shouldBlur,
setShouldBlur,
Expand Down Expand Up @@ -102,9 +228,11 @@ export default function BareChatInput({
setMentions,
showMentionPopup,
} = useMentions();
const [maxInputHeight, setMaxInputHeight] = useState(maxInputHeightBasic);
const maxInputHeight = useKeyboardHeight(maxInputHeightBasic);
const inputRef = useRef<TextInput>(null);

usePasteHandler(addAttachment);

const processReferences = useCallback(
(text: string): string => {
const references = text.match(REF_REGEX);
Expand Down Expand Up @@ -194,94 +322,6 @@ export default function BareChatInput({
[handleSelectMention, controlledText]
);

const renderTextWithMentionsWeb = useMemo(() => {
if (!controlledText || mentions.length === 0) {
return null;
}

const sortedMentions = [...mentions].sort((a, b) => a.start - b.start);
const textParts: JSX.Element[] = [];

// Handle text before first mention
if (sortedMentions[0].start > 0) {
textParts.push(
<RawText key="text-start" color="transparent">
{controlledText.slice(0, sortedMentions[0].start)}
</RawText>
);
}

// Handle mentions and text between them
sortedMentions.forEach((mention, index) => {
textParts.push(
<Text
key={`mention-${mention.id}-${index}`}
color="$positiveActionText"
backgroundColor="$positiveBackground"
>
{mention.display}
</Text>
);

// Add text between this mention and the next one (or end of text)
const nextStart =
sortedMentions[index + 1]?.start ?? controlledText.length;
if (mention.end < nextStart) {
textParts.push(
<RawText key={`text-${index}`} color="transparent">
{controlledText.slice(mention.end, nextStart)}
</RawText>
);
}
});

return textParts;
}, [mentions, controlledText]);

const renderTextWithMentions = useMemo(() => {
if (!controlledText || mentions.length === 0) {
return <RawText color="$primaryText">{controlledText}</RawText>;
}

const sortedMentions = [...mentions].sort((a, b) => a.start - b.start);
const textParts: JSX.Element[] = [];

// Handle text before first mention
if (sortedMentions[0].start > 0) {
textParts.push(
<RawText key="text-start" color="$primaryText">
{controlledText.slice(0, sortedMentions[0].start)}
</RawText>
);
}

// Handle mentions and text between them
sortedMentions.forEach((mention, index) => {
textParts.push(
<Text
key={`mention-${mention.id}-${index}`}
color="$positiveActionText"
backgroundColor="$positiveBackground"
>
{mention.display}
</Text>
);

// Add text between this mention and the next one (or end of text)
const nextStart =
sortedMentions[index + 1]?.start ?? controlledText.length;
if (mention.end < nextStart) {
textParts.push(
<RawText key={`text-${index}`} color="$primaryText">
{controlledText.slice(mention.end, nextStart)}
</RawText>
);
}
});

return textParts;
}, [mentions, controlledText]);

const sendMessage = useCallback(
async (isEdit?: boolean) => {
const jsonContent = textAndMentionsToContent(controlledText, mentions);
Expand Down Expand Up @@ -434,18 +474,6 @@ export default function BareChatInput({
runSendMessage(true);
}, [runSendMessage, editingPost]);

// Make sure the user can still see some of the scroller when the keyboard is up
useEffect(() => {
Keyboard.addListener('keyboardDidShow', () => {
const keyboardHeight = Keyboard.metrics()?.height || 300;
setMaxInputHeight(maxInputHeightBasic - keyboardHeight);
});

Keyboard.addListener('keyboardDidHide', () => {
setMaxInputHeight(maxInputHeightBasic);
});
}, [maxInputHeightBasic]);

// Handle autofocus
useEffect(() => {
if (!shouldBlur && shouldAutoFocus && !hasAutoFocused) {
Expand Down Expand Up @@ -569,51 +597,6 @@ export default function BareChatInput({
setMentions,
]);

// Handle pastes on web
useEffect(() => {
// For now, we only check to make sure we're on web,
// we don't check if the input is focused. This allows users to paste
// images before they select the input. We may want to change this behavior
// if this feels weird, but it feels like a nice quality of life improvement.
// We can do this because there is only ever one input on the screen at a time,
// unlike the old app where you could have both the main chat input and the
// thread input on screen at the same time.
if (!isWeb) return;

const handlePaste = async (e: ClipboardEvent) => {
const items = Array.from(e.clipboardData?.items || []);
const image = items.find((item) => item.type.includes('image'));

if (!image) return;

const file = image.getAsFile();
if (!file) return;

const uri = URL.createObjectURL(file);

const img = new Image();

img.onload = () => {
addAttachment({
type: 'image',
file: {
uri,
height: img.height,
width: img.width,
},
});
};

img.src = uri;
};

document.addEventListener('paste', handlePaste);

return () => {
document.removeEventListener('paste', handlePaste);
};
}, [addAttachment]);

const handleCancelEditing = useCallback(() => {
setEditingPost?.(undefined);
setHasSetInitialContent(false);
Expand Down Expand Up @@ -728,9 +711,15 @@ export default function BareChatInput({
...(isWeb ? { outlineStyle: 'none' } : {}),
}}
>
{isWeb ? undefined : renderTextWithMentions}
{isWeb ? undefined : (
<TextWithMentions
text={controlledText}
mentions={mentions}
textColor="$primaryText"
/>
)}
</TextInput>
{isWeb && mentions.length > 0 && (
{isWeb && controlledText && mentions.length > 0 && (
<View height={inputHeight} position="absolute" pointerEvents="none">
<RawText
paddingHorizontal="$l"
Expand All @@ -740,7 +729,11 @@ export default function BareChatInput({
letterSpacing={-0.032}
color="$primaryText"
>
{renderTextWithMentionsWeb}
<TextWithMentions
text={controlledText}
mentions={mentions}
textColor="transparent"
/>
</RawText>
</View>
)}
Expand Down

0 comments on commit f31353f

Please sign in to comment.