Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/SampleApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@react-navigation/drawer": "7.4.1",
"@react-navigation/native": "^7.1.10",
"@react-navigation/native-stack": "^7.6.2",
"@shopify/flash-list": "^2.1.0",
"@shopify/flash-list": "^2.3.1",
"emoji-mart": "^5.6.0",
"lodash.mergewith": "^4.6.2",
"react": "19.1.0",
Expand Down
69 changes: 53 additions & 16 deletions examples/SampleApp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2732,10 +2732,10 @@
read-yaml-file "^2.1.0"
strip-json-comments "^3.1.1"

"@shopify/flash-list@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-2.1.0.tgz#b1eefcf9fbd01ca04a5f24a6003cda3b46a59f64"
integrity sha512-/EIQlptG456yM5o9qNmNsmaZEFEOGvG3WGyb6GUAxSLlcKUGlPUkPI2NLW5wQSDEY4xSRa5zocUI+9xwmsM4Kg==
"@shopify/flash-list@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-2.3.1.tgz#d4f90b1471a741a97c07d9aadbfaf200e92c86f7"
integrity sha512-7oktg2NQR7KAODjFoDaWe8/OBzyYbdTE3zQTrUBMxjIbxHTHN7UXRX1hX3DHk8KvtkgQdRfZOV8Gjj2l4fGrXw==

"@sideway/address@^4.1.5":
version "4.1.5"
Expand Down Expand Up @@ -3371,6 +3371,13 @@ acorn@^8.14.0, acorn@^8.8.2:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==

agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"

agent-base@^7.1.2:
version "7.1.3"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
Expand Down Expand Up @@ -3584,14 +3591,15 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"

axios@^1.12.2:
version "1.12.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
axios@^1.15.1:
version "1.16.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12"
integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
proxy-from-env "^1.1.0"
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"

axios@^1.6.0:
version "1.7.9"
Expand Down Expand Up @@ -5136,6 +5144,11 @@ follow-redirects@^1.15.6:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==

follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==

for-each@^0.3.3:
version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
Expand Down Expand Up @@ -5172,6 +5185,17 @@ form-data@^4.0.4:
hasown "^2.0.2"
mime-types "^2.1.12"

form-data@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"

fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
Expand Down Expand Up @@ -5465,6 +5489,14 @@ http-parser-js@>=0.5.1:
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.9.tgz#b817b3ca0edea6236225000d795378707c169cec"
integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==

https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"

https-proxy-agent@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
Expand Down Expand Up @@ -7545,6 +7577,11 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==

proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==

punycode@^2.1.0, punycode@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
Expand Down Expand Up @@ -8349,14 +8386,14 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""

stream-chat@^9.36.1:
version "9.41.1"
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.41.1.tgz#a877c8aa800d78b497eec2fad636345d4422309c"
integrity sha512-W8zjfINYol2UtdRMz2t/NN2GyjDrvb4pJgKmhtuRYzCY1u0Cjezcsu5OCNgyAM0QsenlY6tRqnvAU8Qam5R49Q==
stream-chat@^9.41.1:
version "9.44.2"
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.44.2.tgz#97d23ae4ac356b352bb0f20a31a29dc63d3ea6f5"
integrity sha512-TXALWeHyWnSn1KlGYEF0sltEHB26vFd26l5m1qlE9Q1XHo9RPPSyLb5mfXqTEY8b2FAv57Ei3hrT8nSXVWacDQ==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
axios "^1.12.2"
axios "^1.15.1"
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
Expand Down
12 changes: 10 additions & 2 deletions package/src/__tests__/offline-support/offline-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -1569,7 +1569,11 @@ export const Generic = () => {
expect(matchingReadRows[0].lastReadMessageId).toBe('321');
// FIXME: Currently missing from the DB, uncomment when added.
// expect(matchingReadRows[0].firstUnreadMessageId).toBe('123');
expect(matchingReadRows[0].lastRead).toBe(readTimestamp);
expect(
Math.abs(
new Date(matchingReadRows[0].lastRead).getTime() - new Date(readTimestamp).getTime(),
),
).toBeLessThanOrEqual(1);
});
});

Expand Down Expand Up @@ -1613,7 +1617,11 @@ export const Generic = () => {
expect(matchingReadRows[0].lastReadMessageId).toBe('321');
// FIXME: Currently missing from the DB, uncomment when added.
// expect(matchingReadRows[0].firstUnreadMessageId).toBe('123');
expect(matchingReadRows[0].lastRead).toBe(readTimestamp);
expect(
Math.abs(
new Date(matchingReadRows[0].lastRead).getTime() - new Date(readTimestamp).getTime(),
),
).toBeLessThanOrEqual(1);
});
});
});
Expand Down
27 changes: 21 additions & 6 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,18 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
channel,
});

const shouldLoadInitialChannelAtFirstUnreadMessage = useStableCallback((unreadCount?: number) => {
if (messageId || !initialScrollToFirstUnreadMessage || !client.user) {
return false;
}

return (unreadCount ?? channel.countUnread()) > scrollToFirstUnreadThreshold;
});

const hasPendingInitialTargetLoad = useStableCallback(() => {
return !!messageId || shouldLoadInitialChannelAtFirstUnreadMessage();
});

const { setMessages: copyMessagesStateFromChannel, viewabilityChangedCallback } =
usePrunableMessageList({ maximumMessageLimit, setMessages: rawCopyMessagesStateFromChannel });

Expand Down Expand Up @@ -960,6 +972,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
const initChannel = async () => {
setLastRead(new Date());
const unreadCount = channel.countUnread();
const shouldLoadAtFirstUnread = shouldLoadInitialChannelAtFirstUnreadMessage(unreadCount);
if (!channel || !shouldSyncChannel) {
return;
}
Expand Down Expand Up @@ -989,13 +1002,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =

if (messageId) {
await loadChannelAroundMessage({ messageId, setTargetedMessage });
} else if (
initialScrollToFirstUnreadMessage &&
client.user &&
unreadCount > scrollToFirstUnreadThreshold
) {
} else if (shouldLoadAtFirstUnread) {
const clientUserId = client.user?.id;
if (!clientUserId) {
return;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { user, ...ownReadState } = channel.state.read[client.user.id];
const { user, ...ownReadState } = channel.state.read[clientUserId];

await loadChannelAtFirstUnreadMessage({
channelUnreadState: ownReadState,
Expand Down Expand Up @@ -1788,6 +1802,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
enableMessageGroupingByUser,
enforceUniqueReaction,
error,
hasPendingInitialTargetLoad,
hideDateSeparators,
hideStickyDateHeader,
highlightedMessageId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const useCreateChannelContext = ({
setChannelUnreadState,
setLastRead,
setTargetedMessage,
hasPendingInitialTargetLoad,
StickyHeader,
targetedMessage,
threadList,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const useCreateChannelContext = ({
enableMessageGroupingByUser,
enforceUniqueReaction,
error,
hasPendingInitialTargetLoad,
hideDateSeparators,
hideStickyDateHeader,
highlightedMessageId,
Expand Down
56 changes: 31 additions & 25 deletions package/src/components/MessageList/MessageFlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type MessageFlashListPropsWithContext = Pick<
| 'scrollToFirstUnreadThreshold'
| 'setChannelUnreadState'
| 'setTargetedMessage'
| 'hasPendingInitialTargetLoad'
| 'StickyHeader'
| 'targetedMessage'
| 'threadList'
Expand Down Expand Up @@ -298,6 +299,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
setMessages,
setSelectedPicker,
setTargetedMessage,
hasPendingInitialTargetLoad,
StickyHeader,
targetedMessage,
thread,
Expand Down Expand Up @@ -389,11 +391,15 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>

useEffect(() => {
if (autoscrollToRecent && flashListRef.current) {
if (hasPendingInitialTargetLoad?.()) {
return;
}

flashListRef.current.scrollToEnd({
animated: true,
});
}
}, [autoscrollToRecent]);
}, [autoscrollToRecent, hasPendingInitialTargetLoad]);

const maintainVisibleContentPosition = useMemo(() => {
return {
Expand All @@ -409,18 +415,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
}
}, [disabled]);

const indexToScrollToRef = useRef<number | undefined>(undefined);

const initialIndexToScrollTo = useMemo(() => {
return targetedMessage
? processedMessageList.findIndex((message) => message?.id === targetedMessage)
: -1;
}, [processedMessageList, targetedMessage]);

useEffect(() => {
indexToScrollToRef.current = initialIndexToScrollTo;
}, [initialIndexToScrollTo]);

/**
* Check if a messageId needs to be scrolled to after list loads, and scroll to it
* Note: This effect fires on every list change with a small debounce so that scrolling isnt abrupted by an immediate rerender
Expand All @@ -441,13 +435,29 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
scrollToDebounceTimeoutRef.current = setTimeout(() => {
clearTimeout(scrollToDebounceTimeoutRef.current);

// now scroll to it
flashListRef.current?.scrollToIndex({
animated: true,
index: indexOfParentInMessageList,
viewPosition: 0.5,
const scrollToIndex = async () => {
const list = flashListRef.current;

if (!list) {
return false;
}

await list.scrollToIndex({
animated: true,
index: indexOfParentInMessageList,
viewPosition: 0.5,
});

return true;
};

requestAnimationFrame(async () => {
await scrollToIndex();
requestAnimationFrame(async () => {
await scrollToIndex();
setTargetedMessage(undefined);
});
});
setTargetedMessage(undefined);
}, WAIT_FOR_SCROLL_TIMEOUT);
}
}, [loadChannelAroundMessage, processedMessageList, setTargetedMessage, targetedMessage]);
Expand All @@ -457,8 +467,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
(message) => message?.id === messageId,
);

indexToScrollToRef.current = indexOfParentInMessageList;

try {
if (indexOfParentInMessageList === -1) {
clearTimeout(scrollToDebounceTimeoutRef.current);
Expand Down Expand Up @@ -530,7 +538,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
setScrollToBottomButtonVisible(true);
return;
} else {
indexToScrollToRef.current = undefined;
setAutoscrollToRecent(true);
}
const latestNonCurrentMessageBeforeUpdate = latestNonCurrentMessageBeforeUpdateRef.current;
Expand Down Expand Up @@ -1072,9 +1079,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
data={processedMessageList}
drawDistance={800}
getItemType={getItemTypeInternal}
initialScrollIndex={
indexToScrollToRef.current === -1 ? undefined : indexToScrollToRef.current
}
keyboardShouldPersistTaps='handled'
keyExtractor={keyExtractor}
ListFooterComponent={FooterComponent}
Expand Down Expand Up @@ -1151,6 +1155,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
scrollToFirstUnreadThreshold,
setChannelUnreadState,
setTargetedMessage,
hasPendingInitialTargetLoad,
StickyHeader,
targetedMessage,
threadList,
Expand Down Expand Up @@ -1192,6 +1197,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
enableMessageGroupingByUser,
error,
FlatList,
hasPendingInitialTargetLoad,
hideStickyDateHeader,
highlightedMessageId,
InlineDateSeparator,
Expand Down
6 changes: 6 additions & 0 deletions package/src/contexts/channelContext/ChannelContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ export type ChannelContextValue = {
setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void;
setLastRead: React.Dispatch<React.SetStateAction<Date | undefined>>;
setTargetedMessage: (messageId?: string) => void;
/**
* Returns true when Channel is about to load an initial targeted message.
*
* @internal
*/
hasPendingInitialTargetLoad?: () => boolean;
/**
* Abort controller for cancelling async requests made for uploading images/files
* Its a map of filename and AbortController
Expand Down
Loading