diff --git a/example/app/(tabs)/index.tsx b/example/app/(tabs)/index.tsx index 4afd5fe3..08f8c1bf 100644 --- a/example/app/(tabs)/index.tsx +++ b/example/app/(tabs)/index.tsx @@ -1,164 +1,168 @@ -import { Link, type LinkProps } from "expo-router"; -import { useCallback } from "react"; -import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, useColorScheme, View } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; +import { Link, type LinkProps } from 'expo-router'; +import { useCallback } from 'react'; +import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, useColorScheme, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; -import { LegendList } from "@legendapp/list"; -import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; -import { ThemedText } from "~/components/ThemedText"; -import { ThemedView } from "~/components/ThemedView"; +import { LegendList } from '@legendapp/list'; +import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { ThemedText } from '~/components/ThemedText'; +import { ThemedView } from '~/components/ThemedView'; // @ts-expect-error nativeFabricUIManager is not defined in the global object types -export const IsNewArchitecture = Platform.OS === "web" || global.nativeFabricUIManager != null; +export const IsNewArchitecture = Platform.OS === 'web' || global.nativeFabricUIManager != null; type ListElement = { id: number; title: string; - url: LinkProps["href"]; + url: LinkProps['href']; index: number; }; const data: ListElement[] = [ { - title: "Bidirectional Infinite List", - url: "/bidirectional-infinite-list", + title: 'Bidirectional Infinite List', + url: '/bidirectional-infinite-list', }, { - title: "Chat example", - url: "/chat-example", + title: 'Chat example', + url: '/chat-example', }, { - title: "AI Chat", - url: "/ai-chat", + title: 'AI Chat', + url: '/ai-chat', }, { - title: "Infinite chat", - url: "/chat-infinite", + title: 'Infinite chat', + url: '/chat-infinite', }, { - title: "Countries List", - url: "/countries", + title: 'Countries List', + url: '/countries', }, { - title: "Countries with headers", - url: "/countries-with-headers", + title: 'Countries with headers', + url: '/countries-with-headers', }, { - title: "Countries with headers fixed", - url: "/countries-with-headers-fixed", + title: 'Countries with headers fixed', + url: '/countries-with-headers-fixed', }, { - title: "Countries with headers sticky", - url: "/countries-with-headers-sticky", + title: 'Countries with headers sticky', + url: '/countries-with-headers-sticky', }, { - title: "Lazy List", - url: "/lazy-list", + title: 'Lazy List', + url: '/lazy-list', }, { - title: "Always render", - url: "/always-render", + title: 'Always render', + url: '/always-render', }, { - title: "MVCP test", - url: "/mvcp-test", + title: 'MVCP test', + url: '/mvcp-test', }, { - title: "Accurate scrollToIndex", - url: "/accurate-scrollto", + title: 'Accurate scrollToIndex', + url: '/accurate-scrollto', }, { - title: "Accurate scrollToIndex 2", - url: "/accurate-scrollto-2", + title: 'Accurate scrollToIndex 2', + url: '/accurate-scrollto-2', }, { - title: "Columns", - url: "/columns", + title: 'Columns', + url: '/columns', }, { - title: "Product shelf", - url: "/product-shelf", + title: 'Product shelf', + url: '/product-shelf', }, { - title: "Cards Columns", - url: "/cards-columns", + title: 'Cards Columns', + url: '/cards-columns', }, { - title: "Chat keyboard", - url: "/chat-keyboard", + title: 'Chat keyboard', + url: '/chat-keyboard', }, { - title: "Movies FlashList", - url: "/movies-flashlist", + title: 'Chat keyboard overlap', + url: '/chat-keyboard-overlap', }, { - title: "Initial scroll index precise navigation", - url: "/initial-scroll-index", + title: 'Movies FlashList', + url: '/movies-flashlist', }, { - title: "Initial scroll index(free element height)", - url: "/initial-scroll-index-free-height", + title: 'Initial scroll index precise navigation', + url: '/initial-scroll-index', }, { - title: "Initial scroll index(start at the end)", - url: "/initial-scroll-start-at-the-end", + title: 'Initial scroll index(free element height)', + url: '/initial-scroll-index-free-height', }, { - title: "Initial Scroll Index keyed", - url: "/initial-scroll-index-keyed", + title: 'Initial scroll index(start at the end)', + url: '/initial-scroll-start-at-the-end', }, { - title: "Mutable elements", - url: "/mutable-cells", + title: 'Initial Scroll Index keyed', + url: '/initial-scroll-index-keyed', }, { - title: "Extra data", - url: "/extra-data", + title: 'Mutable elements', + url: '/mutable-cells', }, { - title: "Countries List(FlashList)", - url: "/countries-flashlist", + title: 'Extra data', + url: '/extra-data', }, { - title: "Filter elements", - url: "/filter-elements", + title: 'Countries List(FlashList)', + url: '/countries-flashlist', }, { - title: "Video feed", - url: "/video-feed", + title: 'Filter elements', + url: '/filter-elements', }, { - title: "Countries Reorder", - url: "/countries-reorder", + title: 'Video feed', + url: '/video-feed', }, { - title: "Cards FlashList", - url: "/cards-flashlist", + title: 'Countries Reorder', + url: '/countries-reorder', }, { - title: "Cards no recycle", - url: "/cards-no-recycle", + title: 'Cards FlashList', + url: '/cards-flashlist', }, { - title: "Cards FlatList", - url: "/cards-flatlist", + title: 'Cards no recycle', + url: '/cards-no-recycle', }, { - title: "Add to the end", - url: "/add-to-end", + title: 'Cards FlatList', + url: '/cards-flatlist', }, { - title: "Chat resize outer", - url: "/chat-resize-outer", + title: 'Add to the end', + url: '/add-to-end', }, { - title: "Accurate scrollToHuge", - url: "/accurate-scrollto-huge", + title: 'Chat resize outer', + url: '/chat-resize-outer', }, { - title: "Chat keyboard big", - url: "/chat-keyboard-big", + title: 'Accurate scrollToHuge', + url: '/accurate-scrollto-huge', + }, + { + title: 'Chat keyboard big', + url: '/chat-keyboard-big', }, ].map( (v, i) => @@ -171,7 +175,7 @@ const data: ListElement[] = [ const RightIcon = () => ; const ListItem = ({ title, url, index }: ListElement) => { - const theme = useColorScheme() ?? "light"; + const theme = useColorScheme() ?? 'light'; return ( @@ -179,7 +183,7 @@ const ListItem = ({ title, url, index }: ListElement) => { @@ -194,7 +198,7 @@ const ListItem = ({ title, url, index }: ListElement) => { const ListElements = () => { const height = useBottomTabBarHeight(); const onLayout = useCallback((event: LayoutChangeEvent) => { - console.log("onlayout", event.nativeEvent.layout); + console.log('onlayout', event.nativeEvent.layout); }, []); return ( @@ -203,16 +207,16 @@ const ListElements = () => { estimatedItemSize={60} keyExtractor={(item) => item.id.toString()} ListFooterComponent={} - ListFooterComponentStyle={{ height: Platform.OS === "ios" ? height : 0 }} + ListFooterComponentStyle={{ height: Platform.OS === 'ios' ? height : 0 }} ListHeaderComponent={ - - {IsNewArchitecture ? "New" : "Old"} Architecture, {__DEV__ ? "DEV" : "PROD"}, 2.0 + + {IsNewArchitecture ? 'New' : 'Old'} Architecture, {__DEV__ ? 'DEV' : 'PROD'}, 2.0 } onItemSizeChanged={(info) => { - console.log("item size changed", info); + console.log('item size changed', info); }} onLayout={onLayout} renderItem={({ item, index }) => } @@ -227,11 +231,11 @@ const styles = StyleSheet.create({ }, item: { borderBottomWidth: 1, - flexDirection: "row", + flexDirection: 'row', height: 60, - justifyContent: "space-between", + justifyContent: 'space-between', padding: 16, - width: "100%", + width: '100%', }, }); diff --git a/example/app/chat-keyboard-overlap/index.tsx b/example/app/chat-keyboard-overlap/index.tsx new file mode 100644 index 00000000..db19b142 --- /dev/null +++ b/example/app/chat-keyboard-overlap/index.tsx @@ -0,0 +1,275 @@ +import { type PropsWithChildren, useMemo, useState } from 'react'; +import { BlurView } from 'expo-blur'; +import { Button, type LayoutChangeEvent, Platform, StyleSheet, Text, TextInput, View } from 'react-native'; +import { KeyboardGestureArea, KeyboardProvider, KeyboardStickyView } from 'react-native-keyboard-controller'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { KeyboardAvoidingLegendList } from '@legendapp/list/keyboard'; + +type Message = { + id: string; + text: string; + sender: 'user' | 'bot'; + timeStamp: number; +}; + +let idCounter = 0; +const MS_PER_SECOND = 1000; + +const defaultChatMessages: Message[] = [ + { + id: String(idCounter++), + sender: 'user', + text: 'Hi, I have a question about your product', + timeStamp: Date.now() - MS_PER_SECOND * 5, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'Hello there! How can I assist you today?', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'user', + text: "I'm looking for information about pricing plans", + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'We offer several pricing tiers based on your needs', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'Our basic plan starts at $9.99 per month', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'user', + text: 'Do you offer any discounts for annual billing?', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'Yes! You can save 20% with our annual billing option', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'user', + text: 'That sounds great. What features are included?', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'The basic plan includes all core features plus 10GB storage', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'Premium plans include priority support and additional tools', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'user', + text: 'I think the basic plan would work for my needs', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: 'Perfect! I can help you get set up with that', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'user', + text: 'Thanks for your help so far', + timeStamp: Date.now() - MS_PER_SECOND * 4, + }, + { + id: String(idCounter++), + sender: 'bot', + text: "You're welcome! Is there anything else I can assist with today?", + timeStamp: Date.now() - MS_PER_SECOND * 3, + }, +]; + +function ChatMessage({ item }: { item: Message }) { + return ( + <> + + {item.text} + + + {new Date(item.timeStamp).toLocaleTimeString()} + + + ); +} + +const ChatKeyboard = () => { + const [messages, setMessages] = useState(defaultChatMessages); + const [inputText, setInputText] = useState(''); + const [inputHeight, setInputHeight] = useState(0); // Default estimate + const insets = useSafeAreaInsets(); + + const handleInputLayout = (event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setInputHeight(height); + }; + + const contentContainerStyle = useMemo( + () => [styles.contentContainer, { paddingBottom: inputHeight }], + [inputHeight], + ); + + const inputContainerStyle = useMemo( + () => [styles.inputContainer, { paddingBottom: insets.bottom + 10, marginTop: -inputHeight }], + [inputHeight], + ); + + const sendMessage = () => { + const text = inputText || 'Empty message'; + if (text.trim()) { + setMessages((messagesNew) => [ + ...messagesNew, + { id: String(idCounter++), sender: 'user', text: text, timeStamp: Date.now() }, + ]); + setInputText(''); + setTimeout(() => { + setMessages((messagesNew) => [ + ...messagesNew, + { + id: String(idCounter++), + sender: 'bot', + text: `Answer: ${text.toUpperCase()}`, + timeStamp: Date.now(), + }, + ]); + }, 300); + } + }; + + return ( + + + + {inputHeight !== 0 && ( + item.id} + maintainScrollAtEnd + maintainVisibleContentPosition + renderItem={ChatMessage} + safeAreaInsetBottom={insets.bottom} + style={styles.list} + /> + )} + + + + +