diff --git a/__tests__/usecase/buffers/ui/Buffer.tsx b/__tests__/usecase/buffers/ui/Buffer.tsx index d1cef97..3e841ae 100644 --- a/__tests__/usecase/buffers/ui/Buffer.tsx +++ b/__tests__/usecase/buffers/ui/Buffer.tsx @@ -2,11 +2,13 @@ import { fireEvent, render, screen, - waitFor + waitFor, + within } from '@testing-library/react-native'; import React from 'react'; import { ScrollView } from 'react-native'; import Buffer from '../../../../src/usecase/buffers/ui/Buffer'; +import { CellContainer } from '@shopify/flash-list'; jest.useFakeTimers(); @@ -60,53 +62,60 @@ describe(Buffer, () => { measureNickWidth(); - // Simulate layout event for the FlatList - const listElement = screen.getByLabelText('Message list'); - fireEvent(listElement, 'layout', { + // Simulate layout event for the ScrollView + const scrollView = screen.UNSAFE_getByType(ScrollView); + fireEvent(scrollView, 'layout', { nativeEvent: { layout: { height: 26.5, width: 1024, x: 0, y: 0 } } }); // Simulate layout event for first line - let message = screen.getByTestId('renderCell(0)'); - fireEvent(message, 'layout', { + let messageCell = screen + .UNSAFE_getAllByType(CellContainer) + .find((container) => within(container).queryByText('Second message')); + expect(messageCell).toBeDefined(); + fireEvent(messageCell, 'layout', { nativeEvent: { - layout: { height: 26.5, width: 1024, x: 0, y: 0 } + layout: { height: 50, width: 1024, x: 0, y: 0 } } }); + jest.advanceTimersToNextTimer(); + bufferRef.current?.scrollToLine('86c2fefd0'); expect(ScrollView.prototype.scrollTo).toHaveBeenNthCalledWith(1, { animated: false, - y: 0 + x: 0, + y: 50 }); - // This is effectively a no-op, we are already at 0, 0. However, scrollTo - // triggers this and the VirtualizedList will update internal state based - // on the layout properties, so fire it here as well. - fireEvent.scroll(listElement, { + fireEvent.scroll(scrollView, { nativeEvent: { contentInset: { bottom: 0, left: 0, right: 0, top: 0 }, - contentOffset: { x: 0, y: 0 }, - contentSize: { height: 26.5, width: 1024 }, + contentOffset: { x: 0, y: 50 }, + contentSize: { height: 50, width: 1024 }, layoutMeasurement: { height: 26.5, width: 1024 } } }); // Simulate layout event for second line - message = screen.getByTestId('renderCell(1)'); - fireEvent(message, 'layout', { + messageCell = screen + .UNSAFE_getAllByType(CellContainer) + .find((container) => within(container).queryByText('First message')); + expect(messageCell).toBeDefined(); + fireEvent(messageCell, 'layout', { nativeEvent: { - layout: { height: 26.5, width: 1024, x: 0, y: 26.5 } + layout: { height: 26.5, width: 1024, x: 0, y: 50 } } }); await waitFor(() => { expect(ScrollView.prototype.scrollTo).toHaveBeenNthCalledWith(2, { animated: false, - y: 26.5 + x: 0, + y: 50 }); }); }); diff --git a/package-lock.json b/package-lock.json index 6414b48..33cd57f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@react-native-async-storage/async-storage": "1.21.0", "@reduxjs/toolkit": "^1.9.7", + "@shopify/flash-list": "1.6.3", "date-fns": "^3.3.1", "emoji-regex": "^10.3.0", "expo": "~50.0.15", @@ -4451,6 +4452,25 @@ "join-component": "^1.1.0" } }, + "node_modules/@shopify/flash-list": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.6.3.tgz", + "integrity": "sha512-XM2iu4CeD9SOEUxaGG3UkxfUxGPWG9yacga1yQSgskAjUsRDFTsD3y4Dyon9n8MfDwgrRpEwuijd+7NeQQoWaQ==", + "dependencies": { + "recyclerlistview": "4.2.0", + "tslib": "2.4.0" + }, + "peerDependencies": { + "@babel/runtime": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@shopify/flash-list/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -12580,6 +12600,20 @@ "node": ">= 4" } }, + "node_modules/recyclerlistview": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.0.tgz", + "integrity": "sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A==", + "dependencies": { + "lodash.debounce": "4.0.8", + "prop-types": "15.8.1", + "ts-object-utils": "0.0.5" + }, + "peerDependencies": { + "react": ">= 15.2.1", + "react-native": ">= 0.30.0" + } + }, "node_modules/redent": { "version": "3.0.0", "dev": true, @@ -13843,6 +13877,11 @@ "version": "0.1.13", "license": "Apache-2.0" }, + "node_modules/ts-object-utils": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", + "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==" + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", diff --git a/package.json b/package.json index f81b5dd..ecbac82 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "react-redux": "^8.0.2", "redux": "^4.0.5", "redux-persist": "^6.0.0", - "redux-thunk": "^2.4.2" + "redux-thunk": "^2.4.2", + "@shopify/flash-list": "1.6.3" }, "devDependencies": { "@babel/core": "^7.19.3", diff --git a/src/usecase/buffers/ui/Buffer.tsx b/src/usecase/buffers/ui/Buffer.tsx index 03315da..d665a40 100644 --- a/src/usecase/buffers/ui/Buffer.tsx +++ b/src/usecase/buffers/ui/Buffer.tsx @@ -1,13 +1,7 @@ import * as React from 'react'; -import { - Button, - CellRendererProps, - FlatList, - ListRenderItem, - Text, - View -} from 'react-native'; +import { Button, Text, View } from 'react-native'; +import { FlashList, ListRenderItem } from '@shopify/flash-list'; import { useEffect, useState } from 'react'; import { ParseShape } from 'react-native-parsed-text'; import BufferLine from './BufferLine'; @@ -63,7 +57,7 @@ interface State { export default class Buffer extends React.PureComponent { static readonly DEFAULT_LINE_INCREMENT = 300; - linesList = React.createRef>(); + linesList = React.createRef>(); state: State = { nickWidth: 0, @@ -90,31 +84,7 @@ export default class Buffer extends React.PureComponent { } } - onCellLayout?: (index: number) => void; - - onScrollToIndexFailed = async (info: { - index: number; - highestMeasuredFrameIndex: number; - averageItemLength: number; - }) => { - this.linesList.current?.scrollToIndex({ - index: info.highestMeasuredFrameIndex, - animated: false - }); - - await new Promise((resolve) => { - this.onCellLayout = (index: number) => { - if (index > info.highestMeasuredFrameIndex) resolve(); - }; - }); - this.onCellLayout = undefined; - - this.linesList.current?.scrollToIndex({ - index: info.index, - animated: false, - viewPosition: 0.5 - }); - }; + resolveViewableItems?: () => void; scrollToLine = async (lineId: string) => { const index = this.props.lines.findIndex( @@ -122,6 +92,23 @@ export default class Buffer extends React.PureComponent { ); if (index < 0) return; + const listView = this.linesList.current?.recyclerlistview_unsafe; + if (!listView) return; + + while (!listView.getLayout(index)?.isOverridden) { + this.linesList.current?.scrollToIndex({ + index: index, + animated: false + }); + + console.log('waiting') + await new Promise((resolve) => { + this.resolveViewableItems = resolve; + }); + this.resolveViewableItems = undefined; + console.log('done') + } + this.linesList.current?.scrollToIndex({ index: index, animated: false, @@ -151,26 +138,6 @@ export default class Buffer extends React.PureComponent { ); }; - renderCell: React.FC> = ({ - index, - children, - onLayout, - style - }) => { - return ( - { - onLayout?.(event); - this.onCellLayout?.(index); - }} - > - {children} - - ); - }; - render() { const { bufferId, lines, fetchMoreLines, notificationLineId } = this.props; const resetList = notificationLineId && !this.state.listReset; @@ -191,7 +158,7 @@ export default class Buffer extends React.PureComponent { } return ( - { keyboardDismissMode="interactive" keyExtractor={keyExtractor} renderItem={this.renderBuffer} - initialNumToRender={35} - maxToRenderPerBatch={35} - removeClippedSubviews={true} - windowSize={15} - CellRendererComponent={this.renderCell} ListFooterComponent={
{ fetchMoreLines={fetchMoreLines} /> } - onScrollToIndexFailed={this.onScrollToIndexFailed} + onViewableItemsChanged={() => { + this.resolveViewableItems?.(); + }} + estimatedItemSize={26.5} /> ); } diff --git a/src/usecase/buffers/ui/BufferLine.tsx b/src/usecase/buffers/ui/BufferLine.tsx index 1de15aa..4789010 100644 --- a/src/usecase/buffers/ui/BufferLine.tsx +++ b/src/usecase/buffers/ui/BufferLine.tsx @@ -5,7 +5,6 @@ import { formatDateDayChange } from '../../../lib/helpers/date-formatter'; import { cof } from '../../../lib/weechat/colors'; import Default, { styles } from './themes/Default'; import { isSameDay } from 'date-fns'; -import { memo } from 'react'; interface Props { line: WeechatLine; @@ -56,4 +55,4 @@ const BufferLine: React.FC = ({ ); }; -export default memo(BufferLine); +export default BufferLine;