Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pin / unpin tokens #1370

Merged
merged 15 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/core/state/pinnedAssets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import create from 'zustand';

import { createStore } from '../internal/createStore';

type PinnedAsset = {
uniqueId: string;
createdAt: number;
};

type UpdatePinnedAssetArgs = {
uniqueId: string;
};

type UpdatePinnedAssetFn = ({ uniqueId }: UpdatePinnedAssetArgs) => void;

export interface PinnedAssetState {
pinnedAssets: PinnedAsset[];
addPinnedAsset: UpdatePinnedAssetFn;
removedPinnedAsset: UpdatePinnedAssetFn;
}

export const pinnedAssets = createStore<PinnedAssetState>(
(set, get) => ({
pinnedAssets: [],
addPinnedAsset: ({ uniqueId }: UpdatePinnedAssetArgs) => {
const { pinnedAssets } = get();
set({
pinnedAssets: [
...pinnedAssets,
{ uniqueId, createdAt: new Date().getTime() },
],
});
},
removedPinnedAsset: ({ uniqueId }: UpdatePinnedAssetArgs) => {
const { pinnedAssets } = get();
set({
pinnedAssets: pinnedAssets.filter(
({ uniqueId: _uniqueId }) => _uniqueId !== uniqueId,
),
});
},
}),
{
persist: {
name: 'pinned_assets',
version: 1,
},
},
);

export const usePinnedAssetStore = create(pinnedAssets);
1 change: 1 addition & 0 deletions src/design-system/styles/designTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,7 @@ export const symbolNames = selectSymbolNames(
'chart.line.downtrend.xyaxis',
'dollarsign.circle',
'square.grid.2x2.fill',
'pin.fill',
);
export type SymbolName = (typeof symbolNames)[number];

Expand Down
27 changes: 27 additions & 0 deletions src/design-system/symbols/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4778,4 +4778,31 @@ export default {
viewBox: { width: 26.66015625, height: 26.66015625 },
},
},
'pin.fill': {
regular: {
name: 'pin.fill',
path: 'M0.0023 19.725C0.0023 20.895 0.7823 21.655 2.0123 21.655H9.4023V28.045C9.4023 30.135 10.2823 31.865 10.6223 31.865C10.9523 31.865 11.8223 30.135 11.8223 28.045V21.655H19.2223C20.4523 21.655 21.2323 20.895 21.2323 19.725C21.2323 16.845 18.9223 13.795 15.0823 12.405L14.6323 6.125C16.6223 4.995 18.2623 3.705 18.9723 2.785C19.3323 2.325 19.5123 1.855 19.5123 1.445C19.5123 0.615 18.8623 -0.005 17.9123 -0.005H3.3323C2.3623 -0.005 1.7323 0.615 1.7323 1.445C1.7323 1.855 1.9023 2.325 2.2523 2.785C2.9623 3.705 4.6123 4.995 6.6023 6.125L6.1523 12.405C2.3123 13.795 0.0023 16.845 0.0023 19.725Z',
viewBox: { width: 21.232421875, height: 31.869140625 },
},
medium: {
name: 'pin.fill',
path: 'M0.0049 20.0705C0.0049 21.3005 0.8349 22.1205 2.1349 22.1205H9.4449V28.5905C9.4449 30.5205 10.4149 32.4605 10.8049 32.4605C11.1749 32.4605 12.1449 30.5205 12.1449 28.5905V22.1205H19.4549C20.7549 22.1205 21.5849 21.3005 21.5849 20.0705C21.5849 17.1305 19.2549 14.1105 15.3949 12.6905L14.9449 6.3305C16.9849 5.1705 18.5949 3.9005 19.3049 2.9905C19.6749 2.5005 19.8649 2.0105 19.8649 1.5705C19.8649 0.6705 19.1649 0.0005 18.1449 0.0005H3.4449C2.4249 0.0005 1.7249 0.6705 1.7249 1.5705C1.7249 2.0105 1.9149 2.5005 2.2849 2.9905C2.9949 3.9005 4.6049 5.1705 6.6449 6.3305L6.2049 12.6905C2.3349 14.1105 0.0049 17.1305 0.0049 20.0705Z',
viewBox: { width: 21.587890625, height: 32.45703125 },
},
semibold: {
name: 'pin.fill',
path: 'M0.0012 20.3202C0.0012 21.6102 0.8913 22.4802 2.2313 22.4802H9.4913V28.9802C9.4913 30.7902 10.5313 32.8802 10.9413 32.8802C11.3513 32.8802 12.3913 30.7902 12.3913 28.9802V22.4802H19.6513C20.9913 22.4802 21.8713 21.6102 21.8713 20.3202C21.8713 17.3402 19.5113 14.3302 15.6413 12.8802L15.1913 6.4702C17.2712 5.2902 18.8713 4.0302 19.5613 3.1302C19.9513 2.6402 20.1413 2.1202 20.1413 1.6502C20.1413 0.7002 19.4113 0.0002 18.3313 0.0002H3.5413C2.4612 0.0002 1.7413 0.7002 1.7413 1.6502C1.7413 2.1202 1.9313 2.6402 2.3113 3.1302C3.0113 4.0302 4.6113 5.3002 6.6813 6.4702L6.2312 12.8802C2.3613 14.3302 0.0012 17.3402 0.0012 20.3202Z',
viewBox: { width: 21.875, height: 32.880859375 },
},
bold: {
name: 'pin.fill',
path: 'M0.0039 20.642C0.0039 22.012 0.9439 22.932 2.3539 22.932H9.5139V29.522C9.5139 31.162 10.6739 33.452 11.1139 33.452C11.5539 33.452 12.7139 31.162 12.7139 29.522V22.932H19.8839C21.2839 22.932 22.2339 22.012 22.2339 20.642C22.2339 17.612 19.8339 14.612 15.9539 13.152L15.5039 6.662C17.6239 5.452 19.1939 4.202 19.8939 3.322C20.3039 2.802 20.4939 2.252 20.4939 1.762C20.4939 0.742 19.7139 0.002 18.5839 0.002H3.6539C2.5139 0.002 1.7339 0.742 1.7339 1.762C1.7339 2.252 1.9239 2.802 2.3339 3.322C3.0339 4.202 4.6039 5.472 6.7239 6.662L6.2939 13.152C2.3939 14.612 0.0039 17.612 0.0039 20.642Z',
viewBox: { width: 22.23046875, height: 33.455078125 },
},
heavy: {
name: 'pin.fill',
path: 'M0.0029 21.1516C0.0029 22.6316 1.0129 23.6216 2.5329 23.6216H9.5729V30.3116C9.5729 31.7216 10.8829 34.3016 11.3729 34.3016C11.8629 34.3016 13.1829 31.7216 13.1829 30.3116V23.6216H20.2229C21.7429 23.6216 22.7529 22.6316 22.7529 21.1516C22.7529 18.0316 20.3329 15.0616 16.4029 13.5516L15.9729 6.9616C18.1529 5.7316 19.6829 4.4716 20.3729 3.6116C20.7929 3.0716 21.0229 2.4916 21.0229 1.9416C21.0229 0.8216 20.1529 0.0016 18.9329 0.0016H3.8129C2.5929 0.0016 1.7229 0.8216 1.7229 1.9416C1.7229 2.4916 1.9529 3.0716 2.3829 3.6116C3.0629 4.4716 4.5929 5.7316 6.7829 6.9616L6.3429 13.5516C2.4229 15.0616 0.0029 18.0316 0.0029 21.1516Z',
viewBox: { width: 22.75, height: 34.302734375 },
},
},
} as const;
65 changes: 65 additions & 0 deletions src/entries/popup/hooks/useFilteredPinnedAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import uniqBy from 'lodash/uniqBy';
import { useCallback, useMemo } from 'react';

import { usePinnedAssetStore } from '~/core/state/pinnedAssets';
import { ParsedUserAsset } from '~/core/types/assets';

export const useFilteredPinnedAssets = (assets: ParsedUserAsset[]) => {
const { pinnedAssets } = usePinnedAssetStore();

const isPinned = useCallback(
(assetUniqueId: string) =>
pinnedAssets.some(({ uniqueId }) => uniqueId === assetUniqueId),
[pinnedAssets],
);

const computeUniqueAssets = useCallback(
(assets: ParsedUserAsset[]) => {
const filteredAssets = assets.filter(
({ uniqueId }) => !isPinned(uniqueId),
);

return uniqBy(
filteredAssets.sort(
(a, b) =>
parseFloat(b?.native?.balance?.amount) -
parseFloat(a?.native?.balance?.amount),
),
'uniqueId',
);
},
[isPinned],
);

const computePinnedAssets = useCallback(
(assets: ParsedUserAsset[]) => {
const filteredAssets = assets.filter((asset) => isPinned(asset.uniqueId));

const sortedAssets = filteredAssets.sort((a, b) => {
const pinnedFirstAsset = pinnedAssets.find(
({ uniqueId }) => uniqueId === a.uniqueId,
);

const pinnedSecondAsset = pinnedAssets.find(
({ uniqueId }) => uniqueId === b.uniqueId,
);

// This won't happen, but we'll just return to it's
// default sorted order just in case it will happen
if (!pinnedFirstAsset || !pinnedSecondAsset) return 0;

return pinnedFirstAsset.createdAt - pinnedSecondAsset.createdAt;
});

return sortedAssets;
},
[isPinned, pinnedAssets],
);

const filteredAssets = useMemo(
() => [...computePinnedAssets(assets), ...computeUniqueAssets(assets)],
[assets, computePinnedAssets, computeUniqueAssets],
);

return filteredAssets;
};
22 changes: 22 additions & 0 deletions src/entries/popup/hooks/usePress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useCallback, useRef, useState } from 'react';

export const usePress = (onPressed: () => void) => {
const pressTimerRef = useRef<NodeJS.Timeout>();
const [pressed, setPressed] = useState(false);

const startPress = useCallback(() => {
setPressed(false);
if (pressTimerRef.current) clearTimeout(pressTimerRef.current);
pressTimerRef.current = setTimeout(() => {
onPressed();
setPressed(true);
}, 500);
}, [onPressed]);

const endPress = useCallback(() => {
if (pressed) setPressed(false);
if (pressTimerRef.current) clearTimeout(pressTimerRef.current);
}, [pressed]);

return { pressed, startPress, endPress };
};
63 changes: 63 additions & 0 deletions src/entries/popup/hooks/useTokenPressMouseEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MouseEvent, useCallback, useState } from 'react';

import { usePinnedAssetStore } from '~/core/state/pinnedAssets';
import { ParsedUserAsset } from '~/core/types/assets';

import { usePress } from './usePress';

interface TokenPressMouseEventHookArgs {
token: ParsedUserAsset;
onClick: () => void;
}

export const useTokenPressMouseEvents = ({
token,
onClick,
}: TokenPressMouseEventHookArgs) => {
const [ready, setReady] = useState(false);
const { pinnedAssets, addPinnedAsset, removedPinnedAsset } =
usePinnedAssetStore();

const onPressed = useCallback(() => {
const pinned = pinnedAssets.some(
({ uniqueId }) => uniqueId === token.uniqueId,
);

if (pinned) {
removedPinnedAsset({ uniqueId: token.uniqueId });
return;
}

addPinnedAsset({ uniqueId: token.uniqueId });
}, [addPinnedAsset, pinnedAssets, removedPinnedAsset, token.uniqueId]);

const { pressed, startPress, endPress } = usePress(onPressed);

const onMouseDown = (e: MouseEvent<HTMLDivElement>) => {
if (e.button === 0) {
if (!ready) setReady(true);
startPress();
}
};

const onMouseUp = () => {
if (ready) {
setReady(false);
if (!pressed) {
endPress();
onClick();
}
}
};

const onMouseLeave = () => {
if (ready) {
setReady(false);
if (!pressed) {
endPress();
}
}
};

return { onMouseDown, onMouseUp, onMouseLeave };
};
34 changes: 33 additions & 1 deletion src/entries/popup/pages/home/TokenDetails/TokenContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import config from '~/core/firebase/remoteConfig';
import { i18n } from '~/core/languages';
import { shortcuts } from '~/core/references/shortcuts';
import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags';
import { usePinnedAssetStore } from '~/core/state/pinnedAssets';
import { useSelectedTokenStore } from '~/core/state/selectedToken';
import { ParsedUserAsset } from '~/core/types/assets';
import { truncateAddress } from '~/core/utils/address';
import { isNativeAsset } from '~/core/utils/chains';
import { copyAddress } from '~/core/utils/copy';
import { goToNewTab } from '~/core/utils/tabs';
import { getTokenBlockExplorer } from '~/core/utils/transactions';
import { Text } from '~/design-system';
import { Text, TextOverflow } from '~/design-system';
import { triggerAlert } from '~/design-system/components/Alert/Alert';

import {
Expand All @@ -34,6 +35,11 @@ export function TokenContextMenu({ children, token }: TokenContextMenuProps) {
const { isWatchingWallet } = useWallets();
const { featureFlags } = useFeatureFlagsStore();
const setSelectedToken = useSelectedTokenStore((s) => s.setSelectedToken);
const { pinnedAssets, removedPinnedAsset, addPinnedAsset } =
usePinnedAssetStore();
const pinned = pinnedAssets.some(
({ uniqueId }) => uniqueId === token.uniqueId,
);

// if we are navigating to new page (swap/send) the menu closes automatically,
// we don't want deselect the token in that case
Expand Down Expand Up @@ -130,6 +136,32 @@ export function TokenContextMenu({ children, token }: TokenContextMenuProps) {
</Text>
</ContextMenuItem>
)}
<ContextMenuItem
symbolLeft="pin.fill"
onSelect={() => {
if (pinned) {
removedPinnedAsset({ uniqueId: token.uniqueId });
return;
}

addPinnedAsset({ uniqueId: token.uniqueId });
}}
>
<TextOverflow
size="14pt"
weight="semibold"
color="label"
testId="account-name"
>
{pinned
? i18n.t('token_details.more_options.unpin_token', {
name: token.name,
})
: i18n.t('token_details.more_options.pin_token', {
name: token.name,
})}
</TextOverflow>
</ContextMenuItem>
</ContextMenuContent>
</DetailsMenuWrapper>
);
Expand Down
19 changes: 19 additions & 0 deletions src/entries/popup/pages/home/TokenMarkedHighlighter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Box } from '~/design-system';

export function TokenMarkedHighlighter() {
return (
<Box
background="accent"
style={{
position: 'absolute',
height: '60%',
top: '50%',
left: '1.4px',
transform: 'translateY(-50%)',
borderTopRightRadius: '3px',
borderBottomRightRadius: '3px',
width: '2px',
}}
/>
);
}
Loading
Loading