Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
aebc11f
feat: installed flashlist v2
OtavioStasiak Aug 25, 2025
500f2c7
fix: start initial tests
OtavioStasiak Aug 25, 2025
5fb7de1
fix: reverse
OtavioStasiak Aug 25, 2025
3696c61
feat: start adjust scroll to bottom
OtavioStasiak Aug 26, 2025
10e605d
fix: scroll to bottom
OtavioStasiak Aug 26, 2025
932ecbc
fix: keep scroll on botto
OtavioStasiak Aug 26, 2025
4e80135
fix: separator
OtavioStasiak Aug 26, 2025
478655c
fix: roomView ref
OtavioStasiak Aug 26, 2025
a4ec939
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Aug 26, 2025
e0c0fb0
fix: initial render
OtavioStasiak Aug 26, 2025
424e13d
fix: scrollHandler
OtavioStasiak Sep 1, 2025
cddd570
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Sep 1, 2025
ab2fc35
fix: scroll and treshhold
OtavioStasiak Sep 2, 2025
0e97e46
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Sep 2, 2025
8ab5d85
fix: list bottom
OtavioStasiak Sep 11, 2025
c0191b1
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Sep 11, 2025
af0d1b8
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Nov 18, 2025
e00ecfa
fix: list
OtavioStasiak Nov 24, 2025
6171749
created patch package
OtavioStasiak Dec 17, 2025
0700e0d
delete unused package
OtavioStasiak Dec 19, 2025
f4b4a6c
fix: scroll
OtavioStasiak Dec 19, 2025
1105dd5
code improvements
OtavioStasiak Dec 19, 2025
02a4591
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Dec 19, 2025
b04ba5a
fix: lint
OtavioStasiak Dec 19, 2025
61c87ca
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Dec 30, 2025
b7ce20e
fix: reduce flickering on load initial data
OtavioStasiak Dec 30, 2025
2d64d86
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Dec 31, 2025
238d11e
fix: yarn install
OtavioStasiak Dec 31, 2025
edda574
reduce flickering
OtavioStasiak Jan 5, 2026
599957e
Merge branch 'develop' into chore-try-flashlist-v2
OtavioStasiak Jan 5, 2026
9366e65
fix: keyboard
OtavioStasiak Jan 5, 2026
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 app/containers/message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined,
isEncrypted: this.isEncrypted
}}>
<MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} />
{/* @ts-ignore*/}
<Message
id={id}
Expand Down Expand Up @@ -507,7 +508,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
pinned={pinned}
autoTranslateLanguage={autoTranslateLanguage}
/>
<MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} />
</MessageContext.Provider>
);
}
Expand Down
67 changes: 41 additions & 26 deletions app/views/RoomView/List/components/List.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, { runOnJS, useAnimatedScrollHandler } from 'react-native-reanimated';
import React, { useEffect, useMemo, useState } from 'react';
import { type NativeScrollEvent, type NativeSyntheticEvent, StyleSheet, View, Keyboard } from 'react-native';
import { FlashList } from '@shopify/flash-list';

import { isIOS } from '../../../../lib/methods/helpers';
import scrollPersistTaps from '../../../../lib/methods/helpers/scrollPersistTaps';
import NavBottomFAB from './NavBottomFAB';
import { type IListProps } from '../definitions';
import NavBottomFAB from './NavBottomFAB';
import { SCROLL_LIMIT } from '../constants';
import { useRoomContext } from '../../context';

Expand All @@ -21,37 +19,54 @@ const styles = StyleSheet.create({
const List = ({ listRef, jumpToBottom, ...props }: IListProps) => {
const [visible, setVisible] = useState(false);
const { isAutocompleteVisible } = useRoomContext();
const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
if (event.contentOffset.y > SCROLL_LIMIT) {
runOnJS(setVisible)(true);
} else {
runOnJS(setVisible)(false);
}

const maintainVisibleContentPositionConfig = useMemo(
() => ({
autoscrollToBottomThreshold: 0.05,
startRenderingFromBottom: true,
animateAutoScrollToBottom: true
}),
[]
);

const onScrollHandler = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentScroll = Math.round(e.nativeEvent?.contentSize?.height) - Math.round(e.nativeEvent?.contentOffset.y);
const layoutLimit = e.nativeEvent.layoutMeasurement.height + SCROLL_LIMIT;

if (layoutLimit < currentScroll) {
setVisible(true);
} else {
setVisible(false);
}
});
};

// Scroll to end when keyboard opens
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
if (listRef?.current) {
listRef.current.scrollToEnd({ animated: true });
}
});

return () => {
keyboardDidShowListener.remove();
};
}, [listRef]);

return (
<View style={styles.list}>
{/* @ts-ignore */}
<Animated.FlatList
<FlashList
ref={listRef}
accessibilityElementsHidden={isAutocompleteVisible}
importantForAccessibility={isAutocompleteVisible ? 'no-hide-descendants' : 'yes'}
testID='room-view-messages'
ref={listRef}
keyExtractor={item => item.id}
contentContainerStyle={styles.contentContainer}
style={styles.list}
inverted
removeClippedSubviews={isIOS}
initialNumToRender={7}
onEndReachedThreshold={0.5}
maxToRenderPerBatch={5}
windowSize={10}
onScroll={onScrollHandler}
scrollEventThrottle={16}
onScroll={scrollHandler}
keyboardShouldPersistTaps='handled'
maintainVisibleContentPosition={maintainVisibleContentPositionConfig}
{...props}
{...scrollPersistTaps}
/>
<NavBottomFAB visible={visible} onPress={jumpToBottom} />
</View>
Expand Down
7 changes: 3 additions & 4 deletions app/views/RoomView/List/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { type RefObject } from 'react';
import { type FlatListProps } from 'react-native';
import { type FlatList } from 'react-native-gesture-handler';
import { type FlashListProps, type FlashListRef } from '@shopify/flash-list';

import { type TAnyMessageModel } from '../../../definitions';

export type TListRef = RefObject<FlatList<TAnyMessageModel> | null>;
export type TListRef = RefObject<FlashListRef<TAnyMessageModel>> | undefined;

export type TMessagesIdsRef = RefObject<string[]>;

export interface IListProps extends FlatListProps<TAnyMessageModel> {
export interface IListProps extends FlashListProps<TAnyMessageModel> {
listRef: TListRef;
jumpToBottom: () => void;
}
Expand Down
3 changes: 2 additions & 1 deletion app/views/RoomView/List/hooks/useMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ export const useMessages = ({
}

readThread();
setMessages(newMessages);
const reversedList = newMessages.reverse();
setMessages(reversedList);
messagesIds.current = newMessages.map(m => m.id);
});
}, [rid, tmid, showMessageInMainThread, serverVersion, hideSystemMessages]);
Expand Down
25 changes: 5 additions & 20 deletions app/views/RoomView/List/hooks/useScroll.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { type ViewToken, type ViewabilityConfigCallbackPairs } from 'react-native';
import { type ViewToken } from 'react-native';

import { type IListContainerRef, type IListProps, type TListRef, type TMessagesIdsRef } from '../definitions';
import { VIEWABILITY_CONFIG } from '../constants';
import { type IListContainerRef, type TListRef, type TMessagesIdsRef } from '../definitions';

export const useScroll = ({ listRef, messagesIds }: { listRef: TListRef; messagesIds: TMessagesIdsRef }) => {
const [highlightedMessageId, setHighlightedMessageId] = useState<string | null>(null);
Expand All @@ -21,21 +20,9 @@ export const useScroll = ({ listRef, messagesIds }: { listRef: TListRef; message
);

const jumpToBottom = useCallback(() => {
listRef.current?.scrollToOffset({ offset: -100 });
listRef?.current?.scrollToEnd();
}, [listRef]);

const onViewableItemsChanged: IListProps['onViewableItemsChanged'] = ({ viewableItems: vi }) => {
viewableItems.current = vi;
};

const viewabilityConfigCallbackPairs = useRef<ViewabilityConfigCallbackPairs>([
{ onViewableItemsChanged, viewabilityConfig: VIEWABILITY_CONFIG }
]);

const handleScrollToIndexFailed: IListProps['onScrollToIndexFailed'] = params => {
listRef.current?.scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
};

const setHighlightTimeout = () => {
if (highlightTimeout.current) {
clearTimeout(highlightTimeout.current);
Expand All @@ -59,7 +46,7 @@ export const useScroll = ({ listRef, messagesIds }: { listRef: TListRef; message

// if found message, scroll to it
if (index !== -1) {
listRef.current?.scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 });
listRef?.current?.scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 });

// wait for scroll animation to finish
await new Promise(res => setTimeout(res, 300));
Expand All @@ -76,7 +63,7 @@ export const useScroll = ({ listRef, messagesIds }: { listRef: TListRef; message
resolve();
} else {
// if message not on state yet, scroll to top, so it triggers onEndReached and try again
listRef.current?.scrollToEnd();
listRef?.current?.scrollToEnd();
await setTimeout(() => resolve(jumpToMessage(messageId)), 600);
}
});
Expand All @@ -98,8 +85,6 @@ export const useScroll = ({ listRef, messagesIds }: { listRef: TListRef; message
jumpToBottom,
jumpToMessage,
cancelJumpToMessage,
viewabilityConfigCallbackPairs,
handleScrollToIndexFailed,
highlightedMessageId
};
};
23 changes: 7 additions & 16 deletions app/views/RoomView/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@ const ListContainer = forwardRef<IListContainerRef, IListContainerProps>(
serverVersion,
hideSystemMessages
});
const {
jumpToBottom,
jumpToMessage,
cancelJumpToMessage,
viewabilityConfigCallbackPairs,
handleScrollToIndexFailed,
highlightedMessageId
} = useScroll({ listRef, messagesIds });
const { jumpToBottom, jumpToMessage, cancelJumpToMessage, highlightedMessageId } = useScroll({
listRef,
messagesIds
});

const onEndReached = useDebounce(() => {
fetchMessages();
Expand All @@ -33,7 +29,7 @@ const ListContainer = forwardRef<IListContainerRef, IListContainerProps>(
cancelJumpToMessage
}));

const renderItem: IListProps['renderItem'] = ({ item, index }) => renderRow(item, messages[index + 1], highlightedMessageId);
const renderItem: IListProps['renderItem'] = ({ item, index }) => renderRow(item, messages[index - 1], highlightedMessageId);

return (
<>
Expand All @@ -42,14 +38,9 @@ const ListContainer = forwardRef<IListContainerRef, IListContainerProps>(
listRef={listRef}
data={messages}
renderItem={renderItem}
onEndReached={onEndReached}
onScrollToIndexFailed={handleScrollToIndexFailed}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
onStartReached={onEndReached}
onStartReachedThreshold={0.9}
jumpToBottom={jumpToBottom}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 0
}}
/>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion app/views/RoomView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {

this.messageComposerRef = React.createRef();
this.list = React.createRef();
this.flatList = React.createRef();
this.flatList = React.createRef() as TListRef;
this.joinCode = React.createRef();
this.mounted = false;

Expand Down
16 changes: 4 additions & 12 deletions app/views/RoomsListView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useNavigation } from '@react-navigation/native';
import React, { memo, useContext, useEffect } from 'react';
import { BackHandler, FlatList, RefreshControl } from 'react-native';
import { BackHandler, RefreshControl } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { shallowEqual } from 'react-redux';
import { FlashList } from '@shopify/flash-list';

import ActivityIndicator from '../../containers/ActivityIndicator';
import BackgroundContainer from '../../containers/BackgroundContainer';
Expand All @@ -13,7 +14,7 @@ import { SupportedVersionsExpired } from '../../containers/SupportedVersions';
import i18n from '../../i18n';
import { MAX_SIDEBAR_WIDTH } from '../../lib/constants/tablet';
import { useAppSelector } from '../../lib/hooks/useAppSelector';
import { getRoomAvatar, getRoomTitle, getUidDirectMessage, isIOS, isRead, isTablet } from '../../lib/methods/helpers';
import { getRoomAvatar, getRoomTitle, getUidDirectMessage, isIOS, isRead } from '../../lib/methods/helpers';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import { events, logEvent } from '../../lib/methods/helpers/log';
import { getUserSelector } from '../../selectors/login';
Expand All @@ -22,13 +23,9 @@ import Container from './components/Container';
import ListHeader from './components/ListHeader';
import SectionHeader from './components/SectionHeader';
import RoomsSearchProvider, { RoomsSearchContext } from './contexts/RoomsSearchProvider';
import { useGetItemLayout } from './hooks/useGetItemLayout';
import { useHeader } from './hooks/useHeader';
import { useRefresh } from './hooks/useRefresh';
import { useSubscriptions } from './hooks/useSubscriptions';
import styles from './styles';

const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12;

const RoomsListView = memo(function RoomsListView() {
'use memo';
Expand All @@ -44,7 +41,6 @@ const RoomsListView = memo(function RoomsListView() {
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const navigation = useNavigation();
const { width } = useSafeAreaFrame();
const getItemLayout = useGetItemLayout();
const { subscriptions, loading } = useSubscriptions();
const subscribedRoom = useAppSelector(state => state.room.subscribedRoom);
const changingServer = useAppSelector(state => state.server.changingServer);
Expand Down Expand Up @@ -128,19 +124,15 @@ const RoomsListView = memo(function RoomsListView() {
}

return (
<FlatList
<FlashList
data={searchEnabled ? searchResults : subscriptions}
extraData={searchEnabled ? searchResults : subscriptions}
keyExtractor={item => `${item.rid}-${searchEnabled}`}
style={[styles.list, { backgroundColor: colors.surfaceRoom }]}
renderItem={renderItem}
ListHeaderComponent={ListHeader}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
keyboardShouldPersistTaps='always'
initialNumToRender={INITIAL_NUM_TO_RENDER}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={colors.fontSecondaryInfo} />}
windowSize={9}
onEndReachedThreshold={0.5}
keyboardDismissMode={isIOS ? 'on-drag' : 'none'}
/>
Expand Down
13 changes: 10 additions & 3 deletions metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ const config = {
unstable_allowRequireContext: true
},
resolver: {
// When running E2E tests, prioritize .mock.ts files for app code
// Note: react-native-mmkv's internal mock file is disabled via patch-package
sourceExts: process.env.RUNNING_E2E_TESTS === 'true' ? ['mock.ts', ...sourceExts] : sourceExts
sourceExts: process.env.RUNNING_E2E_TESTS ? ['mock.ts', ...sourceExts] : sourceExts,
// Force flash-list to use source files so our patch is applied
resolveRequest: (context, moduleName, platform) => {
// Redirect flash-list dist imports to src
if (moduleName.startsWith('@shopify/flash-list')) {
const newModuleName = moduleName.replace('@shopify/flash-list', '@shopify/flash-list/src');
return context.resolveRequest(context, newModuleName, platform);
}
return context.resolveRequest(context, moduleName, platform);
}
}
};

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@react-native-firebase/app": "^21.12.2",
"@react-native-firebase/crashlytics": "^21.12.2",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-native-picker/picker": "^2.11.0",
"@react-native-picker/picker": "2.11.0",
"@react-native/codegen": "^0.80.0",
"@react-navigation/drawer": "^7.5.5",
"@react-navigation/elements": "^2.6.1",
Expand All @@ -51,6 +51,7 @@
"@rocket.chat/mobile-crypto": "RocketChat/rocket.chat-mobile-crypto",
"@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile",
"@rocket.chat/ui-kit": "0.31.19",
"@shopify/flash-list": "^2.0.3",
"bytebuffer": "5.0.1",
"color2k": "1.2.4",
"dayjs": "^1.11.18",
Expand Down
Loading
Loading