From 3d6d3d9b039f91288b7d1a2df3779e7cc333e47a Mon Sep 17 00:00:00 2001 From: Kushdeep Singh <63536883+meKushdeepSingh@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:13:09 +0530 Subject: [PATCH] DMAPP-149: Integrate channel categories (#151) * DMAPP-149: Channel Categories - Completed UI integration - Completed API integration - Work in progress: Handling search and category filter functionality * DMAPP-149: Integrate channel categories - Implemented handling of search and category filters. - Ensured reset of the previous filter when a new one is applied. * DMAPP-149: Integrate channel categories enhancements - Disabled onPress event for the active category. - Added a new message to display when data is unavailable. * DMAPP-149: Minor fix in Pill component * DMAPP-149: Code level changes - Minor changes in Pill and channelsDisplayer component --- src/Globals.ts | 9 ++++ src/components/pill/Pill.tsx | 55 ++++++++++++++++++++++ src/components/pill/Pill.types.ts | 11 +++++ src/components/pill/index.ts | 2 + src/components/ui/ChannelCategories.tsx | 52 ++++++++++++++++++++ src/components/ui/ChannelsDisplayer.tsx | 38 +++++++++++++-- src/env.config.js | 2 + src/hooks/channel/useChannelCategories.tsx | 46 ++++++++++++++++++ src/hooks/channel/useChannels.tsx | 38 +++++++++++++-- src/redux/channelSlice.ts | 4 ++ 10 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/components/pill/Pill.tsx create mode 100644 src/components/pill/Pill.types.ts create mode 100644 src/components/pill/index.ts create mode 100644 src/components/ui/ChannelCategories.tsx create mode 100644 src/hooks/channel/useChannelCategories.tsx diff --git a/src/Globals.ts b/src/Globals.ts index 7e4f195f4..19a50918a 100644 --- a/src/Globals.ts +++ b/src/Globals.ts @@ -16,6 +16,7 @@ export default { ENDPOINT_GET_FEEDS: '/feeds/get_feeds', ENDPOINT_GET_SPAM_FEEDS: '/feeds/get_spam_feeds', ENDPOINT_FETCH_CHANNELS: '/v1/channels', + ENDPOINT_FETCH_CHANNEL_CATEGORIES: '/v1/channels/tags/all', ENDPOINT_SEARCH_CHANNELS: '/v1/channels/search', ENDPOINT_FETCH_SUBSCRIPTION: '/channels/_is_user_subscribed', ENDPOINT_SUBSCRIBE_OFFCHAIN: '/channels/subscribe_offchain', @@ -82,6 +83,7 @@ export default { 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAvklEQVR4AcXBsW2FMBiF0Y8r3GQb6jeBxRauYRpo4yGQkMd4A7kg7Z/GUfSKe8703fKDkTATZsJsrr0RlZSJ9r4RLayMvLmJjnQS1d6IhJkwE2bT13U/DBzp5BN73xgRZsJMmM1HOolqb/yWiWpvjJSUiRZWopIykTATZsJs5g+1N6KSMiO1N/5DmAkzYTa9Lh6MhJkwE2ZzSZlo7xvRwson3txERzqJhJkwE2bT6+JhoKTMJ2pvjAgzYSbMfgDlXixqjH6gRgAAAABJRU5ErkJggg==', PASSCODE_LENGTH: 6, + ALL_CATEGORIES: 'All', }, ADJUSTMENTS: { @@ -165,6 +167,13 @@ export default { // Notification IC_NOTIFICATION: '#e20880', + + // Pill + PILL_BG_DEFAULT: '#F5F6F8', + PILL_TEXT_DEFAULT: '#17181B', + + // Border + BORDER_DEFAULT: '#C4CBD5', }, SCREENS: { WELCOME: 'Welcome', diff --git a/src/components/pill/Pill.tsx b/src/components/pill/Pill.tsx new file mode 100644 index 000000000..c2dbe012a --- /dev/null +++ b/src/components/pill/Pill.tsx @@ -0,0 +1,55 @@ +import React, {FC} from 'react'; +import {Pressable, StyleSheet, Text} from 'react-native'; +import Globals from 'src/Globals'; + +import {PillProps} from '.'; + +const Pill: FC = ({data, value, onChange, disabled}) => { + const isActive = data.value === value; + return ( + onChange(data)} + style={[ + styles.mainView, + isActive ? styles.activeView : styles.inactiveView, + ]}> + + {data.label} + + + ); +}; + +export {Pill}; + +const styles = StyleSheet.create({ + mainView: { + height: 40, + borderRadius: 48, + paddingHorizontal: 16, + paddingVertical: 8, + marginRight: 8, + }, + activeView: { + backgroundColor: Globals.COLORS.BLACK, + }, + inactiveView: { + backgroundColor: Globals.COLORS.PILL_BG_DEFAULT, + }, + labelText: { + fontSize: 14, + fontWeight: '500', + lineHeight: 20, + }, + activeText: { + color: Globals.COLORS.WHITE, + }, + inactiveText: { + color: Globals.COLORS.PILL_TEXT_DEFAULT, + }, +}); diff --git a/src/components/pill/Pill.types.ts b/src/components/pill/Pill.types.ts new file mode 100644 index 000000000..3f4d3d1b5 --- /dev/null +++ b/src/components/pill/Pill.types.ts @@ -0,0 +1,11 @@ +export type PillProps = { + data: PillData; + value: string | number; + onChange: (value: PillData) => void; + disabled?: boolean; +}; + +export type PillData = { + label: string; + value: string | number; +}; diff --git a/src/components/pill/index.ts b/src/components/pill/index.ts new file mode 100644 index 000000000..8a4743d82 --- /dev/null +++ b/src/components/pill/index.ts @@ -0,0 +1,2 @@ +export * from './Pill'; +export * from './Pill.types'; diff --git a/src/components/ui/ChannelCategories.tsx b/src/components/ui/ChannelCategories.tsx new file mode 100644 index 000000000..5690aa415 --- /dev/null +++ b/src/components/ui/ChannelCategories.tsx @@ -0,0 +1,52 @@ +import React, {FC, useState} from 'react'; +import {ScrollView, StyleSheet, View} from 'react-native'; +import Globals from 'src/Globals'; +import {useChannelCategories} from 'src/hooks/channel/useChannelCategories'; + +import {Pill} from '../pill'; + +type ChannelCategoriesProps = { + onChangeCategory: (category: string) => void; + value: string; + disabled: boolean; +}; + +const ChannelCategories: FC = ({ + onChangeCategory, + value, + disabled, +}) => { + const {isLoading, channelCategories} = useChannelCategories(); + + if (!isLoading && channelCategories?.length > 0) { + return ( + + + {channelCategories.map((item, index) => ( + { + onChangeCategory(category.value as string); + }} + /> + ))} + + + ); + } + return null; +}; + +export {ChannelCategories}; + +const styles = StyleSheet.create({ + mainView: { + height: 40, + width: '100%', + flexDirection: 'row', + marginBottom: 16, + }, +}); diff --git a/src/components/ui/ChannelsDisplayer.tsx b/src/components/ui/ChannelsDisplayer.tsx index 2a1082d36..ac92c56f8 100644 --- a/src/components/ui/ChannelsDisplayer.tsx +++ b/src/components/ui/ChannelsDisplayer.tsx @@ -17,24 +17,30 @@ import { } from 'src/redux/channelSlice'; import GLOBALS from '../../Globals'; +import Globals from '../../Globals'; +import {ChannelCategories} from './ChannelCategories'; const ChannelsDisplayer = () => { const [searchTimer, setSearchTimer] = useState(); const DEBOUNCE_TIMEOUT = 500; //time in millisecond which we want to wait for then to finish typing - const [search, setSearch] = React.useState(''); + const [search, setSearch] = React.useState(''); const [showSearchResults, setShowSearchResults] = useState(false); + const [selectedCategory, setSelectedCategory] = useState( + Globals.CONSTANTS.ALL_CATEGORIES, + ); const channelResults = useSelector(selectChannels); const channelsReachedEnd = useSelector(selectChannelsReachedEnd); const { loadMoreChannels, loadSearchResults, + resetChannelData, isLoadingChannels, isLoadingSearchResults, searchResults, - } = useChannels(); + } = useChannels({tag: selectedCategory, showSearchResults}); const isLoadingSubscriptions = useSelector(selectIsLoadingSubscriptions); const {refreshSubscriptions} = useSubscriptions(); @@ -74,6 +80,7 @@ const ChannelsDisplayer = () => { return; } setShowSearchResults(true); + setSelectedCategory(Globals.CONSTANTS.ALL_CATEGORIES); await loadSearchResults(channelName); }; @@ -91,6 +98,15 @@ const ChannelsDisplayer = () => { } catch (e) {} }; + const handleCategoryChange = (category: string) => { + if (search.length || showSearchResults) { + setSearch(''); + setShowSearchResults(false); + } + resetChannelData(); + setSelectedCategory(category as string); + }; + return ( <> @@ -110,6 +126,12 @@ const ChannelsDisplayer = () => { /> + + {channels.length === 0 && ( {!isLoading && !isLoadingSubscriptions ? ( @@ -117,7 +139,11 @@ const ChannelsDisplayer = () => { ) : ( // Show channel fetching label @@ -137,7 +163,7 @@ const ChannelsDisplayer = () => { item.channel.toString()} initialNumToRender={20} showsVerticalScrollIndicator={false} @@ -175,6 +201,10 @@ const styles = StyleSheet.create({ flex: 1, width: '100%', }, + channelListContentContainerStyle: { + paddingTop: 10, + paddingBottom: 80, // Add some padding to the bottom to display last item content + }, infodisplay: { width: '100%', justifyContent: 'center', diff --git a/src/env.config.js b/src/env.config.js index 5f457653d..e8dbb34f9 100644 --- a/src/env.config.js +++ b/src/env.config.js @@ -28,6 +28,7 @@ const { ENDPOINT_GET_FEEDS, ENDPOINT_GET_SPAM_FEEDS, ENDPOINT_FETCH_CHANNELS, + ENDPOINT_FETCH_CHANNEL_CATEGORIES, ENDPOINT_FETCH_SUBSCRIPTION, ENDPOINT_SUBSCRIBE_OFFCHAIN, ENDPOINT_UNSUBSCRIBE_OFFCHAIN, @@ -60,6 +61,7 @@ export default { ENDPOINT_GET_FEEDS, ENDPOINT_GET_SPAM_FEEDS, ENDPOINT_FETCH_CHANNELS, + ENDPOINT_FETCH_CHANNEL_CATEGORIES, ENDPOINT_FETCH_SUBSCRIPTION, ENDPOINT_SUBSCRIBE_OFFCHAIN, ENDPOINT_UNSUBSCRIBE_OFFCHAIN, diff --git a/src/hooks/channel/useChannelCategories.tsx b/src/hooks/channel/useChannelCategories.tsx new file mode 100644 index 000000000..490e7d02e --- /dev/null +++ b/src/hooks/channel/useChannelCategories.tsx @@ -0,0 +1,46 @@ +import {useEffect, useState} from 'react'; +import Globals from 'src/Globals'; +import {PillData} from 'src/components/pill'; +import envConfig from 'src/env.config'; + +type ChannelCategoriesReturnType = { + isLoading: boolean; + channelCategories: PillData[]; +}; + +const useChannelCategories = (): ChannelCategoriesReturnType => { + const [channelCategories, setChannelCategories] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + getChannelCategories(); + }, []); + + const getChannelCategories = async () => { + setIsLoading(true); + try { + const requestURL = + envConfig.EPNS_SERVER + envConfig.ENDPOINT_FETCH_CHANNEL_CATEGORIES; + const resJson = await fetch(requestURL).then(response => response.json()); + // Modify data for pill component + const modifiedData = resJson?.tags?.tags.map((category: string) => ({ + label: category, + value: category, + })); + setChannelCategories([ + { + label: Globals.CONSTANTS.ALL_CATEGORIES, + value: Globals.CONSTANTS.ALL_CATEGORIES, + }, + ...modifiedData, + ]); + } catch (e) { + } finally { + setIsLoading(false); + } + }; + + return {isLoading, channelCategories}; +}; + +export {useChannelCategories}; diff --git a/src/hooks/channel/useChannels.tsx b/src/hooks/channel/useChannels.tsx index 6df32a113..81e590ec7 100644 --- a/src/hooks/channel/useChannels.tsx +++ b/src/hooks/channel/useChannels.tsx @@ -1,17 +1,25 @@ import {useEffect, useState} from 'react'; import {useDispatch, useSelector} from 'react-redux'; +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'; -const useChannels = () => { +export type UseChannelsProps = { + tag: string; + showSearchResults: boolean; +}; + +const useChannels = ({tag, showSearchResults}: UseChannelsProps) => { const [isLoadingChannels, setChannelsLoading] = useState(false); const [isLoadingSearchResults, setSearchResultsLoading] = useState(false); @@ -24,10 +32,16 @@ const useChannels = () => { const channelsReachedEnd = useSelector(selectChannelsReachedEnd); useEffect(() => { - if (!channelsReachedEnd && !isLoadingChannels && channelsPage !== 0) { + if ( + !channelsReachedEnd && + !isLoadingChannels && + channelsPage !== 0 && + tag && + !showSearchResults + ) { loadChannels({page: channelsPage}); } - }, [channelsPage]); + }, [channelsPage, tag]); const loadMoreChannels = () => { if (channelsReachedEnd || isLoadingChannels) return; @@ -40,7 +54,10 @@ const useChannels = () => { setChannelsLoading(true); try { const apiURL = envConfig.EPNS_SERVER + envConfig.ENDPOINT_FETCH_CHANNELS; - const requestURL = `${apiURL}?limit=${GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL}&page=${page}`; + 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) { dispatch(addChannels(resJson.channels)); @@ -49,11 +66,23 @@ const useChannels = () => { dispatch(setChannelsReachedEnd(true)); } } catch (e) { + console.error(e); } finally { setChannelsLoading(false); } }; + /***************************************************/ + /** This function will reset all channel data **/ + /** Currently handled for onChangeCategory **/ + /***************************************************/ + const resetChannelData = () => { + dispatch(setChannelsPage(1)); + dispatch(setChannelsReachedEnd(false)); + setChannelsLoading(false); + dispatch(resetChannels()); + }; + const loadSearchResults = async (query: string) => { setSearchResultsLoading(true); try { @@ -72,6 +101,7 @@ const useChannels = () => { return { loadMoreChannels, loadSearchResults, + resetChannelData, isLoadingChannels, isLoadingSearchResults, searchResults, diff --git a/src/redux/channelSlice.ts b/src/redux/channelSlice.ts index 842cd81a8..56d02441f 100644 --- a/src/redux/channelSlice.ts +++ b/src/redux/channelSlice.ts @@ -50,6 +50,9 @@ const channelSlice = createSlice({ addChannels: (state, action) => { state.channels = [...state.channels, ...action.payload]; }, + resetChannels: state => { + state.channels = []; + }, removeChannelSubscription: ( state, action: PayloadAction<{channel: string}>, @@ -79,6 +82,7 @@ const channelSlice = createSlice({ export const { addChannels, + resetChannels, setChannelsPage, setChannelsReachedEnd, addChannelSubscription,