diff --git a/assets/ui/base@1x.png b/assets/ui/base@1x.png new file mode 100644 index 000000000..4f648e0a2 Binary files /dev/null and b/assets/ui/base@1x.png differ diff --git a/assets/ui/base@2x.png b/assets/ui/base@2x.png new file mode 100644 index 000000000..220883bae Binary files /dev/null and b/assets/ui/base@2x.png differ diff --git a/assets/ui/base@3x.png b/assets/ui/base@3x.png new file mode 100644 index 000000000..06dc92841 Binary files /dev/null and b/assets/ui/base@3x.png differ diff --git a/assets/ui/cyber@1x.png b/assets/ui/cyber@1x.png new file mode 100644 index 000000000..3e3b3ad8f Binary files /dev/null and b/assets/ui/cyber@1x.png differ diff --git a/assets/ui/cyber@2x.png b/assets/ui/cyber@2x.png new file mode 100644 index 000000000..ad449ddf4 Binary files /dev/null and b/assets/ui/cyber@2x.png differ diff --git a/assets/ui/cyber@3x.png b/assets/ui/cyber@3x.png new file mode 100644 index 000000000..417e0e48f Binary files /dev/null and b/assets/ui/cyber@3x.png differ diff --git a/assets/ui/ethereum.png b/assets/ui/ethereum.png deleted file mode 100644 index c0f1de796..000000000 Binary files a/assets/ui/ethereum.png and /dev/null differ diff --git a/assets/ui/ethereum@1x.png b/assets/ui/ethereum@1x.png new file mode 100644 index 000000000..cfb195c47 Binary files /dev/null and b/assets/ui/ethereum@1x.png differ diff --git a/assets/ui/ethereum@2x.png b/assets/ui/ethereum@2x.png new file mode 100644 index 000000000..76ad9214b Binary files /dev/null and b/assets/ui/ethereum@2x.png differ diff --git a/assets/ui/ethereum@3x.png b/assets/ui/ethereum@3x.png new file mode 100644 index 000000000..95fcc8328 Binary files /dev/null and b/assets/ui/ethereum@3x.png differ diff --git a/assets/ui/linea@1x.png b/assets/ui/linea@1x.png new file mode 100644 index 000000000..9834996a3 Binary files /dev/null and b/assets/ui/linea@1x.png differ diff --git a/assets/ui/linea@2x.png b/assets/ui/linea@2x.png new file mode 100644 index 000000000..b53e4ef0f Binary files /dev/null and b/assets/ui/linea@2x.png differ diff --git a/assets/ui/linea@3x.png b/assets/ui/linea@3x.png new file mode 100644 index 000000000..dfbe31aa6 Binary files /dev/null and b/assets/ui/linea@3x.png differ diff --git a/assets/ui/optimism.png b/assets/ui/optimism.png deleted file mode 100644 index b50fc06b9..000000000 Binary files a/assets/ui/optimism.png and /dev/null differ diff --git a/assets/ui/optimism@1x.png b/assets/ui/optimism@1x.png new file mode 100644 index 000000000..1a6be497c Binary files /dev/null and b/assets/ui/optimism@1x.png differ diff --git a/assets/ui/optimism@2x.png b/assets/ui/optimism@2x.png new file mode 100644 index 000000000..e6e84cb5a Binary files /dev/null and b/assets/ui/optimism@2x.png differ diff --git a/assets/ui/optimism@3x.png b/assets/ui/optimism@3x.png new file mode 100644 index 000000000..7e8d6276f Binary files /dev/null and b/assets/ui/optimism@3x.png differ diff --git a/assets/ui/polygon.png b/assets/ui/polygon.png deleted file mode 100644 index 8f955aa7a..000000000 Binary files a/assets/ui/polygon.png and /dev/null differ diff --git a/assets/ui/polygon@1x.png b/assets/ui/polygon@1x.png new file mode 100644 index 000000000..2491efd00 Binary files /dev/null and b/assets/ui/polygon@1x.png differ diff --git a/assets/ui/polygon@2x.png b/assets/ui/polygon@2x.png new file mode 100644 index 000000000..5abd0029e Binary files /dev/null and b/assets/ui/polygon@2x.png differ diff --git a/assets/ui/polygon@3x.png b/assets/ui/polygon@3x.png new file mode 100644 index 000000000..0a8fdbc4d Binary files /dev/null and b/assets/ui/polygon@3x.png differ diff --git a/assets/ui/polygonZkEVM.png b/assets/ui/polygonZkEVM.png deleted file mode 100644 index 8ac0e7b99..000000000 Binary files a/assets/ui/polygonZkEVM.png and /dev/null differ diff --git a/assets/ui/polygonZkEVM@1x.png b/assets/ui/polygonZkEVM@1x.png new file mode 100644 index 000000000..ccd63db43 Binary files /dev/null and b/assets/ui/polygonZkEVM@1x.png differ diff --git a/assets/ui/polygonZkEVM@2x.png b/assets/ui/polygonZkEVM@2x.png new file mode 100644 index 000000000..f23c89c78 Binary files /dev/null and b/assets/ui/polygonZkEVM@2x.png differ diff --git a/assets/ui/polygonZkEVM@3x.png b/assets/ui/polygonZkEVM@3x.png new file mode 100644 index 000000000..abdfdb64a Binary files /dev/null and b/assets/ui/polygonZkEVM@3x.png differ diff --git a/src/Globals.ts b/src/Globals.ts index 77b166ebf..ba572ec5d 100644 --- a/src/Globals.ts +++ b/src/Globals.ts @@ -176,6 +176,7 @@ export default { // Border BORDER_DEFAULT: '#C4CBD5', BORDER_SEPARATOR: '#E5E5E5', + BORDER_DROPDOWN: '#EAEBF2', }, SCREENS: { WELCOME: 'Welcome', diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 000000000..8fa413a44 --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,169 @@ +import React, {FC, useEffect, useRef, useState} from 'react'; +import { + FlatList, + Image, + Platform, + Pressable, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import Modal from 'react-native-modal'; +import Globals from 'src/Globals'; + +import {DropdownProps} from './Dropdown.types'; + +const Dropdown: FC = ({style, data, onChange, value}) => { + // State + const [isVisible, setIsVisible] = useState(false); + const [selectedOption, setSelectedOption] = useState(data[0]); + const [dropdownTop, setDropdownTop] = useState(0); + const buttonRef = useRef(null); + + // Set default value + useEffect(() => { + if (value.length == 0) { + setSelectedOption(data[0]); + } + }, [value]); + + // Open dropdown + const openDropdown = () => { + if (buttonRef.current) { + buttonRef.current?.measure( + ( + _fx: number, + _fy: number, + _width: number, + height: number, + _px: number, + py: number, + ) => { + const topOffset = + Platform.OS === 'ios' ? py + height + 15 : py + height - 10; + setDropdownTop(topOffset); + }, + ); + } + setTimeout(() => setIsVisible(true), 100); + }; + + // Handle option select + const handleSelect = (option: any) => { + setSelectedOption(option); + setTimeout(() => setIsVisible(false), 100); + onChange(option); + }; + return ( + <> + {/* Dropdown Field */} + + {selectedOption?.icon && ( + + )} + + + + {/* Dropdown Options */} + + setTimeout(() => setIsVisible(false), 100)}> + + item.value} + renderItem={({item}) => ( + handleSelect(item)}> + {item.icon && ( + + )} + {item.label} + + )} + ItemSeparatorComponent={() => } + /> + + + + + ); +}; + +export {Dropdown}; + +const styles = StyleSheet.create({ + mainView: { + backgroundColor: Globals.COLORS.WHITE, + borderWidth: 1.5, + borderColor: Globals.COLORS.BORDER_DROPDOWN, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 12, + borderRadius: 12, + height: 48, + }, + caretIcon: { + height: 24, + width: 24, + resizeMode: 'contain', + }, + overlay: { + flex: 1, + position: 'relative', + zIndex: 1, + paddingHorizontal: 16, + }, + dropdownContainer: { + backgroundColor: Globals.COLORS.WHITE, + borderWidth: 1, + borderColor: Globals.COLORS.BORDER_DROPDOWN, + padding: 8, + borderRadius: 12, + maxHeight: 380, + zIndex: 2, + }, + option: { + flexDirection: 'row', + alignItems: 'center', + padding: 4, + borderRadius: 8, + }, + activeOption: { + backgroundColor: Globals.COLORS.PILL_BG_DEFAULT, + }, + optionGap: { + marginTop: 12, + }, + optionIcon: { + height: 24, + width: 24, + resizeMode: 'contain', + }, + optionText: { + marginLeft: 4, + }, + modalStyles: {margin: 0}, +}); diff --git a/src/components/dropdown/Dropdown.types.ts b/src/components/dropdown/Dropdown.types.ts new file mode 100644 index 000000000..7a9394cbe --- /dev/null +++ b/src/components/dropdown/Dropdown.types.ts @@ -0,0 +1,15 @@ +import {ImageSourcePropType, ViewStyle} from 'react-native'; +import {chainNameType} from 'src/helpers/ChainHelper'; + +export type DropdownProps = { + style?: ViewStyle; + data: DropdownOption[]; + onChange: (data: DropdownOption) => void; + value: string; +}; + +export type DropdownOption = { + value: string; + label: string; + icon: ImageSourcePropType | null; +}; diff --git a/src/components/dropdown/index.ts b/src/components/dropdown/index.ts new file mode 100644 index 000000000..7b97b2e60 --- /dev/null +++ b/src/components/dropdown/index.ts @@ -0,0 +1,2 @@ +export * from './Dropdown'; +export * from './Dropdown.types'; diff --git a/src/components/ui/ChannelCategories.tsx b/src/components/ui/ChannelCategories.tsx index e9ee865ee..65b6beaae 100644 --- a/src/components/ui/ChannelCategories.tsx +++ b/src/components/ui/ChannelCategories.tsx @@ -1,6 +1,5 @@ -import React, {FC, useState} from 'react'; +import React, {FC, useEffect, useRef} 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'; @@ -16,12 +15,30 @@ const ChannelCategories: FC = ({ value, disabled, }) => { + // Ref + const scrollViewRef = useRef(null); + + // Hooks const {isLoading, channelCategories} = useChannelCategories(); + // Scroll to start + useEffect(() => { + if (!isLoading && channelCategories?.length > 0) { + const shouldScrollToStart = channelCategories[0].value === value; + if (shouldScrollToStart && scrollViewRef.current) { + scrollViewRef.current.scrollTo({ + x: 0, + animated: true, + }); + } + } + }, [value]); + if (!isLoading && channelCategories?.length > 0) { return ( diff --git a/src/components/ui/ChannelsDisplayer.tsx b/src/components/ui/ChannelsDisplayer.tsx index 3847a4e4e..b92459d18 100644 --- a/src/components/ui/ChannelsDisplayer.tsx +++ b/src/components/ui/ChannelsDisplayer.tsx @@ -14,6 +14,8 @@ import EPNSActivity from 'src/components/loaders/EPNSActivity'; import ChannelItem from 'src/components/ui/ChannelItem'; import {usePushApi} from 'src/contexts/PushApiContext'; import {useSheets} from 'src/contexts/SheetContext'; +import ENV_CONFIG from 'src/env.config'; +import {ChainHelper} from 'src/helpers/ChainHelper'; import useChannels from 'src/hooks/channel/useChannels'; import useSubscriptions from 'src/hooks/channel/useSubscriptions'; import { @@ -24,21 +26,30 @@ import { import GLOBALS from '../../Globals'; import Globals from '../../Globals'; +import {Dropdown, DropdownOption} from '../dropdown'; import {ChannelCategories} from './ChannelCategories'; const ChannelsDisplayer = () => { + // Get allowed chains + const chainList = ChainHelper.getSelectChains(ENV_CONFIG.ALLOWED_NETWORKS); + + // State const [search, setSearch] = React.useState(''); const [selectedCategory, setSelectedCategory] = useState( Globals.CONSTANTS.ALL_CATEGORIES, ); + const [selectedChain, setSelectedChain] = useState(''); + // Redux const channelResults = useSelector(selectChannels); + const isLoadingSubscriptions = useSelector(selectIsLoadingSubscriptions); + + // Hooks const {isLoading, isLoadingMore, resetChannelData, loadMore} = useChannels({ tag: selectedCategory, searchQuery: search, + filter: selectedChain, }); - - const isLoadingSubscriptions = useSelector(selectIsLoadingSubscriptions); const {refreshSubscriptions} = useSubscriptions(); const {userPushSDKInstance} = usePushApi(); const {openSheet} = useSheets(); @@ -65,12 +76,20 @@ const ChannelsDisplayer = () => { } resetChannelData(); setSelectedCategory(category as string); + setSelectedChain(''); + }; + + const handleChainChange = (option: DropdownOption) => { + resetChannelData(); + setSelectedCategory(Globals.CONSTANTS.ALL_CATEGORIES); + setSelectedChain(chainList[0].value !== option.value ? option.value : ''); }; return ( {/* Render Search Bar */} + {/* Render Search Bar */} { handleChannelSearch(e); }} value={search} - placeholder={'Search for channel name or address'} + placeholder={'Search for channel name...'} placeholderTextColor="#7D7F89" /> + + {/* Render Dropdown Field */} + {/* Render Channel Categories(tags) */} @@ -96,7 +123,7 @@ const ChannelsDisplayer = () => { /> {/* Render No Data View */} - {channelResults.length === 0 && ( + {channelResults?.length === 0 && ( {!isLoading && !isLoadingSubscriptions ? ( // Show channel not found label @@ -124,7 +151,7 @@ const ChannelsDisplayer = () => { )} {/* Render Channel List */} - {channelResults.length !== 0 && !isLoadingSubscriptions && ( + {channelResults?.length !== 0 && !isLoadingSubscriptions && ( ): DropdownOption[] => { + return chainIdList?.map((key: number) => { + return { + value: key.toString(), + label: + ChainHelper.networkName?.[ + key as keyof typeof ChainHelper.networkName + ] ?? '', + icon: ChainHelper.chainIdToLogo(key), + }; + }); + }, }; diff --git a/src/hooks/channel/useChannels.tsx b/src/hooks/channel/useChannels.tsx index 3de16ebd2..ecf775d84 100644 --- a/src/hooks/channel/useChannels.tsx +++ b/src/hooks/channel/useChannels.tsx @@ -9,11 +9,12 @@ import {addChannels, resetChannels, setChannels} from 'src/redux/channelSlice'; export type UseChannelsProps = { tag: string; searchQuery: string; + filter: string; }; const DEBOUNCE_TIMEOUT = 500; //time in millisecond which we want to wait for then to finish typing -const useChannels = ({tag, searchQuery}: UseChannelsProps) => { +const useChannels = ({tag, searchQuery, filter}: UseChannelsProps) => { const [searchTimer, setSearchTimer] = useState(); const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(true); @@ -25,9 +26,9 @@ const useChannels = ({tag, searchQuery}: UseChannelsProps) => { const dispatch = useDispatch(); useEffect(() => { - console.log('first', {page, tag, searchQuery}); + console.log('run side effect', {page, tag, searchQuery, filter}); handleChannelInterval(); - }, [page, tag, searchQuery]); + }, [page, tag, searchQuery, filter]); const handleChannelInterval = () => { if (searchTimer) { @@ -41,15 +42,18 @@ const useChannels = ({tag, searchQuery}: UseChannelsProps) => { }; const getChannelsData = () => { - console.log('second', {page, tag, searchQuery}); + console.log('first run'); if (searchQuery.trim().length) { + console.log('search'); handleSearchAPI(); } else { + console.log('normal'); handleChannelAPI(); } }; const loadMore = () => { + console.log('load more'); if (!isEndReached && !isLoading && !isLoadingMore) { setIsLoadingMore(true); setPage(page + 1); @@ -66,7 +70,12 @@ const useChannels = ({tag, searchQuery}: UseChannelsProps) => { if (tag.length > 0 && tag !== Globals.CONSTANTS.ALL_CATEGORIES) { requestURL = `${requestURL}&tag=${tag}`; } + if (filter.length > 0) { + requestURL = `${requestURL}&filter=${filter}`; + } + console.log({requestURL}); const resJson = await fetch(requestURL).then(response => response.json()); + console.log({resJson}); if (page > 1) { dispatch(addChannels(resJson.channels)); } else { @@ -93,6 +102,7 @@ const useChannels = ({tag, searchQuery}: UseChannelsProps) => { const results = await userPushSDKInstance?.channel.search(query, { page: page, limit: GLOBALS.CONSTANTS.FEED_ITEMS_TO_PULL, + filter: filter.length > 0 ? Number(filter) : undefined, }); if (page > 1) { dispatch(addChannels(results));