GetStream / react-native-bidirectional-infinite-scroll

📜 React Native - Bidirectional Infinite Smooth Scroll
https://getstream.github.io/react-native-bidirectional-infinite-scroll/
MIT License
236 stars 27 forks source link

Horizontal flatlist? #5

Open sfalihi opened 3 years ago

sfalihi commented 3 years ago

it throw error with horizontal=true does it support horizontal flatlist?

vishalnarkhede commented 3 years ago

@sfalihi No!! Not at the moment

vishalnarkhede commented 3 years ago

Going to keep this open to see if there is interest from more users for this!!

brobin002 commented 3 years ago

Yep I am very interrested in this feature...

day-me-an commented 3 years ago

Also interested.

Findiglay commented 3 years ago

I'm also interested in this feature for a horizontal time-based calendar-like component that allows users to scroll back and forth infinately.

the-kenneth commented 3 years ago

Definitely interested as well

bonnmh commented 3 years ago

Has this issue been resolved? :(

raajnadar-cb commented 3 years ago

Yes I need this feature ✌🏽

hampani commented 2 years ago

Interested!

faizansahib commented 2 years ago

just write horizontal in flatlist it will work

jacobmolby commented 2 years ago

just write horizontal in flatlist it will work

No it will not. onEndReached and onStartReached are not firing then.

DJWassink commented 2 years ago

Also interested 👍

AndrzejRokosz commented 2 years ago

yes it would be nice to have it 👍

Nautman commented 2 years ago

I'm also interested in this feature for a horizontal time-based calendar-like component that allows users to scroll back and forth infinately. @Findiglay

I am also facing the same problem. Did you by any chance find out how to have a bidirectional scroll in a calendar-like component?

pdpino commented 2 years ago

@Nautman @Findiglay or anyone interested in a calendar-like component with horizontal bidirectional infinite scroll*,

you can check this library https://github.com/hoangnm/react-native-week-view for a week view (disclaimer: I'm a maintainer), plus we are currently working on a month view: https://github.com/hoangnm/react-native-week-view/issues/95#issuecomment-1160885796 :smiley:

(*) for now is implemented with a FlatList plus some workarounds

hadnet commented 1 year ago

@vishalnarkhede please, man, add the horizontal feature!

krmao commented 8 months ago

here is it !

/* eslint-disable @typescript-eslint/no-unused-vars */
// noinspection JSUnusedLocalSymbols

/**
 * https://github.com/GetStream/react-native-bidirectional-infinite-scroll
 */
// noinspection JSUnusedGlobalSymbols

import React, { MutableRefObject, useRef, useState } from 'react';
import {
  ActivityIndicator,
  FlatList as FlatListType,
  FlatListProps,
  ScrollViewProps,
  StyleSheet,
  View,
} from 'react-native';
import { FlatList } from '@stream-io/flat-list-mvcp';

const styles = StyleSheet.create({
  indicatorContainer: {
    paddingVertical: 5,
    width: '100%',
  },
});

export type Props<T> = Omit<FlatListProps<T>, 'maintainVisibleContentPosition'> & {
  ref?: ((instance: FlatListType<T> | null) => void) | MutableRefObject<FlatListType<T> | null> | null;
  /**
   * Called once when the scroll position gets close to end of list. This must return a promise.
   * You can `onEndReachedThreshold` as distance from end of list, when this function should be called.
   */
  onEndReached: () => Promise<void>;
  /**
   * Called once when the scroll position gets close to begining of list. This must return a promise.
   * You can `onStartReachedThreshold` as distance from beginning of list, when this function should be called.
   */
  onStartReached: () => Promise<void>;
  /** Color for inline loading indicator */
  activityIndicatorColor?: string;
  /**
   * Enable autoScrollToTop.
   * In chat type applications, you want to auto scroll to bottom, when new message comes it.
   */
  enableAutoscrollToTop?: boolean;
  /**
   * If `enableAutoscrollToTop` is true, the scroll threshold below which auto scrolling should occur.
   */
  autoscrollToTopThreshold?: number;
  /** Scroll distance from beginning of list, when onStartReached should be called. */
  onStartReachedThreshold?: number;
  /**
   * Scroll distance from end of list, when onStartReached should be called.
   * Please note that this is different from onEndReachedThreshold of FlatList from react-native.
   */
  onEndReachedThreshold?: number;
  /** If true, inline loading indicators will be shown. Default - true */
  showDefaultLoadingIndicators?: boolean;
  /** Custom UI component for header inline loading indicator */
  HeaderLoadingIndicator?: React.ComponentType;
  /** Custom UI component for footer inline loading indicator */
  FooterLoadingIndicator?: React.ComponentType;
  /** Custom UI component for header indicator of FlatList. Only used when `showDefaultLoadingIndicators` is false */
  ListHeaderComponent?: React.ComponentType;
  /** Custom UI component for footer indicator of FlatList. Only used when `showDefaultLoadingIndicators` is false */
  ListFooterComponent?: React.ComponentType;
};
/**
 * Note:
 * - `onEndReached` and `onStartReached` must return a promise.
 * - `onEndReached` and `onStartReached` only get called once, per content length.
 * - maintainVisibleContentPosition is fixed, and can't be modified through props.
 * - doesn't accept `ListFooterComponent` via prop, since it is occupied by `FooterLoadingIndicator`.
 *    Set `showDefaultLoadingIndicators` to use `ListFooterComponent`.
 * - doesn't accept `ListHeaderComponent` via prop, since it is occupied by `HeaderLoadingIndicator`
 *    Set `showDefaultLoadingIndicators` to use `ListHeaderComponent`.
 */
export const BidirectionalFlatList = React.forwardRef(
  <T extends any>(
    props: Props<T>,
    ref: ((instance: FlatListType<T> | null) => void) | MutableRefObject<FlatListType<T> | null> | null,
  ) => {
    const {
      activityIndicatorColor = 'black',
      autoscrollToTopThreshold = 100,
      data,
      enableAutoscrollToTop,
      FooterLoadingIndicator,
      HeaderLoadingIndicator,
      ListHeaderComponent,
      ListFooterComponent,
      onEndReached = () => Promise.resolve(),
      onEndReachedThreshold = 10,
      onScroll,
      onStartReached = () => Promise.resolve(),
      onStartReachedThreshold = 10,
      showDefaultLoadingIndicators = true,
      horizontal = false,
    } = props;
    const [onStartReachedInProgress, setOnStartReachedInProgress] = useState(false);
    const [onEndReachedInProgress, setOnEndReachedInProgress] = useState(false);

    const onStartReachedTracker = useRef<Record<number, boolean>>({});
    const onEndReachedTracker = useRef<Record<number, boolean>>({});

    const onStartReachedInPromise = useRef<Promise<void> | null>(null);
    const onEndReachedInPromise = useRef<Promise<void> | null>(null);

    const maybeCallOnStartReached = () => {
      // If onStartReached has already been called for given data length, then ignore.
      if (data?.length && onStartReachedTracker.current[data.length]) {
        return;
      }

      if (data?.length) {
        onStartReachedTracker.current[data.length] = true;
      }

      setOnStartReachedInProgress(true);
      const p = () => {
        return new Promise<void>(resolve => {
          onStartReachedInPromise.current = null;
          setOnStartReachedInProgress(false);
          resolve();
        });
      };

      if (onEndReachedInPromise.current) {
        onEndReachedInPromise.current.finally(() => {
          onStartReachedInPromise.current = onStartReached().then(p);
        });
      } else {
        onStartReachedInPromise.current = onStartReached().then(p);
      }
    };

    const maybeCallOnEndReached = () => {
      // If onEndReached has already been called for given data length, then ignore.
      // console.log(
      //   '-- maybeCallOnEndReached',
      //   data?.length,
      //   data?.length ? onEndReachedTracker.current[data.length] : false,
      // );

      if (data?.length && onEndReachedTracker.current[data.length]) {
        return;
      }

      if (data?.length) {
        onEndReachedTracker.current[data.length] = true;
      }

      setOnEndReachedInProgress(true);
      const p = () => {
        return new Promise<void>(resolve => {
          onStartReachedInPromise.current = null;
          setOnEndReachedInProgress(false);
          resolve();
        });
      };

      if (onStartReachedInPromise.current) {
        onStartReachedInPromise.current.finally(() => {
          onEndReachedInPromise.current = onEndReached().then(p);
        });
      } else {
        onEndReachedInPromise.current = onEndReached().then(p);
      }
    };

    const handleScroll: ScrollViewProps['onScroll'] = event => {
      // Call the parent onScroll handler, if provided.
      onScroll?.(event);

      const offset = event.nativeEvent?.contentOffset?.x;
      const visibleLength = horizontal
        ? event.nativeEvent.layoutMeasurement.width
        : event.nativeEvent.layoutMeasurement.height;
      const contentLength = horizontal ? event.nativeEvent.contentSize.width : event.nativeEvent.contentSize.height;
      // Check if scroll has reached either start of end of list.
      const isScrollAtStart = offset < onStartReachedThreshold;
      const isScrollAtEnd = contentLength - visibleLength - offset < onEndReachedThreshold;
      // console.log('-- isScrollAtStart=', isScrollAtStart, 'isScrollAtEnd=', isScrollAtEnd, 'horizontal=', horizontal);

      if (isScrollAtStart) {
        maybeCallOnStartReached();
      }

      if (isScrollAtEnd) {
        maybeCallOnEndReached();
      }
    };

    const renderHeaderLoadingIndicator = () => {
      if (!showDefaultLoadingIndicators) {
        if (ListHeaderComponent) {
          return <ListHeaderComponent />;
        } else {
          return null;
        }
      }

      if (!onStartReachedInProgress) {
        return null;
      }

      if (HeaderLoadingIndicator) {
        return <HeaderLoadingIndicator />;
      }

      return (
        <View style={styles.indicatorContainer}>
          <ActivityIndicator size={'small'} color={activityIndicatorColor} />
        </View>
      );
    };

    const renderFooterLoadingIndicator = () => {
      if (!showDefaultLoadingIndicators) {
        if (ListFooterComponent) {
          return <ListFooterComponent />;
        } else {
          return null;
        }
      }

      if (!onEndReachedInProgress) {
        return null;
      }

      if (FooterLoadingIndicator) {
        return <FooterLoadingIndicator />;
      }

      return (
        <View style={styles.indicatorContainer}>
          <ActivityIndicator size={'small'} color={activityIndicatorColor} />
        </View>
      );
    };

    return (
      <>
        <FlatList<T>
          {...props}
          ref={ref}
          progressViewOffset={50}
          ListHeaderComponent={renderHeaderLoadingIndicator}
          ListFooterComponent={renderFooterLoadingIndicator}
          onEndReached={null}
          onScroll={handleScroll}
          /*
            // v0.73.0-rc.2
            // https://github.com/facebook/react-native/commit/1a1a79871b2d040764537433b431bc3b416904e3
            maintainVisibleContentPosition={{
            autoscrollToTopThreshold: enableAutoscrollToTop ? autoscrollToTopThreshold : undefined,
            minIndexForVisible: 1,
          }}*/
        />
      </>
    );
  },
) as unknown as BidirectionalFlatListType;

type BidirectionalFlatListType = <T extends any>(props: Props<T>) => React.ReactElement;