Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG]: Resize mode cover with flat list #4166

Closed
Rossella-Mascia-Neosyn opened this issue Sep 12, 2024 · 7 comments
Closed

[BUG]: Resize mode cover with flat list #4166

Rossella-Mascia-Neosyn opened this issue Sep 12, 2024 · 7 comments
Labels
bug Missing info Some information from template are missing Missing repro Issue reproduction is missing Platform: Android

Comments

@Rossella-Mascia-Neosyn
Copy link

Rossella-Mascia-Neosyn commented Sep 12, 2024

Version

6.5.0

What platforms are you having the problem on?

Android

System Version

14

On what device are you experiencing the issue?

Real device

Architecture

Old architecture

What happened?

when the resize mode and cover the video does not always fit

/* eslint-disable react-hooks/exhaustive-deps */
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Dimensions, FlatList, ListRenderItemInfo, StyleSheet, Text, View, ViewabilityConfig, ViewToken} from 'react-native';

import {PostProps} from '../../types/Post';
import Spinner from './Spinner';
import VideoItem from './VideoItem';

export const SCREEN_HEIGHT = Dimensions.get('window').height - 194;
export const SCREEN_WIDTH = Dimensions.get('window').width;

const SpinnerComponent = (loading: boolean) => (loading ? <Spinner /> : null);

export type FeedReelScrollProps = {
  fetchData: (offset: number, limit: number) => Promise<PostProps[]>;
  initialData: PostProps[];
  limit: number;
};

const FeedReelScroll: React.FC<FeedReelScrollProps> = ({fetchData, initialData, limit}) => {
  const [data, setData] = useState<PostProps[]>();
  const [offset, setOffset] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0);
  const [isRefreshing, setsRefreshing] = useState<boolean>(false);

  useEffect(() => {
    setData(initialData);
    setHasMore(true);
    setOffset(0);
  }, [initialData]);

  const viewabilityConfig = useRef<ViewabilityConfig>({
    itemVisiblePercentThreshold: 80,
  }).current;

  const onViewableItemsChanged = useRef(
    debounce(({viewableItems}: {viewableItems: ViewToken[]}) => {
      if (viewableItems.length > 0) {
        setCurrentVisibleIndex(viewableItems[0].index || 0);
      }
    }),
  ).current;

  const getItemLayout = useCallback(
    (data: ArrayLike<PostProps> | null | undefined, index: number) => ({
      length: SCREEN_HEIGHT,
      offset: SCREEN_HEIGHT * index,
      index,
    }),
    [],
  );

  const removeDuplicates = (originalData: PostProps[]): PostProps[] => {
    const uniqueDataMap = new Map();
    originalData?.forEach(item => {
      if (!uniqueDataMap.has(item.id)) {
        uniqueDataMap.set(item.id, item);
      }
    });
    return Array.from(uniqueDataMap.values());
  };

  const fetchFeed = useCallback(
    debounce(async (fetchOffset: number) => {
      if (isLoading || !hasMore) {
        return;
      }
      setIsLoading(true);
      try {
        const newData = await fetchData(fetchOffset, limit);
        setOffset(fetchOffset + limit);
        if (newData?.length < limit) {
          setHasMore(false);
        }
        setData(prev => removeDuplicates([...(prev || []), ...newData]));
      } catch (error) {
        console.error('fetchFeed: ', error);
      } finally {
        setIsLoading(false);
      }
    }, 200),
    [isLoading, hasMore, data],
  );

  const keyExtractor = useCallback((item: PostProps) => item.id.toString(), []);

  const renderVideoList = useCallback(
    ({index, item}: ListRenderItemInfo<PostProps>) => {
      return (
        <View style={styles.post} key={index}>
          <VideoItem isVisible={currentVisibleIndex === index} item={item} preload={Math.abs(currentVisibleIndex + 5) >= index} />
          <View style={styles.overlayComponent}>{item.overlayComponent}</View>
          <View style={styles.bodyContent}>{item.bodyContent}</View>
        </View>
      );
    },
    [currentVisibleIndex],
  );

  const memoizedValue = useMemo(() => renderVideoList, [currentVisibleIndex, data]);

  const onRefresh = async () => {
    setData(initialData);
    setHasMore(true);
    setOffset(0);
    setsRefreshing(true);
    try {
      await fetchFeed(offset);
    } catch (error) {
      console.error('On refresh:', error);
    } finally {
      setsRefreshing(false);
    }
  };

  return (
    <FlatList
      data={data || []}
      keyExtractor={keyExtractor}
      renderItem={memoizedValue}
      viewabilityConfig={viewabilityConfig}
      onViewableItemsChanged={onViewableItemsChanged}
      pagingEnabled
      windowSize={2}
      disableIntervalMomentum
      removeClippedSubviews
      initialNumToRender={1}
      maxToRenderPerBatch={2}
      onEndReachedThreshold={0.1}
      decelerationRate="normal"
      showsVerticalScrollIndicator={false}
      scrollEventThrottle={16}
      getItemLayout={getItemLayout}
      onEndReached={async () => {
        await fetchFeed(offset);
      }}
      // ListFooterComponent={SpinnerComponent(isLoading)}
      onRefresh={onRefresh}
      refreshing={isRefreshing}
      style={{flex: 1}}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
  },
  post: {
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
    position: 'relative',
  },
  overlayComponent: {
    position: 'absolute',
  },
  bodyContent: {
    position: 'absolute',
    top: 10,
    left: 10,
  },
});
export default FeedReelScroll;

import {useIsFocused} from '@react-navigation/native';
import React, {memo, useEffect, useState} from 'react';
import {Dimensions, Pressable, StyleSheet, TouchableHighlight, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import Video from 'react-native-video';
import {PostProps} from '../../types/Post';

export type VideoItemProps = {
  item: PostProps;
  isVisible: boolean;
  preload: boolean;
};
export const SCREEN_HEIGHT = Dimensions.get('window').height - 196;
export const SCREEN_WIDTH = Dimensions.get('window').width;
const VideoItem: React.FC<VideoItemProps> = ({isVisible, item, preload}) => {
  const [paused, setPaused] = useState<string | null>(null);
  const [isPaused, setIsPaused] = useState(false);
  const [videoLoaded, setVideoLoaded] = useState(false);

  const isFocused = useIsFocused();

  useEffect(() => {
    setIsPaused(!isVisible);
    if (!isVisible) {
      setPaused(null);
      setVideoLoaded(false);
    }
  }, [isVisible]);

  useEffect(() => {
    if (!isFocused) {
      setIsPaused(true);
    }
    if (isFocused && isVisible) {
      setIsPaused(false);
    }
  }, [isFocused]);

  const handlerVideoLoad = () => {
    setVideoLoaded(true);
  };

  const _onPressPost = () => {
    const file = {
      id: item.id,
      source: item.video.source,
    };
    item.onPressPost && item.onPressPost(file);
  };

  return (
    <View style={styles.container}>
      <Pressable style={styles.videoContainer} onPress={_onPressPost}>
        {!videoLoaded && (
          <FastImage
            source={{
              uri: item.video.thumb,
              priority: FastImage.priority.high,
            }}
            resizeMode={FastImage.resizeMode.cover}
          />
        )}

        {isVisible || preload ? (
          <Video
            poster={item.video.poster}
            source={isVisible || preload ? {uri: item.video.source?.uri} : undefined}
            bufferConfig={{
              minBufferMs: 2500,
              maxBufferMs: 3000,
              bufferForPlaybackMs: 2500,
              bufferForPlaybackAfterRebufferMs: 2500,
              cacheSizeMB: 200,
            }}
            ignoreSilentSwitch="ignore"
            playWhenInactive={false}
            playInBackground={false}
            controls={false}
            disableFocus={true}
            style={styles.video}
            paused={isPaused}
            repeat
            hideShutterView
            minLoadRetryCount={5}
            resizeMode="cover"
            shutterColor="transparent"
            onReadyForDisplay={handlerVideoLoad}
          />
        ) : null}
      </Pressable>
    </View>
  );
};

const areEqual = (prevProps: VideoItemProps, nextProps: VideoItemProps) => {
  return prevProps.item.id === nextProps.item.id && prevProps.isVisible === nextProps.isVisible;
};

export default memo(VideoItem, areEqual);

const styles = StyleSheet.create({
  container: {
    position: 'relative',
    flex: 1,
  },
  video: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
  videoContainer: {
    flex: 1,
  },
});

Reproduction Link

repository link

Reproduction

Step to reproduce this bug are:

Copy link

Thank you for your issue report. Please note that the following information is missing or incomplete:

  • reproduction
  • reproduction link

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

@github-actions github-actions bot added Platform: Android Missing info Some information from template are missing Missing repro Issue reproduction is missing labels Sep 12, 2024
@freeboub
Copy link
Collaborator

freeboub commented Sep 14, 2024

@Rossella-Mascia-Neosyn Can you please provide your sample code in a git repository ? It is very easier for reproduction ...
Notice that I am currently working on optimizing this use case, I will have a look, I maybe already have a local fix

Copy link

Thank you for your issue report. Please note that the following information is missing or incomplete:

  • reproduction
  • reproduction link

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

Copy link

Thank you for your issue report. Please note that the following information is missing or incomplete:

  • reproduction

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

@freeboub
Copy link
Collaborator

freeboub commented Sep 18, 2024

@Rossella-Mascia-Neosyn Thank you for the sample, I am not sure to understand the issue, is this happening randomly while navigating between videos, or is it only on initial load ?
I see something ugly on initial load, but not while navigating ...
Maybe is it possible to have a video, it would be easier to understand the issue

@freeboub
Copy link
Collaborator

freeboub commented Oct 1, 2024

duplicates: #4202

@freeboub
Copy link
Collaborator

freeboub commented Oct 5, 2024

I close this ticket, let's continue in #4202

@freeboub freeboub closed this as completed Oct 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Missing info Some information from template are missing Missing repro Issue reproduction is missing Platform: Android
Projects
None yet
Development

No branches or pull requests

2 participants