-
-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
maintainVisibleContentPosition interferes with ListEmptyComponent positioning in LegendList #92
Comments
Thanks! We'll look into those bugs and fix them asap. And sticky headers is in progress: #72 |
I can confirm that ListEmptyComponent styling breaks with maintainVisibleContentPosition. For me, simply pulling the ScrollView fixes it and makes the ListEmptyComponent appear. Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-14.at.05.49.07.mp4 |
I believe the ListEmptyComponent bug should be fixed in beta.7. Is that working correctly for you now? If so let's close this issue as the TypeError is also fixed and we're tracking the stickyIndices feature in #28. |
okay please let me update and test this out. |
Actually after I updated the version to beta.8, I have a very huge issue with the UI, I am very terrible at explaining by typing, please checkout this video demo below where I explain it. Watch the demo: https://vimeo.com/1057060759 |
Here is the video of @Yembot31013 since Vimeo requires a fucking sign-up bfafbc4e-b9ba744c.mp4 |
@Yembot31013 Hmm, I'm having trouble reproducing that. Which version broke it? Can you share more code to help me test it? |
thanks for your response. The version that broke it is beta.8. so based on my observation, you can reproduce this issue if you keep adding to the data array dynamically when it was first empty. so for example:
import { ActivityIndicator, View } from "react-native"
import { Text } from "../Text"
export function EmptyChat({ loading }: { loading: boolean }) {
if (loading) {
return (
<View style={{ alignItems: "center", marginTop: 20 }}>
<ActivityIndicator size="large" color="#000" />
</View>
)
}
return (
<View
style={{
alignItems: "center",
// marginTop: 20,
display: "flex",
// backgroundColor: "red",
padding: 10,
}}
>
<Text
style={{
color: "#999",
padding: 20,
backgroundColor: "#F7F7F7",
fontWeight: 400,
fontSize: 12,
borderRadius: 8,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
text={"All messages sent by participants in this conversation are encrypted. Learn More"}
/>
</View>
)
}
import { MaterialCommunityIcons } from "@expo/vector-icons"
import { LegendList, type LegendListRef } from "@legendapp/list"
import { type IWaveformRef } from "@simform_solutions/react-native-audio-waveform"
import { format } from "date-fns"
import type React from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { StyleSheet, View } from "react-native"
import { TouchableOpacity } from "react-native-gesture-handler"
import { EmptyChat } from "@/components/conversation/EmptyChat"
import { UnreadIndicator } from "@/components/MessageType/UnreadIndicator"
import { type Message } from "@/models/databases/Message"
import { type Thread } from "@/models/databases/Thread"
import { type ChatMessage } from "@/screens/chats/ConversationScreen"
type MessageSection = {
type: "date" | "unread" | "message"
id: string
data: Message | number | null
}
type Props = {
thread: Thread | null
messageData: Message[] | []
loading: boolean
user: { id: string } | null
threadDetails: { conversationType: "individual" | "group" }
handleReply: (message: ChatMessage) => void
currentPlayingRef: React.RefObject<IWaveformRef> | undefined
}
export default function MessageListContainer({
thread,
messageData,
loading,
user,
handleReply,
threadDetails,
currentPlayingRef,
}: Props) {
// const flatListRef = useRef<FlashList<MessageSection> | null>(null)
const flatListRef = useRef<LegendListRef | null>(null)
const [unreadCount, setUnreadCount] = useState(0)
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
useEffect(() => {
if (thread) {
thread.getUnreadCount(user!.id).then((count) => {
setUnreadCount(count)
})
}
}, [thread, user])
const usePreparedMessages = (messages: Message[]) => {
return useMemo(() => {
const sections: MessageSection[] = []
let currentDate: string | null = null
messages.forEach((message, index) => {
const messageDate = format(new Date(message.timestamp), "yyyy-MM-dd")
if (messageDate !== currentDate) {
sections.push({
type: "date",
id: `date-${messageDate}`,
data: message.timestamp.getTime(),
})
currentDate = messageDate
}
if (
index ===
messages.findIndex(
async (msg) =>
msg.status === "delivered" && (await msg.sender).userIdentifier !== user!.id,
) &&
unreadCount > 0
) {
sections.push({
type: "unread",
id: "unread",
data: null,
})
}
sections.push({
type: "message",
id: message.id,
data: message,
})
})
return sections
}, [messages])
}
const preparedMessages = usePreparedMessages(messageData)
const renderItem = ({ item }: { item: MessageSection }) => {
switch (item.type) {
case "date":
return (
<View style={styles.stickyHeader}>
<DateHeader date={item.data as any} />
</View>
)
case "unread":
return <UnreadIndicator />
case "message":
return (
<EnhancedMessage
message={item.data as Message}
handleReply={handleReply}
conversationType={threadDetails.conversationType}
currentPlayingRef={currentPlayingRef}
/>
)
}
}
const scrollToBottom = () => {
flatListRef.current?.scrollToEnd({ animated: true })
setShowScrollToBottom(false)
}
const handleScroll = ({ nativeEvent }: any) => {
const isCloseToBottom =
nativeEvent.layoutMeasurement.height + nativeEvent.contentOffset.y >=
nativeEvent.contentSize.height - 20
setShowScrollToBottom(!isCloseToBottom)
}
return (
<View style={{ flex: 1 }}>
<LegendList
ref={flatListRef}
data={preparedMessages}
renderItem={renderItem}
estimatedItemSize={234}
keyExtractor={(item) => item?.id}
onScroll={handleScroll}
recycleItems={true}
alignItemsAtEnd={preparedMessages.length > 0}
maintainScrollAtEnd={preparedMessages.length > 0}
maintainScrollAtEndThreshold={0.1}
maintainVisibleContentPosition={preparedMessages.length > 0}
initialScrollIndex={preparedMessages.length - 1}
showsVerticalScrollIndicator={false}
ListEmptyComponent={<EmptyChat loading={loading} />}
/>
{showScrollToBottom && (
<TouchableOpacity style={styles.scrollToBottomButton} onPress={scrollToBottom}>
<MaterialCommunityIcons name="arrow-down" size={24} color="#333333" />
</TouchableOpacity>
)}
</View>
)
}
const styles = StyleSheet.create({
scrollToBottomButton: {
alignItems: "center",
backgroundColor: "white",
borderRadius: 20,
bottom: 20,
elevation: 5,
height: 50,
justifyContent: "center",
position: "absolute",
right: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
width: 50,
},
stickyHeader: {
backgroundColor: "#fff",
paddingVertical: 8,
paddingHorizontal: 16,
zIndex: 1,
},
}) so when the preparedMessages is an empty array |
Thanks a lot for this amazing project. I just left FlashList for LegendList today because scrolling is not smooth with FlashList and the low-resource Android phone.
When using
maintainVisibleContentPosition={true}
in LegendList, it affects the positioning ofListEmptyComponent
, forcing it to center within the list container height instead of respecting the component's intended position styles. Also, if usingmaintainVisibleContentPosition={true}
andkeyExtractor={(item) => item.id}
I am getting an error ofTypeError: Cannot read property 'id' of undefined
even when data is an empty arrayAdditionally, LegendList currently lacks support for
stickyHeaderIndices
which is essential for chat applications to maintain date headers visible while scrolling through message groups.Environment:
Reproduction:
Expected Behavior:
The text was updated successfully, but these errors were encountered: