diff --git a/examples/ExpoMessaging/app/channel/[cid]/index.tsx b/examples/ExpoMessaging/app/channel/[cid]/index.tsx
index d4a9295937..2a11073c17 100644
--- a/examples/ExpoMessaging/app/channel/[cid]/index.tsx
+++ b/examples/ExpoMessaging/app/channel/[cid]/index.tsx
@@ -1,6 +1,12 @@
import React, { useContext, useEffect, useState } from 'react';
import type { Channel as StreamChatChannel } from 'stream-chat';
-import { Channel, MessageInput, MessageFlashList, useChatContext } from 'stream-chat-expo';
+import {
+ Channel,
+ MessageInput,
+ useChatContext,
+ MessageFlashList,
+ ThreadContextValue,
+} from 'stream-chat-expo';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { AuthProgressLoader } from '../../../components/AuthProgressLoader';
import { AppContext } from '../../../context/AppContext';
@@ -45,7 +51,7 @@ export default function ChannelScreen() {
payload,
) => {
const { message, defaultHandler, emitter } = payload;
- const { shared_location } = message;
+ const { shared_location } = message ?? {};
if (emitter === 'messageContent' && shared_location) {
// Create url params from shared_location
const params = Object.entries(shared_location)
@@ -61,26 +67,26 @@ export default function ChannelScreen() {
}
return (
-
-
+
+
+
-
{
+ onThreadSelect={(thread: ThreadContextValue['thread']) => {
setThread(thread);
- router.push(`/channel/${channel.cid}/thread/${thread.cid}`);
+ router.push(`/channel/${channel.cid}/thread/${thread?.cid ?? ''}`);
}}
/>
-
-
+
+
);
}
diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json
index 81c89fa72e..5caee9252e 100644
--- a/examples/ExpoMessaging/package.json
+++ b/examples/ExpoMessaging/package.json
@@ -18,7 +18,7 @@
"@react-native-firebase/messaging": "^23.4.0",
"@react-navigation/elements": "^2.6.4",
"@react-navigation/native": "^7.1.8",
- "@shopify/flash-list": "2.0.2",
+ "@shopify/flash-list": "^2.1.0",
"expo": "54.0.13",
"expo-audio": "~1.0.13",
"expo-build-properties": "~1.0.9",
diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock
index 714a1e5430..c3ec79aa75 100644
--- a/examples/ExpoMessaging/yarn.lock
+++ b/examples/ExpoMessaging/yarn.lock
@@ -2248,12 +2248,10 @@
read-yaml-file "^2.1.0"
strip-json-comments "^3.1.1"
-"@shopify/flash-list@2.0.2":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-2.0.2.tgz#644748f883fccf8cf2e0ca251e0ef88673b89120"
- integrity sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==
- dependencies:
- tslib "2.8.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==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
@@ -6079,7 +6077,22 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""
-stream-chat@^9.17.0, stream-chat@^9.9.0:
+stream-chat@^9.23.0:
+ version "9.23.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.23.0.tgz#e7e5cf729861597e7198907c1cab22a57d68a2fc"
+ integrity sha512-UW112HYsLnYb4RMIXBtAouNQCCe0weVzNivjezsw+JKK1b/TX0JLBi+wK25mBUEO+coOGKfXiye6IB3gao8ipw==
+ dependencies:
+ "@types/jsonwebtoken" "^9.0.8"
+ "@types/ws" "^8.5.14"
+ axios "^1.12.2"
+ base64-js "^1.5.1"
+ form-data "^4.0.4"
+ isomorphic-ws "^5.0.0"
+ jsonwebtoken "^9.0.2"
+ linkifyjs "^4.3.2"
+ ws "^8.18.1"
+
+stream-chat@^9.9.0:
version "9.20.3"
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.20.3.tgz#5f47d6f46d146202c743282f5fb7350f4a640922"
integrity sha512-206Lea0ZAVWbfYZkIwLG5m+++ELD3f8EAEL/YzbMDL++E2vU2WhQ2d1HNb1ROXURZUF0Sy845htTw1rwnahomw==
@@ -6310,7 +6323,7 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
-tslib@2.8.1, tslib@^2.0.0, tslib@^2.1.0:
+tslib@^2.0.0, tslib@^2.1.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx
index 852b4490e4..56a64bcaf9 100644
--- a/examples/SampleApp/App.tsx
+++ b/examples/SampleApp/App.tsx
@@ -59,6 +59,11 @@ import type { LocalMessage, StreamChat, TextComposerMiddleware } from 'stream-ch
import { Toast } from './src/components/ToastComponent/Toast';
import { useClientNotificationsToastHandler } from './src/hooks/useClientNotificationsToastHandler';
import AsyncStore from './src/utils/AsyncStore.ts';
+import {
+ MessageListImplementationConfigItem,
+ MessageListModeConfigItem,
+ MessageListPruningConfigItem,
+} from './src/components/SecretMenu.tsx';
init({ data });
@@ -91,7 +96,15 @@ const Stack = createStackNavigator();
const UserSelectorStack = createStackNavigator();
const App = () => {
const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient();
- const [messageListImplementation, setMessageListImplementation] = useState<'flashlist' | 'flatlist' | null>(null);
+ const [messageListImplementation, setMessageListImplementation] = useState<
+ MessageListImplementationConfigItem['id'] | undefined
+ >(undefined);
+ const [messageListMode, setMessageListMode] = useState<
+ MessageListModeConfigItem['mode'] | undefined
+ >(undefined);
+ const [messageListPruning, setMessageListPruning] = useState<
+ MessageListPruningConfigItem['value'] | undefined
+ >(undefined);
const colorScheme = useColorScheme();
const streamChatTheme = useStreamChatTheme();
@@ -133,14 +146,26 @@ const App = () => {
}
}
});
- const getMessageListImplementation = async () => {
- const storedValue = await AsyncStore.getItem(
+ const getMessageListConfig = async () => {
+ const messageListImplementationStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-messagelist-implementation',
- { id: 'flashlist' }
+ { id: 'flatlist' },
);
- setMessageListImplementation(storedValue?.id as ('flashlist' | 'flatlist'));
- }
- getMessageListImplementation();
+ const messageListModeStoredValue = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messagelist-mode',
+ { mode: 'default' },
+ );
+ const messageListPruningStoredValue = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messagelist-pruning',
+ { value: undefined },
+ );
+ setMessageListImplementation(
+ messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
+ );
+ setMessageListMode(messageListModeStoredValue?.mode as MessageListModeConfigItem['mode']);
+ setMessageListPruning(messageListPruningStoredValue?.value as MessageListPruningConfigItem['value']);
+ };
+ getMessageListConfig();
return () => {
unsubscribeOnNotificationOpen();
unsubscribeForegroundEvent();
@@ -172,7 +197,7 @@ const App = () => {
});
}, [chatClient]);
- if (!messageListImplementation) {
+ if (!messageListImplementation || !messageListMode) {
return;
}
@@ -194,7 +219,17 @@ const App = () => {
dark: colorScheme === 'dark',
}}
>
-
+
{isConnecting && !chatClient ? (
) : chatClient ? (
diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json
index e1ecfaea7c..b0903a998d 100644
--- a/examples/SampleApp/package.json
+++ b/examples/SampleApp/package.json
@@ -37,7 +37,7 @@
"@react-navigation/drawer": "7.4.1",
"@react-navigation/native": "^7.1.10",
"@react-navigation/stack": "^7.3.3",
- "@shopify/flash-list": "^2.0.3",
+ "@shopify/flash-list": "^2.1.0",
"emoji-mart": "^5.6.0",
"lodash.mergewith": "^4.6.2",
"react": "19.1.0",
diff --git a/examples/SampleApp/src/components/SecretMenu.tsx b/examples/SampleApp/src/components/SecretMenu.tsx
index 95762b85eb..0037645b82 100644
--- a/examples/SampleApp/src/components/SecretMenu.tsx
+++ b/examples/SampleApp/src/components/SecretMenu.tsx
@@ -12,13 +12,38 @@ import {
View,
Platform,
StyleSheet,
+ ScrollView,
} from 'react-native';
-import { Close, Notification, Delete, useTheme } from 'stream-chat-react-native';
+import { Close, Edit, Notification, Delete, Folder, ZIP, useTheme } from 'stream-chat-react-native';
import { styles as menuDrawerStyles } from './MenuDrawer.tsx';
import AsyncStore from '../utils/AsyncStore.ts';
import { StreamChat } from 'stream-chat';
import { LabeledTextInput } from '../screens/AdvancedUserSelectorScreen.tsx';
+const isAndroid = Platform.OS === 'android';
+
+export type NotificationConfigItem = { label: string; name: string; id: string };
+export type MessageListImplementationConfigItem = { label: string; id: 'flatlist' | 'flashlist' };
+export type MessageListModeConfigItem = { label: string; mode: 'default' | 'livestream' };
+export type MessageListPruningConfigItem = { label: string; value: 100 | 500 | 1000 | undefined };
+
+const messageListImplementationConfigItems: MessageListImplementationConfigItem[] = [
+ { label: 'FlatList', id: 'flatlist' },
+ { label: 'FlashList', id: 'flashlist' },
+];
+
+const messageListModeConfigItems: MessageListModeConfigItem[] = [
+ { label: 'Default', mode: 'default' },
+ { label: 'Livestreaming', mode: 'livestream' },
+];
+
+const messageListPruningConfigItems: MessageListPruningConfigItem[] = [
+ { label: 'None', value: undefined },
+ { label: '100 Messages', value: 100 },
+ { label: '500 Messages', value: 500 },
+ { label: '1000 Messages', value: 1000 },
+];
+
export const SlideInView = ({
visible,
children,
@@ -55,11 +80,6 @@ export const SlideInView = ({
);
};
-const isAndroid = Platform.OS === 'android';
-
-type NotificationConfigItem = { label: string; name: string; id: string };
-type MessageListImplementationConfigItem = { label: string; id: string };
-
const SecretMenuNotificationConfigItem = ({
notificationConfigItem,
storeProvider,
@@ -124,7 +144,7 @@ const SecretMenuNotificationConfigItem = ({
);
};
-const SecretMenuMessageListConfigItem = ({
+const SecretMenuMessageListImplementationConfigItem = ({
messageListImplementationConfigItem,
storeMessageListImplementation,
isSelected,
@@ -141,9 +161,43 @@ const SecretMenuMessageListConfigItem = ({
);
+const SecretMenuMessageListModeConfigItem = ({
+ messageListModeConfigItem,
+ storeMessageListMode,
+ isSelected,
+}: {
+ messageListModeConfigItem: MessageListModeConfigItem;
+ storeMessageListMode: (item: MessageListModeConfigItem) => void;
+ isSelected: boolean;
+}) => (
+ storeMessageListMode(messageListModeConfigItem)}
+ >
+ {messageListModeConfigItem.label}
+
+);
+
+const SecretMenuMessageListPruningConfigItem = ({
+ messageListPruningConfigItem,
+ storeMessageListPruning,
+ isSelected,
+}: {
+ messageListPruningConfigItem: MessageListPruningConfigItem;
+ storeMessageListPruning: (item: MessageListPruningConfigItem) => void;
+ isSelected: boolean;
+}) => (
+ storeMessageListPruning(messageListPruningConfigItem)}
+ >
+ {messageListPruningConfigItem.label}
+
+);
+
/*
-* TODO: Please rewrite this entire component.
-*/
+ * TODO: Please rewrite this entire component.
+ */
export const SecretMenu = ({
close,
@@ -156,7 +210,13 @@ export const SecretMenu = ({
}) => {
const [selectedProvider, setSelectedProvider] = useState(null);
const [selectedMessageListImplementation, setSelectedMessageListImplementation] = useState<
- string | null
+ MessageListImplementationConfigItem['id'] | null
+ >(null);
+ const [selectedMessageListMode, setSelectedMessageListMode] = useState<
+ MessageListModeConfigItem['mode'] | null
+ >(null);
+ const [selectedMessageListPruning, setSelectedMessageListPruning] = useState<
+ MessageListPruningConfigItem['value'] | null
>(null);
const {
theme: {
@@ -172,14 +232,6 @@ export const SecretMenu = ({
[],
);
- const messageListImplementationConfigItems = useMemo(
- () => [
- { label: 'FlashList', id: 'flashlist' },
- { label: 'FlatList', id: 'flatlist' },
- ],
- [],
- );
-
useEffect(() => {
const getSelectedConfig = async () => {
const notificationProvider = await AsyncStore.getItem(
@@ -190,11 +242,23 @@ export const SecretMenu = ({
'@stream-rn-sampleapp-messagelist-implementation',
messageListImplementationConfigItems[0],
);
- setSelectedProvider(notificationProvider?.id ?? 'firebase');
- setSelectedMessageListImplementation(messageListImplementation?.id ?? 'flashlist');
+ const messageListMode = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messagelist-mode',
+ messageListModeConfigItems[0],
+ );
+ const messageListPruning = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messagelist-pruning',
+ messageListPruningConfigItems[0],
+ );
+ setSelectedProvider(notificationProvider?.id ?? notificationConfigItems[0].id);
+ setSelectedMessageListImplementation(
+ messageListImplementation?.id ?? messageListImplementationConfigItems[0].id,
+ );
+ setSelectedMessageListMode(messageListMode?.mode ?? messageListModeConfigItems[0].mode);
+ setSelectedMessageListPruning(messageListPruning?.value);
};
getSelectedConfig();
- }, [notificationConfigItems, messageListImplementationConfigItems]);
+ }, [notificationConfigItems]);
const storeProvider = useCallback(async (item: NotificationConfigItem) => {
await AsyncStore.setItem('@stream-rn-sampleapp-push-provider', item);
@@ -209,6 +273,16 @@ export const SecretMenu = ({
[],
);
+ const storeMessageListMode = useCallback(async (item: MessageListModeConfigItem) => {
+ await AsyncStore.setItem('@stream-rn-sampleapp-messagelist-mode', item);
+ setSelectedMessageListMode(item.mode);
+ }, []);
+
+ const storeMessageListPruning = useCallback(async (item: MessageListPruningConfigItem) => {
+ await AsyncStore.setItem('@stream-rn-sampleapp-messagelist-pruning', item);
+ setSelectedMessageListPruning(item.value);
+ }, []);
+
const removeAllDevices = useCallback(async () => {
const { devices } = await chatClient.getDevices(chatClient.userID);
for (const device of devices ?? []) {
@@ -218,74 +292,108 @@ export const SecretMenu = ({
return (
-
-
-
+
+
+
+
+
+ Notification Provider
+
+
+ {notificationConfigItems.map((item) => (
+
+ ))}
+
+
+
+
+
+
+ Message List implementation
+
+ {messageListImplementationConfigItems.map((item) => (
+
+ ))}
+
+
+
+
+
+
+ Message List mode
+
+ {messageListModeConfigItems.map((item) => (
+
+ ))}
+
+
+
+
+
+
+ Message List pruning
+
+ {messageListPruningConfigItems.map((item) => (
+
+ ))}
+
+
+
+
+
- Notification Provider
+ Remove all devices
-
- {notificationConfigItems.map((item) => (
-
- ))}
-
-
-
-
-
-
- Message List implementation
-
- {messageListImplementationConfigItems.map((item) => (
-
- ))}
-
-
-
-
-
-
- Remove all devices
-
-
-
-
-
- Close
-
-
+
+
+
+
+ Close
+
+
+
diff --git a/examples/SampleApp/src/context/AppContext.ts b/examples/SampleApp/src/context/AppContext.ts
index 6a75fb8e99..9ec7c80990 100644
--- a/examples/SampleApp/src/context/AppContext.ts
+++ b/examples/SampleApp/src/context/AppContext.ts
@@ -3,13 +3,20 @@ import React from 'react';
import type { StreamChat } from 'stream-chat';
import type { LoginConfig } from '../types';
+import {
+ MessageListImplementationConfigItem,
+ MessageListModeConfigItem,
+ MessageListPruningConfigItem,
+} from '../components/SecretMenu.tsx';
type AppContextType = {
chatClient: StreamChat | null;
loginUser: (config: LoginConfig) => void;
logout: () => void;
switchUser: (userId?: string) => void;
- messageListImplementation: 'flatlist' | 'flashlist';
+ messageListImplementation: MessageListImplementationConfigItem['id'];
+ messageListMode: MessageListModeConfigItem['mode'];
+ messageListPruning: MessageListPruningConfigItem['value'];
};
export const AppContext = React.createContext({} as AppContextType);
diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx
index e4a0c06653..1b059bf3f8 100644
--- a/examples/SampleApp/src/screens/ChannelScreen.tsx
+++ b/examples/SampleApp/src/screens/ChannelScreen.tsx
@@ -5,6 +5,7 @@ import {
Channel,
ChannelAvatar,
MessageInput,
+ MessageList,
MessageFlashList,
ThreadContextValue,
useAttachmentPickerContext,
@@ -32,7 +33,6 @@ import { channelMessageActions } from '../utils/messageActions.tsx';
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
import { useStreamChatContext } from '../context/StreamChatContext.tsx';
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
-import { MessageList } from 'stream-chat-react-native-core';
export type ChannelScreenNavigationProp = StackNavigationProp<
StackNavigatorParamList,
@@ -119,7 +119,8 @@ export const ChannelScreen: React.FC = ({
params: { channel: channelFromProp, channelId, messageId },
},
}) => {
- const { chatClient, messageListImplementation } = useAppContext();
+ const { chatClient, messageListImplementation, messageListMode, messageListPruning } =
+ useAppContext();
const navigation = useNavigation();
const { bottom } = useSafeAreaInsets();
const {
@@ -215,12 +216,19 @@ export const ChannelScreen: React.FC = ({
messageId={messageId}
NetworkDownIndicator={() => null}
thread={selectedThread}
+ maximumMessageLimit={messageListPruning}
>
{messageListImplementation === 'flashlist' ? (
-
+
) : (
-
+
)}
diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock
index ff8c1c7832..b95479fc13 100644
--- a/examples/SampleApp/yarn.lock
+++ b/examples/SampleApp/yarn.lock
@@ -2620,12 +2620,10 @@
read-yaml-file "^2.1.0"
strip-json-comments "^3.1.1"
-"@shopify/flash-list@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-2.0.3.tgz#222427d1e09bf5cdd8a219d0a5a80f6f1d20465d"
- integrity sha512-jUlHuZFoPdqRCDvOqsb2YkTttRPyV8Tb/EjCx3gE2wjr4UTM+fE0Ltv9bwBg0K7yo/SxRNXaW7xu5utusRb0xA==
- dependencies:
- tslib "2.8.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==
"@sideway/address@^4.1.5":
version "4.1.5"
@@ -3474,6 +3472,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==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.4"
+ proxy-from-env "^1.1.0"
+
axios@^1.6.0:
version "1.7.9"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
@@ -8179,14 +8186,14 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""
-stream-chat@^9.17.0:
- version "9.17.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2"
- integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg==
+stream-chat@^9.23.0:
+ version "9.23.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.23.0.tgz#e7e5cf729861597e7198907c1cab22a57d68a2fc"
+ integrity sha512-UW112HYsLnYb4RMIXBtAouNQCCe0weVzNivjezsw+JKK1b/TX0JLBi+wK25mBUEO+coOGKfXiye6IB3gao8ipw==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
- axios "^1.6.0"
+ axios "^1.12.2"
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
@@ -8450,16 +8457,16 @@ ts-api-utils@^2.1.0:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
-tslib@2.8.1, tslib@^2.1.0, tslib@^2.4.0:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
- integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
-
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+tslib@^2.1.0, tslib@^2.4.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
diff --git a/package/package.json b/package/package.json
index dfe68a4321..a892d446d7 100644
--- a/package/package.json
+++ b/package/package.json
@@ -79,14 +79,14 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
- "stream-chat": "^9.17.0",
+ "stream-chat": "^9.23.0",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"@emoji-mart/data": ">=1.1.0",
"@op-engineering/op-sqlite": ">=14.0.0",
"@react-native-community/netinfo": ">=11.3.1",
- "@shopify/flash-list": ">=2.0.3",
+ "@shopify/flash-list": ">=2.1.0",
"emoji-mart": ">=5.4.0",
"react-native": ">=0.73.0",
"react-native-gesture-handler": ">=2.18.0",
@@ -112,11 +112,11 @@
"@babel/core": "^7.27.4",
"@babel/runtime": "^7.27.6",
"@op-engineering/op-sqlite": "^14.0.3",
+ "@shopify/flash-list": "^2.1.0",
"@react-native-community/eslint-config": "3.2.0",
"@react-native-community/eslint-plugin": "1.3.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native/babel-preset": "0.79.3",
- "@shopify/flash-list": "^2.0.3",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "13.2.0",
"@types/better-sqlite3": "^7.6.13",
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 523e2d7297..9617284455 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -87,6 +87,7 @@ import { useStableCallback, useViewport } from '../../hooks';
import { useAppStateListener } from '../../hooks/useAppStateListener';
import { useAttachmentPickerBottomSheet } from '../../hooks/useAttachmentPickerBottomSheet';
+import { usePrunableMessageList } from '../../hooks/usePrunableMessageList';
import {
LOLReaction,
LoveReaction,
@@ -284,6 +285,7 @@ export type ChannelPropsWithContext = Pick &
| 'maxTimeBetweenGroupedMessages'
| 'NetworkDownIndicator'
| 'StickyHeader'
+ | 'maximumMessageLimit'
>
> &
Pick &
@@ -706,6 +708,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
VideoAttachmentUploadPreview = FileAttachmentUploadPreviewDefault,
VideoThumbnail = VideoThumbnailDefault,
isOnline,
+ maximumMessageLimit,
} = props;
const { thread: threadProps, threadInstance } = threadFromProps;
@@ -761,7 +764,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
} = useChannelDataState(channel);
const {
- copyMessagesStateFromChannel,
+ copyMessagesStateFromChannel: rawCopyMessagesStateFromChannel,
loadChannelAroundMessage: loadChannelAroundMessageFn,
loadChannelAtFirstUnreadMessage,
loadInitialMessagesStateFromChannel,
@@ -773,6 +776,9 @@ const ChannelWithContext = (props: PropsWithChildren) =
channel,
});
+ const { setMessages: copyMessagesStateFromChannel, viewabilityChangedCallback } =
+ usePrunableMessageList({ maximumMessageLimit, setMessages: rawCopyMessagesStateFromChannel });
+
const setReadThrottled = useMemo(
() =>
throttle(
@@ -1702,6 +1708,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
loading: channelMessagesState.loading,
LoadingIndicator,
markRead,
+ maximumMessageLimit,
maxTimeBetweenGroupedMessages,
members: channelState.members ?? {},
NetworkDownIndicator,
@@ -1804,6 +1811,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
loadMore,
loadMoreRecent,
messages: channelMessagesState.messages ?? [],
+ viewabilityChangedCallback,
});
const messagesContext = useCreateMessagesContext({
diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts
index 108510e08f..2abb66883e 100644
--- a/package/src/components/Channel/hooks/useCreateChannelContext.ts
+++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts
@@ -21,6 +21,7 @@ export const useCreateChannelContext = ({
LoadingIndicator,
markRead,
maxTimeBetweenGroupedMessages,
+ maximumMessageLimit,
members,
NetworkDownIndicator,
read,
@@ -64,6 +65,7 @@ export const useCreateChannelContext = ({
loading,
LoadingIndicator,
markRead,
+ maximumMessageLimit,
maxTimeBetweenGroupedMessages,
members,
NetworkDownIndicator,
@@ -96,6 +98,7 @@ export const useCreateChannelContext = ({
targetedMessage,
threadList,
watcherCount,
+ maximumMessageLimit,
],
);
diff --git a/package/src/components/Channel/hooks/useCreatePaginatedMessageListContext.ts b/package/src/components/Channel/hooks/useCreatePaginatedMessageListContext.ts
index 920175183a..a06bafa032 100644
--- a/package/src/components/Channel/hooks/useCreatePaginatedMessageListContext.ts
+++ b/package/src/components/Channel/hooks/useCreatePaginatedMessageListContext.ts
@@ -15,6 +15,7 @@ export const useCreatePaginatedMessageListContext = ({
messages,
setLoadingMore,
setLoadingMoreRecent,
+ viewabilityChangedCallback,
}: PaginatedMessageListContextValue & {
channelId?: string;
}) => {
@@ -31,9 +32,10 @@ export const useCreatePaginatedMessageListContext = ({
messages,
setLoadingMore,
setLoadingMoreRecent,
+ viewabilityChangedCallback,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [channelId, hasMore, loadingMore, loadingMoreRecent, messagesStr],
+ [channelId, hasMore, loadingMore, loadingMoreRecent, messagesStr, viewabilityChangedCallback],
);
return paginatedMessagesContext;
diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts
index 8fccb6d522..a902844f59 100644
--- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts
+++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { type SetStateAction, useEffect, useMemo, useState } from 'react';
import throttle from 'lodash/throttle';
import type { Channel, ChannelState, Event, MessageResponse, StreamChat } from 'stream-chat';
@@ -8,6 +8,15 @@ import { useIsChannelMuted } from './useIsChannelMuted';
import { useLatestMessagePreview } from './useLatestMessagePreview';
import { useChannelsContext } from '../../../contexts';
+import { useStableCallback } from '../../../hooks';
+
+const setLastMessageThrottleTimeout = 500;
+const setLastMessageThrottleOptions = { leading: true, trailing: true };
+
+const refreshUnreadCountThrottleTimeout = 400;
+const refreshUnreadCountThrottleOptions = setLastMessageThrottleOptions;
+
+type LastMessageType = ReturnType | MessageResponse;
export const useChannelPreviewData = (
channel: Channel,
@@ -15,9 +24,21 @@ export const useChannelPreviewData = (
forceUpdateOverride?: number,
) => {
const [forceUpdate, setForceUpdate] = useState(0);
- const [lastMessage, setLastMessage] = useState<
- ReturnType | MessageResponse
- >(channel.state.messages[channel.state.messages.length - 1]);
+ const [lastMessage, setLastMessageInner] = useState(
+ () => channel.state.messages[channel.state.messages.length - 1],
+ );
+ const throttledSetLastMessage = useMemo(
+ () =>
+ throttle(
+ (newLastMessage: SetStateAction) => setLastMessageInner(newLastMessage),
+ setLastMessageThrottleTimeout,
+ setLastMessageThrottleOptions,
+ ),
+ [],
+ );
+ const setLastMessage = useStableCallback((newLastMessage: SetStateAction) =>
+ throttledSetLastMessage(newLastMessage),
+ );
const [unread, setUnread] = useState(channel.countUnread());
const { muted } = useIsChannelMuted(channel);
const { forceUpdate: contextForceUpdate } = useChannelsContext();
@@ -26,6 +47,22 @@ export const useChannelPreviewData = (
const channelLastMessage = channel.lastMessage();
const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`;
+ const refreshUnreadCount = useMemo(
+ () =>
+ throttle(
+ () => {
+ if (muted) {
+ setUnread(0);
+ } else {
+ setUnread(channel.countUnread());
+ }
+ },
+ refreshUnreadCountThrottleTimeout,
+ refreshUnreadCountThrottleOptions,
+ ),
+ [channel, muted],
+ );
+
useEffect(() => {
const unsubscribe = channel.messageComposer.registerDraftEventSubscriptions();
return () => unsubscribe();
@@ -33,22 +70,27 @@ export const useChannelPreviewData = (
useEffect(() => {
const { unsubscribe } = client.on('notification.mark_read', () => {
- setUnread(channel.countUnread());
+ refreshUnreadCount();
});
return unsubscribe;
- }, [channel, client]);
+ }, [client, refreshUnreadCount]);
useEffect(() => {
- if (
+ setLastMessage((prevLastMessage) =>
channelLastMessage &&
- (channelLastMessage.id !== lastMessage?.id ||
- channelLastMessage.updated_at !== lastMessage?.updated_at)
- ) {
- setLastMessage(channelLastMessage);
- }
- const newUnreadCount = channel.countUnread();
- setUnread(newUnreadCount);
- }, [channel, channelLastMessage, channelLastMessageString, channelListForceUpdate, lastMessage]);
+ (channelLastMessage.id !== prevLastMessage?.id ||
+ channelLastMessage.updated_at !== prevLastMessage?.updated_at)
+ ? channelLastMessage
+ : prevLastMessage,
+ );
+ refreshUnreadCount();
+ }, [
+ channelLastMessage,
+ channelLastMessageString,
+ channelListForceUpdate,
+ setLastMessage,
+ refreshUnreadCount,
+ ]);
/**
* This effect listens for the `notification.mark_read` event and sets the unread count to 0
@@ -91,18 +133,6 @@ export const useChannelPreviewData = (
return unsubscribe;
}, [client, channel]);
- const refreshUnreadCount = useMemo(
- () =>
- throttle(() => {
- if (muted) {
- setUnread(0);
- } else {
- setUnread(channel.countUnread());
- }
- }, 400),
- [channel, muted],
- );
-
/**
* This effect listens for the `message.new`, `message.updated`, `message.deleted`, `message.undeleted`, and `channel.truncated` events
*/
@@ -118,7 +148,7 @@ export const useChannelPreviewData = (
const message = event.message;
if (message && (!message.parent_id || message.show_in_channel)) {
setLastMessage(message);
- setUnread(channel.countUnread());
+ refreshUnreadCount();
}
};
@@ -140,7 +170,7 @@ export const useChannelPreviewData = (
];
return () => listeners.forEach((l) => l.unsubscribe());
- }, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate]);
+ }, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate, setLastMessage]);
const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage);
diff --git a/package/src/components/Message/MessageSimple/MessageBubble.tsx b/package/src/components/Message/MessageSimple/MessageBubble.tsx
new file mode 100644
index 0000000000..be813308dc
--- /dev/null
+++ b/package/src/components/Message/MessageSimple/MessageBubble.tsx
@@ -0,0 +1,235 @@
+import React, { useMemo, useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+
+import Animated, {
+ Extrapolation,
+ interpolate,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+
+import { MessageContentProps } from './MessageContent';
+import { MessageSimplePropsWithContext } from './MessageSimple';
+import { ReactionListTopProps } from './ReactionList/ReactionListTop';
+
+import { MessagesContextValue, useTheme } from '../../../contexts';
+
+import { NativeHandlers } from '../../../native';
+
+export type MessageBubbleProps = Pick<
+ MessageSimplePropsWithContext,
+ 'reactionListPosition' | 'MessageContent' | 'ReactionListTop'
+> &
+ Pick<
+ MessageContentProps,
+ | 'isVeryLastMessage'
+ | 'backgroundColor'
+ | 'messageGroupedSingleOrBottom'
+ | 'noBorder'
+ | 'setMessageContentWidth'
+ > &
+ Pick;
+
+export const MessageBubble = React.memo(
+ ({
+ reactionListPosition,
+ messageContentWidth,
+ setMessageContentWidth,
+ MessageContent,
+ ReactionListTop,
+ backgroundColor,
+ isVeryLastMessage,
+ messageGroupedSingleOrBottom,
+ noBorder,
+ }: MessageBubbleProps) => {
+ const {
+ theme: {
+ messageSimple: { contentWrapper },
+ },
+ } = useTheme();
+
+ return (
+
+
+ {reactionListPosition === 'top' && ReactionListTop ? (
+
+ ) : null}
+
+ );
+ },
+);
+
+const AnimatedWrapper = Animated.createAnimatedComponent(View);
+
+export const SwipableMessageBubble = React.memo(
+ (
+ props: MessageBubbleProps &
+ Pick &
+ Pick<
+ MessageSimplePropsWithContext,
+ 'shouldRenderSwipeableWrapper' | 'messageSwipeToReplyHitSlop'
+ > & { onSwipe: () => void },
+ ) => {
+ const {
+ MessageSwipeContent,
+ shouldRenderSwipeableWrapper,
+ messageSwipeToReplyHitSlop,
+ onSwipe,
+ ...messageBubbleProps
+ } = props;
+
+ const {
+ theme: {
+ messageSimple: { contentWrapper, swipeContentContainer },
+ },
+ } = useTheme();
+
+ const translateX = useSharedValue(0);
+ const touchStart = useSharedValue<{ x: number; y: number } | null>(null);
+ const isSwiping = useSharedValue(false);
+ const [shouldRenderAnimatedWrapper, setShouldRenderAnimatedWrapper] = useState(
+ shouldRenderSwipeableWrapper,
+ );
+
+ const SWIPABLE_THRESHOLD = 25;
+
+ const triggerHaptic = NativeHandlers.triggerHaptic;
+
+ const swipeGesture = useMemo(
+ () =>
+ Gesture.Pan()
+ .hitSlop(messageSwipeToReplyHitSlop)
+ .onBegin((event) => {
+ touchStart.value = { x: event.x, y: event.y };
+ })
+ .onTouchesMove((event, state) => {
+ if (!touchStart.value || !event.changedTouches.length) {
+ state.fail();
+ return;
+ }
+
+ const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
+ const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
+ const isHorizontalPanning = xDiff > yDiff;
+
+ if (isHorizontalPanning) {
+ state.activate();
+ isSwiping.value = true;
+ if (!shouldRenderSwipeableWrapper) {
+ runOnJS(setShouldRenderAnimatedWrapper)(isSwiping.value);
+ }
+ } else {
+ state.fail();
+ }
+ })
+ .onStart(() => {
+ translateX.value = 0;
+ })
+ .onChange(({ translationX }) => {
+ if (translationX > 0) {
+ translateX.value = translationX;
+ }
+ })
+ .onEnd(() => {
+ if (translateX.value >= SWIPABLE_THRESHOLD) {
+ runOnJS(onSwipe)();
+ if (triggerHaptic) {
+ runOnJS(triggerHaptic)('impactMedium');
+ }
+ }
+ isSwiping.value = false;
+ translateX.value = withSpring(
+ 0,
+ {
+ dampingRatio: 1,
+ duration: 500,
+ overshootClamping: true,
+ stiffness: 1,
+ },
+ () => {
+ if (!shouldRenderSwipeableWrapper) {
+ runOnJS(setShouldRenderAnimatedWrapper)(isSwiping.value);
+ }
+ },
+ );
+ }),
+ [
+ isSwiping,
+ messageSwipeToReplyHitSlop,
+ onSwipe,
+ touchStart,
+ translateX,
+ triggerHaptic,
+ shouldRenderSwipeableWrapper,
+ ],
+ );
+
+ const messageBubbleAnimatedStyle = useAnimatedStyle(
+ () => ({
+ transform: [{ translateX: translateX.value }],
+ }),
+ [],
+ );
+
+ const swipeContentAnimatedStyle = useAnimatedStyle(
+ () => ({
+ opacity: interpolate(translateX.value, [0, SWIPABLE_THRESHOLD], [0, 1]),
+ transform: [
+ {
+ translateX: interpolate(
+ translateX.value,
+ [0, SWIPABLE_THRESHOLD],
+ [-SWIPABLE_THRESHOLD, 0],
+ Extrapolation.CLAMP,
+ ),
+ },
+ ],
+ }),
+ [],
+ );
+
+ return (
+
+
+ {shouldRenderAnimatedWrapper ? (
+ <>
+
+ {MessageSwipeContent ? : null}
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+ );
+ },
+);
+
+const styles = StyleSheet.create({
+ contentWrapper: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ swipeContentContainer: {
+ position: 'absolute',
+ },
+});
diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx
index 635adabb1c..472d4c47be 100644
--- a/package/src/components/Message/MessageSimple/MessageSimple.tsx
+++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx
@@ -1,18 +1,7 @@
import React, { useMemo, useState } from 'react';
import { Dimensions, LayoutChangeEvent, StyleSheet, View } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-
-import Animated, {
- Extrapolation,
- interpolate,
- runOnJS,
- useAnimatedStyle,
- useSharedValue,
- withSpring,
-} from 'react-native-reanimated';
-
-const AnimatedWrapper = Animated.createAnimatedComponent(View);
+import { MessageBubble, SwipableMessageBubble } from './MessageBubble';
import {
MessageContextValue,
@@ -25,7 +14,6 @@ import {
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useStableCallback } from '../../../hooks/useStableCallback';
-import { NativeHandlers } from '../../../native';
import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils';
import { useMessageData } from '../hooks/useMessageData';
@@ -36,10 +24,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
contentContainer: {},
- contentWrapper: {
- alignItems: 'center',
- flexDirection: 'row',
- },
lastMessageContainer: {
marginBottom: 12,
},
@@ -53,9 +37,6 @@ const styles = StyleSheet.create({
rightAlignItems: {
alignItems: 'flex-end',
},
- swipeContentContainer: {
- position: 'absolute',
- },
});
export type MessageSimplePropsWithContext = Pick<
@@ -149,13 +130,11 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
receiverMessageBackgroundColor,
senderMessageBackgroundColor,
},
- contentWrapper,
headerWrapper,
lastMessageContainer,
messageGroupedSingleOrBottomContainer,
messageGroupedTopContainer,
reactionListTop: { position: reactionPosition },
- swipeContentContainer,
},
},
} = useTheme();
@@ -212,13 +191,6 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
const repliesCurveColor = isMessageReceivedOrErrorType ? grey_gainsboro : backgroundColor;
- const translateX = useSharedValue(0);
- const touchStart = useSharedValue<{ x: number; y: number } | null>(null);
- const isSwiping = useSharedValue(false);
- const [shouldRenderAnimatedWrapper, setShouldRenderAnimatedWrapper] = useState(
- shouldRenderSwipeableWrapper,
- );
-
const onSwipeActionHandler = useStableCallback(() => {
if (customMessageSwipeAction) {
customMessageSwipeAction({ channel, message });
@@ -227,169 +199,6 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
setQuotedMessage(message);
});
- const THRESHOLD = 25;
-
- const triggerHaptic = NativeHandlers.triggerHaptic;
-
- const swipeGesture = useMemo(
- () =>
- Gesture.Pan()
- .hitSlop(messageSwipeToReplyHitSlop)
- .onBegin((event) => {
- touchStart.value = { x: event.x, y: event.y };
- })
- .onTouchesMove((event, state) => {
- if (!touchStart.value || !event.changedTouches.length) {
- state.fail();
- return;
- }
-
- const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
- const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
- const isHorizontalPanning = xDiff > yDiff;
-
- if (isHorizontalPanning) {
- state.activate();
- isSwiping.value = true;
- if (!shouldRenderSwipeableWrapper) {
- runOnJS(setShouldRenderAnimatedWrapper)(isSwiping.value);
- }
- } else {
- state.fail();
- }
- })
- .onStart(() => {
- translateX.value = 0;
- })
- .onChange(({ translationX }) => {
- if (translationX > 0) {
- translateX.value = translationX;
- }
- })
- .onEnd(() => {
- if (translateX.value >= THRESHOLD) {
- runOnJS(onSwipeActionHandler)();
- if (triggerHaptic) {
- runOnJS(triggerHaptic)('impactMedium');
- }
- }
- isSwiping.value = false;
- translateX.value = withSpring(
- 0,
- {
- dampingRatio: 1,
- duration: 500,
- overshootClamping: true,
- stiffness: 1,
- },
- () => {
- if (!shouldRenderSwipeableWrapper) {
- runOnJS(setShouldRenderAnimatedWrapper)(isSwiping.value);
- }
- },
- );
- }),
- [
- isSwiping,
- messageSwipeToReplyHitSlop,
- onSwipeActionHandler,
- touchStart,
- translateX,
- triggerHaptic,
- shouldRenderSwipeableWrapper,
- ],
- );
-
- const messageBubbleAnimatedStyle = useAnimatedStyle(
- () => ({
- transform: [{ translateX: translateX.value }],
- }),
- [],
- );
-
- const swipeContentAnimatedStyle = useAnimatedStyle(
- () => ({
- opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]),
- transform: [
- {
- translateX: interpolate(
- translateX.value,
- [0, THRESHOLD],
- [-THRESHOLD, 0],
- Extrapolation.CLAMP,
- ),
- },
- ],
- }),
- [],
- );
-
- const renderMessageBubble = useMemo(
- () => (
-
-
- {reactionListPosition === 'top' && ReactionListTop ? (
-
- ) : null}
-
- ),
- [
- messageContentWidth,
- reactionListPosition,
- MessageContent,
- ReactionListTop,
- backgroundColor,
- contentWrapper,
- isVeryLastMessage,
- messageGroupedSingleOrBottom,
- noBorder,
- ],
- );
-
- const renderAnimatedMessageBubble = useMemo(
- () => (
-
-
- {shouldRenderAnimatedWrapper ? (
- <>
-
- {MessageSwipeContent ? : null}
-
-
- {renderMessageBubble}
-
- >
- ) : (
- renderMessageBubble
- )}
-
-
- ),
- [
- MessageSwipeContent,
- contentWrapper,
- shouldRenderAnimatedWrapper,
- messageBubbleAnimatedStyle,
- messageSwipeToReplyHitSlop,
- renderMessageBubble,
- swipeContentAnimatedStyle,
- swipeContentContainer,
- swipeGesture,
- ],
- );
-
return (
{
)}
{message.pinned ? : null}
- {enableSwipeToReply ? renderAnimatedMessageBubble : renderMessageBubble}
+ {enableSwipeToReply ? (
+
+ ) : (
+
+ )}
{reactionListPosition === 'bottom' && ReactionListBottom ? : null}
diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx
index 863615053b..a5cdce21bc 100644
--- a/package/src/components/MessageList/MessageFlashList.tsx
+++ b/package/src/components/MessageList/MessageFlashList.tsx
@@ -118,6 +118,7 @@ type MessageFlashListPropsWithContext = Pick<
| 'StickyHeader'
| 'targetedMessage'
| 'threadList'
+ | 'maximumMessageLimit'
> &
Pick &
Pick &
@@ -200,6 +201,15 @@ type MessageFlashListPropsWithContext = Pick<
* ```
*/
setFlatListRef?: (ref: FlashListRef | null) => void;
+ /**
+ * If true, the message list will be used in a live-streaming scenario.
+ * This flag is used to make sure that the auto scroll behaves well, if multiple messages are received.
+ *
+ * This flag is experimental and is subject to change. Please test thoroughly before using it.
+ *
+ * @experimental
+ */
+ isLiveStreaming?: boolean;
};
const WAIT_FOR_SCROLL_TIMEOUT = 0;
@@ -262,6 +272,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
InlineDateSeparator,
InlineUnreadIndicator,
isListActive = false,
+ isLiveStreaming = false,
legacyImageViewerSwipeBehaviour,
loadChannelAroundMessage,
loading,
@@ -271,6 +282,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
loadMoreRecentThread,
loadMoreThread,
markRead,
+ maximumMessageLimit,
Message,
MessageSystem,
myMessageTheme,
@@ -303,7 +315,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false);
const [isUnreadNotificationOpen, setIsUnreadNotificationOpen] = useState(false);
const [stickyHeaderDate, setStickyHeaderDate] = useState();
- const [autoScrollToRecent, setAutoScrollToRecent] = useState(false);
const stickyHeaderDateRef = useRef(undefined);
/**
@@ -337,12 +348,18 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
[myMessageThemeString, theme],
);
- const { dateSeparatorsRef, messageGroupStylesRef, processedMessageList, rawMessageList } =
- useMessageList({
- isFlashList: true,
- noGroupByUser,
- threadList,
- });
+ const {
+ dateSeparatorsRef,
+ messageGroupStylesRef,
+ processedMessageList,
+ rawMessageList,
+ viewabilityChangedCallback,
+ } = useMessageList({
+ isFlashList: true,
+ isLiveStreaming,
+ noGroupByUser,
+ threadList,
+ });
/**
* We need topMessage and channelLastRead values to set the initial scroll position.
@@ -366,13 +383,23 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
[processedMessageList],
);
+ const [autoscrollToRecent, setAutoscrollToRecent] = useState(true);
+
+ useEffect(() => {
+ if (autoscrollToRecent && flashListRef.current) {
+ flashListRef.current.scrollToEnd({
+ animated: true,
+ });
+ }
+ }, [autoscrollToRecent]);
+
const maintainVisibleContentPosition = useMemo(() => {
return {
animateAutoscrollToBottom: true,
- autoscrollToBottomThreshold: autoScrollToRecent || threadList ? 10 : undefined,
+ autoscrollToBottomThreshold: autoscrollToRecent ? 1 : undefined,
startRenderingFromBottom: true,
};
- }, [autoScrollToRecent, threadList]);
+ }, [autoscrollToRecent]);
useEffect(() => {
if (disabled) {
@@ -380,6 +407,18 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
}
}, [disabled]);
+ const indexToScrollToRef = useRef(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
@@ -415,28 +454,15 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const indexOfParentInMessageList = processedMessageList.findIndex(
(message) => message?.id === messageId,
);
- if (indexOfParentInMessageList !== -1) {
- flashListRef.current?.scrollToIndex({
- animated: true,
- index: indexOfParentInMessageList,
- viewPosition: 0.5,
- });
- setTargetedMessage(messageId);
- return;
- }
+
+ indexToScrollToRef.current = indexOfParentInMessageList;
+
try {
if (indexOfParentInMessageList === -1) {
clearTimeout(scrollToDebounceTimeoutRef.current);
- await loadChannelAroundMessage({ messageId });
+ await loadChannelAroundMessage({ messageId, setTargetedMessage });
+ } else {
setTargetedMessage(messageId);
-
- // now scroll to it with animated=true
- flashListRef.current?.scrollToIndex({
- animated: true,
- index: indexOfParentInMessageList,
- viewPosition: 0.5, // try to place message in the center of the screen
- });
- return;
}
} catch (e) {
console.warn('Error while scrolling to message', e);
@@ -471,25 +497,23 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
setScrollToBottomButtonVisible(false);
resetPaginationTrackersRef.current();
- setAutoScrollToRecent(true);
setTimeout(() => {
channelResyncScrollSet.current = true;
if (channel.countUnread() > 0) {
markRead();
}
- setAutoScrollToRecent(false);
}, WAIT_FOR_SCROLL_TIMEOUT);
}
};
- if (isMessageRemovedFromMessageList) {
+ if (isMessageRemovedFromMessageList && !maximumMessageLimit) {
scrollToBottomIfNeeded();
}
messageListLengthBeforeUpdate.current = messageListLengthAfterUpdate;
topMessageBeforeUpdate.current = topMessageAfterUpdate;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [messageListLengthAfterUpdate, topMessageAfterUpdate?.id]);
+ }, [messageListLengthAfterUpdate, topMessageAfterUpdate?.id, maximumMessageLimit]);
useEffect(() => {
if (!processedMessageList.length) {
@@ -500,16 +524,18 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
if (notLatestSet) {
latestNonCurrentMessageBeforeUpdateRef.current =
channel.state.latestMessages[channel.state.latestMessages.length - 1];
- setAutoScrollToRecent(false);
+ setAutoscrollToRecent(false);
setScrollToBottomButtonVisible(true);
return;
+ } else {
+ indexToScrollToRef.current = undefined;
+ setAutoscrollToRecent(true);
}
const latestNonCurrentMessageBeforeUpdate = latestNonCurrentMessageBeforeUpdateRef.current;
latestNonCurrentMessageBeforeUpdateRef.current = undefined;
const latestCurrentMessageAfterUpdate = processedMessageList[processedMessageList.length - 1];
if (!latestCurrentMessageAfterUpdate) {
- setAutoScrollToRecent(true);
return;
}
const didMergeMessageSetsWithNoUpdates =
@@ -527,28 +553,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
}
}, [channel, processedMessageList, shouldScrollToRecentOnNewOwnMessageRef, threadList]);
- /**
- * Effect to scroll to the bottom of the message list when a new message is received if the scroll to bottom button is not visible.
- */
- useEffect(() => {
- const handleEvent = (event: Event) => {
- if (event.message?.user?.id !== client.userID) {
- if (!scrollToBottomButtonVisible) {
- flashListRef.current?.scrollToEnd({
- animated: true,
- });
- } else {
- setAutoScrollToRecent(false);
- }
- }
- };
- const listener: ReturnType = channel.on('message.new', handleEvent);
-
- return () => {
- listener?.unsubscribe();
- };
- }, [channel, client.userID, scrollToBottomButtonVisible]);
-
/**
* Effect to mark the channel as read when the user scrolls to the bottom of the message list.
*/
@@ -695,6 +699,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
if (!viewableItems) {
return;
}
+ viewabilityChangedCallback({ inverted: false, viewableItems });
if (!hideStickyDateHeader) {
updateStickyHeaderDateIfNeeded(viewableItems);
}
@@ -1074,23 +1079,15 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
}
const flatListStyle = useMemo(
- () => ({ ...styles.listContainer, ...listContainer, ...additionalFlashListProps?.style }),
+ () => [styles.listContainer, listContainer, additionalFlashListProps?.style],
[additionalFlashListProps?.style, listContainer],
);
const flatListContentContainerStyle = useMemo(
- () => ({
- ...styles.contentContainer,
- ...contentContainer,
- }),
+ () => [styles.contentContainer, contentContainer],
[contentContainer],
);
- const getItemType = useStableCallback((item: LocalMessage) => {
- const type = getItemTypeInternal(item);
- return client.userID === item.user?.id ? `own-${type}` : type;
- });
-
const currentListHeightRef = useRef(undefined);
const onLayout = useStableCallback((e: LayoutChangeEvent) => {
@@ -1136,7 +1133,10 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
contentContainerStyle={flatListContentContainerStyle}
data={processedMessageList}
drawDistance={800}
- getItemType={getItemType}
+ getItemType={getItemTypeInternal}
+ initialScrollIndex={
+ indexToScrollToRef.current === -1 ? undefined : indexToScrollToRef.current
+ }
keyboardShouldPersistTaps='handled'
keyExtractor={keyExtractor}
ListFooterComponent={FooterComponent}
@@ -1150,6 +1150,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
onViewableItemsChanged={stableOnViewableItemsChanged}
ref={refCallback}
renderItem={renderItem}
+ scrollEventThrottle={isLiveStreaming ? 16 : undefined}
showsVerticalScrollIndicator={false}
style={flatListStyle}
testID='message-flash-list'
@@ -1204,6 +1205,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
loading,
LoadingIndicator,
markRead,
+ maximumMessageLimit,
NetworkDownIndicator,
reloadChannel,
scrollToFirstUnreadThreshold,
@@ -1263,6 +1265,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
loadMoreRecentThread,
loadMoreThread,
markRead,
+ maximumMessageLimit,
Message,
MessageSystem,
myMessageTheme,
diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx
index 8cfc0eca48..ddc300cdb1 100644
--- a/package/src/components/MessageList/MessageList.tsx
+++ b/package/src/components/MessageList/MessageList.tsx
@@ -140,6 +140,7 @@ type MessageListPropsWithContext = Pick<
| 'StickyHeader'
| 'targetedMessage'
| 'threadList'
+ | 'maximumMessageLimit'
> &
Pick &
Pick &
@@ -275,6 +276,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
loadMoreRecentThread,
loadMoreThread,
markRead,
+ maximumMessageLimit,
Message,
MessageSystem,
myMessageTheme,
@@ -322,12 +324,17 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
* NOTE: rawMessageList changes only when messages array state changes
* processedMessageList changes on any state change
*/
- const { dateSeparatorsRef, messageGroupStylesRef, processedMessageList, rawMessageList } =
- useMessageList({
- isLiveStreaming,
- noGroupByUser,
- threadList,
- });
+ const {
+ dateSeparatorsRef,
+ messageGroupStylesRef,
+ processedMessageList,
+ rawMessageList,
+ viewabilityChangedCallback,
+ } = useMessageList({
+ isLiveStreaming,
+ noGroupByUser,
+ threadList,
+ });
const messageListLengthBeforeUpdate = useRef(0);
const messageListLengthAfterUpdate = processedMessageList.length;
@@ -348,10 +355,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const minIndexForVisible = Math.min(1, processedMessageList.length);
- const autoscrollToTopThreshold = useMemo(
- () => (isLiveStreaming ? 64 : autoscrollToRecent ? 10 : undefined),
- [autoscrollToRecent, isLiveStreaming],
- );
+ const autoscrollToTopThreshold = autoscrollToRecent ? (isLiveStreaming ? 300 : 10) : undefined;
const maintainVisibleContentPosition = useMemo(
() => ({
@@ -503,6 +507,8 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
}: {
viewableItems: ViewToken[] | undefined;
}) => {
+ viewabilityChangedCallback({ inverted, viewableItems });
+
if (!viewableItems) {
return;
}
@@ -632,13 +638,18 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
};
if (threadList || isMessageRemovedFromMessageList) {
- scrollToBottomIfNeeded();
+ if (maximumMessageLimit) {
+ // pruning has happened, reset the trackers
+ resetPaginationTrackersRef.current();
+ } else {
+ scrollToBottomIfNeeded();
+ }
}
messageListLengthBeforeUpdate.current = messageListLengthAfterUpdate;
topMessageBeforeUpdate.current = topMessageAfterUpdate;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [threadList, messageListLengthAfterUpdate, topMessageAfterUpdate?.id]);
+ }, [threadList, messageListLengthAfterUpdate, topMessageAfterUpdate?.id, maximumMessageLimit]);
useEffect(() => {
if (threadList) {
@@ -673,7 +684,11 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
!didMergeMessageSetsWithNoUpdates ||
processedMessageList.length - messageListLengthBeforeUpdate.current > 0;
- setAutoscrollToRecent(shouldForceScrollToRecent);
+ // we don't want this behaviour while pruning, as it may scroll unnecessarily in
+ // certain scenarios
+ if ((maximumMessageLimit && shouldForceScrollToRecent) || !maximumMessageLimit) {
+ setAutoscrollToRecent(shouldForceScrollToRecent);
+ }
if (!didMergeMessageSetsWithNoUpdates) {
const shouldScrollToRecentOnNewOwnMessage = shouldScrollToRecentOnNewOwnMessageRef.current();
@@ -688,7 +703,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
}, WAIT_FOR_SCROLL_TIMEOUT); // flatlist might take a bit to update, so a small delay is needed
}
}
- }, [channel, threadList, processedMessageList, shouldScrollToRecentOnNewOwnMessageRef]);
+ }, [
+ channel,
+ threadList,
+ processedMessageList,
+ shouldScrollToRecentOnNewOwnMessageRef,
+ maximumMessageLimit,
+ ]);
const goToMessage = useStableCallback(async (messageId: string) => {
const indexOfParentInMessageList = processedMessageList.findIndex(
@@ -1288,6 +1309,7 @@ export const MessageList = (props: MessageListProps) => {
loadChannelAroundMessage,
loading,
LoadingIndicator,
+ maximumMessageLimit,
markRead,
NetworkDownIndicator,
reloadChannel,
@@ -1348,6 +1370,7 @@ export const MessageList = (props: MessageListProps) => {
loadMoreRecentThread,
loadMoreThread,
markRead,
+ maximumMessageLimit,
Message,
MessageSystem,
myMessageTheme,
diff --git a/package/src/components/MessageList/hooks/useMessageList.ts b/package/src/components/MessageList/hooks/useMessageList.ts
index 088e6e2ac9..01969ce221 100644
--- a/package/src/components/MessageList/hooks/useMessageList.ts
+++ b/package/src/components/MessageList/hooks/useMessageList.ts
@@ -56,7 +56,7 @@ export const useMessageList = (params: UseMessageListParams) => {
const { hideDateSeparators, maxTimeBetweenGroupedMessages } = useChannelContext();
const { deletedMessagesVisibilityType, getMessagesGroupStyles = getGroupStyles } =
useMessagesContext();
- const { messages } = usePaginatedMessageListContext();
+ const { messages, viewabilityChangedCallback } = usePaginatedMessageListContext();
const { threadMessages } = useThreadContext();
const messageList = threadList ? threadMessages : messages;
@@ -129,7 +129,8 @@ export const useMessageList = (params: UseMessageListParams) => {
processedMessageList: data,
/** Raw messages from the channel state */
rawMessageList: messageList,
+ viewabilityChangedCallback,
}),
- [data, messageList],
+ [data, messageList, viewabilityChangedCallback],
);
};
diff --git a/package/src/components/ProgressControl/WaveProgressBar.tsx b/package/src/components/ProgressControl/WaveProgressBar.tsx
index dbf7091dd4..d454caa565 100644
--- a/package/src/components/ProgressControl/WaveProgressBar.tsx
+++ b/package/src/components/ProgressControl/WaveProgressBar.tsx
@@ -110,6 +110,7 @@ export const WaveProgressBar = React.memo(
} = useTheme();
const pan = Gesture.Pan()
+ .enabled(showProgressDrag)
.maxPointers(1)
.onStart((event) => {
const currentProgress = (state.value + event.x) / fullWidth;
diff --git a/package/src/contexts/channelContext/ChannelContext.tsx b/package/src/contexts/channelContext/ChannelContext.tsx
index 62542bdedc..ce034b3e4c 100644
--- a/package/src/contexts/channelContext/ChannelContext.tsx
+++ b/package/src/contexts/channelContext/ChannelContext.tsx
@@ -147,6 +147,12 @@ export type ChannelContextValue = {
* to still consider them grouped together
*/
maxTimeBetweenGroupedMessages?: number;
+ /**
+ * The maximum number of messages that can be loaded into the state when new messages arrive.
+ * Any excess messages will be pruned from the back of the list (oldest first), unless we are
+ * currently near them within the viewport.
+ */
+ maximumMessageLimit?: number;
/**
* Custom UI component for sticky header of channel.
*
diff --git a/package/src/contexts/paginatedMessageListContext/PaginatedMessageListContext.tsx b/package/src/contexts/paginatedMessageListContext/PaginatedMessageListContext.tsx
index d9b51e038d..7f14c58439 100644
--- a/package/src/contexts/paginatedMessageListContext/PaginatedMessageListContext.tsx
+++ b/package/src/contexts/paginatedMessageListContext/PaginatedMessageListContext.tsx
@@ -2,6 +2,7 @@ import React, { PropsWithChildren, useContext } from 'react';
import type { ChannelState } from 'stream-chat';
+import { ViewabilityChangedCallbackInput } from '../../hooks/usePrunableMessageList';
import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
import { isTestEnvironment } from '../utils/isTestEnvironment';
@@ -24,6 +25,10 @@ export type PaginatedMessageListContextValue = {
* Messages from client state
*/
messages: ChannelState['messages'];
+ /**
+ * A callback that is to be passed to onViewableItemsChanged in the underlying `MessageList`
+ */
+ viewabilityChangedCallback: (config: ViewabilityChangedCallbackInput) => void;
/**
* Has more messages to load
*/
diff --git a/package/src/hooks/usePrunableMessageList.ts b/package/src/hooks/usePrunableMessageList.ts
new file mode 100644
index 0000000000..4973ec74f1
--- /dev/null
+++ b/package/src/hooks/usePrunableMessageList.ts
@@ -0,0 +1,79 @@
+import { useRef } from 'react';
+
+import { ViewToken } from 'react-native';
+
+import { Channel } from 'stream-chat';
+
+import { useStableCallback } from './useStableCallback';
+
+import { ChannelPropsWithContext } from '../components';
+
+export type VisibleRangeConfig = { first: number; last: number; inverted: boolean };
+export type ViewabilityChangedCallbackInput = {
+ viewableItems: ViewToken[] | undefined;
+ inverted: boolean;
+};
+
+// The number of messages from an edge we want to be viewing before we stop pruning
+const calculateSafeGap = (maximumMessages: number) => 0.2 * maximumMessages;
+
+const isNearEnd = ({
+ rangeConfig,
+ maximumMessageLimit,
+}: {
+ rangeConfig: VisibleRangeConfig;
+ maximumMessageLimit: number;
+}) => {
+ const { first, last, inverted } = rangeConfig;
+
+ const safeGap = calculateSafeGap(maximumMessageLimit);
+
+ if (!inverted) return first <= safeGap;
+
+ return last >= maximumMessageLimit - 1 - safeGap;
+};
+
+export function usePrunableMessageList({
+ // setter to update the array used by the List
+ setMessages: rawSetMessages,
+ maximumMessageLimit,
+}: {
+ setMessages: (channel: Channel) => void;
+} & Pick) {
+ // Track visible index range (index in `channel.messages`)
+ const visibleRangeConfigRef = useRef({ first: 0, inverted: true, last: -1 });
+
+ const viewabilityChangedCallback = useStableCallback(
+ ({ viewableItems, inverted = true }: ViewabilityChangedCallbackInput) => {
+ if (!viewableItems?.length || maximumMessageLimit == null) return;
+ let first = Infinity;
+ let last = -1;
+ for (const v of viewableItems) {
+ if (v.index == null) continue;
+ if (v.index < first) first = v.index;
+ if (v.index > last) last = v.index;
+ }
+ if (first !== Infinity) visibleRangeConfigRef.current = { first, inverted, last };
+ },
+ );
+
+ // Prune when length exceeds MAX, but only if the viewport is far from the back edge
+ const setMessages = useStableCallback((channel: Channel) => {
+ const rangeConfig = visibleRangeConfigRef.current;
+
+ if (
+ maximumMessageLimit == null ||
+ channel.state.messages.length <= maximumMessageLimit ||
+ isNearEnd({ maximumMessageLimit, rangeConfig })
+ ) {
+ rawSetMessages(channel);
+ return;
+ }
+
+ channel.state.pruneOldest(maximumMessageLimit);
+
+ rawSetMessages(channel);
+ });
+
+ return { setMessages, viewabilityChangedCallback };
+}
diff --git a/package/yarn.lock b/package/yarn.lock
index f905a5a148..0e0cfcab86 100644
--- a/package/yarn.lock
+++ b/package/yarn.lock
@@ -2120,12 +2120,10 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
-"@shopify/flash-list@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-2.0.3.tgz#222427d1e09bf5cdd8a219d0a5a80f6f1d20465d"
- integrity sha512-jUlHuZFoPdqRCDvOqsb2YkTttRPyV8Tb/EjCx3gE2wjr4UTM+fE0Ltv9bwBg0K7yo/SxRNXaW7xu5utusRb0xA==
- dependencies:
- tslib "2.8.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==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
@@ -2956,13 +2954,13 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
-axios@^1.6.0:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.1.tgz#7c118d2146e9ebac512b7d1128771cdd738d11e3"
- integrity sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==
+axios@^1.12.2:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
+ integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
dependencies:
follow-redirects "^1.15.6"
- form-data "^4.0.0"
+ form-data "^4.0.4"
proxy-from-env "^1.1.0"
b4a@^1.6.4:
@@ -4651,16 +4649,6 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
-form-data@^4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
- integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.8"
- es-set-tostringtag "^2.1.0"
- mime-types "^2.1.12"
-
form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
@@ -8359,14 +8347,14 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
-stream-chat@^9.17.0:
- version "9.17.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2"
- integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg==
+stream-chat@^9.23.0:
+ version "9.23.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.23.0.tgz#e7e5cf729861597e7198907c1cab22a57d68a2fc"
+ integrity sha512-UW112HYsLnYb4RMIXBtAouNQCCe0weVzNivjezsw+JKK1b/TX0JLBi+wK25mBUEO+coOGKfXiye6IB3gao8ipw==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
- axios "^1.6.0"
+ axios "^1.12.2"
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
@@ -8699,16 +8687,16 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@2.8.1, tslib@^2.4.0:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
- integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
-
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+tslib@^2.4.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"