diff --git a/lavamoat/build-webpack/policy.json b/lavamoat/build-webpack/policy.json index 8d15a9aa99..2dfb40a900 100644 --- a/lavamoat/build-webpack/policy.json +++ b/lavamoat/build-webpack/policy.json @@ -1099,13 +1099,13 @@ "eslint>debug": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/code-frame": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/generator": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-environment-visitor": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/parser": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types": true, - "jest>@jest/core>jest-snapshot>@babel/traverse>globals": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-environment-visitor": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration": true + "jest>@jest/core>jest-snapshot>@babel/traverse>globals": true } }, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/code-frame": { @@ -1172,141 +1172,141 @@ "define": true } }, - "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types": { - "globals": { - "console.warn": true, - "process.env.BABEL_TYPES_8_BREAKING": true - }, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name": { "packages": { - "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types>@babel/helper-string-parser": true, - "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types>@babel/helper-validator-identifier": true, - "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/types": true } }, - "jest>@jest/core>jest-snapshot>@babel/types": { - "globals": { - "console.trace": true, - "process.env.BABEL_TYPES_8_BREAKING": true - }, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template": { "packages": { - "jest>@jest/core>jest-snapshot>@babel/types>@babel/helper-string-parser": true, - "jest>@jest/core>jest-snapshot>@babel/types>@babel/helper-validator-identifier": true, - "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/parser": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/types": true } }, - "lavamoat>@babel/highlight": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame": { + "globals": { + "console.warn": true, + "process.emitWarning": true + }, "packages": { - "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true, - "lavamoat>@babel/highlight>chalk": true, - "react>loose-envify>js-tokens": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk": true, + "lavamoat>@babel/highlight": true } }, - "lavamoat>@babel/highlight>chalk": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk": { "globals": { "process.env.TERM": true, "process.platform": true }, "packages": { - "lavamoat>@babel/highlight>chalk>ansi-styles": true, - "lavamoat>@babel/highlight>chalk>escape-string-regexp": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>escape-string-regexp": true, "supports-color": true } }, - "lavamoat>@babel/highlight>chalk>ansi-styles": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles": { "packages": { - "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert": true } }, - "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert": { "packages": { - "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert>color-name": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert>color-name": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/types": { + "globals": { + "console.warn": true, + "process.env.BABEL_TYPES_8_BREAKING": true + }, "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/types": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/types>@babel/helper-string-parser": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-function-name>@babel/types>@babel/helper-validator-identifier": true, + "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables": { "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/parser": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/types": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables>@babel/types": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables>@babel/types": { "globals": { "console.warn": true, - "process.emitWarning": true + "process.env.BABEL_TYPES_8_BREAKING": true }, "packages": { - "lavamoat>@babel/highlight": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables>@babel/types>@babel/helper-string-parser": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-hoist-variables>@babel/types>@babel/helper-validator-identifier": true, + "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk": { - "globals": { - "process.env.TERM": true, - "process.platform": true - }, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration": { "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>escape-string-regexp": true, - "supports-color": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration>@babel/types": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration>@babel/types": { + "globals": { + "console.warn": true, + "process.env.BABEL_TYPES_8_BREAKING": true + }, "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration>@babel/types>@babel/helper-string-parser": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/helper-split-export-declaration>@babel/types>@babel/helper-validator-identifier": true, + "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert": { + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types": { + "globals": { + "console.warn": true, + "process.env.BABEL_TYPES_8_BREAKING": true + }, "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/template>@babel/code-frame>chalk>ansi-styles>color-convert>color-name": true + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types>@babel/helper-string-parser": true, + "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types>@babel/helper-validator-identifier": true, + "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/types": { + "jest>@jest/core>jest-snapshot>@babel/types": { "globals": { - "console.warn": true, + "console.trace": true, "process.env.BABEL_TYPES_8_BREAKING": true }, "packages": { - "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/types>@babel/helper-string-parser": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name>@babel/types>@babel/helper-validator-identifier": true + "jest>@jest/core>jest-snapshot>@babel/types>@babel/helper-string-parser": true, + "jest>@jest/core>jest-snapshot>@babel/types>@babel/helper-validator-identifier": true, + "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables": { + "lavamoat>@babel/highlight": { "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables>@babel/types": true + "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true, + "lavamoat>@babel/highlight>chalk": true, + "react>loose-envify>js-tokens": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables>@babel/types": { + "lavamoat>@babel/highlight>chalk": { "globals": { - "console.warn": true, - "process.env.BABEL_TYPES_8_BREAKING": true + "process.env.TERM": true, + "process.platform": true }, "packages": { - "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables>@babel/types>@babel/helper-string-parser": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables>@babel/types>@babel/helper-validator-identifier": true + "lavamoat>@babel/highlight>chalk>ansi-styles": true, + "lavamoat>@babel/highlight>chalk>escape-string-regexp": true, + "supports-color": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration": { + "lavamoat>@babel/highlight>chalk>ansi-styles": { "packages": { - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration>@babel/types": true + "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration>@babel/types": { - "globals": { - "console.warn": true, - "process.env.BABEL_TYPES_8_BREAKING": true - }, + "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert": { "packages": { - "lavamoat>lavamoat-core>@babel/types>to-fast-properties": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration>@babel/types>@babel/helper-string-parser": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration>@babel/types>@babel/helper-validator-identifier": true + "lavamoat>@babel/highlight>chalk>ansi-styles>color-convert>color-name": true } }, "lint-staged>execa>merge-stream": { diff --git a/package.json b/package.json index c8ab11630e..a8725cb71c 100644 --- a/package.json +++ b/package.json @@ -265,7 +265,8 @@ "follow-redirects": "1.15.4", "ip": "2.0.1", "viem": "1.21.4", - "@solana/web3.js": "1.90.0" + "@solana/web3.js": "1.90.0", + "jose": "4.15.5" }, "lavamoat": { "allowScripts": { diff --git a/src/core/state/pinnedAssets/index.ts b/src/core/state/pinnedAssets/index.ts new file mode 100644 index 0000000000..6d1d0b4b55 --- /dev/null +++ b/src/core/state/pinnedAssets/index.ts @@ -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( + (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); diff --git a/src/design-system/styles/designTokens.ts b/src/design-system/styles/designTokens.ts index 9dd484b1a3..4e594490ea 100644 --- a/src/design-system/styles/designTokens.ts +++ b/src/design-system/styles/designTokens.ts @@ -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]; diff --git a/src/design-system/symbols/generated/index.ts b/src/design-system/symbols/generated/index.ts index eae7a60e44..76b7e34e78 100644 --- a/src/design-system/symbols/generated/index.ts +++ b/src/design-system/symbols/generated/index.ts @@ -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; diff --git a/src/entries/popup/hooks/usePress.ts b/src/entries/popup/hooks/usePress.ts new file mode 100644 index 0000000000..775471fbec --- /dev/null +++ b/src/entries/popup/hooks/usePress.ts @@ -0,0 +1,22 @@ +import { useCallback, useRef, useState } from 'react'; + +export const usePress = (onPressed: () => void) => { + const pressTimerRef = useRef(); + 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 }; +}; diff --git a/src/entries/popup/hooks/useTokenPressMouseEvents.ts b/src/entries/popup/hooks/useTokenPressMouseEvents.ts new file mode 100644 index 0000000000..f3e7287f67 --- /dev/null +++ b/src/entries/popup/hooks/useTokenPressMouseEvents.ts @@ -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) => { + 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 }; +}; diff --git a/src/entries/popup/pages/home/TokenDetails/TokenContextMenu.tsx b/src/entries/popup/pages/home/TokenDetails/TokenContextMenu.tsx index b90210f780..7ed97dec69 100644 --- a/src/entries/popup/pages/home/TokenDetails/TokenContextMenu.tsx +++ b/src/entries/popup/pages/home/TokenDetails/TokenContextMenu.tsx @@ -4,6 +4,7 @@ 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'; @@ -11,7 +12,7 @@ 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 { @@ -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 @@ -130,6 +136,32 @@ export function TokenContextMenu({ children, token }: TokenContextMenuProps) { )} + { + if (pinned) { + removedPinnedAsset({ uniqueId: token.uniqueId }); + return; + } + + addPinnedAsset({ uniqueId: token.uniqueId }); + }} + > + + {pinned + ? i18n.t('token_details.more_options.unpin_token', { + name: token.name, + }) + : i18n.t('token_details.more_options.pin_token', { + name: token.name, + })} + + ); diff --git a/src/entries/popup/pages/home/TokenMarkedHighlighter.tsx b/src/entries/popup/pages/home/TokenMarkedHighlighter.tsx new file mode 100644 index 0000000000..dcce381820 --- /dev/null +++ b/src/entries/popup/pages/home/TokenMarkedHighlighter.tsx @@ -0,0 +1,19 @@ +import { Box } from '~/design-system'; + +export function TokenMarkedHighlighter() { + return ( + + ); +} diff --git a/src/entries/popup/pages/home/Tokens.tsx b/src/entries/popup/pages/home/Tokens.tsx index 6b24d6330b..2f0e29b18c 100644 --- a/src/entries/popup/pages/home/Tokens.tsx +++ b/src/entries/popup/pages/home/Tokens.tsx @@ -22,6 +22,7 @@ import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme' import { useHideAssetBalancesStore } from '~/core/state/currentSettings/hideAssetBalances'; import { useHideSmallBalancesStore } from '~/core/state/currentSettings/hideSmallBalances'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; +import { usePinnedAssetStore } from '~/core/state/pinnedAssets'; import { ParsedUserAsset } from '~/core/types/assets'; import { truncateAddress } from '~/core/utils/address'; import { isCustomChain } from '~/core/utils/chains'; @@ -46,11 +47,13 @@ import useKeyboardAnalytics from '../../hooks/useKeyboardAnalytics'; import { useKeyboardShortcut } from '../../hooks/useKeyboardShortcut'; import { useRainbowNavigate } from '../../hooks/useRainbowNavigate'; import { useSystemSpecificModifierKey } from '../../hooks/useSystemSpecificModifierKey'; +import { useTokenPressMouseEvents } from '../../hooks/useTokenPressMouseEvents'; import { useTokensShortcuts } from '../../hooks/useTokensShortcuts'; import { ROUTES } from '../../urls'; import { TokensSkeleton } from './Skeletons'; import { TokenContextMenu } from './TokenDetails/TokenContextMenu'; +import { TokenMarkedHighlighter } from './TokenMarkedHighlighter'; const TokenRow = memo(function TokenRow({ token, @@ -60,18 +63,35 @@ const TokenRow = memo(function TokenRow({ testId: string; }) { const navigate = useRainbowNavigate(); - - const openDetails = () => + const openDetails = () => { navigate(ROUTES.TOKEN_DETAILS(token.uniqueId), { state: { skipTransitionOnRoute: ROUTES.HOME }, }); + }; + + const { onMouseDown, onMouseUp, onMouseLeave } = useTokenPressMouseEvents({ + token, + onClick: openDetails, + }); return ( - - - - - + + + + + + + ); }); @@ -83,6 +103,7 @@ export function Tokens() { const { hideSmallBalances } = useHideSmallBalancesStore(); const { trackShortcut } = useKeyboardAnalytics(); const { modifierSymbol } = useSystemSpecificModifierKey(); + const { pinnedAssets } = usePinnedAssetStore(); const { data: assets = [], @@ -123,19 +144,68 @@ export function Tokens() { }, ); - const allAssets = useMemo( - () => - uniqBy([...assets, ...customNetworkAssets], 'uniqueId').sort( + const combinedAssets = useMemo( + () => [...assets, ...customNetworkAssets], + [assets, customNetworkAssets], + ); + + 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, 'uniqueId').sort( (a: ParsedUserAsset, b: ParsedUserAsset) => parseFloat(b?.native?.balance?.amount) - parseFloat(a?.native?.balance?.amount), - ), - [assets, customNetworkAssets], + ); + }, + [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(combinedAssets), + ...computeUniqueAssets(combinedAssets), + ], + [combinedAssets, computePinnedAssets, computeUniqueAssets], ); const containerRef = useContainerRef(); const assetsRowVirtualizer = useVirtualizer({ - count: allAssets?.length || 0, + count: filteredAssets?.length || 0, getScrollElement: () => containerRef.current, estimateSize: () => 52, overscan: 20, @@ -162,7 +232,7 @@ export function Tokens() { return ; } - if (!allAssets?.length) { + if (!filteredAssets?.length) { return ; } @@ -200,19 +270,21 @@ export function Tokens() { {assetsRowVirtualizer.getVirtualItems().map((virtualItem) => { const { key, size, start, index } = virtualItem; - const token = allAssets[index]; + const token = filteredAssets[index]; + const pinned = pinnedAssets.some( + ({ uniqueId }) => uniqueId === token.uniqueId, + ); + return ( + {pinned && } ); diff --git a/static/json/languages/en_US.json b/static/json/languages/en_US.json index cb2daa3100..81ed74349d 100644 --- a/static/json/languages/en_US.json +++ b/static/json/languages/en_US.json @@ -1455,7 +1455,9 @@ "copy_address": "Copy Address", "view_on_explorer": "View on %{explorer}", "hide": "Hide Token", - "report": "Report Token" + "report": "Report Token", + "unpin_token": "Unpin %{name}", + "pin_token": "Pin %{name}" }, "about": { "about_token": "About %{name}", diff --git a/yarn.lock b/yarn.lock index 641b7b16ec..fb4b54f903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10964,10 +10964,10 @@ jju@^1.4.0: resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== -jose@4.13.1: - version "4.13.1" - resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c" - integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ== +jose@4.13.1, jose@4.15.5: + version "4.15.5" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.5.tgz#6475d0f467ecd3c630a1b5dadd2735a7288df706" + integrity sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg== jpeg-js@^0.4.2: version "0.4.4"