Skip to content

Commit

Permalink
Wallet Switcher v2 (#6318)
Browse files Browse the repository at this point in the history
* new wallet switcher UI

* dnd flatlist autoscrolling

* fix layout/spacing issues

* pinned wallet grid edit mode jiggle animation

* add long press option and optional callback data to dropdown menu component

* address context menu on long press, refactor context menu to use new dropdown component

* add drag handler icon

* selected wallet shadow, watching & hw wallet badge fixes

* fix address label abbreviation & truncation

* fix total owned wallet balance formatting

* fix autoscrolling can scroll logic & add inset behavior

* general purpose feature hint tooltip component

* edit wallets hint tooltip

* fix dnd provider gesture disabled toggle logic

* add DraggableScrollView based on DraggableFlatList

* fix gesture problems by switching to draggable scrollview

* fixes for DnD children layout changes & draggable scrollview refactor

* fix different account emoji in list vs pinned grid

* dynamically size pinned account avatars

* copy & settings dropdown menu options, fix account emoji size

* localization

* misc. styling fixes for light mode, android, design spec matching

* update dropdown menu component to work for checkbox & regular items

* tooltip localization

* remove unused wallet item fields

* auto pin addresses

* refactor wallet list loading animation transition

* autoscroll easing & scroll to end logic fix

* fix remove pinned address logic

* integrate auto-pin wallet feature with backend implementation

* misc. DnD bug fixes, worklet haptic activation, and styling cleanup

* fix dnd onUpdate logic, add onUpdateWorklet for haptic trigger on reorder

* android button & padding fixes, grid layout jank fix

* misc. cleanup

* dnd: refactor offset update logic

* delete unused component

* move components to screen/components directory

* migrate DraggableFlatList to use useDraggableScroll

* update import, change max panel height

* fix various shadows

* fix shadow opacity, revert dnd offset drift fix for useDraggableScroll

* fix tooltip zindex, add shadow

* improve jiggle animation

* minor adjustments

* hide total balance display if user only has watched wallets

* fix total balance NaN by fixing addDisplay utility, add new test for utility

* custom spring config for wallet draggable return to origin

* fix navigation param address type error

* prevent auto pin from running multiple times, remove total balance text

* add back testID to add wallet button to fix e2e

* remove concept of per item activation delays in dnd, move logic to gesture onStart from onBegin

* make grid wallets & other wallets gesture wait for each other to prevent ghost activations

* remove unused code

* undo changes to dropdown component for data type
  • Loading branch information
maxbbb authored Jan 8, 2025
1 parent f9f0e84 commit 9e2a040
Show file tree
Hide file tree
Showing 46 changed files with 2,789 additions and 1,413 deletions.
32 changes: 25 additions & 7 deletions src/components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const DropdownMenuRoot = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuContent = DropdownMenuPrimitive.Content;
export const DropdownMenuItem = DropdownMenuPrimitive.create(
styled(DropdownMenuPrimitive.Item)({
height: 34,
}),
'Item'
);
export const DropdownMenuCheckboxItem = DropdownMenuPrimitive.create(
styled(DropdownMenuPrimitive.CheckboxItem)({
height: 34,
}),
Expand All @@ -35,6 +41,7 @@ export type MenuItemIcon = Omit<IconConfig, 'iconValue' | 'iconType'> & (MenuIte
export type MenuItem<T> = Omit<MenuActionConfig, 'icon'> & {
actionKey: T;
actionTitle: string;
destructive?: boolean;
icon?: MenuItemIcon | { iconType: string; iconValue: string };
};

Expand All @@ -43,10 +50,12 @@ export type MenuConfig<T extends string> = Omit<_MenuConfig, 'menuItems' | 'menu
menuItems: Array<MenuItem<T>>;
};

type DropDownMenuProps<T extends string> = {
type DropdownMenuProps<T extends string> = {
children: React.ReactElement;
menuConfig: MenuConfig<T>;
onPressMenuItem: (actionKey: T) => void;
triggerAction?: 'press' | 'longPress';
menuItemType?: 'checkbox';
} & DropdownMenuContentProps;

const buildIconConfig = (icon?: MenuItemIcon) => {
Expand Down Expand Up @@ -75,17 +84,21 @@ export function DropdownMenu<T extends string>({
side = 'right',
alignOffset = 5,
avoidCollisions = true,
}: DropDownMenuProps<T>) {
triggerAction = 'press',
menuItemType,
}: DropdownMenuProps<T>) {
const handleSelectItem = useCallback(
(actionKey: T) => {
onPressMenuItem(actionKey);
},
[onPressMenuItem]
);

const MenuItemComponent = menuItemType === 'checkbox' ? DropdownMenuCheckboxItem : DropdownMenuItem;

return (
<DropdownMenuRoot>
<DropdownMenuTrigger>{children}</DropdownMenuTrigger>
<DropdownMenuTrigger action={triggerAction}>{children}</DropdownMenuTrigger>
<DropdownMenuContent
loop={loop}
side={side}
Expand All @@ -97,19 +110,24 @@ export function DropdownMenu<T extends string>({
>
{!!menuConfig.menuTitle?.trim() && (
<DropdownMenuPrimitive.Group>
<DropdownMenuItem disabled>
<MenuItemComponent disabled>
<DropdownMenuItemTitle>{menuConfig.menuTitle}</DropdownMenuItemTitle>
</DropdownMenuItem>
</MenuItemComponent>
</DropdownMenuPrimitive.Group>
)}
{menuConfig.menuItems?.map(item => {
const Icon = buildIconConfig(item.icon as MenuItemIcon);

return (
<DropdownMenuItem value={item.menuState ?? 'off'} key={item.actionKey} onSelect={() => handleSelectItem(item.actionKey)}>
<MenuItemComponent
value={item.menuState ?? 'off'}
destructive={item.destructive}
key={item.actionKey}
onSelect={() => handleSelectItem(item.actionKey)}
>
<DropdownMenuItemTitle>{item.actionTitle}</DropdownMenuItemTitle>
{Icon}
</DropdownMenuItem>
</MenuItemComponent>
);
})}
</DropdownMenuContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/SmoothPager/ListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const TapToDismiss = memo(function TapToDismiss() {
});

const PANEL_INSET = 8;
const PANEL_WIDTH = DEVICE_WIDTH - PANEL_INSET * 2;
export const PANEL_WIDTH = DEVICE_WIDTH - PANEL_INSET * 2;
const PANEL_BORDER_RADIUS = 42;
const LIST_SCROLL_INDICATOR_BOTTOM_INSET = { bottom: PANEL_BORDER_RADIUS };

Expand Down
67 changes: 67 additions & 0 deletions src/components/animations/JiggleAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useEffect } from 'react';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
cancelAnimation,
useAnimatedReaction,
SharedValue,
withSequence,
} from 'react-native-reanimated';

type JiggleAnimationProps = {
amplitude?: number;
duration?: number;
children: React.ReactNode;
enabled: boolean | SharedValue<boolean>;
};

export function JiggleAnimation({ children, amplitude = 2, duration = 125, enabled }: JiggleAnimationProps) {
const rotation = useSharedValue(0);
const internalEnabled = useSharedValue(typeof enabled === 'boolean' ? enabled : false);

// Randomize some initial values to avoid sync with other jiggles
// Randomize duration (5% variance)
const instanceDuration = duration * (1 + (Math.random() - 0.5) * 0.1);

// Randomize initial rotation that's at least 50% of the amplitude
const minInitialRotation = amplitude * 0.5;
const rotationRange = amplitude - minInitialRotation;
const initialRotation = minInitialRotation + Math.random() * rotationRange;

// Randomize initial direction
const initialDirection = Math.random() < 0.5 ? -1 : 1;
const firstRotation = initialRotation * initialDirection;

useEffect(() => {
if (typeof enabled === 'boolean') {
internalEnabled.value = enabled;
}
}, [enabled, internalEnabled]);

useAnimatedReaction(
() => {
return typeof enabled === 'boolean' ? internalEnabled.value : (enabled as SharedValue<boolean>).value;
},
enabled => {
if (enabled) {
rotation.value = withSequence(
withTiming(firstRotation, { duration: instanceDuration / 2 }),
withRepeat(withTiming(-amplitude * initialDirection, { duration: instanceDuration }), -1, true)
);
} else {
cancelAnimation(rotation);
rotation.value = withTiming(0, { duration: instanceDuration / 2 });
}
}
);

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ rotate: `${rotation.value}deg` }],
};
});

return <Animated.View style={animatedStyle}>{children}</Animated.View>;
}
1 change: 1 addition & 0 deletions src/components/animations/animationConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const springAnimations = createSpringConfigs({
keyboardConfig: disableForTestingEnvironment({ damping: 500, mass: 3, stiffness: 1000 }),
sliderConfig: disableForTestingEnvironment({ damping: 40, mass: 1.25, stiffness: 450 }),
slowSpring: disableForTestingEnvironment({ damping: 500, mass: 3, stiffness: 800 }),
walletDraggableConfig: disableForTestingEnvironment({ damping: 36, mass: 0.8, stiffness: 800 }),
snappierSpringConfig: disableForTestingEnvironment({ damping: 42, mass: 0.8, stiffness: 800 }),
snappySpringConfig: disableForTestingEnvironment({ damping: 100, mass: 0.8, stiffness: 275 }),
springConfig: disableForTestingEnvironment({ damping: 100, mass: 1.2, stiffness: 750 }),
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/MintsCard/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function Menu() {
);

return (
<DropdownMenu<MintsFilter> menuConfig={menuConfig} onPressMenuItem={onPressMenuItem}>
<DropdownMenu<MintsFilter> menuItemType="checkbox" menuConfig={menuConfig} onPressMenuItem={onPressMenuItem}>
<ButtonPressAnimation>
<Inset top="2px">
<Inline alignVertical="center" space={{ custom: 5 }}>
Expand Down
Loading

0 comments on commit 9e2a040

Please sign in to comment.