Skip to content

Commit

Permalink
Add Optional InfiniteList Support to AgendaList (wix#2270)
Browse files Browse the repository at this point in the history
* Start working on migration of the agenda list

* Use prop useInfiniteList to enable the new agnedaList

* Remove console.log

* Implement update date in scroll

* Mark the new field as experimental

* Add support on onEndReached, set debounce for date updates on viewed item change

* remove nl

* Add support on onEndReachedThreshold

* Fix Inbar PR comments

* Add disableScrollOnDataChange on InfiniteListProps

* Add comment on _onEndReached applying callback

* use disableScrollOnDataChange as a variable

* remove props from all places
  • Loading branch information
yakirza17 authored Jul 5, 2023
1 parent 93d6423 commit 72f9b5d
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 11 deletions.
19 changes: 16 additions & 3 deletions src/expandableCalendar/agendaList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {UpdateSources, todayString} from './commons';
import constants from '../commons/constants';
import styleConstructor from './style';
import Context from './Context';
import InfiniteAgendaList from './infiniteAgendaList';

const viewabilityConfig = {
itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible
Expand All @@ -59,6 +60,14 @@ export interface AgendaListProps extends SectionListProps<any, DefaultSectionT>
viewOffset?: number;
/** enable scrolling the agenda list to the next date with content when pressing a day without content */
scrollToNextEvent?: boolean;
/**
* @experimental
* If defined, uses InfiniteList instead of SectionList. This feature is experimental and subject to change.
*/
infiniteListProps?: {
itemHeight?: number;
titleHeight?: number;
};
}

/**
Expand All @@ -68,6 +77,10 @@ export interface AgendaListProps extends SectionListProps<any, DefaultSectionT>
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
*/
const AgendaList = (props: AgendaListProps) => {
if (props.infiniteListProps) {
return <InfiniteAgendaList {...props} />;
}

const {
theme,
sections,
Expand All @@ -89,7 +102,7 @@ const AgendaList = (props: AgendaListProps) => {
} = props;

const {date, updateSource, setDate, setDisabled} = useContext(Context);

const style = useRef(styleConstructor(theme));
const list = useRef<any>();
const _topSection = useRef(sections[0]?.title);
Expand Down Expand Up @@ -271,15 +284,15 @@ const AgendaList = (props: AgendaListProps) => {

interface AgendaSectionHeaderProps {
title?: string;
onLayout: TextProps['onLayout'];
onLayout?: TextProps['onLayout'];
style: TextProps['style'];
}

function areTextPropsEqual(prev: AgendaSectionHeaderProps, next: AgendaSectionHeaderProps): boolean {
return isEqual(prev.style, next.style) && prev.title === next.title;
}

const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
export const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
return (
<Text allowFontScaling={false} style={props.style} onLayout={props.onLayout}>
{props.title}
Expand Down
258 changes: 258 additions & 0 deletions src/expandableCalendar/infiniteAgendaList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import PropTypes from 'prop-types';

import isUndefined from 'lodash/isUndefined';
import debounce from 'lodash/debounce';
import InfiniteList from '../infinite-list';

import XDate from 'xdate';

import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {
DefaultSectionT,
SectionListData,
} from 'react-native';

import {useDidUpdate} from '../hooks';
import {getMoment} from '../momentResolver';
import {isGTE, isToday} from '../dateutils';
import {getDefaultLocale} from '../services';
import {UpdateSources, todayString} from './commons';
import styleConstructor from './style';
import Context from './Context';
import constants from "../commons/constants";
import {parseDate} from "../interface";
import {LayoutProvider} from "recyclerlistview/dist/reactnative/core/dependencies/LayoutProvider";
import {AgendaListProps, AgendaSectionHeader} from "./agendaList";

/**
* @description: AgendaList component that use InfiniteList to improve performance
* @note: Should be wrapped with 'CalendarProvider'
* @extends: InfiniteList
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
*/
const InfiniteAgendaList = (props: AgendaListProps) => {
const {
theme,
sections,
scrollToNextEvent,
avoidDateUpdates,
onScroll,
renderSectionHeader,
sectionStyle,
dayFormatter,
dayFormat = 'dddd, MMM d',
useMoment,
markToday = true,
infiniteListProps,
renderItem,
onEndReached,
onEndReachedThreshold
} = props;

const {date, updateSource, setDate} = useContext(Context);

const style = useRef(styleConstructor(theme));
const list = useRef<any>();
const _topSection = useRef(sections[0]?.title);
const didScroll = useRef(false);
const sectionScroll = useRef(false);
const [data, setData] = useState([] as any[]);

useEffect(() => {
const items = sections.reduce((acc: any, cur: any) => {
return [...acc, {title: cur.title, isTitle: true}, ...cur.data];
}, []);
setData(items);

if (date !== _topSection.current) {
setTimeout(() => {
scrollToSection(date);
}, 500);
}
}, [sections]);

useDidUpdate(() => {
// NOTE: on first init data should set first section to the current date!!!
if (updateSource !== UpdateSources.LIST_DRAG && updateSource !== UpdateSources.CALENDAR_INIT) {
scrollToSection(date);
}
}, [date]);

const getSectionIndex = (date: string) => {
let dataIndex = 0;

for (let i = 0; i < sections.length; i++) {
if (sections[i].title === date) {
return dataIndex;
}
dataIndex += sections[i].data.length + 1;
}
};

const getNextSectionIndex = (date: string) => {
const cur = new XDate(date);
let dataIndex = 0;

for (let i = 0; i < sections.length; i++) {
const titleDate = parseDate(sections[i].title);
if (isGTE(titleDate,cur)) {
return dataIndex;
}
dataIndex += sections[i].data.length + 1;
}
};

const getSectionTitle = useCallback((title: string) => {
if (!title) return;

let sectionTitle = title;

if (dayFormatter) {
sectionTitle = dayFormatter(title);
} else if (dayFormat) {
if (useMoment) {
const moment = getMoment();
sectionTitle = moment(title).format(dayFormat);
} else {
sectionTitle = new XDate(title).toString(dayFormat);
}
}

if (markToday) {
const string = getDefaultLocale().today || todayString;
const today = isToday(title);
sectionTitle = today ? `${string}, ${sectionTitle}` : sectionTitle;
}

return sectionTitle;
}, []);

const scrollToSection = useCallback(debounce((d) => {
const sectionIndex = scrollToNextEvent ? getNextSectionIndex(d) : getSectionIndex(d);
if (isUndefined(sectionIndex)) {
return;
}

if (list?.current && sectionIndex !== undefined) {
sectionScroll.current = true; // to avoid setDate() in _onVisibleIndicesChanged
_topSection.current = sections[findItemTitleIndex(sectionIndex)]?.title;

list.current?.scrollToIndex(sectionIndex, true);
}
}, 1000, {leading: false, trailing: true}), [ sections]);

const layoutProvider = useMemo(
() => new LayoutProvider(
(index) => data[index]?.isTitle ? 'title': 'page',
(type, dim) => {
dim.width = constants.screenWidth;
dim.height = type === 'title' ? infiniteListProps?.titleHeight ?? 60 : infiniteListProps?.itemHeight ?? 80;
}
),
[data]
);

const _onScroll = useCallback((rawEvent: any) => {
if (!didScroll.current) {
didScroll.current = true;
scrollToSection.cancel();
}

// Convert to a format similar to NativeSyntheticEvent<NativeScrollEvent>
const event = {
nativeEvent: {
contentOffset: rawEvent.nativeEvent.contentOffset,
layoutMeasurement: rawEvent.nativeEvent.layoutMeasurement,
contentSize: rawEvent.nativeEvent.contentSize,
},
};
onScroll?.(event as any);
}, [onScroll]);

const _onVisibleIndicesChanged = useCallback(debounce((all: number[]) => {
if (all && all.length && !sectionScroll.current) {
const topItemIndex = all[0];
const topSection = data[findItemTitleIndex(topItemIndex)];
if (topSection && topSection !== _topSection.current) {
_topSection.current = topSection.title;
if (didScroll.current && !avoidDateUpdates) {
// to avoid setDate() on first load (while setting the initial context.date value)
setDate?.(topSection.title, UpdateSources.LIST_DRAG);
}
}
}
}, 1000, {leading: false, trailing: true},), [avoidDateUpdates, setDate, data]);

const findItemTitleIndex = useCallback((itemIndex: number) => {
let titleIndex = itemIndex;
while (titleIndex > 0 && !data[titleIndex]?.isTitle) {
titleIndex--;
}

return titleIndex;
}, [data]);

const _onMomentumScrollEnd = useCallback(() => {
sectionScroll.current = false;
}, []);

const headerTextStyle = useMemo(() => [style.current.sectionText, sectionStyle], [sectionStyle]);

const _renderSectionHeader = useCallback((info: {section: SectionListData<any, DefaultSectionT>}) => {
const title = info?.section?.title;

if (renderSectionHeader) {
return renderSectionHeader(title);
}

const headerTitle = getSectionTitle(title);
return <AgendaSectionHeader title={headerTitle} style={headerTextStyle}/>;
}, [headerTextStyle]);

const _renderItem = useCallback((_type: any, item: any) => {
if (item?.isTitle) {
return _renderSectionHeader({section: item});
}

if (renderItem) {
return renderItem({item} as any);
}

return <></>;
}, [renderItem]);

const _onEndReached = useCallback(() => {
if (onEndReached) {
onEndReached({distanceFromEnd: 0}); // The RecyclerListView doesn't provide the distanceFromEnd, so we just pass 0
}
}, [onEndReached]);

return (
<InfiniteList
ref={list}
renderItem={_renderItem}
data={data}
style={style.current.container}
layoutProvider={layoutProvider}
onScroll={_onScroll}
onVisibleIndicesChanged={_onVisibleIndicesChanged}
scrollViewProps={{onMomentumScrollEnd: _onMomentumScrollEnd}}
onEndReached={_onEndReached}
onEndReachedThreshold={onEndReachedThreshold as number | undefined}
disableScrollOnDataChange
/>
);
};


export default InfiniteAgendaList;

InfiniteAgendaList.displayName = 'InfiniteAgendaList';
InfiniteAgendaList.propTypes = {
dayFormat: PropTypes.string,
dayFormatter: PropTypes.func,
useMoment: PropTypes.bool,
markToday: PropTypes.bool,
sectionStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
avoidDateUpdates: PropTypes.bool
};
Loading

0 comments on commit 72f9b5d

Please sign in to comment.