diff --git a/.i18n-codegen.json b/.i18n-codegen.json index ded70c3006bd..d54cdcd12b53 100644 --- a/.i18n-codegen.json +++ b/.i18n-codegen.json @@ -229,6 +229,18 @@ "trans": "Translate", "sourceMap": "inline" } + }, + { + "input": "./packages/mask/src/plugins/Avatar/locales/en-US.json", + "output": "./packages/mask/src/plugins/Avatar/locales/i18n_generated", + "parser": "i18next", + "generator": { + "type": "i18next/react-hooks", + "hooks": "useI18N", + "namespace": "com.maskbook.avatar", + "trans": "Translate", + "sourceMap": "inline" + } } ] } diff --git a/cspell.json b/cspell.json index c5db6969c1ab..148bf7539e3f 100644 --- a/cspell.json +++ b/cspell.json @@ -127,6 +127,7 @@ "multicall", "multihop", "newsfeed", + "nftavatar", "nftrss", "nftscan", "nums", @@ -227,7 +228,7 @@ "aicanft", "algr", "anft", - "avator", + "armv", "blocto", "bsct", "btcb", @@ -286,6 +287,7 @@ "nrge", "nrgt", "nyfi", + "okex", "openocean", "pausable", "pkts", diff --git a/package.json b/package.json index 2c521581f49c..baaa50f1801d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mask-network", - "packageManager": "pnpm@7.0.0", + "packageManager": "pnpm@7.1.1", "engines": { "node": ">=17.0.0", "pnpm": ">=7.0.0", @@ -33,16 +33,16 @@ "@emotion/serialize": "^1.0.3", "@emotion/styled": "^11.8.1", "@emotion/utils": "^1.1.0", - "@mui/icons-material": "5.6.2", - "@mui/lab": "5.0.0-alpha.80", - "@mui/material": "5.6.4", - "@mui/system": "5.6.4", + "@mui/icons-material": "5.8.0", + "@mui/lab": "5.0.0-alpha.82", + "@mui/material": "5.8.0", + "@mui/system": "5.8.0", "@solana/web3.js": "1.39.1", "@types/masknet__global-types": "workspace:*", "@types/react": "18.0.8", - "@types/react-dom": "^18.0.3", - "@types/web": "^0.0.64", - "i18next": "^21.7.1", + "@types/react-dom": "^18.0.4", + "@types/web": "^0.0.66", + "i18next": "^21.8.2", "immer": "9.0.12", "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -56,9 +56,9 @@ "web3-core-method": "1.7.3" }, "devDependencies": { - "@commitlint/cli": "^16.2.4", - "@commitlint/config-conventional": "^16.2.4", - "@dimensiondev/eslint-plugin": "^0.0.2-20220414093950-7d54c58", + "@commitlint/cli": "^17.0.0", + "@commitlint/config-conventional": "^17.0.0", + "@dimensiondev/eslint-plugin": "0.0.2-20220516081411-2649814", "@dimensiondev/patch-package": "^6.5.0", "@jest/globals": "^28.1.0", "@magic-works/i18n-codegen": "^0.1.0", @@ -67,10 +67,10 @@ "@nice-labs/git-rev": "^3.5.0", "@swc/core": "^1.2.177", "@types/lodash-es": "^4.17.6", - "@typescript-eslint/eslint-plugin": "^5.22.0", - "@typescript-eslint/experimental-utils": "^5.22.0", - "@typescript-eslint/parser": "^5.22.0", - "cspell": "^5.20.0", + "@typescript-eslint/eslint-plugin": "^5.25.0", + "@typescript-eslint/experimental-utils": "^5.25.0", + "@typescript-eslint/parser": "^5.25.0", + "cspell": "^5.21.0", "eslint": "8.15.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-react": "^7.29.4", @@ -78,17 +78,17 @@ "eslint-plugin-unicorn": "^42.0.0", "eslint-plugin-unused-imports": "^2.0.0", "gulp": "^4.0.2", - "husky": "^7.0.4", + "husky": "^8.0.1", "jest": "^28.1.0", "lint-staged": "^12.4.1", "prettier": "^2.6.2", - "ts-jest": "^28.0.1", + "ts-jest": "^28.0.2", "ts-node": "^10.7.0", "typescript": "4.7.0-beta" }, "pnpm": { "overrides": { - "@types/node": "^17.0.31", + "@types/node": "^17.0.34", "@types/react": "18.0.9", "@solana/web3.js": "1.39.1" }, diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index 329c14763de5..7654e02a4c00 100644 --- a/packages/.eslintrc.json +++ b/packages/.eslintrc.json @@ -40,6 +40,8 @@ "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", "no-useless-concat": "error", + "no-useless-rename": "error", + "no-useless-catch": "error", "no-loss-of-precision": "error", "prefer-regex-literals": "error", "react/jsx-boolean-value": "error", @@ -59,6 +61,7 @@ "unicorn/no-new-buffer": "error", "unicorn/no-thenable": "error", "unicorn/no-useless-promise-resolve-reject": "error", + "unicorn/no-array-reduce": ["error", { "allowSimpleOperations": false }], "unicorn/prefer-add-event-listener": "error", "unicorn/prefer-date-now": "error", "unicorn/prefer-dom-node-dataset": "error", @@ -70,6 +73,7 @@ "@dimensiondev/browser/prefer-location-assign": "error", "@dimensiondev/jsx/no-class-component": "error", "@dimensiondev/jsx/no-template-literal": "error", + "@dimensiondev/no-for-in": "error", "@dimensiondev/no-number-constructor": "off", "@dimensiondev/prefer-early-return": "error", "@dimensiondev/string/no-interpolation": "off", @@ -79,9 +83,9 @@ "@dimensiondev/type/no-instanceof-wrapper": "error", "@dimensiondev/type/no-wrapper-type-reference": "error", "@dimensiondev/unicode/specific-set": "error", + "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-base-to-string": "off", - "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-implied-eval": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-invalid-this": "error", diff --git a/packages/backup-format/src/utils/backupPreview.ts b/packages/backup-format/src/utils/backupPreview.ts index eb1be97c3095..baeb83c4c208 100644 --- a/packages/backup-format/src/utils/backupPreview.ts +++ b/packages/backup-format/src/utils/backupPreview.ts @@ -1,4 +1,5 @@ import type { NormalizedBackup } from '@masknet/backup-format' +import { sumBy } from 'lodash-unified' export interface BackupPreview { personas: number @@ -20,7 +21,7 @@ export function getBackupPreviewInfo(json: NormalizedBackup.Data): BackupPreview return { personas: json.personas.size, - accounts: [...json.personas.values()].reduce((a, b) => a + b.linkedProfiles.size, 0), + accounts: sumBy([...json.personas.values()], (persona) => persona.linkedProfiles.size), posts: json.posts.size, contacts: json.profiles.size, relations: json.relations.length, diff --git a/packages/backup-format/src/utils/hex2buffer.ts b/packages/backup-format/src/utils/hex2buffer.ts index 7204339f8dd7..b4fa7099a733 100644 --- a/packages/backup-format/src/utils/hex2buffer.ts +++ b/packages/backup-format/src/utils/hex2buffer.ts @@ -1,3 +1,5 @@ +import { sum } from 'lodash-unified' + /** @internal */ export function hex2buffer(hexString: string, padded?: boolean) { if (hexString.length % 2) { @@ -20,8 +22,8 @@ export function hex2buffer(hexString: string, padded?: boolean) { } /** @internal */ -function concat(...buf: (Uint8Array | number[])[]) { - const res = new Uint8Array(buf.map((item) => item.length).reduce((prev, cur) => prev + cur)) +function concat(...buf: Array) { + const res = new Uint8Array(sum(buf.map((item) => item.length))) let offset = 0 buf.forEach((item) => { for (let i = 0; i < item.length; i += 1) { diff --git a/packages/backup-format/src/version-1/index.ts b/packages/backup-format/src/version-1/index.ts index 09006f754713..c21a843b3bb4 100644 --- a/packages/backup-format/src/version-1/index.ts +++ b/packages/backup-format/src/version-1/index.ts @@ -89,16 +89,16 @@ interface BackupJSONFileVersion1 { publicKey: EC_Public_JsonWebKey privateKey: EC_Private_JsonWebKey localKey: AESJsonWebKey - previousIdentifiers?: { network: string; userId: string }[] + previousIdentifiers?: Array<{ network: string; userId: string }> nickname?: string }> people?: Array<{ network: string userId: string publicKey: EC_Public_JsonWebKey - previousIdentifiers?: { network: string; userId: string }[] + previousIdentifiers?: Array<{ network: string; userId: string }> nickname?: string - groups?: { network: string; groupID: string; virtualGroupOwner: string | null }[] + groups?: Array<{ network: string; groupID: string; virtualGroupOwner: string | null }> // Note: those props are not existed in the backup, just to make the code more readable privateKey?: EC_Private_JsonWebKey diff --git a/packages/backup-format/src/version-2/index.ts b/packages/backup-format/src/version-2/index.ts index ec5400e4ef55..752e64b71b89 100644 --- a/packages/backup-format/src/version-2/index.ts +++ b/packages/backup-format/src/version-2/index.ts @@ -322,7 +322,7 @@ interface BackupJSONFileVersion2 { privateKey?: JsonWebKey localKey?: JsonWebKey nickname?: string - linkedProfiles: [/** ProfileIdentifier.toText() */ string, LinkedProfileDetails][] + linkedProfiles: Array<[/** ProfileIdentifier.toText() */ string, LinkedProfileDetails]> createdAt: number // Unix timestamp updatedAt: number // Unix timestamp }> @@ -345,7 +345,7 @@ interface BackupJSONFileVersion2 { postBy: string // ProfileIdentifier.toText() identifier: string // PostIVIdentifier.toText() postCryptoKey?: JsonWebKey - recipients: 'everyone' | [/** ProfileIdentifier.toText() */ string, { reason: RecipientReasonJSON[] }][] + recipients: 'everyone' | Array<[/** ProfileIdentifier.toText() */ string, { reason: RecipientReasonJSON[] }]> /** @deprecated */ recipientGroups: never[] foundAt: number // Unix timestamp diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 794d858b72de..146c914615be 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@dimensiondev/holoflows-kit": "^0.9.0-20210902104757-7c3d0d0", - "@hookform/resolvers": "^2.8.8", + "@hookform/resolvers": "^2.8.10", "@masknet/backup-format": "workspace:*", "@masknet/icons": "workspace:*", "@masknet/plugin-example": "workspace:*", @@ -43,13 +43,13 @@ "date-fns": "2.28.0", "html-to-image": "^1.9.0", "json-stable-stringify": "^1.0.1", - "react-avatar-editor": "^12.0.0", + "react-avatar-editor": "^13.0.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-hook-form": "^7.30.0", + "react-hook-form": "^7.31.1", "react-qrcode-logo": "^2.7.0", "react-router-dom": "^6.3.0", - "react-to-print": "^2.14.6", + "react-to-print": "^2.14.7", "react-use": "^17.3.2", "unstated-next": "^1.1.0", "urlcat": "^2.0.4", @@ -58,10 +58,10 @@ "wallet.ts": "^1.0.1", "web3-core-helpers": "1.7.3", "web3-utils": "1.7.3", - "zod": "^3.14.4" + "zod": "^3.16.0" }, "devDependencies": { - "@babel/core": "^7.17.10", + "@babel/core": "^7.17.12", "@storybook/addon-actions": "^6.4.22", "@storybook/addon-essentials": "^6.4.22", "@storybook/addon-links": "^6.4.22", @@ -71,6 +71,6 @@ "@types/use-subscription": "^1.0.0", "@types/uuid": "^8.3.4", "babel-loader": "^8.2.5", - "webpack": "^5.72.0" + "webpack": "^5.72.1" } } diff --git a/packages/dashboard/src/components/CreateWalletForm/index.tsx b/packages/dashboard/src/components/CreateWalletForm/index.tsx index ddb77638de16..2a5df398e1a0 100644 --- a/packages/dashboard/src/components/CreateWalletForm/index.tsx +++ b/packages/dashboard/src/components/CreateWalletForm/index.tsx @@ -31,11 +31,11 @@ const useStyles = makeStyles()((theme) => ({ // TODO: actions, and icon may be an img url export interface CreateWalletFormProps { - options: { + options: Array<{ label: string icon: React.ReactNode value: number - }[] + }> } export function CreateWalletForm(props: CreateWalletFormProps) { diff --git a/packages/dashboard/src/components/PageFrame/FeaturePromotions/index.tsx b/packages/dashboard/src/components/PageFrame/FeaturePromotions/index.tsx index 1dd049428263..a2533c89433e 100644 --- a/packages/dashboard/src/components/PageFrame/FeaturePromotions/index.tsx +++ b/packages/dashboard/src/components/PageFrame/FeaturePromotions/index.tsx @@ -70,7 +70,7 @@ export const FeaturePromotions = memo(() => {
openTwitter(PluginId.RedPacket)} src={new URL('./SendLuckyDrop.png', import.meta.url).toString()} /> { )} - {(_, { backupJson: backupBasicInfoJson, handleRestore: handleRestore }) => ( + {(_, { backupJson: backupBasicInfoJson, handleRestore }) => ( <> diff --git a/packages/dashboard/src/initialization/Dashboard.tsx b/packages/dashboard/src/initialization/Dashboard.tsx index 84236295ed53..c16bcb6cc643 100644 --- a/packages/dashboard/src/initialization/Dashboard.tsx +++ b/packages/dashboard/src/initialization/Dashboard.tsx @@ -30,8 +30,7 @@ export default function DashboardRoot() { const pluginID = usePluginID() const PluginsWeb3State = useAllPluginsWeb3State() - // TODO: - // migrate EVM plugin + // TODO: migrate EVM plugin fixWeb3State(PluginsWeb3State[NetworkPluginID.PLUGIN_EVM], Web3Context) // #region theme diff --git a/packages/dashboard/src/locales/qya-AA.json b/packages/dashboard/src/locales/qya-AA.json index bd02ffc4a6ae..dfab6f0d573f 100644 --- a/packages/dashboard/src/locales/qya-AA.json +++ b/packages/dashboard/src/locales/qya-AA.json @@ -187,6 +187,7 @@ "wallets_empty_tokens_tip": "crwdns1789:0crwdne1789:0", "wallets_empty_collectible_tip": "crwdns1791:0crwdne1791:0", "wallets_address_copied": "crwdns1793:0crwdne1793:0", + "public_key_copied": "crwdns16574:0crwdne16574:0", "wallets_address_copy": "crwdns1795:0crwdne1795:0", "wallets_history_types": "crwdns1797:0crwdne1797:0", "wallets_history_value": "crwdns1799:0crwdne1799:0", diff --git a/packages/dashboard/src/pages/Personas/components/ContactTableRow/index.tsx b/packages/dashboard/src/pages/Personas/components/ContactTableRow/index.tsx index a31944b549ee..48ba5fda28c4 100644 --- a/packages/dashboard/src/pages/Personas/components/ContactTableRow/index.tsx +++ b/packages/dashboard/src/pages/Personas/components/ContactTableRow/index.tsx @@ -137,7 +137,7 @@ export const ContactTableRowUI = memo( {/* To support emoji */} {String.fromCodePoint( first.codePointAt(0) ?? SPACE_POINT, - last.codePointAt(0) ?? SPACE_POINT, + last?.codePointAt(0) ?? SPACE_POINT, )} {contact.fingerprint ? : null} diff --git a/packages/dashboard/src/pages/Wallets/components/Balance/index.tsx b/packages/dashboard/src/pages/Wallets/components/Balance/index.tsx index d782c2df2c3c..88763228ff6d 100644 --- a/packages/dashboard/src/pages/Wallets/components/Balance/index.tsx +++ b/packages/dashboard/src/pages/Wallets/components/Balance/index.tsx @@ -122,10 +122,13 @@ export const Balance = memo( {showOperations && ( - -
diff --git a/packages/mask/src/components/CompositionDialog/EncryptionTargetSelector.tsx b/packages/mask/src/components/CompositionDialog/EncryptionTargetSelector.tsx index 761a2bd490a4..50fba1981300 100644 --- a/packages/mask/src/components/CompositionDialog/EncryptionTargetSelector.tsx +++ b/packages/mask/src/components/CompositionDialog/EncryptionTargetSelector.tsx @@ -147,7 +147,10 @@ export function EncryptionTargetSelector(props: EncryptionTargetSelectorProps) { {e2eDisabledMessage} {noLocalKeyMessage} props.onChange(EncryptionTargetType.E2E)} + onItemClick={() => { + props.onChange(EncryptionTargetType.E2E) + setAnchorEl(null) + }} itemTail={} disabled={!!props.e2eDisabled} value={EncryptionTargetType.E2E} diff --git a/packages/mask/src/components/DataSource/useNextID.ts b/packages/mask/src/components/DataSource/useNextID.ts index 7988024e2b59..c68873a46e22 100644 --- a/packages/mask/src/components/DataSource/useNextID.ts +++ b/packages/mask/src/components/DataSource/useNextID.ts @@ -65,8 +65,7 @@ export function useNextIDConnectStatus() { const personaConnectStatus = usePersonaConnectStatus() const lastState = useSetupGuideStatusState() const lastRecognized = useLastRecognizedIdentity() - const [username] = useState(lastState.username || lastRecognized.identifier?.userId || '') - + const username = lastRecognized.identifier?.userId || lastState.username || '' const { value: VerificationStatus = NextIDVerificationStatus.Other, retry, @@ -117,7 +116,7 @@ export function useNextIDConnectStatus() { isOpenedVerifyDialog = true isOpenedFromButton = false return NextIDVerificationStatus.WaitingVerify - }, [username, enableNextID, isOpenedVerifyDialog, currentPersonaIdentifier.value]) + }, [username, enableNextID, isOpenedVerifyDialog, personaConnectStatus, currentPersonaIdentifier.value]) return { isVerified: VerificationStatus === NextIDVerificationStatus.Verified, diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx index 1ab0de6a8474..04c7deb6aa61 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -15,7 +15,7 @@ import { type PostContext, usePostInfoDetails, usePostInfo } from '@masknet/plug import { Some } from 'ts-results' function progressReducer( - state: { key: string; progress: SuccessDecryption | FailureDecryption | DecryptionProgress }[], + state: Array<{ key: string; progress: SuccessDecryption | FailureDecryption | DecryptionProgress }>, payload: { type: 'refresh' key: string diff --git a/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx b/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx index a018a42e6b65..68a9c33a14b4 100644 --- a/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx +++ b/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx @@ -4,12 +4,15 @@ import { registeredPlugins, usePostInfoDetails, Plugin, - usePluginI18NField, + PluginI18NFieldRender, } from '@masknet/plugin-infra/content-script' import { extractTextFromTypedMessage } from '@masknet/typed-message' -import { Switch } from '@mui/material' import Services from '../../extension/service' import MaskPostExtraInfoWrapper from '../../plugins/MaskPluginWrapper' +import { HTMLProps, useCallback } from 'react' +import { Button, Skeleton, useTheme } from '@mui/material' +import { PluginIcon } from '@masknet/icons' +import { makeStyles } from '@masknet/theme' import { useI18N } from '../../utils' function useDisabledPlugins() { @@ -52,25 +55,71 @@ export function PossiblePluginSuggestionPostInspector() { return } export function PossiblePluginSuggestionUI(props: { plugins: Plugin.DeferredDefinition[] }) { - const { t } = useI18N() - const t2 = usePluginI18NField() const { plugins } = props + const { t } = useI18N() + const theme = useTheme() + const onClick = useCallback((x: Plugin.DeferredDefinition) => { + Services.Settings.setPluginMinimalModeEnabled(x.ID, false) + }, []) + const _plugins = useActivatedPluginsSNSAdaptor('any') if (!plugins.length) return null + return ( <> {plugins.map((x) => ( } + publisher={ + x.publisher ? : undefined + } + publisherLink={x.publisher?.link} + wrapperProps={_plugins.find((y) => y.ID === x.ID)?.wrapperProps} action={ - Services.Settings.setPluginMinimalModeEnabled(x.ID, false)} - /> + } + content={} /> ))} ) } + +const useRectangleStyles = makeStyles()(() => ({ + rectangle: { + background: 'rgba(255, 255, 255, 0.5)', + }, +})) +interface RectangleProps extends HTMLProps {} + +export function Rectangle(props: RectangleProps) { + const { classes } = useRectangleStyles() + return ( +
+ + + +
+ ) +} diff --git a/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx b/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx index c917be2bf891..5d09af03a749 100644 --- a/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx +++ b/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx @@ -1,7 +1,7 @@ import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra/content-script' const PluginRenderer = createInjectHooksRenderer( - useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, + useActivatedPluginsSNSAdaptor.visibility.useAnyMode, (x) => x.SearchResultBox, ) diff --git a/packages/mask/src/components/shared/AbstractTab.tsx b/packages/mask/src/components/shared/AbstractTab.tsx index 6b6d5c8dbc73..fc124d5bbcfc 100644 --- a/packages/mask/src/components/shared/AbstractTab.tsx +++ b/packages/mask/src/components/shared/AbstractTab.tsx @@ -23,11 +23,13 @@ interface TabPanelProps extends BoxProps { export interface AbstractTabProps extends withClasses<'tab' | 'tabs' | 'tabPanel' | 'indicator' | 'focusTab' | 'tabPaper' | 'flexContainer'> { - tabs: (Omit & { - cb?: () => void - disableFocusRipple?: boolean - disableRipple?: boolean - })[] + tabs: Array< + Omit & { + cb?: () => void + disableFocusRipple?: boolean + disableRipple?: boolean + } + > state?: readonly [number, (next: number) => void] index?: number disableFocusRipple?: boolean diff --git a/packages/mask/src/components/shared/ApplicationBoard.tsx b/packages/mask/src/components/shared/ApplicationBoard.tsx index ad043a459618..bba3a2fd9cfc 100644 --- a/packages/mask/src/components/shared/ApplicationBoard.tsx +++ b/packages/mask/src/components/shared/ApplicationBoard.tsx @@ -4,7 +4,7 @@ import { Typography } from '@mui/material' import { useChainId } from '@masknet/web3-shared-evm' import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra/content-script' import { useCurrentWeb3NetworkPluginID, useAccount, NetworkPluginID } from '@masknet/plugin-infra/web3' -import { EMPTY_LIST, CrossIsolationMessages, formatPersonaPublicKey } from '@masknet/shared-base' +import { formatPersonaPublicKey } from '@masknet/shared-base' import { getCurrentSNSNetwork } from '../../social-network-adaptor/utils' import { activatedSocialNetworkUI } from '../../social-network' import { useI18N } from '../../utils' @@ -31,7 +31,7 @@ const useStyles = makeStyles<{ shouldScroll: boolean }>()((theme, props) => { gridGap: 10, justifyContent: 'space-between', height: 320, - width: props.shouldScroll ? 575 : 562, + width: props.shouldScroll ? 589 : 576, '::-webkit-scrollbar': { backgroundColor: 'transparent', width: 20, @@ -122,38 +122,32 @@ function ApplicationBoardContent(props: Props) { const applicationList = useMemo( () => snsAdaptorPlugins - .reduce((acc, cur) => { - if (!cur.ApplicationEntries) return acc - const currentWeb3NetworkSupportedChainIds = cur.enableRequirement.web3?.[currentWeb3Network] + .flatMap(({ ID, ApplicationEntries, enableRequirement }) => { + if (!ApplicationEntries) return [] + const currentWeb3NetworkSupportedChainIds = enableRequirement.web3?.[currentWeb3Network] const isWalletConnectedRequired = currentWeb3NetworkSupportedChainIds !== undefined - const currentSNSIsSupportedNetwork = cur.enableRequirement.networks.networks[currentSNSNetwork] + const currentSNSIsSupportedNetwork = enableRequirement.networks.networks[currentSNSNetwork] const isSNSEnabled = currentSNSIsSupportedNetwork === undefined || currentSNSIsSupportedNetwork - - return acc.concat( - cur.ApplicationEntries.map((x) => { - return { - entry: x, - enabled: isSNSEnabled, - pluginId: cur.ID, - isWalletConnectedRequired: !account && isWalletConnectedRequired, - isWalletConnectedEVMRequired: Boolean( - account && - currentWeb3Network !== NetworkPluginID.PLUGIN_EVM && - isWalletConnectedRequired, - ), - } - }) ?? EMPTY_LIST, - ) - }, EMPTY_LIST) - .sort( - (a, b) => - (a.entry.appBoardSortingDefaultPriority ?? 0) - (b.entry.appBoardSortingDefaultPriority ?? 0), - ) + return ApplicationEntries.map((entry) => ({ + entry, + enabled: isSNSEnabled, + pluginId: ID, + isWalletConnectedRequired: !account && isWalletConnectedRequired, + isWalletConnectedEVMRequired: Boolean( + account && currentWeb3Network !== NetworkPluginID.PLUGIN_EVM && isWalletConnectedRequired, + ), + })) + }) + .sort((a, b) => { + return (a.entry.appBoardSortingDefaultPriority ?? 0) - (b.entry.appBoardSortingDefaultPriority ?? 0) + }) .filter((x) => Boolean(x.entry.RenderEntryComponent)), [snsAdaptorPlugins, currentWeb3Network, chainId, account], ) - const recommendFeatureAppList = applicationList.filter((x) => x.entry.recommendFeature) + const recommendFeatureAppList = applicationList + .filter((x) => x.entry.recommendFeature) + .sort((a, b) => (a.entry.appBoardSortingDefaultPriority ?? 0) - (b.entry.appBoardSortingDefaultPriority ?? 0)) const listedAppList = applicationList.filter((x) => !x.entry.recommendFeature).filter((x) => !getUnlistedApp(x)) const { classes } = useStyles({ shouldScroll: listedAppList.length > 12 }) @@ -216,7 +210,7 @@ function RenderEntryComponent({ application }: { application: Application }) { const verifyPersona = useCallback(() => { closeApplicationBoard() - CrossIsolationMessages.events.verifyNextID.sendToAll(undefined) + ApplicationEntryStatus.personaNextIDReset?.() }, []) const clickHandler = (() => { @@ -233,6 +227,7 @@ function RenderEntryComponent({ application }: { application: Application }) { // #region tooltip hint const tooltipHint = (() => { + if (ApplicationEntryStatus.isLoading) return if (application.isWalletConnectedRequired) return t('application_tooltip_hint_connect_wallet') if (application.isWalletConnectedEVMRequired) return t('application_tooltip_hint_switch_to_evm_wallet') if (!application.entry.nextIdRequired) return @@ -269,6 +264,7 @@ interface ApplicationEntryStatusContextProps { currentPersonaPublicKey: string | undefined currentSNSConnectedPersonaPublicKey: string | undefined personaConnectAction: (() => void) | undefined + personaNextIDReset: (() => void) | undefined isLoading: boolean } @@ -282,6 +278,7 @@ const ApplicationEntryStatusContext = createContext) { const personaConnectStatus = usePersonaConnectStatus() const nextIDConnectStatus = useNextIDConnectStatus() - const { value: ApplicationCurrentStatus, retry } = usePersonaAgainstSNSConnectStatus() + const { + value: ApplicationCurrentStatus, + retry, + loading: personaAgainstSNSConnectStatusLoading, + } = usePersonaAgainstSNSConnectStatus() useEffect(() => { return MaskMessages.events.currentPersonaIdentifier.on(retry) @@ -302,6 +303,7 @@ function ApplicationEntryStatusProvider(props: PropsWithChildren<{}>) { ) { shouldVerifyNextId: Boolean(!nextIDConnectStatus.isVerified && ApplicationCurrentStatus), currentPersonaPublicKey, currentSNSConnectedPersonaPublicKey, - isLoading: nextIDConnectStatus.loading, + isLoading: nextIDConnectStatus.loading || personaAgainstSNSConnectStatusLoading, }}> {props.children} diff --git a/packages/mask/src/components/shared/ApplicationSettingPluginList.tsx b/packages/mask/src/components/shared/ApplicationSettingPluginList.tsx index 60cce9311e2c..e1ae1c7860e9 100644 --- a/packages/mask/src/components/shared/ApplicationSettingPluginList.tsx +++ b/packages/mask/src/components/shared/ApplicationSettingPluginList.tsx @@ -2,7 +2,6 @@ import { useActivatedPluginsSNSAdaptor, Plugin } from '@masknet/plugin-infra/con import { useMemo, useState, useCallback } from 'react' import { List, ListItem, Typography } from '@mui/material' import { makeStyles, getMaskColor } from '@masknet/theme' -import { EMPTY_LIST } from '@masknet/shared-base' import { useI18N } from '../../utils' import { PersistentStorages } from '../../../shared' @@ -25,7 +24,7 @@ export function getUnlistedApp(app: Application): boolean { } // #endregion -const useStyles = makeStyles()((theme) => ({ +const useStyles = makeStyles<{ iconFilterColor?: string }>()((theme, { iconFilterColor }) => ({ list: { display: 'grid', gap: theme.spacing(2, 1), @@ -55,6 +54,9 @@ const useStyles = makeStyles()((theme) => ({ width: 36, height: 36, }, + ...(iconFilterColor + ? { filter: `drop-shadow(0px 6px 12px ${iconFilterColor})`, backdropFilter: 'blur(16px)' } + : {}), }, loadingWrapper: { display: 'flex', @@ -78,31 +80,20 @@ const useStyles = makeStyles()((theme) => ({ })) export function ApplicationSettingPluginList() { - const { classes } = useStyles() + const { classes } = useStyles({ iconFilterColor: undefined }) const { t } = useI18N() const snsAdaptorPlugins = useActivatedPluginsSNSAdaptor('any') - const applicationList = useMemo( - () => - snsAdaptorPlugins - .reduce((acc, cur) => { - if (!cur.ApplicationEntries) return acc - return acc.concat( - cur.ApplicationEntries.filter( - (x) => x.appBoardSortingDefaultPriority && !x.recommendFeature, - ).map((x) => { - return { - entry: x, - pluginId: cur.ID, - } - }) ?? EMPTY_LIST, - ) - }, EMPTY_LIST) - .sort( - (a, b) => - (a.entry.appBoardSortingDefaultPriority ?? 0) - (b.entry.appBoardSortingDefaultPriority ?? 0), - ), - [snsAdaptorPlugins], - ) + const applicationList = useMemo(() => { + return snsAdaptorPlugins + .flatMap(({ ID, ApplicationEntries: entries }) => + (entries ?? []) + .filter((entry) => entry.appBoardSortingDefaultPriority && !entry.recommendFeature) + .map((entry) => ({ entry, pluginId: ID })), + ) + .sort((a, b) => { + return (a.entry.appBoardSortingDefaultPriority ?? 0) - (b.entry.appBoardSortingDefaultPriority ?? 0) + }) + }, [snsAdaptorPlugins]) const [listedAppList, setListedAppList] = useState(applicationList.filter((x) => !getUnlistedApp(x))) const [unlistedAppList, setUnListedAppList] = useState(applicationList.filter((x) => getUnlistedApp(x))) @@ -135,7 +126,7 @@ interface AppListProps { function AppList(props: AppListProps) { const { appList, setUnlistedApp, isListed } = props - const { classes } = useStyles() + const { classes } = useStyles({ iconFilterColor: undefined }) const { t } = useI18N() return appList.length > 0 ? ( @@ -168,7 +159,7 @@ interface AppListItemProps { function AppListItem(props: AppListItemProps) { const { application, setUnlistedApp, isListed } = props - const { classes } = useStyles() + const { classes } = useStyles({ iconFilterColor: application.entry.iconFilterColor }) return ( setUnlistedApp(application, isListed)}>
{application.entry.icon}
diff --git a/packages/mask/src/components/shared/ApplicationSettingPluginSwitch.tsx b/packages/mask/src/components/shared/ApplicationSettingPluginSwitch.tsx index e11d80d28614..b8f5de60eed8 100644 --- a/packages/mask/src/components/shared/ApplicationSettingPluginSwitch.tsx +++ b/packages/mask/src/components/shared/ApplicationSettingPluginSwitch.tsx @@ -1,7 +1,7 @@ import { List, ListItem, ListItemAvatar, Avatar, Typography, Box } from '@mui/material' import { openWindow } from '@masknet/shared-base-ui' import { TutorialIcon } from '@masknet/icons' -import { useActivatedPluginsSNSAdaptor, Plugin, PluginI18NFieldRender } from '@masknet/plugin-infra/content-script' +import { useActivatedPluginsSNSAdaptor, PluginI18NFieldRender } from '@masknet/plugin-infra/content-script' import { SettingSwitch } from '@masknet/shared' import { makeStyles, MaskColorVar } from '@masknet/theme' import { Services } from '../../extension/service' @@ -72,17 +72,9 @@ export function ApplicationSettingPluginSwitch(props: Props) { return ( {snsAdaptorPlugins - .reduce<{ entry: Plugin.SNSAdaptor.ApplicationEntry; pluginId: string }[]>((acc, cur) => { - if (!cur.ApplicationEntries) return acc - return acc.concat( - cur.ApplicationEntries.map((x) => { - return { - entry: x, - pluginId: cur.ID, - } - }) ?? [], - ) - }, []) + .flatMap(({ ID, ApplicationEntries: entries }) => + (entries ?? []).map((entry) => ({ entry, pluginId: ID })), + ) .filter((x) => x.entry.category === 'dapp') .sort((a, b) => (a.entry.marketListSortingPriority ?? 0) - (b.entry.marketListSortingPriority ?? 0)) .map((x) => ( diff --git a/packages/mask/src/components/shared/DraggableDiv.tsx b/packages/mask/src/components/shared/DraggableDiv.tsx index ca2cf87eec02..f30c9a7bbe4f 100644 --- a/packages/mask/src/components/shared/DraggableDiv.tsx +++ b/packages/mask/src/components/shared/DraggableDiv.tsx @@ -39,7 +39,6 @@ export function DraggableDiv({ cancel="p, h1, input, button, address" handle="nav" {...DraggableProps} - // @ts-expect-error @types/react 18 children={
} />
diff --git a/packages/mask/src/components/shared/SelectRecipients/ProfileInList.tsx b/packages/mask/src/components/shared/SelectRecipients/ProfileInList.tsx index 8be4ab514fc4..3f2a3f62be39 100644 --- a/packages/mask/src/components/shared/SelectRecipients/ProfileInList.tsx +++ b/packages/mask/src/components/shared/SelectRecipients/ProfileInList.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react' import { ListItemText, Checkbox, ListItemAvatar, ListItem } from '@mui/material' import { makeStyles, ShadowRootTooltip } from '@masknet/theme' import Highlighter from 'react-highlight-words' -import { formatPersonaPublicKey, ProfileInformationFromNextID } from '@masknet/shared-base' +import { formatPersonaFingerprint, ProfileInformationFromNextID } from '@masknet/shared-base' import { Avatar } from '../../../utils/components/Avatar' import { CopyIcon } from '@masknet/icons' import { truncate } from 'lodash-unified' @@ -112,7 +112,7 @@ export function ProfileInList(props: ProfileInListProps) { (ev: React.MouseEvent) => props.onChange(ev, !props.selected), [props], ) - const textToHighlight = formatPersonaPublicKey(profile.linkedPersona?.publicKeyAsHex?.toUpperCase() ?? '', 4) + const textToHighlight = formatPersonaFingerprint(profile.linkedPersona?.rawPublicKey?.toUpperCase() ?? '', 3) return ( { - const publicHexKey = profile.linkedPersona?.publicKeyAsHex - if (!publicHexKey) return - copyToClipboard(publicHexKey.toUpperCase()) + const rawPublicKey = profile.linkedPersona?.rawPublicKey + if (!rawPublicKey) return + copyToClipboard(rawPublicKey.toUpperCase()) }} /> {profile.fromNextID &&
Next.ID
} diff --git a/packages/mask/src/components/shared/SelectRecipients/SelectRecipients.tsx b/packages/mask/src/components/shared/SelectRecipients/SelectRecipients.tsx index 1109596e0435..142868c4d93b 100644 --- a/packages/mask/src/components/shared/SelectRecipients/SelectRecipients.tsx +++ b/packages/mask/src/components/shared/SelectRecipients/SelectRecipients.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { ProfileInformation as Profile, EMPTY_LIST, NextIDPlatform } from '@masknet/shared-base' +import { ProfileInformation as Profile, EMPTY_LIST, NextIDPlatform, ECKeyIdentifier } from '@masknet/shared-base' import type { LazyRecipients } from '../../CompositionDialog/CompositionUI' import { SelectRecipientsDialogUI } from './SelectRecipientsDialog' import { useCurrentIdentity } from '../../DataSource/useActivatedUI' @@ -23,7 +23,12 @@ const resolveNextIDPlatform = (value: string) => { if (isValidAddress(value)) return NextIDPlatform.Ethereum if (value.length >= 44) return NextIDPlatform.NextID if (/^\w{1,15}$/.test(value)) return NextIDPlatform.Twitter - return undefined + return +} + +const resolveValueToSearch = (value: string) => { + if (value.length === 44) return new ECKeyIdentifier('secp256k1', value).publicKeyAsHex ?? value + return value.toLowerCase() } export function SelectRecipientsUI(props: SelectRecipientsUIProps) { @@ -32,11 +37,12 @@ export function SelectRecipientsUI(props: SelectRecipientsUIProps) { const [valueToSearch, setValueToSearch] = useState('') const currentIdentity = useCurrentIdentity() const type = resolveNextIDPlatform(valueToSearch) + const value = resolveValueToSearch(valueToSearch) const { loading: searchLoading, value: NextIDResults } = useNextIDBoundByPlatform( type ?? NextIDPlatform.NextID, - valueToSearch.toLowerCase(), + value, ) - const NextIDItems = useTwitterIdByWalletSearch(NextIDResults, valueToSearch, type) + const NextIDItems = useTwitterIdByWalletSearch(NextIDResults, value, type) const profileItems = items.recipients?.filter((x) => x.identifier !== currentIdentity?.identifier) const searchedList = uniqBy( profileItems?.concat(NextIDItems) ?? [], diff --git a/packages/mask/src/components/shared/SelectRecipients/SelectRecipientsDialog.tsx b/packages/mask/src/components/shared/SelectRecipients/SelectRecipientsDialog.tsx index 2b18c7fa2ffd..291f14d9658d 100644 --- a/packages/mask/src/components/shared/SelectRecipients/SelectRecipientsDialog.tsx +++ b/packages/mask/src/components/shared/SelectRecipients/SelectRecipientsDialog.tsx @@ -100,7 +100,13 @@ export function SelectRecipientsDialogUI(props: SelectRecipientsDialogUIProps) { }, [props.open]) const itemsAfterSearch = useMemo(() => { const fuse = new Fuse(items, { - keys: ['identifier.userId', 'nickname', 'walletAddress', 'linkedPersona.publicKeyAsHex'], + keys: [ + 'identifier.userId', + 'nickname', + 'walletAddress', + 'linkedPersona.rawPublicKey', + 'linkedPersona.publicKeyAsHex', + ], isCaseSensitive: false, ignoreLocation: true, threshold: 0, diff --git a/packages/mask/src/database/helpers/pagination.ts b/packages/mask/src/database/helpers/pagination.ts index 9d267b7a5ca3..ea6f4619e7c6 100644 --- a/packages/mask/src/database/helpers/pagination.ts +++ b/packages/mask/src/database/helpers/pagination.ts @@ -3,7 +3,7 @@ import type { IDBPSafeTransaction } from '../../../background/database/utils/ope export async function queryTransactionPaged< DBType extends DBSchema, - TxStores extends StoreNames[], + TxStores extends Array>, Mode extends 'readonly' | 'readwrite', RecordType extends IDBPCursorWithValueIteratorValue['value'], >( diff --git a/packages/mask/src/extension/background-script/EthereumServices/nonce.ts b/packages/mask/src/extension/background-script/EthereumServices/nonce.ts index 4963d8a79965..54a828312be7 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/nonce.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/nonce.ts @@ -7,7 +7,7 @@ class NonceManager { constructor(private address: string) {} private nonce = NonceManager.INITIAL_NONCE private locked = false - private tasks: (() => void)[] = [] + private tasks: Array<() => void> = [] private lock() { this.locked = true diff --git a/packages/mask/src/extension/background-script/EthereumServices/provider.ts b/packages/mask/src/extension/background-script/EthereumServices/provider.ts index f564b08afa85..633c9c06999a 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/provider.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/provider.ts @@ -9,14 +9,12 @@ import * as Injected from './providers/Injected' import * as Fortmatic from './providers/Fortmatic' // #region connect WalletConnect -// step 1: -// Generate the connection URI and render a QRCode for scanning by the user +// Step 1: Generate the connection URI and render a QRCode for scanning by the user export async function createConnectionURI() { return (await WalletConnect.createConnector()).uri } -// step2: -// If user confirmed the request we will receive the 'connect' event +// Step 2: If user confirmed the request we will receive the 'connect' event type Account = { account?: string; chainId: ChainId } let deferredConnect: Promise | null = null let resolveConnect: ((result: Account) => void) | undefined diff --git a/packages/mask/src/extension/background-script/EthereumServices/providers/WalletConnect.ts b/packages/mask/src/extension/background-script/EthereumServices/providers/WalletConnect.ts index b5f1133b1544..14e20bc09ccb 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/providers/WalletConnect.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/providers/WalletConnect.ts @@ -92,10 +92,10 @@ const onConnect = () => onUpdate(null) const onUpdate = async ( error: Error | null, payload?: { - params: { + params: Array<{ chainId: number accounts: string[] - }[] + }> }, ) => { if (error) return diff --git a/packages/mask/src/extension/background-script/EthereumServices/rpc/abi.ts b/packages/mask/src/extension/background-script/EthereumServices/rpc/abi.ts index 5460b661eaa2..0bf230349bc6 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/rpc/abi.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/rpc/abi.ts @@ -16,10 +16,10 @@ const coder = ABICoder as unknown as ABICoder.AbiCoder type InternalItem = { name: string - parameters: { + parameters: Array<{ name: string type: string - }[] + }> } const ABI_MAP: Map = new Map() diff --git a/packages/mask/src/extension/background-script/Jobs/SettingListeners.ts b/packages/mask/src/extension/background-script/Jobs/SettingListeners.ts index b9c7573c7720..56860c55572a 100644 --- a/packages/mask/src/extension/background-script/Jobs/SettingListeners.ts +++ b/packages/mask/src/extension/background-script/Jobs/SettingListeners.ts @@ -5,12 +5,12 @@ import { MaskMessages } from '../../../utils' export default function (signal: AbortSignal) { if (!isEnvironment(Environment.ManifestBackground)) return - const obj = ToBeListened() - for (const _key in obj) { - const key = _key as keyof MaskSettingsEvents + const listeners = ToBeListened() + const keys = Object.keys(listeners) as Array + for (const key of keys) { signal.addEventListener( 'abort', - obj[key].addListener((data: any) => MaskMessages.events[key].sendToAll(data as never)), + listeners[key].addListener((data) => MaskMessages.events[key].sendToAll(data as never)), ) } } diff --git a/packages/mask/src/extension/mask-sdk/hmr-bridge.ts b/packages/mask/src/extension/mask-sdk/hmr-bridge.ts index cf4907f75154..9d5545e99dc3 100644 --- a/packages/mask/src/extension/mask-sdk/hmr-bridge.ts +++ b/packages/mask/src/extension/mask-sdk/hmr-bridge.ts @@ -4,10 +4,10 @@ export const hmr_sdkServer = { ...maskSDKServer } if (import.meta.webpackHot) { import.meta.webpackHot.accept('./bridge', () => { - for (const key in hmr_sdkServer) { + for (const key of Object.keys(hmr_sdkServer)) { if (!(key in maskSDKServer)) Reflect.deleteProperty(hmr_sdkServer, key) } - for (const key in maskSDKServer) { + for (const key of Object.keys(maskSDKServer)) { // @ts-expect-error hmr_sdkServer[key] = maskSDKServer[key] } diff --git a/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx b/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx index b47e4de10d78..09e057afa2b9 100644 --- a/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx @@ -12,12 +12,15 @@ import type { ConnectedWalletInfo } from '../type' import { NextIDProof } from '@masknet/web3-providers' import Service from '../../../../service' import { usePopupCustomSnackbar } from '@masknet/theme' +import { useLocation, useNavigate } from 'react-router-dom' const ConnectedWallets = memo(() => { const { t } = useI18N() const chainId = useChainId() const { NameService } = useWeb3State() const wallets = useWallets() + const navigate = useNavigate() + const location = useLocation() const { proofs, currentPersona, refreshProofs, fetchProofsLoading } = PersonaContext.useContainer() const { showSnackbar } = usePopupCustomSnackbar() @@ -107,6 +110,13 @@ const ConnectedWallets = memo(() => { ) const navigateToConnectWallet = async () => { + const params = new URLSearchParams(location.search) + const internal = params.get('internal') + + if (internal) { + navigate(PopupRoutes.ConnectWallet) + return + } await Service.Helper.openPopupWindow(PopupRoutes.ConnectWallet) window.close() } diff --git a/packages/mask/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx b/packages/mask/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx index 1bfc6b0934a3..10ef1fda1fa4 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx @@ -146,7 +146,7 @@ const useStyles = makeStyles()({ export interface Prior1559TransferProps { selectedAsset?: Asset - otherWallets: { name: string; address: string }[] + otherWallets: Array<{ name: string; address: string }> openAssetMenu: (anchorElOrEvent: HTMLElement | SyntheticEvent) => void } diff --git a/packages/mask/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx b/packages/mask/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx index 7cbfe4f51144..b250aa196786 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx @@ -176,7 +176,7 @@ const useStyles = makeStyles()({ const MIN_GAS_LIMIT = 21000 export interface Transfer1559Props { selectedAsset?: Asset - otherWallets: { name: string; address: string }[] + otherWallets: Array<{ name: string; address: string }> openAssetMenu: (anchorElOrEvent: HTMLElement | SyntheticEvent) => void } diff --git a/packages/mask/src/extension/popups/pages/Wallet/components/DeriveWalletTable/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/components/DeriveWalletTable/index.tsx index c5fd78aead04..0c625ce957aa 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/components/DeriveWalletTable/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/components/DeriveWalletTable/index.tsx @@ -41,7 +41,7 @@ const useStyles = makeStyles()({ export interface DeriveWalletTableProps { loading: boolean - dataSource?: { address: string; added: boolean; selected: boolean }[] + dataSource?: Array<{ address: string; added: boolean; selected: boolean }> onCheck: (checked: boolean, index: number) => void confirmLoading: boolean } diff --git a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx index c0a8bd58ef41..7a227a1f65d2 100644 --- a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx +++ b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx @@ -7,17 +7,12 @@ import type { Project } from '../types' const useStyles = makeStyles()((theme) => { return { - root: { - marginLeft: theme.spacing(-0.5), - marginRight: theme.spacing(-0.5), - marginTop: theme.spacing(1), - }, + root: {}, content: { padding: theme.spacing(0), }, button: { flex: 1, - margin: `${theme.spacing(0)} ${theme.spacing(0.5)}`, }, } }) diff --git a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx index 581c3601a1bb..b1fe65404fac 100644 --- a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx @@ -1,14 +1,15 @@ import { useI18N } from '../../../utils' -import { Tab, Tabs, Paper, Card, CardHeader, CardContent, Link, Typography, Avatar } from '@mui/material' +import { Tab, Tabs, Paper, Card, CardHeader, CardContent, Link, Typography, Avatar, Box } from '@mui/material' import { useState } from 'react' import { makeStyles } from '@masknet/theme' import { CollectionView } from './CollectionView' import { DetailsView } from './DetailsView' -import { formatWeiToEther, useChainId } from '@masknet/web3-shared-evm' +import { ChainId, formatWeiToEther, useChainId } from '@masknet/web3-shared-evm' import { useFetchProject } from '../hooks/useProject' import { ActionBar } from './ActionBar' import { resolveProjectLinkOnArtBlocks, resolveUserLinkOnArtBlocks } from '../pipes' import { ArtBlocksLogoUrl } from '../constants' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' const useStyles = makeStyles()((theme) => { return { @@ -53,6 +54,7 @@ const useStyles = makeStyles()((theme) => { interface CollectibleProps { projectId: string + chainId?: ChainId } export function Collectible(props: CollectibleProps) { @@ -129,7 +131,11 @@ export function Collectible(props: CollectibleProps) { {pages[tabIndex]} - + + + + + ) } diff --git a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx index d86872021181..a55e7c7bea67 100644 --- a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx @@ -7,7 +7,6 @@ import { base } from '../base' import { extractTextFromTypedMessage } from '@masknet/typed-message' import { Collectible } from './Collectible' import type { ChainId } from '@masknet/web3-shared-evm' -import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' import { ArtBlocksIcon } from '@masknet/icons' const sns: Plugin.SNSAdaptor.Definition = { @@ -39,11 +38,7 @@ const sns: Plugin.SNSAdaptor.Definition = { function Renderer(props: React.PropsWithChildren<{ chainId: ChainId; projectId: string }>) { usePluginWrapper(true) - return ( - - - - ) + return } export default sns diff --git a/packages/mask/src/plugins/Avatar/Application/NFTAvatar.tsx b/packages/mask/src/plugins/Avatar/Application/NFTAvatar.tsx new file mode 100644 index 000000000000..33205b509739 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTAvatar.tsx @@ -0,0 +1,46 @@ +import { EnhanceableSite } from '@masknet/shared-base' +import { Avatar, Stack } from '@mui/material' +import { PointIcon } from '../assets/point' +import { TwitterIcon } from '../assets/twitter' +import { RainbowBox } from '../SNSAdaptor/RainbowBox' + +export const SOCIAL_MEDIA_ICON_MAPPING: Record = { + [EnhanceableSite.Twitter]: , + [EnhanceableSite.Localhost]: null, +} + +interface NFTAvatarProps { + hasBorder: boolean + platform?: string + avatar?: string + owner?: boolean +} + +export function NFTAvatar(props: NFTAvatarProps) { + const { avatar, hasBorder, platform = '', owner = false } = props + + return ( + + {hasBorder ? ( + + + + ) : ( + + )} + + + {SOCIAL_MEDIA_ICON_MAPPING[EnhanceableSite.Twitter]} + + {owner ? : null} + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/NFTAvatarsDialog.tsx b/packages/mask/src/plugins/Avatar/Application/NFTAvatarsDialog.tsx new file mode 100644 index 000000000000..d76817e316b6 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTAvatarsDialog.tsx @@ -0,0 +1,106 @@ +import { useCallback, useState } from 'react' +import { NFTListDialog } from './NFTListDialog' +import { InjectedDialog } from '@masknet/shared' +import { UploadAvatarDialog } from './UploadAvatarDialog' +import type { BindingProof } from '@masknet/shared-base' +import type { SelectTokenInfo, TokenInfo } from '../types' +import { PersonaPage } from './PersonaPage' +import { DialogContent } from '@mui/material' +import { useI18N } from '../locales/i18n_generated' +import { isSameAddress } from '@masknet/web3-shared-evm' +import { makeStyles } from '@masknet/theme' + +const useStyles = makeStyles()((theme) => ({ + root: { + margin: 0, + padding: '1px !important', + '::-webkit-scrollbar': { + backgroundColor: 'transparent', + width: 20, + }, + '::-webkit-scrollbar-thumb': { + borderRadius: '20px', + width: 5, + border: '7px solid rgba(0, 0, 0, 0)', + backgroundColor: theme.palette.mode === 'dark' ? 'rgba(250, 250, 250, 0.2)' : 'rgba(0, 0, 0, 0.2)', + backgroundClip: 'padding-box', + }, + }, +})) +enum CreateNFTAvatarStep { + Persona = 'persona', + NFTList = 'NFTList', + UploadAvatar = 'UploadAvatar', +} + +interface NFTAvatarsDialogProps { + open: boolean + onClose: () => void +} + +export function NFTAvatarDialog(props: NFTAvatarsDialogProps) { + const [step, setStep] = useState(CreateNFTAvatarStep.Persona) + const [wallets, setWallets] = useState() + const [selectedTokenInfo, setSelectedTokenInfo] = useState() + const [tokenInfo, setTokenInfo] = useState() + const [proof, setProof] = useState() + const t = useI18N() + const { classes } = useStyles() + + const onPersonaChange = (proof: BindingProof, wallets?: BindingProof[], tokenInfo?: TokenInfo) => { + setWallets(wallets) + setTokenInfo(tokenInfo) + setProof(proof) + } + + const onSelected = (info: SelectTokenInfo) => { + setSelectedTokenInfo(info) + } + + const onNext = useCallback(() => { + if (step === CreateNFTAvatarStep.Persona) setStep(CreateNFTAvatarStep.NFTList) + else if (step === CreateNFTAvatarStep.NFTList) setStep(CreateNFTAvatarStep.UploadAvatar) + }, [step]) + + const onBack = useCallback(() => { + if (step === CreateNFTAvatarStep.UploadAvatar) setStep(CreateNFTAvatarStep.NFTList) + else if (step === CreateNFTAvatarStep.NFTList) setStep(CreateNFTAvatarStep.Persona) + else props.onClose() + }, [step]) + + const onClose = useCallback(() => { + setStep(CreateNFTAvatarStep.Persona) + props.onClose() + }, [props.onClose]) + + return ( + + + {step === CreateNFTAvatarStep.Persona ? ( + + ) : null} + {step === CreateNFTAvatarStep.NFTList ? ( + + ) : null} + {step === CreateNFTAvatarStep.UploadAvatar ? ( + isSameAddress(x.identity, selectedTokenInfo?.account))} + account={selectedTokenInfo?.account} + image={selectedTokenInfo?.image} + token={selectedTokenInfo?.token} + onBack={onBack} + onClose={onClose} + /> + ) : null} + + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/NFTInfo.tsx b/packages/mask/src/plugins/Avatar/Application/NFTInfo.tsx new file mode 100644 index 000000000000..77647436ee5a --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTInfo.tsx @@ -0,0 +1,65 @@ +import { Box, CircularProgress, Link, Stack, Typography } from '@mui/material' +import { makeStyles, useStylesExtends } from '@masknet/theme' +import { LinkIcon } from '../assets/link' +import { ChainId, resolveOpenSeaLink } from '@masknet/web3-shared-evm' +import { formatTokenId } from '../utils' +import { useI18N } from '../locales/i18n_generated' +import { ApplicationRoundIcon } from '../assets/application' +import { formatPersonaName } from '@masknet/shared-base' + +const useStyles = makeStyles()(() => ({ + root: { + width: 160, + }, + nft: { + display: 'flex', + alignItems: 'center', + }, +})) +interface NFTInfoProps extends withClasses<'root'> { + nft?: { name: string; address: string; tokenId: string; symbol: string; chainId: ChainId } + owner: boolean + loading?: boolean +} + +export function NFTInfo(props: NFTInfoProps) { + const { nft, owner, loading = false } = props + const classes = useStylesExtends(useStyles(), props) + const t = useI18N() + + if (loading) return + return ( + + {!nft ? ( + + {t.persona_set_nft()} + + ) : !owner ? ( + + {t.persona_verification_failed()} + + ) : ( + + + + + {formatPersonaName(nft.name.replace(/#\d+/, ''))} + + + + {formatTokenId(nft.symbol, nft.tokenId)} + + + + + + + + )} + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/NFTList.tsx b/packages/mask/src/plugins/Avatar/Application/NFTList.tsx new file mode 100644 index 000000000000..eb9615527ce4 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTList.tsx @@ -0,0 +1,103 @@ +import { makeStyles, useTabs } from '@masknet/theme' +import { ChainId, ERC721TokenDetailed } from '@masknet/web3-shared-evm' +import { TabContext, TabPanel } from '@mui/lab' +import { Tab, Tabs, Typography } from '@mui/material' +import { Application_NFT_LIST_PAGE, SUPPORTED_CHAIN_IDS } from '../constants' +import type { TokenInfo } from '../types' +import { NFTListPage } from './NFTListPage' + +const useStyles = makeStyles<{ currentTab: Application_NFT_LIST_PAGE }>()((theme, props) => ({ + selected: { + backgroundColor: theme.palette.mode === 'dark' ? 'black' : 'white', + border: 'none', + borderTop: `1px solid ${theme.palette.mode === 'dark' ? '#2F3336' : '#EFF3F4'}`, + color: `${theme.palette.text.primary} !important`, + minHeight: 37, + height: 37, + zIndex: 1, + }, + tab: { + backgroundColor: theme.palette.mode === 'dark' ? '#15171A' : '#F6F8F8', + color: theme.palette.text.secondary, + border: `1px solid ${theme.palette.mode === 'dark' ? '#2F3336' : '#EFF3F4'}`, + minHeight: 37, + height: 37, + zIndex: 1, + }, + tabPanel: { + padding: theme.spacing(1), + paddingTop: 50, + paddingBottom: 80, + }, +})) +interface NFTListProps { + address: string + tokenInfo?: TokenInfo + onSelect: (token: ERC721TokenDetailed) => void + onChangePage?: (page: Application_NFT_LIST_PAGE) => void + tokens?: ERC721TokenDetailed[] + children?: React.ReactElement +} + +export function NFTList(props: NFTListProps) { + const { address, onSelect, tokenInfo, onChangePage, tokens = [], children } = props + + const [currentTab, onChange, tabs] = useTabs( + Application_NFT_LIST_PAGE.Application_nft_tab_eth_page, + Application_NFT_LIST_PAGE.Application_nft_tab_polygon_page, + ) + + const { classes } = useStyles({ currentTab }) + const _onChange = (event: unknown, value: any) => { + onChange(event, value) + onChangePage?.(value) + } + + if (!address) return null + return ( + + + {SUPPORTED_CHAIN_IDS.map((x, i) => { + const curChainId = currentTab === tabs.ETH ? ChainId.Mainnet : ChainId.Matic + return ( + + {x === ChainId.Mainnet + ? Application_NFT_LIST_PAGE.Application_nft_tab_eth_page + : Application_NFT_LIST_PAGE.Application_nft_tab_polygon_page} + + } + value={x === ChainId.Mainnet ? tabs.ETH : tabs.Polygon} + className={curChainId === x ? classes.selected : classes.tab} + /> + ) + })} + + {SUPPORTED_CHAIN_IDS.map((x, i) => ( + + y.contractDetailed.chainId === x) ?? []} + tokenInfo={tokenInfo} + chainId={x} + address={address} + onSelect={onSelect} + children={children} + /> + + ))} + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/NFTListDialog.tsx b/packages/mask/src/plugins/Avatar/Application/NFTListDialog.tsx new file mode 100644 index 000000000000..1488f4fc3deb --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTListDialog.tsx @@ -0,0 +1,279 @@ +import { makeStyles, useCustomSnackbar } from '@masknet/theme' +import { ChainId, ERC721TokenDetailed, isSameAddress, SocketState, useCollectibles } from '@masknet/web3-shared-evm' +import { Box, Button, DialogActions, DialogContent, Skeleton, Stack, Typography } from '@mui/material' +import { useCallback, useState, useEffect } from 'react' +import { downloadUrl } from '../../../utils' +import { AddNFT } from '../SNSAdaptor/AddNFT' +import type { BindingProof } from '@masknet/shared-base' +import type { SelectTokenInfo, TokenInfo } from '../types' +import { range, uniqBy } from 'lodash-unified' +import { Translate, useI18N } from '../locales' +import { AddressNames } from './WalletList' +import { NFTList } from './NFTList' +import { Application_NFT_LIST_PAGE } from '../constants' +import { NetworkPluginID, useAccount, useCurrentWeb3NetworkPluginID } from '@masknet/plugin-infra/web3' +import { NFTWalletConnect } from './WalletConnect' + +const useStyles = makeStyles()((theme) => ({ + AddressNames: { + position: 'absolute', + top: 10, + right: 10, + }, + + button: { + width: 219.5, + borderRadius: 999, + }, + AddCollectiblesButton: { + fontWeight: 600, + color: '#1D9BF0', + }, + actions: { + padding: theme.spacing(2), + backgroundColor: theme.palette.mode === 'dark' ? 'black' : 'white', + position: 'absolute', + left: 0, + bottom: 0, + width: 'calc(100% - 32px)', + }, + content: { + height: 612, + padding: 0, + backgroundColor: theme.palette.mode === 'dark' ? 'black' : 'white', + }, + error: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + margin: 'auto', + paddingTop: 260, + }, + skeleton: { + width: 97, + height: 97, + objectFit: 'cover', + borderRadius: '100%', + boxSizing: 'border-box', + padding: 6, + margin: theme.spacing(0.5, 1), + }, + skeletonBox: { + marginLeft: 'auto', + marginRight: 'auto', + }, + gallery: { + display: 'flex', + flexFlow: 'row wrap', + overflowY: 'auto', + '&::-webkit-scrollbar': { + display: 'none', + }, + }, +})) + +function isSameToken(token?: ERC721TokenDetailed, tokenInfo?: TokenInfo) { + if (!token && !tokenInfo) return false + return isSameAddress(token?.contractDetailed.address, tokenInfo?.address) && token?.tokenId === tokenInfo?.tokenId +} +interface NFTListDialogProps { + onNext: () => void + tokenInfo?: TokenInfo + wallets?: BindingProof[] + onSelected: (info: SelectTokenInfo) => void +} + +export function NFTListDialog(props: NFTListDialogProps) { + const { onNext, wallets, onSelected, tokenInfo } = props + const { classes } = useStyles() + + const account = useAccount() + const [open_, setOpen_] = useState(false) + const [selectedAccount, setSelectedAccount] = useState('') + const [selectedToken, setSelectedToken] = useState() + const [disabled, setDisabled] = useState(false) + const t = useI18N() + const [tokens, setTokens] = useState([]) + const [currentPage, setCurrentPage] = useState( + Application_NFT_LIST_PAGE.Application_nft_tab_eth_page, + ) + + const POLYGON_PAGE = Application_NFT_LIST_PAGE.Application_nft_tab_polygon_page + + const currentPluginId = useCurrentWeb3NetworkPluginID() + const { + data: collectibles, + error, + retry, + state, + } = useCollectibles(selectedAccount, currentPage !== POLYGON_PAGE ? ChainId.Mainnet : ChainId.Matic) + + const { showSnackbar } = useCustomSnackbar() + const onChange = useCallback((address: string) => { + setSelectedAccount(address) + }, []) + + const onSelect = (token: ERC721TokenDetailed) => { + setSelectedToken(token) + } + + const onSave = useCallback(async () => { + if (!selectedToken?.info?.imageURL) return + setDisabled(true) + try { + const image = await downloadUrl(selectedToken.info.imageURL) + onSelected({ image: URL.createObjectURL(image), account: selectedAccount, token: selectedToken }) + onNext() + setDisabled(false) + } catch (error) { + console.log(error) + } + setDisabled(false) + }, [selectedToken, selectedAccount]) + + const onClick = useCallback(() => { + if (!account && !wallets?.length) { + showSnackbar('Please connect your wallet!', { variant: 'error' }) + return + } + setOpen_(true) + }, [account, wallets, showSnackbar]) + + useEffect(() => { + setDisabled(!selectedToken || isSameToken(selectedToken, tokenInfo)) + }, [selectedToken, tokenInfo]) + + useEffect(() => setSelectedAccount(account || wallets?.[0]?.identity || ''), [account, wallets]) + + const onAddClick = (token: ERC721TokenDetailed) => { + setTokens((_tokens) => uniqBy([..._tokens, token], (x) => x.contractDetailed.address && x.tokenId)) + } + + const onChangePage = (name: Application_NFT_LIST_PAGE) => { + setCurrentPage(name) + setSelectedToken(undefined) + } + + const AddCollectible = ( + + + {currentPluginId !== NetworkPluginID.PLUGIN_EVM ? ( + , + }} + /> + ) : currentPage === POLYGON_PAGE ? ( + , + }} + /> + ) : ( + t.collectible_no_eth() + )} + + {currentPluginId === NetworkPluginID.PLUGIN_EVM ? ( + + ) : null} + + ) + + const LoadStatus = ( +
+ {range(8).map((i) => ( + + ))} +
+ ) + + const Retry = ( + + {t.no_collectible_found()} + + + ) + + const NoNFTList = () => { + if (currentPage === POLYGON_PAGE && tokens.length === 0) return AddCollectible + else if (currentPage === POLYGON_PAGE && tokens.length) return + if (state !== SocketState.done) { + return LoadStatus + } + if (error) { + return Retry + } + if (tokens.length === 0 && collectibles.length === 0) { + return AddCollectible + } + + return + } + + if (!wallets?.length && (currentPluginId !== NetworkPluginID.PLUGIN_EVM || !account)) + return ( + + + + ) + + return ( + <> + + + {((account && currentPluginId === NetworkPluginID.PLUGIN_EVM) || Boolean(wallets?.length)) && ( + x.contractDetailed.address && x.tokenId)} + children={NoNFTList()} + /> + )} + + + {tokens.length || collectibles.length ? ( + + + {t.collectible_not_found()} + + + {t.add_collectible()} + + + ) : null} + + + + setOpen_(false)} + onAddClick={onAddClick} + /> + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/NFTListPage.tsx b/packages/mask/src/plugins/Avatar/Application/NFTListPage.tsx new file mode 100644 index 000000000000..065ead46a817 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTListPage.tsx @@ -0,0 +1,118 @@ +import { makeStyles } from '@masknet/theme' +import { + ChainId, + createERC721Token, + ERC721TokenDetailed, + EthereumTokenType, + useImageChecker, +} from '@masknet/web3-shared-evm' +import { Box, Skeleton } from '@mui/material' +import { useState } from 'react' +import { NFTImage } from '../SNSAdaptor/NFTImage' +import type { TokenInfo } from '../types' + +const useStyles = makeStyles()((theme) => ({ + root: {}, + + button: { + textAlign: 'center', + paddingTop: theme.spacing(1), + display: 'flex', + justifyContent: 'space-around', + flexDirection: 'row', + color: '#1D9BF0', + }, + gallery: { + display: 'flex', + flexFlow: 'row wrap', + overflowY: 'auto', + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + skeleton: { + width: 97, + height: 97, + objectFit: 'cover', + borderRadius: '100%', + boxSizing: 'border-box', + padding: 6, + margin: theme.spacing(0.5, 1), + }, + skeletonBox: { + marginLeft: 'auto', + marginRight: 'auto', + }, +})) + +interface NFTListPageProps { + chainId: ChainId + address: string + tokenInfo?: TokenInfo + tokens: ERC721TokenDetailed[] + onSelect?: (token: ERC721TokenDetailed) => void + children?: React.ReactElement +} + +export function NFTListPage(props: NFTListPageProps) { + const { classes } = useStyles() + const { onSelect, chainId, tokenInfo, tokens, children } = props + const [selectedToken, setSelectedToken] = useState( + tokenInfo + ? createERC721Token( + { + name: '', + address: tokenInfo.address, + chainId, + symbol: '', + type: EthereumTokenType.ERC721, + }, + {}, + tokenInfo.tokenId, + ) + : undefined, + ) + + const onChange = (token: ERC721TokenDetailed) => { + if (!token) return + setSelectedToken(token) + onSelect?.(token) + } + + return ( + <> + + + {children ?? + tokens.map((token: ERC721TokenDetailed, i) => ( + onChange(token)} + /> + ))} + + + + ) +} + +interface NFTImageCollectibleAvatarProps { + token: ERC721TokenDetailed + onChange: (token: ERC721TokenDetailed) => void + selectedToken?: ERC721TokenDetailed +} + +function NFTImageCollectibleAvatar({ token, onChange, selectedToken }: NFTImageCollectibleAvatarProps) { + const { classes } = useStyles() + const { value: isImageToken, loading } = useImageChecker(token.info?.imageURL) + + if (loading) + return ( +
+ +
+ ) + return isImageToken ? : null +} diff --git a/packages/mask/src/plugins/Avatar/Application/PersonaItem.tsx b/packages/mask/src/plugins/Avatar/Application/PersonaItem.tsx new file mode 100644 index 000000000000..7aee1cc9a42c --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/PersonaItem.tsx @@ -0,0 +1,94 @@ +import { makeStyles } from '@masknet/theme' +import { Box, Typography, ListItemButton } from '@mui/material' +import { NFTAvatar } from './NFTAvatar' +import { NFTInfo } from './NFTInfo' +import { MoreIcon } from '../assets/more' +import { RSS3_KEY_SNS } from '../constants' +import { useCheckTokenOwner, useTokenOwner } from '../hooks/useTokenOwner' +import { getAvatarId } from '../../../social-network-adaptor/twitter.com/utils/user' +import type { TokenInfo } from '../types' +import { useCallback } from 'react' +import type { BindingProof } from '@masknet/shared-base' +import { usePersonaNFTAvatar } from '../hooks/usePersonaNFTAvatar' +import { ChainId } from '@masknet/web3-shared-evm' + +const useStyles = makeStyles<{ disabled: boolean }>()((theme, props) => ({ + root: { + margin: theme.spacing(2, 0.5), + border: `1px solid ${theme.palette.divider}`, + borderRadius: 16, + padding: 16, + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + }, + + userInfo: { + fontSize: 14, + marginLeft: 16, + flex: 1, + }, +})) + +interface PersonaItemProps { + owner?: boolean + avatar: string + userId: string + nickname?: string + proof?: BindingProof + onSelect?: (proof: BindingProof, tokenInfo?: TokenInfo) => void +} + +export function PersonaItem(props: PersonaItemProps) { + const { userId, onSelect, owner = false, proof, avatar, nickname = '' } = props + const { classes } = useStyles({ disabled: !owner }) + const { value: _avatar, loading } = usePersonaNFTAvatar(userId, getAvatarId(avatar) ?? '', RSS3_KEY_SNS.TWITTER) + const { value: token, loading: loadingToken } = useTokenOwner( + _avatar?.address ?? '', + _avatar?.tokenId ?? '', + _avatar?.chainId, + ) + const { loading: loadingCheckOwner, isOwner } = useCheckTokenOwner(userId, token?.owner) + + const onClick = useCallback(() => { + if (!proof) return + onSelect?.(proof, _avatar && isOwner ? { address: _avatar?.address, tokenId: _avatar?.tokenId } : undefined) + }, [_avatar, proof]) + + return ( + + + + + {nickname || _avatar?.nickname} + + + @{userId} + + + + + + + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/PersonaPage.tsx b/packages/mask/src/plugins/Avatar/Application/PersonaPage.tsx new file mode 100644 index 000000000000..38a67e5b0312 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/PersonaPage.tsx @@ -0,0 +1,107 @@ +import { BindingProof, NextIDPlatform } from '@masknet/shared-base' +import { makeStyles } from '@masknet/theme' +import { Box, CircularProgress, DialogContent, Stack, Typography } from '@mui/material' +import { useCallback, useState } from 'react' +import { useSubscription } from 'use-subscription' +import { CloseIcon } from '../assets/close' +import { context } from '../context' +import { usePersonas } from '../hooks/usePersonas' +import { useI18N } from '../locales/i18n_generated' +import type { TokenInfo } from '../types' +import { PersonaItem } from './PersonaItem' +import { InfoIcon } from '../assets/info' +import { useMyPersonas } from '../../../components/DataSource/useMyPersonas' + +const useStyles = makeStyles()((theme) => ({ + messageBox: { + display: 'flex', + borderRadius: 4, + padding: 8, + backgroundColor: theme.palette.mode === 'dark' ? '#15171A' : '#F9F9F9', + fontSize: 14, + alignItems: 'center', + color: theme.palette.text.primary, + }, +})) + +interface PersonaPageProps { + onNext: () => void + onClose(): void + onChange: (proof: BindingProof, wallets?: BindingProof[], tokenInfo?: TokenInfo) => void +} + +export function PersonaPage(props: PersonaPageProps) { + const { onNext, onChange, onClose } = props + const [visible, setVisible] = useState(true) + const currentIdentity = useSubscription(context.lastRecognizedProfile) + const { classes } = useStyles() + const { loading, value: persona } = usePersonas() + const myPersonas = useMyPersonas() + const t = useI18N() + + const onSelect = useCallback( + (proof: BindingProof, tokenInfo?: TokenInfo) => { + onChange(proof, persona?.wallets, tokenInfo) + onNext() + }, + [persona?.wallets], + ) + + return ( + + {loading ? ( + + + + ) : ( + <> + {visible ? ( + + + + {t.persona_hint()} + + setVisible(false)} /> + + ) : null} + {persona?.binds?.proofs + .filter((proof) => proof.platform === NextIDPlatform.Twitter) + .filter((x) => x.identity.toLowerCase() === currentIdentity?.identifier?.userId.toLowerCase()) + .map((x, i) => ( + + ))} + + {myPersonas?.[0] && + myPersonas[0].linkedProfiles + .filter((x) => x.identifier.network === currentIdentity?.identifier?.network) + .map((x, i) => + persona?.binds.proofs.some( + (y) => y.identity === x.identifier.userId.toLowerCase(), + ) ? null : ( + + ), + )} + {persona?.binds?.proofs + .filter((proof) => proof.platform === NextIDPlatform.Twitter) + .filter((x) => x.identity.toLowerCase() !== currentIdentity?.identifier?.userId.toLowerCase()) + .map((x, i) => ( + + ))} + + )} + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/UploadAvatarDialog.tsx b/packages/mask/src/plugins/Avatar/Application/UploadAvatarDialog.tsx new file mode 100644 index 000000000000..c310ea817d8e --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/UploadAvatarDialog.tsx @@ -0,0 +1,202 @@ +import { Button, DialogActions, DialogContent, Slider } from '@mui/material' +import AvatarEditor from 'react-avatar-editor' +import { makeStyles, useCustomSnackbar } from '@masknet/theme' +import { useCallback, useState } from 'react' +import { NextIDStorage, Twitter, TwitterBaseAPI } from '@masknet/web3-providers' +import type { ERC721TokenDetailed } from '@masknet/web3-shared-evm' +import { getAvatarId } from '../../../social-network-adaptor/twitter.com/utils/user' +import { usePersonaConnectStatus } from '../../../components/DataSource/usePersonaConnectStatus' +import { BindingProof, fromHex, PersonaIdentifier, ProfileIdentifier, toBase64 } from '@masknet/shared-base' +import type { NextIDAvatarMeta } from '../types' +import { useI18N } from '../locales/i18n_generated' +import { context } from '../context' +import { PluginNFTAvatarRPC } from '../messages' +import { PLUGIN_ID, RSS3_KEY_SNS } from '../constants' +import { useSubscription } from 'use-subscription' +import Services from '../../../extension/service' + +const useStyles = makeStyles()((theme) => ({ + actions: { + padding: theme.spacing(0, 2, 2, 2), + }, + cancel: { + backgroundColor: theme.palette.background.default, + color: theme.palette.mode === 'dark' ? '#FFFFFF' : '#111418', + border: 'none', + '&:hover': { + border: 'none', + }, + }, +})) + +interface UploadAvatarDialogProps { + account?: string + isBindAccount?: boolean + image?: string | File + token?: ERC721TokenDetailed + proof?: BindingProof + onBack: () => void + onClose: () => void +} + +type AvatarInfo = TwitterBaseAPI.AvatarInfo & { avatarId: string } + +async function saveToRSS3(info: NextIDAvatarMeta, account: string, identifier: ProfileIdentifier) { + const avatar = await PluginNFTAvatarRPC.saveNFTAvatar( + account, + info, + identifier.network, + RSS3_KEY_SNS.TWITTER, + ).catch((error) => { + console.log(error) + return + }) + return avatar +} + +async function saveToNextID(info: NextIDAvatarMeta, persona?: PersonaIdentifier, proof?: BindingProof) { + if (!proof?.identity || !persona?.publicKeyAsHex) return + const payload = await NextIDStorage.getPayload( + persona.publicKeyAsHex, + proof?.platform, + proof?.identity, + info, + PLUGIN_ID, + ) + if (!payload.ok) { + return + } + const result = await Services.Identity.generateSignResult(persona, payload.val.signPayload) + if (!result) return + const response = await NextIDStorage.set( + payload.val.uuid, + persona.publicKeyAsHex, + toBase64(fromHex(result.signature.signature)), + proof.platform, + proof.identity, + payload.val.createdAt, + info, + PLUGIN_ID, + ) + return response.ok +} + +async function Save( + account: string, + isBindAccount: boolean, + token: ERC721TokenDetailed, + data: AvatarInfo, + persona: PersonaIdentifier, + proof: BindingProof, + identifier: ProfileIdentifier, +) { + if (!data) return false + + const info: NextIDAvatarMeta = { + nickname: data.nickname, + userId: data.userId, + imageUrl: data.imageUrl, + avatarId: data.avatarId, + address: token.contractDetailed.address, + tokenId: token.tokenId, + chainId: token.contractDetailed.chainId, + } + + if (isBindAccount) { + return saveToNextID(info, persona, proof) + } + return saveToRSS3(info, account, identifier) +} + +async function uploadAvatar(blob: Blob, userId: string): Promise { + try { + const media = await Twitter.uploadUserAvatar(userId, blob) + const data = await Twitter.updateProfileImage(userId, media.media_id_string) + if (!data) { + return + } + const avatarId = getAvatarId(data?.imageUrl ?? '') + return { ...data, avatarId } + } catch (err) { + return + } +} +export function UploadAvatarDialog(props: UploadAvatarDialogProps) { + const { image, account, token, onClose, onBack, proof, isBindAccount = false } = props + const { classes } = useStyles() + const identifier = useSubscription(context.currentVisitingProfile) + const [editor, setEditor] = useState(null) + const [scale, setScale] = useState(1) + const { showSnackbar } = useCustomSnackbar() + const [disabled, setDisabled] = useState(false) + const { currentConnectedPersona } = usePersonaConnectStatus() + const t = useI18N() + + const onSave = useCallback(() => { + if (!editor) return + editor.getImage().toBlob(async (blob) => { + if (!blob || !account || !token || !currentConnectedPersona?.linkedProfiles[0].identifier || !proof) return + setDisabled(true) + + const avatarData = await uploadAvatar(blob, currentConnectedPersona?.linkedProfiles[0].identifier.userId) + if (!avatarData) { + setDisabled(false) + return + } + + const response = await Save( + account, + isBindAccount, + token, + avatarData, + currentConnectedPersona.identifier, + proof, + currentConnectedPersona.linkedProfiles[0].identifier, + ) + + if (!response) { + showSnackbar(t.upload_avatar_failed_message(), { variant: 'error' }) + setDisabled(false) + return + } + showSnackbar(t.upload_avatar_success_message(), { variant: 'success' }) + location.reload() + onClose() + setDisabled(false) + }) + }, [account, editor, identifier, onClose, currentConnectedPersona, proof, isBindAccount]) + + if (!account || !image || !token || !proof) return null + + return ( + <> + + setEditor(e)} + image={image!} + border={50} + style={{ width: '100%', height: '100%' }} + scale={scale ?? 1} + rotate={0} + borderRadius={300} + /> + setScale(value as number)} + aria-label="Scale" + /> + + + + + + + ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/WalletConnect.tsx b/packages/mask/src/plugins/Avatar/Application/WalletConnect.tsx new file mode 100644 index 000000000000..14f261fad9cd --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/WalletConnect.tsx @@ -0,0 +1,114 @@ +import { WalletMessages } from '@masknet/plugin-wallet' +import { useRemoteControlledDialog } from '@masknet/shared-base-ui' +import { makeStyles, useStylesExtends } from '@masknet/theme' +import { resolveProviderHomeLink, useProviderType } from '@masknet/web3-shared-evm' +import { Box, Button, Link, Typography } from '@mui/material' +import { WalletIcon } from '../assets/wallet' +import LaunchIcon from '@mui/icons-material/Launch' +import { useI18N } from '../locales/i18n_generated' +import type { HTMLProps } from 'react' +import { ApplicationSmallIcon } from '../assets/application' +import { NetworkPluginID } from '@masknet/plugin-infra/web3' + +const useStyles = makeStyles()((theme) => ({ + root: { + background: + 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%), linear-gradient(90deg, rgba(28, 104, 243, 0.2) 0%, rgba(249, 55, 55, 0.2) 100%), #FFFFFF', + borderRadius: 16, + padding: 14, + position: 'relative', + height: 196, + margin: theme.spacing(2), + }, + title: { + display: 'flex', + color: '#07101b', + }, + button: { + textAlign: 'center', + alignItems: 'center', + position: 'absolute', + bottom: 16, + width: '100%', + }, + link: { + padding: 0, + marginLeft: theme.spacing(0.5), + marginTop: 2, + color: '#6E767D', + }, + rectangle: { + marginLeft: theme.spacing(1), + marginTop: theme.spacing(6), + }, +})) + +interface NFTWalletConnectProps extends withClasses<'root'> {} + +export function NFTWalletConnect(props: NFTWalletConnectProps) { + const classes = useStylesExtends(useStyles(), props) + const providerType = useProviderType() + const t = useI18N() + + const { setDialog: openSelectProviderDialog } = useRemoteControlledDialog( + WalletMessages.events.selectProviderDialogUpdated, + ) + return ( + + + + + {t.application_dialog_title()} + + + + {t.provider_by()} + + + Mask Network + + + + + + + + + + + ) +} + +const useRectangleStyles = makeStyles()(() => ({ + rectangle: { + height: 8, + background: 'rgba(255, 255, 255, 0.5)', + borderRadius: 8, + }, +})) +interface RectangleProps extends HTMLProps {} + +export function Rectangle(props: RectangleProps) { + const { classes } = useRectangleStyles() + return ( +
+
+
+
+
+ ) +} diff --git a/packages/mask/src/plugins/Avatar/Application/WalletList.tsx b/packages/mask/src/plugins/Avatar/Application/WalletList.tsx new file mode 100644 index 000000000000..aebe972e725b --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/WalletList.tsx @@ -0,0 +1,331 @@ +import { ImageIcon, useSnackbarCallback } from '@masknet/shared' +import { makeStyles, MaskColorVar, ShadowRootMenu, ShadowRootTooltip, useStylesExtends } from '@masknet/theme' +import { isSameAddress } from '@masknet/web3-shared-evm' +import { Button, Divider, IconProps, Link, ListItemIcon, MenuItem, Stack, Typography, useTheme } from '@mui/material' +import { memo, useCallback, useEffect, useState } from 'react' +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown' +import { WalletSettingIcon } from '../assets/setting' +import { CheckedIcon, UncheckIcon } from '../assets/checked' +import { useRemoteControlledDialog } from '@masknet/shared-base-ui' +import { WalletMessages } from '@masknet/plugin-wallet' +import { NFTWalletConnect } from './WalletConnect' +import { BindingProof, PopupRoutes } from '@masknet/shared-base' +import { + NetworkPluginID, + useChainId, + useCurrentWeb3NetworkPluginID, + useNetworkDescriptor, + useReverseAddress, + useWeb3State, +} from '@masknet/plugin-infra/web3' +import { Services } from '../../../extension/service' +import { useI18N } from '../locales/i18n_generated' +import { useCopyToClipboard } from 'react-use' +import { LinkIcon } from '../assets/link' +import { CopyIcon } from '../assets/copy' +import classNames from 'classnames' +import { VerifyIcon } from '../assets/verify' +import { noop } from 'lodash-unified' +import { Verify2Icon } from '../assets/Verify2' +import { formatAddress } from '../utils' + +const useStyles = makeStyles()((theme) => ({ + root: { + borderRadius: 9999, + paddingLeft: 4, + paddingRight: 4, + cursor: 'pointer', + backgroundColor: theme.palette.mode === 'dark' ? '#15171A' : '#F6F8F8', + }, + wrapper: {}, + address: { + lineHeight: 1.5, + }, + copy: { + color: theme.palette.secondary.main, + }, + + icon: { + width: 24, + height: 24, + }, + iconShadow: { + filter: 'drop-shadow(0px 0px 6px rgba(28, 104, 243, 0.6))', + }, + change: { + marginLeft: theme.spacing(4), + backgroundColor: MaskColorVar.twitterButton, + borderRadius: 9999, + fontWeight: 600, + fontSize: 14, + }, + divider: { + borderColor: theme.palette.mode === 'dark' ? '#2F3336' : '#F2F5F6', + marginLeft: 16, + marginRight: 16, + }, + paper: { + backgroundColor: 'black', + width: 335, + }, +})) + +interface AddressNamesProps extends withClasses<'root'> { + onChange: (address: string) => void + account: string + wallets: BindingProof[] +} + +export function AddressNames(props: AddressNamesProps) { + const { onChange, account, wallets } = props + const classes = useStylesExtends(useStyles(), props) + const [anchorEl, setAnchorEl] = useState(null) + const onClose = () => setAnchorEl(null) + const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) + const t = useI18N() + const currentPluginId = useCurrentWeb3NetworkPluginID() + + const [selectedWallet, setSelectedWallet] = useState(account || wallets?.[0]?.identity || '') + const onClick = useCallback((address: string) => { + onChange(address) + setSelectedWallet(address) + onClose() + }, []) + + const theme = useTheme() + + useEffect(() => { + if (!account && !wallets?.[0]?.identity) return + setSelectedWallet(account || wallets?.[0]?.identity) + }, [account, wallets?.[0]?.identity]) + + const { setDialog: openSelectProviderDialog } = useRemoteControlledDialog( + WalletMessages.events.selectProviderDialogUpdated, + ) + const chainId = useChainId() + const openPopupsWindow = useCallback(() => { + Services.Helper.openPopupWindow(PopupRoutes.ConnectedWallets, { + chainId, + internal: true, + }) + }, [chainId]) + + const onConnectWallet = useCallback(() => { + openSelectProviderDialog({ open: true, pluginID: NetworkPluginID.PLUGIN_EVM }) + onClose() + }, [openSelectProviderDialog, onClose]) + + const walletItem = ( + selectedWallet: string, + wallet: string, + enableChange: boolean, + onClick: (wallet: string) => void, + onChange?: () => void, + verify?: boolean, + isETH?: boolean, + ) => ( + onClick(account)}> + + {selectedWallet === wallet ? ( + <> + + + ) : ( + + )} + + + {enableChange && ( + + )} + + ) + + if (!wallets.length && (currentPluginId !== NetworkPluginID.PLUGIN_EVM || !account)) return + + return ( + + + isSameAddress(x.identity, account)) + ? true + : currentPluginId === NetworkPluginID.PLUGIN_EVM + } + /> + + + + {account ? ( + walletItem( + selectedWallet, + account, + Boolean(account), + () => onClick(account), + onConnectWallet, + wallets.some((x) => isSameAddress(x.identity, account)), + wallets.some((x) => isSameAddress(x.identity, account)) + ? true + : currentPluginId === NetworkPluginID.PLUGIN_EVM, + ) + ) : ( + + + + )} + + {wallets + .sort((a, b) => Number.parseInt(b.created_at, 10) - Number.parseInt(a.created_at, 10)) + ?.filter((x) => !isSameAddress(x.identity, account)) + .map((x) => ( + <> + {walletItem( + selectedWallet, + x.identity, + false, + () => onClick(x.identity), + () => noop, + true, + true, + )} + + + ))} + + { + openPopupsWindow() + onClose() + }}> + + + + + {t.wallet_settings()} + + + + + + ) +} + +const useWalletUIStyles = makeStyles()((theme) => ({ + root: {}, + address: { + fontSize: 10, + }, + copy: { + fontSize: 16, + cursor: 'pointer', + }, + link: { + color: theme.palette.text.secondary, + lineHeight: 0, + }, + linkIcon: { + width: 16, + height: 16, + }, + walletName: { + color: theme.palette.mode === 'dark' ? '#D9D9D9' : '#0F1419', + }, + walletAddress: { + color: theme.palette.mode === 'dark' ? '#6E767D' : '#536471', + }, +})) + +interface WalletUIProps { + address: string + verify?: boolean + isETH?: boolean +} + +function WalletUI(props: WalletUIProps) { + const { Utils } = useWeb3State() + const { isETH, address, verify = false } = props + + const { classes } = useWalletUIStyles() + const chainId = useChainId() + const currentPluginId = useCurrentWeb3NetworkPluginID() + const networkDescriptor = useNetworkDescriptor(chainId, isETH ? NetworkPluginID.PLUGIN_EVM : currentPluginId) + const { value: domain } = useReverseAddress(address, NetworkPluginID.PLUGIN_EVM) + if (!address) return null + return ( + + + + + + {currentPluginId === NetworkPluginID.PLUGIN_EVM + ? domain ?? formatAddress(address, 4) + : formatAddress(address, 4)} + + {verify ? : null} + + + + {formatAddress(address, 4)} + + + + + + + + + ) +} + +interface CopyIconButtonProps extends IconProps { + text: string +} +const CopyIconButton = memo(({ text, ...props }) => { + const t = useI18N() + const theme = useTheme() + const [, copyToClipboard] = useCopyToClipboard() + const [open, setOpen] = useState(false) + + const onCopy = useSnackbarCallback({ + executor: async () => copyToClipboard(text), + deps: [], + successText: t.copy_success_of_wallet_address(), + }) + + return ( + {t.copied()}} + open={open} + onMouseLeave={() => setOpen(false)} + disableFocusListener + disableTouchListener> + + + ) +}) diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx index 97c7c707fe60..2dc642f172f3 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx @@ -1,11 +1,11 @@ import { makeStyles } from '@masknet/theme' -import { ERC721TokenDetailed, isSameAddress, useAccount } from '@masknet/web3-shared-evm' -import { Button, DialogContent, Typography } from '@mui/material' +import { ChainId, ERC721TokenDetailed, isSameAddress, useAccount } from '@masknet/web3-shared-evm' +import { Button, DialogContent, InputBase, Typography } from '@mui/material' import { useCallback, useState } from 'react' import { InjectedDialog } from '@masknet/shared' -import { InputBox } from '../../../extension/options-page/DashboardComponents/InputBox' import { useI18N } from '../../../utils' import { createNFT } from '../utils' +import { WalletRPC } from '../../Wallet/messages' const useStyles = makeStyles()((theme) => ({ root: {}, @@ -17,6 +17,13 @@ const useStyles = makeStyles()((theme) => ({ input: { marginTop: theme.spacing(1), marginBottom: theme.spacing(1), + display: 'block', + width: '100%', + border: `1px solid ${theme.palette.mode === 'dark' ? '#2F3336' : '#EFF3F4'}`, + alignItems: 'center', + padding: theme.spacing(1), + boxSizing: 'border-box', + borderRadius: 8, }, message: { '&:before': { @@ -27,9 +34,12 @@ const useStyles = makeStyles()((theme) => ({ }, })) export interface AddNFTProps { + account?: string onClose: () => void - onAddClick: (token: ERC721TokenDetailed) => void + chainId?: ChainId + onAddClick?: (token: ERC721TokenDetailed) => void open: boolean + title?: string } export function AddNFT(props: AddNFTProps) { const { t } = useI18N() @@ -37,8 +47,8 @@ export function AddNFT(props: AddNFTProps) { const [address, setAddress] = useState('') const [tokenId, setTokenId] = useState('') const [message, setMessage] = useState('') - const { onClose, open, onAddClick } = props - const account = useAccount() + const { onClose, open, onAddClick, title, chainId, account } = props + const _account = useAccount() const onClick = useCallback(async () => { if (!address) { @@ -50,14 +60,18 @@ export function AddNFT(props: AddNFTProps) { return } - createNFT(address, tokenId) - .then((token) => { - if (!token || !isSameAddress(token?.info.owner, account)) { + createNFT(address, tokenId, chainId) + .then(async (token) => { + if (chainId && token && token.contractDetailed.chainId !== chainId) { + setMessage('chain does not match.') + return + } + if (!token || !isSameAddress(token?.info.owner, account ?? _account)) { setMessage(t('nft_owner_hint')) return } - - onAddClick(token) + await WalletRPC.addToken(token) + onAddClick?.(token) handleClose() }) .catch((error) => setMessage(t('nft_owner_hint'))) @@ -78,19 +92,31 @@ export function AddNFT(props: AddNFTProps) { } return ( - +
- onAddressChange(address)} /> + onAddressChange(e.target.value)} + />
- onTokenIdChange(tokenId)} /> + onTokenIdChange(e.target.value)} + />
{message ? ( - + {message} ) : null} diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx index 93a23fbbab07..17c4115fe58f 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx @@ -2,6 +2,7 @@ import { GearSettingsIcon } from '@masknet/icons' import { makeStyles, useStylesExtends } from '@masknet/theme' import { Typography } from '@mui/material' import { useI18N } from '../../../utils' +import { ApplicationSmallIcon } from '../assets/application' const useStyles = makeStyles()((theme) => ({ root: { @@ -10,11 +11,10 @@ const useStyles = makeStyles()((theme) => ({ justifyContent: 'center', alignItems: 'center', borderRadius: 9999, - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), - border: '1px solid', - backgroundColor: theme.palette.mode === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)', - color: theme.palette.mode === 'dark' ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)', + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + border: `1px solid ${theme.palette.mode === 'dark' ? '#2F3336' : '#EFF3F4'}`, + color: theme.palette.text.primary, cursor: 'pointer', }, icon: { @@ -25,10 +25,11 @@ const useStyles = makeStyles()((theme) => ({ text: { display: 'flex', alignItems: 'center', + marginLeft: 4, }, })) -interface NFTAvatarButtonProps extends withClasses<'root'> { +interface NFTAvatarButtonProps extends withClasses<'root' | 'text'> { onClick: () => void showSetting?: boolean } @@ -40,9 +41,9 @@ export function NFTAvatarButton(props: NFTAvatarButtonProps) { return (
+ - 🔥 - {t('nft_avatar')} + {t('nft_avatar')} {props.showSetting ? : null}
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx index 42a0d94f1b07..fc1cb595c64b 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx @@ -34,6 +34,7 @@ export function NFTBadge(props: NFTBadgeProps) { const { value = { amount: '0', symbol: 'ETH', name: '', slug: '' }, loading } = useNFT( avatar.address, avatar.tokenId, + avatar.chainId, ) const { amount, symbol, name, slug } = value @@ -45,7 +46,10 @@ export function NFTBadge(props: NFTBadgeProps) { e.preventDefault() openWindow(resolveOpenSeaLink(avatar.address, avatar.tokenId)) }}> - + { userId: string @@ -23,7 +23,7 @@ const useStyles = makeStyles()(() => ({ export function NFTBadgeTimeline(props: NFTBadgeTimelineProps) { const { userId, avatarId, width, height, snsKey } = props - const { loading, value: _avatar } = useNFTAvatar(userId, snsKey) + const { loading, value: _avatar } = usePersonaNFTAvatar(userId, avatarId, snsKey) const [avatar, setAvatar] = useState() const [avatarId_, setAvatarId_] = useState('') const classes = useStylesExtends(useStyles(), props) diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx index efd80e29ec7f..5a019eb2032c 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx @@ -1,11 +1,12 @@ import { makeStyles } from '@masknet/theme' import { ERC721TokenDetailed, isSameAddress } from '@masknet/web3-shared-evm' import classNames from 'classnames' +import { SelectedIcon } from '../assets/selected' const useStyles = makeStyles()((theme) => ({ imgBackground: { position: 'relative', - margin: theme.spacing(0.5, 1), + margin: theme.spacing(1.25, 1, 1.25, 1.5), borderRadius: '100%', display: 'flex', alignItems: 'center', @@ -13,10 +14,11 @@ const useStyles = makeStyles()((theme) => ({ }, icon: { position: 'absolute', - top: 5, - right: 5, - width: 24, - height: 24, + top: 2, + right: 2, + width: 20, + height: 20, + color: theme.palette.primary.main, }, image: { width: 97, @@ -25,11 +27,11 @@ const useStyles = makeStyles()((theme) => ({ borderRadius: '100%', boxSizing: 'border-box', '&:hover': { - border: `4px solid ${theme.palette.primary.main}`, + border: `1px solid ${theme.palette.primary.main}`, }, }, selected: { - border: `4px solid ${theme.palette.primary.main}`, + border: `1px solid ${theme.palette.primary.main}`, }, })) @@ -37,6 +39,7 @@ interface NFTImageProps { token: ERC721TokenDetailed selectedToken?: ERC721TokenDetailed onChange: (token: ERC721TokenDetailed) => void + showBadge?: boolean } function isSameNFT(a: ERC721TokenDetailed, b?: ERC721TokenDetailed) { @@ -48,7 +51,7 @@ function isSameNFT(a: ERC721TokenDetailed, b?: ERC721TokenDetailed) { } export function NFTImage(props: NFTImageProps) { - const { token, onChange, selectedToken } = props + const { token, onChange, selectedToken, showBadge = false } = props const { classes } = useStyles() return ( @@ -58,6 +61,7 @@ export function NFTImage(props: NFTImageProps) { src={token.info.imageURL} className={classNames(classes.image, isSameNFT(token, selectedToken) ? classes.selected : '')} /> + {showBadge && isSameNFT(token, selectedToken) ? : null}
) } diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/amount.png b/packages/mask/src/plugins/Avatar/SNSAdaptor/background/amount.png deleted file mode 100644 index 9df02cb6ee4a..000000000000 Binary files a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/amount.png and /dev/null differ diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noamount.png b/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noamount.png deleted file mode 100644 index 175fcf651e94..000000000000 Binary files a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noamount.png and /dev/null differ diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noname.png b/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noname.png deleted file mode 100644 index e39d479cb745..000000000000 Binary files a/packages/mask/src/plugins/Avatar/SNSAdaptor/background/noname.png and /dev/null differ diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/index.tsx index cbabb7a159eb..2f43bfb07606 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/index.tsx @@ -1,9 +1,56 @@ -import type { Plugin } from '@masknet/plugin-infra' +import type { Plugin } from '@masknet/plugin-infra/content-script' +import { ApplicationEntry } from '@masknet/shared' +import { useState } from 'react' +import { NFTAvatarDialog } from '../Application/NFTAvatarsDialog' import { base } from '../base' +import { setupContext } from '../context' +import { Trans } from 'react-i18next' +import { PluginI18NFieldRender } from '@masknet/plugin-infra/content-script' +import { ApplicationIcon } from '../assets/application' const sns: Plugin.SNSAdaptor.Definition = { ...base, - init(signal) {}, + init(signal, context) { + setupContext(context) + }, + ApplicationEntries: [ + (() => { + const name = { fallback: 'NFT PFP' } + const icon = + const recommendFeature = { + description: , + backgroundGradient: 'linear-gradient(360deg, #FFECD2 -0.43%, #FCB69F 99.57%)', + } + return { + RenderEntryComponent(EntryComponentProps) { + const [open, setOpen] = useState(false) + return ( + <> + } + icon={icon} + recommendFeature={recommendFeature} + {...EntryComponentProps} + onClick={EntryComponentProps.onClick ?? (() => setOpen(true))} + tooltipHint={ + EntryComponentProps.tooltipHint ?? + (EntryComponentProps.disabled ? undefined : ) + } + /> + + setOpen(false)} /> + + ) + }, + appBoardSortingDefaultPriority: 3, + name, + icon, + ApplicationEntryID: base.ID, + nextIdRequired: true, + recommendFeature, + } + })(), + ], } export default sns diff --git a/packages/mask/src/plugins/Avatar/Services/index.ts b/packages/mask/src/plugins/Avatar/Services/index.ts index 2f25a164f0f5..c59479fc0d79 100644 --- a/packages/mask/src/plugins/Avatar/Services/index.ts +++ b/packages/mask/src/plugins/Avatar/Services/index.ts @@ -42,13 +42,9 @@ export async function saveNFTAvatar( networkPluginId?: NetworkPluginID, chainId?: number, ) { - try { - const avatar = await saveNFTAvatarToRSS(address, nft, '', snsKey) - await setUserAddress(nft.userId, address, network, networkPluginId, chainId) - return avatar - } catch (error) { - throw error - } + const avatar = await saveNFTAvatarToRSS(address, nft, '', snsKey) + await setUserAddress(nft.userId, address, network, networkPluginId, chainId) + return avatar } export async function getAddress(userId: string, network: string, networkPluginId?: NetworkPluginID, chainId?: number) { diff --git a/packages/mask/src/plugins/Avatar/assets/Verify2.tsx b/packages/mask/src/plugins/Avatar/assets/Verify2.tsx new file mode 100644 index 000000000000..32b015145de2 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/Verify2.tsx @@ -0,0 +1,19 @@ +import { createIcon } from '@masknet/icons' +export const Verify2Icon = createIcon( + 'Verify2Icon', + + + + , + '0 0 20 20', +) diff --git a/packages/mask/src/plugins/Avatar/assets/application.tsx b/packages/mask/src/plugins/Avatar/assets/application.tsx new file mode 100644 index 000000000000..7dc9378ccd8a --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/application.tsx @@ -0,0 +1,63 @@ +import { createIcon } from '@masknet/icons' + +export const ApplicationIcon = createIcon( + 'ApplicationIcon', + + + + + + + + + + , + '0 0 24 24', +) + +export const ApplicationRoundIcon = createIcon( + 'ApplicationRoundIcon', + + + + + + + + + + , + '0 0 24 25', +) + +export const ApplicationSmallIcon = createIcon( + 'ApplicationSmallIcon', + + + , + '0 0 21 20', +) diff --git a/packages/mask/src/plugins/Avatar/assets/checked.tsx b/packages/mask/src/plugins/Avatar/assets/checked.tsx new file mode 100644 index 000000000000..4b3ce7be9630 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/checked.tsx @@ -0,0 +1,25 @@ +import { createIcon } from '@masknet/icons' + +export const CheckedIcon = createIcon( + 'CheckedIcon', + + + + , + '0 0 24 24', +) + +export const UncheckIcon = createIcon( + 'UncheckIcon', + + + , + '0 0 24 24', +) diff --git a/packages/mask/src/plugins/Avatar/assets/close.tsx b/packages/mask/src/plugins/Avatar/assets/close.tsx new file mode 100644 index 000000000000..cc9f93529008 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/close.tsx @@ -0,0 +1,12 @@ +import { createIcon } from '@masknet/icons' + +export const CloseIcon = createIcon( + 'CloseIcon', + + + , + '0 0 20 20', +) diff --git a/packages/mask/src/plugins/Avatar/assets/copy.tsx b/packages/mask/src/plugins/Avatar/assets/copy.tsx new file mode 100644 index 000000000000..bf8033061abf --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/copy.tsx @@ -0,0 +1,20 @@ +import { createIcon } from '@masknet/icons' + +export const CopyIcon = createIcon( + 'CopyIcon', + + + + , + '0 0 12 12', +) diff --git a/packages/mask/src/plugins/Avatar/assets/frame.tsx b/packages/mask/src/plugins/Avatar/assets/frame.tsx new file mode 100644 index 000000000000..7ea6f3306244 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/frame.tsx @@ -0,0 +1,44 @@ +import { SvgIcon, SvgIconProps } from '@mui/material' + +const svg = ( + + + + + + + + + + + + + + + + + + + +) + +export const FrameIcon = (props: SvgIconProps) => {svg} diff --git a/packages/mask/src/plugins/Avatar/assets/info.tsx b/packages/mask/src/plugins/Avatar/assets/info.tsx new file mode 100644 index 000000000000..d37e4a7b6f4f --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/info.tsx @@ -0,0 +1,26 @@ +import { createIcon } from '@masknet/icons' + +export const InfoIcon = createIcon( + 'InfoIcon', + + + + + , + '0 0 20 21', +) diff --git a/packages/mask/src/plugins/Avatar/assets/link.tsx b/packages/mask/src/plugins/Avatar/assets/link.tsx new file mode 100644 index 000000000000..80e7df2ab6ea --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/link.tsx @@ -0,0 +1,14 @@ +import { createIcon } from '@masknet/icons' + +export const LinkIcon = createIcon( + 'LinkIcon', + + + , + '0 0 16 16', +) diff --git a/packages/mask/src/plugins/Avatar/assets/more.tsx b/packages/mask/src/plugins/Avatar/assets/more.tsx new file mode 100644 index 000000000000..bae220e80a16 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/more.tsx @@ -0,0 +1,14 @@ +import { SvgIcon, SvgIconProps } from '@mui/material' + +const svg = ( + + + +) + +export const MoreIcon = (props: SvgIconProps) => {svg} diff --git a/packages/mask/src/plugins/Avatar/assets/nftavatar.png b/packages/mask/src/plugins/Avatar/assets/nftavatar.png new file mode 100644 index 000000000000..8ac22a7c4f2f Binary files /dev/null and b/packages/mask/src/plugins/Avatar/assets/nftavatar.png differ diff --git a/packages/mask/src/plugins/Avatar/assets/point.tsx b/packages/mask/src/plugins/Avatar/assets/point.tsx new file mode 100644 index 000000000000..79dc37bd92df --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/point.tsx @@ -0,0 +1,12 @@ +import { createIcon } from '@masknet/icons' +export const PointIcon = createIcon( + 'PointIcon', + + + , + '0 0 9 9', +) diff --git a/packages/mask/src/plugins/Avatar/assets/selected.tsx b/packages/mask/src/plugins/Avatar/assets/selected.tsx new file mode 100644 index 000000000000..d6576f1d8a41 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/selected.tsx @@ -0,0 +1,16 @@ +import { createIcon } from '@masknet/icons' +export const SelectedIcon = createIcon( + 'SelectedIcon', + + + + , + '0 0 24 24', +) diff --git a/packages/mask/src/plugins/Avatar/assets/setting.tsx b/packages/mask/src/plugins/Avatar/assets/setting.tsx new file mode 100644 index 000000000000..25593367bade --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/setting.tsx @@ -0,0 +1,16 @@ +import { createIcon } from '@masknet/icons' + +export const WalletSettingIcon = createIcon( + 'WalletSettingIcon', + + + + , + '0 0 20 20', +) diff --git a/packages/mask/src/plugins/Avatar/assets/twitter.tsx b/packages/mask/src/plugins/Avatar/assets/twitter.tsx new file mode 100644 index 000000000000..a77f6b2beb40 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/twitter.tsx @@ -0,0 +1,20 @@ +import { createIcon } from '@masknet/icons' + +export const TwitterIcon = createIcon( + 'TwitterIcon', + + + + + , + '0 0 15 15', +) diff --git a/packages/mask/src/plugins/Avatar/assets/verify.tsx b/packages/mask/src/plugins/Avatar/assets/verify.tsx new file mode 100644 index 000000000000..d0987974f62d --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/verify.tsx @@ -0,0 +1,17 @@ +import { createIcon } from '@masknet/icons' +export const VerifyIcon = createIcon( + 'VerifyIcon', + + + + , + '0 0 16 16', +) diff --git a/packages/mask/src/plugins/Avatar/assets/wallet.tsx b/packages/mask/src/plugins/Avatar/assets/wallet.tsx new file mode 100644 index 000000000000..181d745050a2 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/assets/wallet.tsx @@ -0,0 +1,32 @@ +import { createIcon } from '@masknet/icons' + +export const WalletIcon = createIcon( + 'WalletIcon', + + + + + + , + '0 0 18 18', +) diff --git a/packages/mask/src/plugins/Avatar/base.ts b/packages/mask/src/plugins/Avatar/base.ts index 68359a709d4f..f75751d6eb86 100644 --- a/packages/mask/src/plugins/Avatar/base.ts +++ b/packages/mask/src/plugins/Avatar/base.ts @@ -1,5 +1,6 @@ import { PLUGIN_ID } from './constants' -import { CurrentSNSNetwork, Plugin } from '@masknet/plugin-infra' +import { languages } from './locales/languages' +import { Plugin, CurrentSNSNetwork } from '@masknet/plugin-infra' export const base: Plugin.Shared.Definition = { ID: PLUGIN_ID, @@ -14,10 +15,12 @@ export const base: Plugin.Shared.Definition = { type: 'opt-in', networks: { [CurrentSNSNetwork.Twitter]: true, - [CurrentSNSNetwork.Facebook]: true, - [CurrentSNSNetwork.Instagram]: true, + [CurrentSNSNetwork.Facebook]: false, + [CurrentSNSNetwork.Instagram]: false, }, }, target: 'stable', }, + + i18n: languages, } diff --git a/packages/mask/src/plugins/Avatar/constants.ts b/packages/mask/src/plugins/Avatar/constants.ts index 7d715a838706..952ee5b534ea 100644 --- a/packages/mask/src/plugins/Avatar/constants.ts +++ b/packages/mask/src/plugins/Avatar/constants.ts @@ -1,10 +1,10 @@ import { PluginId } from '@masknet/plugin-infra' +import { ChainId } from '@masknet/web3-shared-evm' export const NFT_AVATAR_JSON_SERVER = 'https://configuration.r2d2.to/com.maskbook.avatar.json' export const NFT_AVATAR_DB_NAME = 'com.maskbook.user' export const NFT_AVATAR_DB_NAME_STORAGE = 'com.maskbook.user.storage' -export const NFT_CONTRACT_JSON_VERIFIED_SERVER = 'https://configuration.r2d2.to/com.maskbook.verified_nft.json' export const PLUGIN_ID = PluginId.Avatar export const PLUGIN_NAME = 'Avatar' export const PLUGIN_DESCRIPTION = 'NFT Avatar' @@ -14,3 +14,10 @@ export enum RSS3_KEY_SNS { FACEBOOK = '_facebook_nfts', INSTAGRAM = '_instagram_nfts', } + +export enum Application_NFT_LIST_PAGE { + Application_nft_tab_eth_page = 'ETH', + Application_nft_tab_polygon_page = 'Polygon', +} + +export const SUPPORTED_CHAIN_IDS: ChainId[] = [ChainId.Mainnet, ChainId.Matic] diff --git a/packages/mask/src/plugins/Avatar/context.ts b/packages/mask/src/plugins/Avatar/context.ts new file mode 100644 index 000000000000..e6904a0c28f5 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/context.ts @@ -0,0 +1,5 @@ +import type { Plugin } from '@masknet/plugin-infra/content-script' +export let context: Plugin.SNSAdaptor.SNSAdaptorContext +export function setupContext(x: typeof context) { + context = x +} diff --git a/packages/mask/src/plugins/Avatar/hooks/useNFT.ts b/packages/mask/src/plugins/Avatar/hooks/useNFT.ts index c2be4ebcedc1..5e3c0344e83b 100644 --- a/packages/mask/src/plugins/Avatar/hooks/useNFT.ts +++ b/packages/mask/src/plugins/Avatar/hooks/useNFT.ts @@ -1,16 +1,32 @@ +import { ChainId } from '@masknet/web3-shared-evm' import { useAsyncRetry } from 'react-use' import type { NFT } from '../types' -import { getNFT } from '../utils' +import { getNFT, getNFTByChain } from '../utils' const NFTCache = new Map>() -export function useNFT(address: string, tokenId: string) { +export function useNFT(address: string, tokenId: string, chainId?: ChainId) { return useAsyncRetry(async () => { if (!address || !tokenId) return - let f = NFTCache.get(`${address}-${tokenId}`) + let f = NFTCache.get(`${address}-${tokenId}-${chainId ?? ChainId.Mainnet}`) if (!f) { - f = getNFT(address, tokenId) - NFTCache.set(`${address}-${tokenId}`, f) + f = _getNFT(address, tokenId, chainId ?? ChainId.Mainnet) + NFTCache.set(`${address}-${tokenId}-${chainId ?? ChainId.Mainnet}`, f) } return f - }, [address, tokenId, NFTCache, getNFT]) + }, [address, tokenId, NFTCache, getNFT, chainId]) +} + +async function _getNFT(address: string, tokenId: string, chainId: ChainId) { + if (chainId === ChainId.Mainnet) { + return getNFT(address, tokenId) + } + const nft = await getNFTByChain(address, tokenId, chainId) + return { + amount: '0', + name: nft?.contractDetailed.name ?? '', + symbol: nft?.contractDetailed.symbol ?? 'ETH', + image: nft?.info.imageURL ?? '', + owner: nft?.info.owner ?? '', + slug: '', + } } diff --git a/packages/mask/src/plugins/Avatar/hooks/usePersonaNFTAvatar.ts b/packages/mask/src/plugins/Avatar/hooks/usePersonaNFTAvatar.ts new file mode 100644 index 000000000000..73db8002948a --- /dev/null +++ b/packages/mask/src/plugins/Avatar/hooks/usePersonaNFTAvatar.ts @@ -0,0 +1,22 @@ +import { useAsyncRetry } from 'react-use' +import { activatedSocialNetworkUI } from '../../../social-network' +import type { RSS3_KEY_SNS } from '../constants' +import { PluginNFTAvatarRPC } from '../messages' +import { getNFTAvatarByUserId } from '../utils' + +export function usePersonaNFTAvatar(userId: string, avatarId: string, snsKey: RSS3_KEY_SNS) { + return useAsyncRetry(async () => { + const avatarMetaFromPersona = await getNFTAvatarByUserId(userId, avatarId) + if (avatarMetaFromPersona) return avatarMetaFromPersona + + const avatarMeta = await PluginNFTAvatarRPC.getNFTAvatar( + userId, + activatedSocialNetworkUI.networkIdentifier, + snsKey, + ) + + if (!avatarMeta) return + + return { ...avatarMeta, imageUrl: '', nickname: '' } + }, [userId]) +} diff --git a/packages/mask/src/plugins/Avatar/hooks/usePersonaVerified.ts b/packages/mask/src/plugins/Avatar/hooks/usePersonaVerified.ts new file mode 100644 index 000000000000..2df5604f5e0f --- /dev/null +++ b/packages/mask/src/plugins/Avatar/hooks/usePersonaVerified.ts @@ -0,0 +1,23 @@ +import type { NextIDPlatform } from '@masknet/shared-base' +import { NextIDProof } from '@masknet/web3-providers' +import { useAsyncRetry } from 'react-use' +import { useSubscription } from 'use-subscription' +import Services from '../../../extension/service' +import { activatedSocialNetworkUI } from '../../../social-network' +import { context } from '../context' + +export function usePersonaVerify() { + const platform = activatedSocialNetworkUI.configuration.nextIDConfig?.platform as NextIDPlatform + const visitingPersonaIdentifier = useSubscription(context.lastRecognizedProfile) + return useAsyncRetry(async () => { + if (!visitingPersonaIdentifier?.identifier) return + const persona = await Services.Identity.queryPersonaByProfile(visitingPersonaIdentifier.identifier) + if (!persona?.identifier.publicKeyAsHex) return + const isVerified = await NextIDProof.queryIsBound( + persona.identifier.publicKeyAsHex, + platform, + visitingPersonaIdentifier.identifier.userId, + ) + return { isVerified } + }, [platform, visitingPersonaIdentifier]) +} diff --git a/packages/mask/src/plugins/Avatar/hooks/usePersonas.ts b/packages/mask/src/plugins/Avatar/hooks/usePersonas.ts new file mode 100644 index 000000000000..c44a73ddc0b0 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/hooks/usePersonas.ts @@ -0,0 +1,38 @@ +import { NextIDPlatform } from '@masknet/shared-base' +import { NextIDProof } from '@masknet/web3-providers' +import { first } from 'lodash-unified' +import { useAsyncRetry } from 'react-use' +import { useSubscription } from 'use-subscription' +import { usePersonaConnectStatus } from '../../../components/DataSource/usePersonaConnectStatus' +import { activatedSocialNetworkUI } from '../../../social-network' +import { context } from '../context' +import { sortPersonaBindings } from '../utils' + +export function usePersonas(userId?: string) { + const personaConnectStatus = usePersonaConnectStatus() + const currentIdentifier = useSubscription(context.currentVisitingProfile) + const platform = activatedSocialNetworkUI.configuration.nextIDConfig?.platform as NextIDPlatform + const identifier = useSubscription(context.lastRecognizedProfile) + return useAsyncRetry(async () => { + if (!identifier?.identifier?.userId) return + const personaBindings = await NextIDProof.queryExistedBindingByPlatform( + platform, + userId?.toLowerCase() ?? identifier.identifier.userId.toLowerCase(), + ) + + const currentPersonaBinding = first( + personaBindings.sort((a, b) => + sortPersonaBindings(a, b, userId?.toLowerCase() ?? identifier.identifier?.userId.toLowerCase() ?? ''), + ), + ) + if (!currentPersonaBinding) return + + const isOwner = userId + ? currentIdentifier?.identifier?.toText() === userId.toLowerCase() + : currentIdentifier?.identifier && + identifier.identifier && + currentIdentifier?.identifier.toText() === identifier.identifier.toText() + const wallets = currentPersonaBinding?.proofs.filter((proof) => proof.platform === NextIDPlatform.Ethereum) + return { wallets, isOwner, binds: currentPersonaBinding, status: personaConnectStatus } + }, [currentIdentifier, identifier, personaConnectStatus.hasPersona, platform, userId]) +} diff --git a/packages/mask/src/plugins/Avatar/hooks/useTokenOwner.ts b/packages/mask/src/plugins/Avatar/hooks/useTokenOwner.ts new file mode 100644 index 000000000000..878dd96181cd --- /dev/null +++ b/packages/mask/src/plugins/Avatar/hooks/useTokenOwner.ts @@ -0,0 +1,45 @@ +import { + ChainId, + isSameAddress, + safeNonPayableTransactionCall, + useAccount, + useERC721TokenContract, +} from '@masknet/web3-shared-evm' +import { useAsyncRetry } from 'react-use' +import { activatedSocialNetworkUI } from '../../../social-network' +import { PluginNFTAvatarRPC } from '../messages' +import { getNFTByOpensea } from '../utils' +import { usePersonas } from './usePersonas' + +export function useTokenOwner(address: string, tokenId: string, chainId?: ChainId) { + const ERC721Contract = useERC721TokenContract(address, chainId ?? ChainId.Mainnet) + return useAsyncRetry(async () => { + if (!ERC721Contract || !tokenId) return + const nft = await getNFTByOpensea(address, tokenId) + if (nft) return nft + const allSettled = await Promise.allSettled([ + safeNonPayableTransactionCall(ERC721Contract?.methods.ownerOf(tokenId)), + safeNonPayableTransactionCall(ERC721Contract.methods.name()), + safeNonPayableTransactionCall(ERC721Contract.methods.symbol()), + ]) + const result = allSettled.map((x) => (x.status === 'fulfilled' ? x.value : undefined)) + return { owner: result[0], name: result[1], symbol: result[2] } + }, [ERC721Contract, tokenId]) +} + +export function useCheckTokenOwner(userId: string, owner?: string) { + const account = useAccount() + const { value: persona, loading } = usePersonas(userId) + const { value: address, loading: loadingAddress } = useAsyncRetry( + () => PluginNFTAvatarRPC.getAddress(userId, activatedSocialNetworkUI.networkIdentifier), + [userId], + ) + + return { + loading: loading || loadingAddress, + isOwner: Boolean( + (address && owner && isSameAddress(address, owner)) || + persona?.wallets.some((x) => isSameAddress(x.identity, owner)), + ), + } +} diff --git a/packages/mask/src/plugins/Avatar/locales/en-US.json b/packages/mask/src/plugins/Avatar/locales/en-US.json new file mode 100644 index 000000000000..a267c4b7a6bb --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/en-US.json @@ -0,0 +1,27 @@ +{ + "application_dialog_title": "NFT PFP", + "application_edit_profile_dialog_title": "Edit Profile", + "persona_set_nft": "Set NFT PFPs", + "persona_verification_failed": "NFT PFP verification failed", + "cancel": "Cancel", + "save": "Save", + "change": "Change", + "connect_your_wallet": "Connect your wallet", + "upload_avatar_failed_message": "Sorry, failed to save NFT Avatar. Please set again.", + "upload_avatar_success_message": "Update NFT Avatar Success!", + "collectible_not_found": "Can' find it.", + "add_collectible": "Add Collectibles", + "set_avatar_title": "Set NFT Avatar", + "set_PFP_title": "Set NFT PFP", + "wallet_settings": "Wallet settings", + "persona_hint": "Customize NFT experience by connecting social accounts. Enjoy Web2 with a whole new Web3 vibe.", + "copy_success_of_wallet_address": "Copy wallet address successfully!", + "copied": "Copied", + "collectible_on_polygon": "Only NFTs on Ethereum are supported to preview. We are currently
working on supporting Polygon NFTs as well.
Please add your collectibles here.", + "collectible_no_eth": "No any collectible is available to preview. Please add your collectible here.", + "no_collectible_found": "No collectible found.", + "retry": "Retry", + "application_hint": "Socialize and show off your NFTs. People can bid,buy, view your valuable NFTs without leaving Twitter.", + "provider_by": "Provided by", + "wallet_non_evm_warning": "The NFT PFP feature currently supports only EVM chains. Support for other
chains will be added in the future." +} diff --git a/packages/mask/src/plugins/Avatar/locales/index.ts b/packages/mask/src/plugins/Avatar/locales/index.ts new file mode 100644 index 000000000000..d6ead60252e4 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/index.ts @@ -0,0 +1,6 @@ +// This file is auto generated. DO NOT EDIT +// Run `npx gulp sync-languages` to regenerate. +// Default fallback language in a family of languages are chosen by the alphabet order +// To overwrite this, please overwrite packages/scripts/src/locale-kit-next/index.ts + +export * from './i18n_generated' diff --git a/packages/mask/src/plugins/Avatar/locales/ja-JP.json b/packages/mask/src/plugins/Avatar/locales/ja-JP.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/ja-JP.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Avatar/locales/ko-KR.json b/packages/mask/src/plugins/Avatar/locales/ko-KR.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/ko-KR.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Avatar/locales/languages.ts b/packages/mask/src/plugins/Avatar/locales/languages.ts new file mode 100644 index 000000000000..67e418468ef3 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/languages.ts @@ -0,0 +1,34 @@ +// This file is auto generated. DO NOT EDIT +// Run `npx gulp sync-languages` to regenerate. +// Default fallback language in a family of languages are chosen by the alphabet order +// To overwrite this, please overwrite packages/scripts/src/locale-kit-next/index.ts +import en_US from './en-US.json' +import ja_JP from './ja-JP.json' +import ko_KR from './ko-KR.json' +import qya_AA from './qya-AA.json' +import zh_CN from './zh-CN.json' +import zh_TW from './zh-TW.json' +export const languages = { + en: en_US, + ja: ja_JP, + ko: ko_KR, + qy: qya_AA, + 'zh-CN': zh_CN, + zh: zh_TW, +} +// @ts-ignore +if (import.meta.webpackHot) { + // @ts-ignore + import.meta.webpackHot.accept( + ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'], + () => + globalThis.dispatchEvent?.( + new CustomEvent('MASK_I18N_HMR', { + detail: [ + 'com.maskbook.avatar', + { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }, + ], + }), + ), + ) +} diff --git a/packages/mask/src/plugins/Avatar/locales/qya-AA.json b/packages/mask/src/plugins/Avatar/locales/qya-AA.json new file mode 100644 index 000000000000..2ba51b407b94 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/qya-AA.json @@ -0,0 +1,27 @@ +{ + "application_dialog_title": "crwdns16640:0crwdne16640:0", + "application_edit_profile_dialog_title": "crwdns16642:0crwdne16642:0", + "persona_set_nft": "crwdns16644:0crwdne16644:0", + "persona_verification_failed": "crwdns16646:0crwdne16646:0", + "cancel": "crwdns16648:0crwdne16648:0", + "save": "crwdns16650:0crwdne16650:0", + "change": "crwdns16652:0crwdne16652:0", + "connect_your_wallet": "crwdns16654:0crwdne16654:0", + "upload_avatar_failed_message": "crwdns16656:0crwdne16656:0", + "upload_avatar_success_message": "crwdns16658:0crwdne16658:0", + "collectible_not_found": "crwdns16660:0crwdne16660:0", + "add_collectible": "crwdns16662:0crwdne16662:0", + "set_avatar_title": "crwdns16664:0crwdne16664:0", + "set_PFP_title": "crwdns16666:0crwdne16666:0", + "wallet_settings": "crwdns16668:0crwdne16668:0", + "persona_hint": "crwdns16670:0crwdne16670:0", + "copy_success_of_wallet_address": "crwdns16672:0crwdne16672:0", + "copied": "crwdns16674:0crwdne16674:0", + "collectible_on_polygon": "crwdns16676:0crwdne16676:0", + "collectible_no_eth": "crwdns16678:0crwdne16678:0", + "no_collectible_found": "crwdns16680:0crwdne16680:0", + "retry": "crwdns16682:0crwdne16682:0", + "application_hint": "crwdns16684:0crwdne16684:0", + "provider_by": "crwdns16686:0crwdne16686:0", + "wallet_non_evm_warning": "crwdns16688:0crwdne16688:0" +} diff --git a/packages/mask/src/plugins/Avatar/locales/zh-CN.json b/packages/mask/src/plugins/Avatar/locales/zh-CN.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/zh-CN.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Avatar/locales/zh-TW.json b/packages/mask/src/plugins/Avatar/locales/zh-TW.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/zh-TW.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Avatar/types.ts b/packages/mask/src/plugins/Avatar/types.ts index f61a59d91dc9..8fddf38cc416 100644 --- a/packages/mask/src/plugins/Avatar/types.ts +++ b/packages/mask/src/plugins/Avatar/types.ts @@ -1,9 +1,11 @@ +import type { ChainId, ERC721TokenDetailed } from '@masknet/web3-shared-evm' + export interface AvatarMetaDB { userId: string tokenId: string address: string avatarId: string - updateFlag?: boolean + chainId?: ChainId } export interface NFT { @@ -14,3 +16,19 @@ export interface NFT { owner: string slug: string } + +export interface SelectTokenInfo { + account: string + token: ERC721TokenDetailed + image: string +} + +export interface TokenInfo { + address: string + tokenId: string +} + +export interface NextIDAvatarMeta extends AvatarMetaDB { + nickname: string + imageUrl: string +} diff --git a/packages/mask/src/plugins/Avatar/utils/index.ts b/packages/mask/src/plugins/Avatar/utils/index.ts index df7dfe24f71f..3c03939f0808 100644 --- a/packages/mask/src/plugins/Avatar/utils/index.ts +++ b/packages/mask/src/plugins/Avatar/utils/index.ts @@ -1,16 +1,49 @@ -import { isNull } from 'lodash-unified' -import { ChainId, NonFungibleAssetProvider, formatBalance } from '@masknet/web3-shared-evm' +import { first, isNull } from 'lodash-unified' +import { + ChainId, + NonFungibleAssetProvider, + formatBalance, + createContract, + createWeb3, + getERC721ContractDetailed, + getERC721TokenDetailedFromChain, + ERC721TokenDetailed, + getERC721TokenAssetFromChain, +} from '@masknet/web3-shared-evm' import { EVM_RPC } from '../../EVM/messages' import Services from '../../../extension/service' -import { getOrderUnitPrice, NonFungibleTokenAPI } from '@masknet/web3-providers' +import { getOrderUnitPrice, NextIDProof, NextIDStorage, NonFungibleTokenAPI } from '@masknet/web3-providers' import { ZERO } from '@masknet/web3-shared-base' import BigNumber from 'bignumber.js' +import { activatedSocialNetworkUI } from '../../../social-network' +import type { NextIDPersonaBindings, NextIDPlatform } from '@masknet/shared-base' +import type { NextIDAvatarMeta } from '../types' +import { PLUGIN_ID } from '../constants' +import type { ERC721 } from '@masknet/web3-contracts/types/ERC721' +import type { AbiItem } from 'web3-utils' +import ERC721ABI from '@masknet/web3-contracts/abis/ERC721.json' function getLastSalePrice(lastSale?: NonFungibleTokenAPI.AssetEvent | null) { if (!lastSale?.total_price || !lastSale?.payment_token?.decimals) return return formatBalance(lastSale.total_price, lastSale.payment_token.decimals) } +export async function getNFTByOpensea(address: string, tokenId: string) { + const asset = await EVM_RPC.getAsset({ + address, + tokenId, + chainId: ChainId.Mainnet, + provider: NonFungibleAssetProvider.OPENSEA, + }) + + if (!asset) return + return { + name: asset?.name ?? '', + symbol: asset?.desktopOrder?.payment_token_contract?.symbol ?? 'ETH', + owner: asset?.top_ownerships[0].owner.address ?? '', + } +} + export async function getNFT(address: string, tokenId: string) { const asset = await EVM_RPC.getAsset({ address, @@ -31,13 +64,48 @@ export async function getNFT(address: string, tokenId: string) { name: asset?.name ?? '', symbol: asset?.desktopOrder?.payment_token_contract?.symbol ?? 'ETH', image: asset?.image_url ?? '', - owner: asset?.top_ownerships[0].owner.address ?? '', + owner: asset?.top_ownerships?.[0]?.owner.address ?? '', slug: asset?.slug ?? '', } } -export async function createNFT(address: string, tokenId: string) { - return EVM_RPC.getNFT({ address, tokenId, chainId: ChainId.Mainnet, provider: NonFungibleAssetProvider.OPENSEA }) +export async function getNFTByChain( + address: string, + tokenId: string, + chainId: ChainId, +): Promise { + const web3 = createWeb3(Services.Ethereum.request, () => ({ + chainId, + })) + const contract = createContract(web3, address, ERC721ABI as AbiItem[]) + if (!contract) return + + const contractDetailed = await getERC721ContractDetailed(contract, address, chainId) + const tokenDetailedFromChain = await getERC721TokenDetailedFromChain(contractDetailed, contract, tokenId) + if (!tokenDetailedFromChain) return + const info = await getERC721TokenAssetFromChain(tokenDetailedFromChain?.info.tokenURI) + const owner = await contract.methods.ownerOf(tokenId).call() + if (info && tokenDetailedFromChain) { + tokenDetailedFromChain.info = { + ...info, + owner, + ...tokenDetailedFromChain.info, + hasTokenDetailed: true, + name: info?.name ?? tokenDetailedFromChain?.info.name ?? '', + } + } + return tokenDetailedFromChain +} + +export async function createNFT(address: string, tokenId: string, chainId?: ChainId) { + const token = await getNFTByChain(address, tokenId, chainId ?? ChainId.Mainnet) + if (token) return token + return EVM_RPC.getNFT({ + address, + tokenId, + chainId: chainId ?? ChainId.Mainnet, + provider: chainId === ChainId.Matic ? NonFungibleAssetProvider.NFTSCAN : NonFungibleAssetProvider.OPENSEA, + }) } export async function getImage(image: string): Promise { @@ -91,3 +159,39 @@ export function formatText(name: string, tokenId: string) { } return `${_name} #${token}` } + +export function formatTokenId(symbol: string, tokenId: string) { + const name = tokenId + return name.length > 18 ? name.slice(0, 12) + '...' : name +} + +export function formatAddress(address: string, size = 0) { + if (size === 0 || size >= 20) return address + return `${address.slice(0, Math.max(0, 2 + size))}...${address.slice(-size)}` +} + +export const sortPersonaBindings = (a: NextIDPersonaBindings, b: NextIDPersonaBindings, userId: string): number => { + const p_a = first(a.proofs.filter((x) => x.identity === userId.toLowerCase())) + const p_b = first(b.proofs.filter((x) => x.identity === userId.toLowerCase())) + + if (!p_a || !p_b) return 0 + if (p_a.created_at > p_b.created_at) return -1 + return 1 +} + +export async function getNFTAvatarByUserId(userId: string, avatarId: string): Promise { + const platform = activatedSocialNetworkUI.configuration.nextIDConfig?.platform as NextIDPlatform + const bindings = await NextIDProof.queryExistedBindingByPlatform(platform, userId.toLowerCase()) + + for (const binding of bindings.sort((a, b) => sortPersonaBindings(a, b, userId))) { + const response = await NextIDStorage.getByIdentity( + binding.persona, + platform, + userId.toLowerCase(), + PLUGIN_ID, + ) + if (!avatarId && response.ok) return response.val + if (response.ok && response.val.avatarId === avatarId) return response.val + } + return +} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx index a51866d7b24a..d17cb3043318 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx @@ -7,16 +7,21 @@ import { useControlledDialog } from '../../../utils/hooks/useControlledDialog' import { MakeOfferDialog } from './MakeOfferDialog' import { PostListingDialog } from './PostListingDialog' import { CheckoutDialog } from './CheckoutDialog' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' +import { useChainId } from '@masknet/web3-shared-evm' const useStyles = makeStyles()((theme) => { return { root: { - marginLeft: theme.spacing(-0.5), - marginRight: theme.spacing(-0.5), + width: 'calc(100% - 24px)', }, button: { flex: 1, - margin: `0 ${theme.spacing(0.5)}`, + backgroundColor: theme.palette.maskColor.dark, + '&:hover': { + backgroundColor: theme.palette.maskColor.dark, + }, + color: 'white', }, } }) @@ -28,6 +33,7 @@ export function ActionBar(props: ActionBarProps) { const { classes } = useStyles() const { asset, assetOrder } = CollectibleState.useContainer() const assets = asset.value + const chainId = useChainId() const { open: openCheckoutDialog, @@ -43,53 +49,55 @@ export function ActionBar(props: ActionBarProps) { if (!asset.value) return null return ( - - {!asset.value.isOwner && asset.value.is_auction && assetOrder.value ? ( - - {t('plugin_collectible_buy_now')} - - ) : null} - {!asset.value.isOwner && asset.value.is_auction ? ( - - {t('plugin_collectible_place_bid')} - - ) : null} + + + {!asset.value.isOwner && asset.value.is_auction && assetOrder.value ? ( + + {t('plugin_collectible_buy_now')} + + ) : null} + {!asset.value.isOwner && asset.value.is_auction ? ( + + {t('plugin_collectible_place_bid')} + + ) : null} - {!asset.value.isOwner && !asset.value.is_auction ? ( - - {t('plugin_collectible_make_offer')} - - ) : null} - {assets?.isOwner ? ( - - {t('plugin_collectible_sell')} - - ) : null} - - - + {!asset.value.isOwner && !asset.value.is_auction ? ( + + {t('plugin_collectible_make_offer')} + + ) : null} + {assets?.isOwner ? ( + + {t('plugin_collectible_sell')} + + ) : null} + + + + ) } diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx index cb4d5b9afac0..eadb415fdc7a 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx @@ -1,6 +1,6 @@ import { ReactElement, useCallback } from 'react' import { Box, Button, CardActions, CardContent, CardHeader, Link, Paper, Tab, Tabs, Typography } from '@mui/material' -import { makeStyles } from '@masknet/theme' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { Trans } from 'react-i18next' import { findIndex } from 'lodash-unified' import formatDateTime from 'date-fns/format' @@ -33,7 +33,6 @@ const useStyles = makeStyles()((theme) => { width: '100%', border: `solid 1px ${theme.palette.divider}`, padding: 0, - marginBottom: 12, }, content: { width: '100%', @@ -141,24 +140,35 @@ export function Collectible(props: CollectibleProps) { setProvider, getEnumAsArray(NonFungibleAssetProvider).map((x) => x.value), resolveCollectibleProviderName, + true, ) // #endregion if (!asset.value || !token) return ( - + Failed to load your collectible on {resolveCollectibleProviderName(provider)}. - {CollectibleProviderSwitcher} - + + {CollectibleProviderSwitcher} + + + + ) const tabs = [ diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx index cd7c207be1eb..c87ac106b624 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx @@ -13,14 +13,19 @@ import { import { makeStyles } from '@masknet/theme' import { first, uniqBy } from 'lodash-unified' import BigNumber from 'bignumber.js' -import { FungibleTokenDetailed, EthereumTokenType, useAccount, useFungibleTokenWatched } from '@masknet/web3-shared-evm' +import { + FungibleTokenDetailed, + EthereumTokenType, + useAccount, + useFungibleTokenWatched, + useChainId, +} from '@masknet/web3-shared-evm' import formatDateTime from 'date-fns/format' import { useI18N } from '../../../utils' import { InjectedDialog } from '@masknet/shared' import { useRemoteControlledDialog } from '@masknet/shared-base-ui' import { UnreviewedWarning } from './UnreviewedWarning' import ActionButton, { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' -import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' import { DateTimePanel } from '../../../web3/UI/DateTimePanel' import { PluginCollectibleRPC } from '../messages' import { toAsset } from '../helpers' @@ -32,6 +37,7 @@ import { rightShift, ZERO } from '@masknet/web3-shared-base' import type { Coin } from '../../Trader/types' import { SelectTokenListPanel } from './SelectTokenListPanel' import { isWyvernSchemaName } from '../utils' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' const useStyles = makeStyles()((theme) => { return { @@ -86,7 +92,7 @@ export function MakeOfferDialog(props: MakeOfferDialogProps) { const { classes } = useStyles() const account = useAccount() - + const chainId = useChainId() const [expirationDateTime, setExpirationDateTime] = useState(new Date()) const [unreviewedChecked, setUnreviewedChecked] = useState(false) const [ToS_Checked, setToS_Checked] = useState(false) @@ -236,7 +242,7 @@ export function MakeOfferDialog(props: MakeOfferDialogProps) { )} - + ) : null} - + diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/PostInspector.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/PostInspector.tsx index bad1491c86bd..0e66ed8a0044 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/PostInspector.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/PostInspector.tsx @@ -1,7 +1,6 @@ import type { CollectibleJSON_Payload } from '../types' import { Collectible } from './Collectible' import { CollectibleState } from '../hooks/useCollectibleState' -import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' export interface PostInspectorProps { payload: CollectibleJSON_Payload @@ -11,15 +10,13 @@ export function PostInspector(props: PostInspectorProps) { const token = props.payload return ( - - - - - + + + ) } diff --git a/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts b/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts index 976962f468f9..f3f79d40d886 100644 --- a/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts +++ b/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts @@ -15,7 +15,6 @@ function useCollectibleState(token?: CollectibleToken) { const [provider, setProvider] = useState(NonFungibleAssetProvider.OPENSEA) const asset = useAsset(token?.contractAddress ?? '', token?.tokenId ?? '', provider) - // #region asset order from sdk const assetOrder = useAssetOrder(provider, token) // #endregion diff --git a/packages/mask/src/plugins/Collectible/types/opensea.ts b/packages/mask/src/plugins/Collectible/types/opensea.ts index 98bbd2557b12..929bccf9b03d 100644 --- a/packages/mask/src/plugins/Collectible/types/opensea.ts +++ b/packages/mask/src/plugins/Collectible/types/opensea.ts @@ -216,10 +216,10 @@ export interface OpenSeaResponse extends Asset { background_color: string | null transfer_fee: string | null transfer_fee_payment_token: OpenSeaFungibleToken | null - top_ownerships: { + top_ownerships: Array<{ owner: OpenSeaCustomAccount quantity: string - }[] + }> creator: OpenSeaCustomAccount endTime: string } diff --git a/packages/mask/src/plugins/Collectible/types/rarible.ts b/packages/mask/src/plugins/Collectible/types/rarible.ts index 28387ada41c6..772dc83e6877 100644 --- a/packages/mask/src/plugins/Collectible/types/rarible.ts +++ b/packages/mask/src/plugins/Collectible/types/rarible.ts @@ -60,10 +60,10 @@ export interface Creator { export interface Meta { name: string description: string - attributes: { + attributes: Array<{ key: string value: string - }[] + }> image: { meta: { PREVIEW: { diff --git a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/ActionBar.tsx b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/ActionBar.tsx index f6dce0601a4b..ef9a9d9fccfd 100644 --- a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/ActionBar.tsx +++ b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/ActionBar.tsx @@ -10,13 +10,14 @@ import { CheckoutDialog } from './CheckoutDialog' const useStyles = makeStyles()((theme) => { return { - root: { - marginLeft: theme.spacing(-0.5), - marginRight: theme.spacing(-0.5), - }, + root: {}, button: { flex: 1, - margin: `0 ${theme.spacing(0.5)}`, + backgroundColor: theme.palette.maskColor.dark, + color: 'white', + '&:hover': { + backgroundColor: theme.palette.maskColor.dark, + }, }, } }) @@ -43,7 +44,7 @@ export function ActionBar(props: ActionBarProps) { if (!asset.value) return null return ( - + {!assetSource?.isSoldOut && !assetSource?.is_owner && assetSource?.is24Auction && diff --git a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx index 12718c75e0e1..40165f6de8cf 100644 --- a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx @@ -16,6 +16,7 @@ import { resolveAssetLinkOnCryptoartAI, resolveWebLinkOnCryptoartAI } from '../p import { Markdown } from '../../Snapshot/SNSAdaptor/Markdown' import { ActionBar } from './ActionBar' import { useChainId } from '@masknet/web3-shared-evm' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' const useStyles = makeStyles()((theme) => { return { @@ -77,7 +78,7 @@ export function Collectible(props: CollectibleProps) { const { t } = useI18N() const { classes } = useStyles() const chainId = useChainId() - const { asset, events, tabIndex, setTabIndex } = CollectibleState.useContainer() + const { asset, events, tabIndex, setTabIndex, chainId: expectChainId } = CollectibleState.useContainer() const assetSource = useMemo(() => { if (!asset.value || asset.error) return @@ -265,7 +266,11 @@ export function Collectible(props: CollectibleProps) { - + + + + + ) } diff --git a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/PostInspector.tsx b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/PostInspector.tsx index b611ed2f1710..745131e28932 100644 --- a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/PostInspector.tsx +++ b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/PostInspector.tsx @@ -1,7 +1,6 @@ import type { PayloadType } from '../types' import { Collectible } from './Collectible' import { CollectibleState } from '../hooks/useCollectibleState' -import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' export interface PostInspectorProps { payload: PayloadType @@ -11,16 +10,14 @@ export function PostInspector(props: PostInspectorProps) { const token = props.payload return ( - - - - - + + + ) } diff --git a/packages/mask/src/plugins/CryptoartAI/hooks/useCollectibleState.ts b/packages/mask/src/plugins/CryptoartAI/hooks/useCollectibleState.ts index 282dd2f04f34..057f45b6c10e 100644 --- a/packages/mask/src/plugins/CryptoartAI/hooks/useCollectibleState.ts +++ b/packages/mask/src/plugins/CryptoartAI/hooks/useCollectibleState.ts @@ -20,6 +20,7 @@ function useCollectibleState(token?: Token) { setTabIndex, offers, events, + chainId: token?.chainId, } } diff --git a/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts b/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts index 0b11a8f2617e..31feb1eb43ff 100644 --- a/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts +++ b/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts @@ -136,13 +136,13 @@ export const getFungibleAssetsFn = const tokenUnavailableFromDebankResults = (await Promise.allSettled(allRequest)) .map((x) => (x.status === 'fulfilled' ? x.value : null)) - .filter((x) => Boolean(x)) as { + .filter((x) => Boolean(x)) as Array<{ chainId: ChainId balance: string price: PriceRecord - }[] + }> - const nativeTokens: Web3Plugin.Asset[] = networks + const nativeTokens: Array> = networks .filter( (t) => t.isMainnet && diff --git a/packages/mask/src/plugins/EVM/hooks/useAsset.ts b/packages/mask/src/plugins/EVM/hooks/useAsset.ts index fbca7176d93e..4ee7eba31865 100644 --- a/packages/mask/src/plugins/EVM/hooks/useAsset.ts +++ b/packages/mask/src/plugins/EVM/hooks/useAsset.ts @@ -7,6 +7,7 @@ import { useChainId, useTokenConstants, resolveIPFSLinkFromURL, + ChainId, } from '@masknet/web3-shared-evm' import { EVM_RPC } from '../messages' import { resolveAvatarLinkOnCurrentProvider } from '../../Collectible/pipes' @@ -17,14 +18,24 @@ export function useAsset(address: string, tokenId: string, provider: NonFungible const { WNATIVE_ADDRESS } = useTokenConstants() return useAsyncRetry(async () => { - const asset = await EVM_RPC.getAsset({ address, tokenId, chainId, provider }) + const asset = await EVM_RPC.getAsset({ + address, + tokenId, + chainId: provider === NonFungibleAssetProvider.OPENSEA ? ChainId.Mainnet : chainId, + provider, + }) + if (!asset) return return { ...asset, image_url: resolveIPFSLinkFromURL(asset?.image_url ?? ''), isOrderWeth: isSameAddress(asset?.desktopOrder?.payment_token ?? '', WNATIVE_ADDRESS) ?? false, isCollectionWeth: asset?.collection?.payment_tokens?.some(currySameAddress(WNATIVE_ADDRESS)) ?? false, isOwner: asset?.top_ownerships.some((item) => isSameAddress(item.owner.address, account)) ?? false, - collectionLinkUrl: resolveAvatarLinkOnCurrentProvider(chainId, asset, provider), + collectionLinkUrl: resolveAvatarLinkOnCurrentProvider( + provider === NonFungibleAssetProvider.OPENSEA ? ChainId.Mainnet : chainId, + asset, + provider, + ), } }, [account, chainId, WNATIVE_ADDRESS, address, tokenId, provider]) } diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx index 7105ba2da5a0..67789590560a 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx @@ -63,8 +63,6 @@ export default function CompletionCard(props: CompletionCardProps) { answer: e.answer || '', })), ) - } catch (error) { - throw error } finally { setSubmitting(false) } diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ConstPromise.ts b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ConstPromise.ts index 0d303203dda1..1c654cd8ab02 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ConstPromise.ts +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ConstPromise.ts @@ -20,8 +20,8 @@ export default class FindTrumanConstPromise { value?: FindTrumanConst err?: any - successCallback: ((value?: FindTrumanConst) => void)[] = [] - failCallback: ((err: any) => void)[] = [] + successCallback: Array<(value?: FindTrumanConst) => void> = [] + failCallback: Array<(err: any) => void> = [] resolve = (value: FindTrumanConst) => { if (this.status !== Status.PENDING) return diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx index 9ab793fd4f4a..fd870fe64ebf 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx @@ -1,7 +1,7 @@ import { useContext, useState } from 'react' import { makeStyles } from '@masknet/theme' import { FindTrumanContext } from '../context' -import { Alert, Avatar, Box, Card, CardHeader, CardMedia, Chip, Skeleton, Tooltip, Typography } from '@mui/material' +import { Avatar, Box, Card, CardHeader, CardMedia, Chip, Skeleton, Tooltip, Typography } from '@mui/material' import type { CompletionQuestionAnswer, PollResult, @@ -18,6 +18,10 @@ import Footer from './Footer' import StageCard from './StageCard' import EncryptionCard from './EncryptionCard' import CompletionCard from './CompletionCard' +import { PluginWalletConnectIcon } from '@masknet/icons' +import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' +import { useChainId } from '@masknet/web3-shared-evm' const useStyles = makeStyles()((theme) => { return { @@ -137,6 +141,7 @@ export function getPostTypeTitle(t: FindTrumanI18nFunction, postType: PostType) export function FindTruman(props: FindTrumanProps) { const { classes } = useStyles() const { address, t } = useContext(FindTrumanContext) + const chainId = useChainId() const { postType, clueId, @@ -182,74 +187,87 @@ export function FindTruman(props: FindTrumanProps) { } return ( - - {postType !== PostType.Encryption ? ( - <> - { - setLoadImg(false) - }} - alt="" - component="img" - height={140} - sx={{ - visibility: loadImg ? 'hidden' : 'unset', - }} - image={storyInfo?.img} - /> - {loadImg && ( - - - - )} - - - {storyInfo.name} - - - - - {isCritical && C} - {isNoncritical && N} - - - + <> + + {postType !== PostType.Encryption ? ( + <> + { + setLoadImg(false) + }} + alt="" + component="img" + height={140} + sx={{ + visibility: loadImg ? 'hidden' : 'unset', + }} + image={storyInfo?.img} + /> + {loadImg && ( + + + + )} + + + {storyInfo.name} + + + + + {isCritical && C} + {isNoncritical && ( + N + )} + + + + - - ) - } + ) + } + /> + {renderCard()} + + ) : ( + + )} + +