Skip to content

Commit

Permalink
DMAPP-152: Refactor Pagination in Channel Screen
Browse files Browse the repository at this point in the history
- Refactored the entire pagination logic for normal pagination to prevent skipping pages during loading.
- Updated search pagination to handle category selection and search keyword scenarios correctly.
  • Loading branch information
meKushdeepSingh committed Jan 15, 2025
1 parent b6f2039 commit 47830d4
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 153 deletions.
100 changes: 29 additions & 71 deletions src/components/ui/ChannelsDisplayer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import '@ethersproject/shims';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {FlatList, Image, StyleSheet, TextInput, View} from 'react-native';
import React, {useEffect, useMemo, useState} from 'react';
import {
FlatList,
Image,
Platform,
StyleSheet,
TextInput,
View,
} from 'react-native';
import {useSelector} from 'react-redux';
import StylishLabel from 'src/components/labels/StylishLabel';
import EPNSActivity from 'src/components/loaders/EPNSActivity';
Expand All @@ -12,7 +19,6 @@ import useSubscriptions from 'src/hooks/channel/useSubscriptions';
import {
Channel,
selectChannels,
selectChannelsReachedEnd,
selectIsLoadingSubscriptions,
} from 'src/redux/channelSlice';

Expand All @@ -21,87 +27,41 @@ import Globals from '../../Globals';
import {ChannelCategories} from './ChannelCategories';

const ChannelsDisplayer = () => {
const [searchTimer, setSearchTimer] = useState<NodeJS.Timeout>();

const DEBOUNCE_TIMEOUT = 500; //time in millisecond which we want to wait for then to finish typing

const [search, setSearch] = React.useState('');
const [showSearchResults, setShowSearchResults] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<string>(
Globals.CONSTANTS.ALL_CATEGORIES,
);

const channelResults = useSelector(selectChannels);
const channelsReachedEnd = useSelector(selectChannelsReachedEnd);
const {
loadMoreChannels,
loadSearchResults,
resetChannelData,
isLoadingChannels,
isLoadingSearchResults,
searchResults,
} = useChannels({tag: selectedCategory, showSearchResults});
const {isLoading, isLoadingMore, resetChannelData, loadMore} = useChannels({
tag: selectedCategory,
searchQuery: search,
});

const isLoadingSubscriptions = useSelector(selectIsLoadingSubscriptions);
const {refreshSubscriptions} = useSubscriptions();
const {userPushSDKInstance} = usePushApi();
const {openSheet} = useSheets();

const channels = useMemo(() => {
return showSearchResults ? searchResults : channelResults;
}, [showSearchResults, searchResults, channelResults]);

const endReached = useMemo(() => {
return showSearchResults ? true : channelsReachedEnd;
}, [showSearchResults, true, channelsReachedEnd]);

const isLoading = useMemo(() => {
return showSearchResults ? isLoadingSearchResults : isLoadingChannels;
}, [showSearchResults, isLoadingSearchResults, isLoadingChannels]);

const loadMore = useMemo(() => {
return showSearchResults ? () => {} : loadMoreChannels;
}, [showSearchResults]);

useEffect(() => {
if (userPushSDKInstance) {
refreshSubscriptions();
loadMore();
}
}, [userPushSDKInstance]);

const selectChannelForSettings = (channel: Channel) => {
openSheet({name: 'NFSettingsSheet', channel});
};

const searchForChannel = async (channelName: string) => {
if (channelName.trim() === '') {
setShowSearchResults(false);
return;
}
setShowSearchResults(true);
setSelectedCategory(Globals.CONSTANTS.ALL_CATEGORIES);
await loadSearchResults(channelName);
};

const handleChannelSearch = async (searchQuery: string) => {
try {
if (searchTimer) {
clearTimeout(searchTimer);
}
setSearch(searchQuery);
setSearchTimer(
setTimeout(() => {
searchForChannel(searchQuery);
}, DEBOUNCE_TIMEOUT),
);
} catch (e) {}
setSearch(searchQuery);
resetChannelData();
setSelectedCategory(Globals.CONSTANTS.ALL_CATEGORIES);
};

const handleCategoryChange = (category: string) => {
if (search.length || showSearchResults) {
if (search.length) {
setSearch('');
setShowSearchResults(false);
}
resetChannelData();
setSelectedCategory(category as string);
Expand All @@ -127,20 +87,20 @@ const ChannelsDisplayer = () => {
</View>

<ChannelCategories
disabled={isLoadingChannels}
disabled={isLoading}
onChangeCategory={handleCategoryChange}
value={selectedCategory}
/>

{channels.length === 0 && (
{channelResults.length === 0 && (
<View style={[styles.infodisplay]}>
{!isLoading && !isLoadingSubscriptions ? (
// Show channel not found label
<StylishLabel
style={styles.infoText}
fontSize={16}
title={
showSearchResults
search.length
? '[dg:No channels match your query, please search for another name/address]'
: '[dg:No results available.]'
}
Expand All @@ -159,26 +119,23 @@ const ChannelsDisplayer = () => {
</View>
)}

{channels.length !== 0 && !isLoadingSubscriptions && (
{channelResults.length !== 0 && !isLoadingSubscriptions && (
<FlatList
data={channels}
data={channelResults}
style={styles.channels}
contentContainerStyle={styles.channelListContentContainerStyle}
keyExtractor={item => item.channel.toString()}
keyExtractor={(item, index) => `${item.name}-${index}-channel-key`}
initialNumToRender={20}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={styles.seperator} />}
onEndReached={() => {
if (!endReached) {
loadMore();
}
}}
onEndReached={loadMore}
onEndReachedThreshold={0.8}
renderItem={({item: channel}) => (
<ChannelItem {...{channel, selectChannelForSettings}} />
)}
ListFooterComponent={() => {
return isLoading ? (
<View style={{paddingBottom: 80, marginTop: 20}}>
return isLoading || isLoadingMore ? (
<View style={styles.footerLoadingView}>
<EPNSActivity style={{}} size="small" />
</View>
) : null;
Expand All @@ -203,7 +160,7 @@ const styles = StyleSheet.create({
},
channelListContentContainerStyle: {
paddingTop: 10,
paddingBottom: 80, // Add some padding to the bottom to display last item content
paddingBottom: Platform.OS === 'android' ? 100 : 140, // Add some padding to the bottom to display last item content
},
infodisplay: {
width: '100%',
Expand Down Expand Up @@ -246,6 +203,7 @@ const styles = StyleSheet.create({
borderColor: '#E5E5E5',
marginVertical: 24,
},
footerLoadingView: {paddingVertical: 10},
});

export default ChannelsDisplayer;
145 changes: 84 additions & 61 deletions src/hooks/channel/useChannels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,130 @@ import Globals from 'src/Globals';
import GLOBALS from 'src/Globals';
import {usePushApi} from 'src/contexts/PushApiContext';
import envConfig from 'src/env.config';
import {
addChannels,
nextChannelsPage,
resetChannels,
selectChannelsPage,
selectChannelsReachedEnd,
setChannelsPage,
setChannelsReachedEnd,
} from 'src/redux/channelSlice';
import {addChannels, resetChannels, setChannels} from 'src/redux/channelSlice';

export type UseChannelsProps = {
tag: string;
showSearchResults: boolean;
searchQuery: string;
};

const useChannels = ({tag, showSearchResults}: UseChannelsProps) => {
const [isLoadingChannels, setChannelsLoading] = useState<boolean>(false);
const [isLoadingSearchResults, setSearchResultsLoading] =
useState<boolean>(false);
const [searchResults, setSearchResults] = useState([]);
const DEBOUNCE_TIMEOUT = 500; //time in millisecond which we want to wait for then to finish typing

const useChannels = ({tag, searchQuery}: UseChannelsProps) => {
const [searchTimer, setSearchTimer] = useState<NodeJS.Timeout>();
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [isEndReached, setIsEndReached] = useState(false); // this confirms that all data is loaded

const {userPushSDKInstance} = usePushApi();

const dispatch = useDispatch();

const channelsPage = useSelector(selectChannelsPage);
const channelsReachedEnd = useSelector(selectChannelsReachedEnd);

useEffect(() => {
if (
!channelsReachedEnd &&
!isLoadingChannels &&
channelsPage !== 0 &&
tag &&
!showSearchResults
) {
loadChannels({page: channelsPage});
console.log('first', {page, tag, searchQuery});
handleChannelInterval();
}, [page, tag, searchQuery]);

const handleChannelInterval = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
}, [channelsPage, tag]);
setSearchTimer(
setTimeout(() => {
getChannelsData();
}, DEBOUNCE_TIMEOUT),
);
};

const loadMoreChannels = () => {
if (channelsReachedEnd || isLoadingChannels) return;
dispatch(nextChannelsPage());
const getChannelsData = () => {
console.log('second', {page, tag, searchQuery});
if (searchQuery.trim().length) {
handleSearchAPI();
} else {
handleChannelAPI();
}
};

const loadChannels = async ({page}: {page: number}) => {
// If we have reached the end of the channels or loading, do nothing
if (channelsReachedEnd || isLoadingChannels) return;
setChannelsLoading(true);
const loadMore = () => {
if (!isEndReached && !isLoading && !isLoadingMore) {
setIsLoadingMore(true);
setPage(page + 1);
}
};

/*************************************************************/
/** This function will handle Normal API call with tags **/
/*************************************************************/
const handleChannelAPI = async () => {
try {
const apiURL = envConfig.EPNS_SERVER + envConfig.ENDPOINT_FETCH_CHANNELS;
let requestURL = `${apiURL}?limit=${GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL}&page=${page}`;
if (tag.length > 0 && tag !== Globals.CONSTANTS.ALL_CATEGORIES) {
requestURL = `${requestURL}&tag=${tag}`;
}
const resJson = await fetch(requestURL).then(response => response.json());
if (resJson.channels.length !== 0) {
if (page > 1) {
dispatch(addChannels(resJson.channels));
dispatch(setChannelsReachedEnd(false));
} else if (resJson.channels.length === 0) {
dispatch(setChannelsReachedEnd(true));
} else {
dispatch(setChannels(resJson.channels));
}
if (resJson.channels.length < GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL) {
setIsEndReached(true);
}
} catch (e) {
console.error(e);
} finally {
setChannelsLoading(false);
setIsLoading(false);
setIsLoadingMore(false);
}
};

/***************************************************/
/** This function will reset all channel data **/
/** Currently handled for onChangeCategory **/
/** This function will handle search API call **/
/***************************************************/
const resetChannelData = () => {
dispatch(setChannelsPage(1));
dispatch(setChannelsReachedEnd(false));
setChannelsLoading(false);
dispatch(resetChannels());
};

const loadSearchResults = async (query: string) => {
setSearchResultsLoading(true);
const handleSearchAPI = async () => {
try {
const results = await userPushSDKInstance?.channel.search(query, {
page: 1,
limit: GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL,
});
setSearchResults(results);
const query = searchQuery.trim();
if (query.length) {
const results = await userPushSDKInstance?.channel.search(query, {
page: page,
limit: GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL,
});
if (page > 1) {
dispatch(addChannels(results));
} else {
dispatch(setChannels(results));
}
if (results.length < GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL) {
setIsEndReached(true);
}
}
} catch (e) {
console.error(e);
} finally {
setSearchResultsLoading(false);
setIsLoading(false);
setIsLoadingMore(false);
}
};

/***************************************************/
/** This function will reset all channel data **/
/** Currently handled for onChangeCategory **/
/***************************************************/
const resetChannelData = () => {
setIsLoading(true);
setPage(1);
dispatch(resetChannels());
setIsLoadingMore(false);
setIsEndReached(false);
};

return {
loadMoreChannels,
loadSearchResults,
isLoading,
isLoadingMore,
loadMore,
resetChannelData,
isLoadingChannels,
isLoadingSearchResults,
searchResults,
};
};

Expand Down
Loading

0 comments on commit 47830d4

Please sign in to comment.