diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx
index 4db59d9c61f..6c63ddc5890 100644
--- a/src/components/coin-icon/ChainImage.tsx
+++ b/src/components/coin-icon/ChainImage.tsx
@@ -12,9 +12,17 @@ import AvalancheBadge from '@/assets/badges/avalanche.png';
import BlastBadge from '@/assets/badges/blast.png';
import DegenBadge from '@/assets/badges/degen.png';
import ApechainBadge from '@/assets/badges/apechain.png';
-import FastImage, { Source } from 'react-native-fast-image';
+import FastImage, { FastImageProps, Source } from 'react-native-fast-image';
-export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | undefined; size?: number }) {
+export function ChainImage({
+ chainId,
+ size = 20,
+ style,
+}: {
+ chainId: ChainId | null | undefined;
+ size?: number;
+ style?: FastImageProps['style'];
+}) {
const source = useMemo(() => {
switch (chainId) {
case ChainId.apechain:
@@ -47,6 +55,10 @@ export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | u
if (!chainId) return null;
return (
-
+
);
}
diff --git a/src/languages/en_US.json b/src/languages/en_US.json
index 0d2e0fcb380..ad8b7f0aaab 100644
--- a/src/languages/en_US.json
+++ b/src/languages/en_US.json
@@ -2986,6 +2986,7 @@
"farcaster": "Farcaster"
},
"sort": {
+ "sort": "Sort",
"volume": "Volume",
"market_cap": "Market Cap",
"top_gainers": "Top Gainers",
diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx
index f4f1b6f78eb..10bb5df4eca 100644
--- a/src/screens/discover/components/NetworkSwitcher.tsx
+++ b/src/screens/discover/components/NetworkSwitcher.tsx
@@ -2,22 +2,28 @@ import { getChainColorWorklet } from '@/__swaps__/utils/swaps';
import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains';
import { ChainId } from '@/chains/types';
import { AbsolutePortal } from '@/components/AbsolutePortal';
+import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView';
import { ChainImage } from '@/components/coin-icon/ChainImage';
-import { globalColors, Text, useBackgroundColor, useColorMode } from '@/design-system';
+import { DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system';
import { useForegroundColor } from '@/design-system/color/useForegroundColor';
-import hiddenTokens from '@/redux/hiddenTokens';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { nonceStore } from '@/state/nonces';
-
+import { useTheme } from '@/theme';
+import MaskedView from '@react-native-masked-view/masked-view';
import chroma from 'chroma-js';
-import { useReducer, useState } from 'react';
-import React, { StyleSheet, View } from 'react-native';
+import { Component, forwardRef, useReducer, useState } from 'react';
+import React, { Pressable, View, ViewStyle } from 'react-native';
import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler';
+import LinearGradient from 'react-native-linear-gradient';
import Animated, {
+ BounceIn,
FadeIn,
FadeOut,
+ FadeOutUp,
LinearTransition,
+ makeMutable,
runOnJS,
+ SharedValue,
SlideInDown,
SlideOutDown,
useAnimatedReaction,
@@ -27,6 +33,7 @@ import Animated, {
withSpring,
withTiming,
} from 'react-native-reanimated';
+import Svg, { Path } from 'react-native-svg';
function getMostUsedChains() {
const noncesByAddress = nonceStore.getState().nonces;
@@ -45,17 +52,16 @@ function getMostUsedChains() {
}
const initialPinnedNetworks = getMostUsedChains().slice(0, 5);
-const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }>(
+const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[] }>(
(set, get) => ({
pinnedNetworks: initialPinnedNetworks,
- unpinnedNetworks: SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinnedNetworks.includes(chainId)),
})
// {
// storageKey: 'network-switcher',
- // version: 1,
+ // // version: 0,
// }
);
-const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }) => {
+const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[] }) => {
useNetworkSwitcherStore.setState(s);
};
@@ -102,24 +108,93 @@ function EditButton({ text, onPress }: { text: string; onPress: VoidFunction })
);
}
-function NetworkOption({
- chainId,
+function ExpandNetworks({
+ hiddenNetworksLength,
+ isExpanded,
+ toggleExpanded,
+}: {
+ hiddenNetworksLength: number;
+ toggleExpanded: (expanded: boolean) => void;
+ isExpanded: boolean;
+}) {
+ const pressed = useSharedValue(false);
+
+ const tap = Gesture.Tap()
+ .onBegin(() => {
+ pressed.value = true;
+ })
+ .onFinalize(() => {
+ pressed.value = false;
+ runOnJS(toggleExpanded)(!isExpanded);
+ });
+
+ const animatedStyles = useAnimatedStyle(() => ({
+ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }],
+ }));
+
+ return (
+
+
+ {!isExpanded && (
+
+
+ {hiddenNetworksLength}
+
+
+ )}
+
+ {isExpanded ? 'Show Less' : 'More Networks'}
+
+
+
+ {isExpanded ? '' : ''}
+
+
+
+
+ );
+}
+
+function AllNetworksOption({
selected,
onPress,
}: {
- chainId: ChainId;
selected: boolean;
onPress?: (e: GestureStateChangeEvent) => void;
}) {
- const name = chainsLabel[chainId];
- if (!name) throw new Error(`No chain name for chainId ${chainId}`);
-
const { isDarkMode } = useColorMode();
- const surfaceSecondary = useBackgroundColor('fillSecondary');
- const chainColor = chroma(getChainColorWorklet(chainId, true)).alpha(0.16).hex();
- const backgroundColor = selected ? chainColor : isDarkMode ? globalColors.white10 : globalColors.grey20;
+ const surfacePrimary = useBackgroundColor('surfacePrimary');
+ const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary;
+
+ const blue = useForegroundColor('blue');
- const borderColor = selected ? chainColor : '#F5F8FF05';
+ const backgroundColor = selected
+ ? chroma.scale([networkSwitcherBackgroundColor, blue])(0.16).hex()
+ : isDarkMode
+ ? globalColors.white10
+ : '#F2F3F4';
+ const borderColor = selected ? chroma(blue).alpha(0.16).hex() : '#F5F8FF05';
const pressed = useSharedValue(false);
@@ -137,14 +212,22 @@ function NetworkOption({
transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }],
}));
+ const overlappingBadge = {
+ borderColor: backgroundColor,
+ borderWidth: 1.67,
+ borderRadius: 16,
+ marginLeft: -9,
+ width: 16 + 1.67 * 2,
+ height: 16 + 1.67 * 2,
+ };
+
return (
-
+
+
+
+
+
+
- {name}
+ All Networks
);
}
-function ExpandNetworks({
- hiddenNetworksLength,
- isExpanded,
- toggleExpanded,
+function NetworkOption({
+ chainId,
+ selected,
+ onPress,
+ style,
}: {
- hiddenNetworksLength: number;
- toggleExpanded: (expanded: boolean) => void;
- isExpanded: boolean;
+ chainId: ChainId;
+ selected: boolean;
+ onPress?: (e: GestureStateChangeEvent) => void;
+ style?: ViewStyle;
}) {
+ const name = chainsLabel[chainId];
+ if (!name) throw new Error(`: No chain name for chainId ${chainId}`);
+
+ const { isDarkMode } = useColorMode();
+
+ const surfacePrimary = useBackgroundColor('surfacePrimary');
+ const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary;
+
+ const chainColor = getChainColorWorklet(chainId, true);
+ const backgroundColor = selected
+ ? chroma.scale([networkSwitcherBackgroundColor, chainColor])(0.16).hex()
+ : isDarkMode
+ ? globalColors.white10
+ : globalColors.grey20;
+
+ const borderColor = selected ? chroma(chainColor).alpha(0.16).hex() : '#F5F8FF05';
+
const pressed = useSharedValue(false);
const tap = Gesture.Tap()
- .onBegin(() => {
+ .onBegin(e => {
pressed.value = true;
+ if (onPress) runOnJS(onPress)(e);
})
.onFinalize(() => {
pressed.value = false;
- runOnJS(toggleExpanded)(!isExpanded);
- });
+ })
+ .enabled(!!onPress);
const animatedStyles = useAnimatedStyle(() => ({
transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }],
@@ -192,42 +300,27 @@ function ExpandNetworks({
return (
- {!isExpanded && (
-
-
- {hiddenNetworksLength}
-
-
- )}
-
- {isExpanded ? 'Show Less' : 'More Networks'}
+
+
+ {name}
-
-
- {isExpanded ? '' : ''}
-
-
);
@@ -240,8 +333,6 @@ const ITEM_WIDTH = 164.5;
const GAP = 12;
const HALF_GAP = 6;
-const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolute', height: ITEM_HEIGHT, width: ITEM_WIDTH } });
-
/*
- what to do if user pins all networks (where to drag to unpin)
@@ -249,6 +340,32 @@ const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolut
*/
+function DraggingItem({
+ chainId,
+ selected,
+ transform,
+}: {
+ chainId: ChainId | null;
+ transform: SharedValue;
+ selected: boolean;
+}) {
+ const draggingStyles = useAnimatedStyle(() => {
+ if (!transform.value) return { opacity: 0 };
+ return {
+ opacity: 1,
+ transform: [{ scale: transform.value.scale }],
+ left: transform.value.x,
+ top: transform.value.y,
+ };
+ });
+
+ return (
+
+ {chainId && }
+
+ );
+}
+
function NetworksGrid({
editing,
selected,
@@ -260,9 +377,10 @@ function NetworksGrid({
unselect: (chainId: ChainId) => void;
select: (chainId: ChainId) => void;
}) {
- const networks = useSharedValue(useNetworkSwitcherStore());
- const pinnedNetworks = useDerivedValue(() => networks.value.pinnedNetworks);
- const unpinnedNetworks = useDerivedValue(() => networks.value.unpinnedNetworks);
+ const pinnedNetworks = useSharedValue(useNetworkSwitcherStore(s => s.pinnedNetworks));
+ const unpinnedNetworks = useDerivedValue(() =>
+ SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !pinnedNetworks.value.includes(chainId))
+ );
const dragging = useSharedValue(null);
const draggingTransform = useSharedValue(null);
@@ -273,15 +391,21 @@ function NetworksGrid({
// Sync back to store
useAnimatedReaction(
- () => networks.value,
- current => runOnJS(setNetworkSwitcherState)(current)
+ () => pinnedNetworks.value,
+ (pinnedNetworks, prev) => {
+ if (!prev) return; // no need to react on initial value
+ runOnJS(setNetworkSwitcherState)({ pinnedNetworks });
+ }
);
// Force rerender when dragging or dropping changes
const [, rerender] = useReducer(x => x + 1, 0);
useAnimatedReaction(
() => [dropping.value, dragging.value],
- () => runOnJS(rerender)()
+ () => {
+ console.log('force rerendering');
+ runOnJS(rerender)();
+ }
);
const positionIndex = (x: number, y: number) => {
@@ -308,17 +432,22 @@ function NetworksGrid({
'worklet';
const isTargetUnpinned = e.y > unpinnedGridY.value;
- if (isTargetUnpinned && pinnedNetworks.value.length === 1) return;
+ if (!isTargetUnpinned && pinnedNetworks.value.length === 1) return;
const index = positionIndex(e.x, e.y);
const position = indexPosition(index, isTargetUnpinned);
draggingTransform.value = { x: position.x, y: position.y, scale: 1 }; // initial position is the grid slot
- draggingTransform.value = withSpring({ x: e.x - ITEM_WIDTH * 0.5, y: e.y - ITEM_HEIGHT * 0.5, scale: 1.05 }); // animate into the center of the pointer
const targetArray = isTargetUnpinned ? unpinnedNetworks.value : pinnedNetworks.value;
const chainId = targetArray[index];
dragging.value = chainId;
+
+ draggingTransform.value = withSpring({
+ x: e.x - ITEM_WIDTH * 0.5,
+ y: e.y - ITEM_HEIGHT * 0.5,
+ scale: 1.05,
+ }); // animate into the center of the pointer
})
.onChange(e => {
'worklet';
@@ -334,37 +463,51 @@ function NetworksGrid({
const isDraggingOverUnpinned = e.y > unpinnedGridY.value;
- const targetArrayKey = isDraggingOverUnpinned ? 'unpinnedNetworks' : 'pinnedNetworks';
- const otherArrayKey = isDraggingOverUnpinned ? 'pinnedNetworks' : 'unpinnedNetworks';
+ const currentIndexAtPinned = pinnedNetworks.value.indexOf(chainId);
+ const isPinned = currentIndexAtPinned !== -1;
- const targetArray = networks.value[targetArrayKey];
- const targetIndex = Math.min(positionIndex(e.x, e.y), targetArray.length - 1);
- const indexInTarget = targetArray.indexOf(chainId);
+ // We don't reorder unpinned networks
+ if (isDraggingOverUnpinned && !isPinned) return;
- if (indexInTarget === -1) {
- networks.modify(v => {
- // Pin/Unpin
- v[otherArrayKey] = v[otherArrayKey].filter(id => id !== chainId);
- v[targetArrayKey].splice(targetIndex, 0, chainId);
- return v;
+ // Unpin
+ if (isDraggingOverUnpinned && isPinned) {
+ pinnedNetworks.modify(networks => {
+ networks.splice(currentIndexAtPinned, 1);
+ return networks;
});
- } else if (indexInTarget !== targetIndex) {
- // Reorder
- networks.modify(v => {
- const [movedChainId] = v[targetArrayKey].splice(indexInTarget, 1);
- v[targetArrayKey].splice(targetIndex, 0, movedChainId);
- return v;
+ return;
+ }
+
+ // Pin
+ if (!isDraggingOverUnpinned && !isPinned) {
+ pinnedNetworks.modify(networks => {
+ networks.push(chainId);
+ return networks;
+ });
+ return;
+ }
+
+ // Reorder
+ const newIndex = Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1);
+ if (newIndex !== currentIndexAtPinned) {
+ pinnedNetworks.modify(networks => {
+ networks.splice(currentIndexAtPinned, 1);
+ networks.splice(newIndex, 0, chainId);
+ return networks;
});
}
})
.onFinalize(e => {
'worklet';
- if (!dragging.value) return;
+ const chainId = dragging.value;
+ if (!chainId) return;
const isDroppingInUnpinned = e.y > unpinnedGridY.value;
- const targetArray = isDroppingInUnpinned ? pinnedNetworks.value : unpinnedNetworks.value;
- const index = Math.min(positionIndex(e.x, e.y), targetArray.length - 1);
+ const index = isDroppingInUnpinned
+ ? unpinnedNetworks.value.indexOf(chainId)
+ : Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1);
+
const { x, y } = indexPosition(index, isDroppingInUnpinned);
droppingTransform.value = draggingTransform.value;
@@ -380,32 +523,14 @@ function NetworksGrid({
() => unpinnedGridY.value,
(newY, prevY) => {
// the layout can recalculate after the drop started
- if (!prevY || !droppingTransform.value || droppingTransform.value.y < prevY) return;
- const { x, y, scale } = droppingTransform.value;
- droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale }, { mass: 0.6 }, completed => {
+ if (!prevY || !droppingTransform.value) return;
+ const { x, y } = droppingTransform.value; // TODO: does not work
+ droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale: 1 }, { mass: 0.6 }, completed => {
if (completed) dropping.value = null;
});
}
);
- const draggingStyles = useAnimatedStyle(() => {
- if (!draggingTransform.value) return {};
- return {
- transform: [{ scale: draggingTransform.value.scale }],
- left: draggingTransform.value.x,
- top: draggingTransform.value.y,
- };
- });
-
- const droppingStyles = useAnimatedStyle(() => {
- if (!droppingTransform.value) return {};
- return {
- transform: [{ scale: droppingTransform.value.scale }],
- left: droppingTransform.value.x,
- top: droppingTransform.value.y,
- };
- });
-
const [isExpanded, setExpanded] = useState(false);
const toggleSelected = (chainId: ChainId) => {
@@ -413,30 +538,31 @@ function NetworksGrid({
else select(chainId);
};
- // const mainGridNetworks = editing
- // ? pinnedNetworks.value
- // : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))];
+ const pinnedGrid = editing
+ ? pinnedNetworks.value
+ : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))];
+
+ const unpinnedGrid = editing ? unpinnedNetworks.value : unpinnedNetworks.value.filter(chainId => !selected.includes(chainId));
return (
- {!!dropping.value && (
-
-
-
- )}
-
- {!!dragging.value && (
-
-
-
- )}
+
+
- {pinnedNetworks.value.map(chainId =>
+ {pinnedGrid.map(chainId =>
chainId === dragging.value || chainId === dropping.value ? (
) : (
@@ -465,17 +591,16 @@ function NetworksGrid({
onLayout={e => (unpinnedGridY.value = e.nativeEvent.layout.y)}
layout={LinearTransition}
entering={FadeIn.duration(250).delay(125)}
- exiting={FadeOut.duration(50)}
style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }}
>
- {unpinnedNetworks.value.length === 0 && (
+ {unpinnedGrid.length === 0 && (
Drag here to unpin networks
)}
- {unpinnedNetworks.value.map(chainId =>
+ {unpinnedGrid.map(chainId =>
chainId === dragging.value || chainId === dropping.value ? (
) : (
@@ -494,12 +619,115 @@ function NetworksGrid({
);
}
+const useCustomizeNetworksBanner = createRainbowStore<{
+ dismissedAt: number; // timestamp
+}>(() => ({ dismissedAt: 0 }), {
+ storageKey: 'CustomizeNetworksBanner',
+ version: 0,
+});
+const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2;
+const dismissCustomizeNetworksBanner = () => {
+ const { dismissedAt } = useCustomizeNetworksBanner.getState();
+ if (Date.now() - dismissedAt < twoWeeks) return;
+ useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() });
+};
+
+function CustomizeNetworksBanner() {
+ const blue = '#268FFF';
+
+ const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt);
+ const isOpen = Date.now() - dismissedAt > twoWeeks;
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+ Customize Your Networks
+
+
+ Tap the{' '}
+
+ Edit
+ {' '}
+ button below to set up
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: VoidFunction; onSelect: VoidFunction; multiple?: boolean }) {
- const backgroundColor = '#191A1C';
+ const { isDarkMode } = useTheme();
+ const surfacePrimary = useBackgroundColor('surfacePrimary');
+ const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary;
const separatorSecondary = useForegroundColor('separatorSecondary');
const separatorTertiary = useForegroundColor('separatorTertiary');
const fill = useForegroundColor('fill');
+ console.log('rerender NetworkSelector');
+
const translationY = useSharedValue(0);
const swipeToClose = Gesture.Pan()
@@ -518,9 +746,17 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void
transform: [{ translateY: translationY.value }],
}));
- const [selected, setSelected] = useState([]);
- const unselect = (chainId: ChainId) => setSelected(s => s.filter(id => id !== chainId));
- const select = (chainId: ChainId) => setSelected(s => (multiple ? [...s, chainId] : [chainId]));
+ const [selected, setSelected] = useState([]);
+ const unselect = (chainId: ChainId) =>
+ setSelected(s => {
+ if (s === 'all') return [];
+ return s.filter(id => id !== chainId);
+ });
+ const select = (chainId: ChainId) =>
+ setSelected(s => {
+ if (s === 'all') return [chainId];
+ return multiple ? [...s, chainId] : [chainId];
+ });
return (
@@ -553,6 +789,7 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void
animatedStyles,
]}
>
+
@@ -564,12 +801,25 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void
{isEditing ? 'Edit' : 'Network'}
- setEditing(s => !s)} />
+ {
+ dismissCustomizeNetworksBanner();
+ setEditing(s => !s);
+ }}
+ />
-
+ {multiple && !isEditing && (
+ <>
+ setSelected('all')} />
+
+ >
+ )}
+
+
diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx
index a92fdbd012c..d9f5d38bb52 100644
--- a/src/screens/discover/components/TrendingTokens.tsx
+++ b/src/screens/discover/components/TrendingTokens.tsx
@@ -13,8 +13,7 @@ import LinearGradient from 'react-native-linear-gradient';
import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { NetworkSelector } from './NetworkSwitcher';
import * as i18n from '@/languages';
-
-const TRANSLATIONS = i18n.l.cards.ens_search;
+import { useTheme } from '@/theme';
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
@@ -84,8 +83,11 @@ function CategoryFilterButton({
iconWidth?: number;
label: string;
}) {
- const backgroundColor = useBackgroundColor('fillTertiary');
- const borderColor = useBackgroundColor('fillSecondary');
+ const { isDarkMode } = useTheme();
+ const fillTertiary = useBackgroundColor('fillTertiary');
+ const fillSecondary = useBackgroundColor('fillSecondary');
+
+ const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary;
const pressed = useSharedValue(false);
@@ -103,7 +105,7 @@ function CategoryFilterButton({
return (
-
+
@@ -258,7 +277,9 @@ function TrendingTokenRow() {
const t = i18n.l.trending_tokens;
function NoResults() {
- const backgroundColor = '#191A1C'; // useBackgroundColor('fillQuaternary');
+ const { isDarkMode } = useTheme();
+ const fillQuaternary = useBackgroundColor('fillQuaternary');
+ const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary;
return (
@@ -349,7 +370,7 @@ export function TrendingTokens() {
side="bottom"
onPressMenuItem={timeframe => setFilter(filter => ({ ...filter, timeframe }))}
>
-
+
-
+