diff --git a/.i18n-codegen.json b/.i18n-codegen.json index 4c2c64e28b8c..cfac4329fdbc 100644 --- a/.i18n-codegen.json +++ b/.i18n-codegen.json @@ -253,6 +253,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 9e1391ac7183..32a4da52a8d3 100644 --- a/cspell.json +++ b/cspell.json @@ -120,7 +120,6 @@ "multicall", "multihop", "newsfeed", - "nftrss", "nftscan", "nums", "openswap", @@ -215,7 +214,7 @@ "aicanft", "algr", "anft", - "avator", + "armv", "blocto", "bsct", "btcb", diff --git a/package.json b/package.json index 2c521581f49c..f3673747e6e7 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "devDependencies": { "@commitlint/cli": "^16.2.4", "@commitlint/config-conventional": "^16.2.4", - "@dimensiondev/eslint-plugin": "^0.0.2-20220414093950-7d54c58", + "@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", diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index 329c14763de5..a06fafd88039 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", @@ -81,7 +85,6 @@ "@dimensiondev/unicode/specific-set": "error", "@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..4b8f76f2e0c2 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) { @@ -21,7 +23,7 @@ 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)) + 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/dashboard/src/components/Restore/RestoreFromCloud.tsx b/packages/dashboard/src/components/Restore/RestoreFromCloud.tsx index f810606257b4..808d9f29f6cd 100644 --- a/packages/dashboard/src/components/Restore/RestoreFromCloud.tsx +++ b/packages/dashboard/src/components/Restore/RestoreFromCloud.tsx @@ -159,7 +159,7 @@ export const RestoreFromCloud = memo(() => { )} - {(_, { backupJson: backupBasicInfoJson, handleRestore: handleRestore }) => ( + {(_, { backupJson: backupBasicInfoJson, handleRestore }) => ( <> 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/encryption/src/encryption/Encryption.ts b/packages/encryption/src/encryption/Encryption.ts index 1e297d84c30d..f05b9132f092 100644 --- a/packages/encryption/src/encryption/Encryption.ts +++ b/packages/encryption/src/encryption/Encryption.ts @@ -94,7 +94,7 @@ async function e2e_v37( target: EncryptTargetE2E, io: EncryptIO, ): Promise<[PayloadWellFormed.EndToEndEncryption, EncryptResult['e2e']]> { - const { authorPublic, postIV: postIV, postKeyEncoded } = context + const { authorPublic, postIV, postKeyEncoded } = context const authorPublicKey = await authorPublic if (!authorPublicKey.some) throw new EncryptError(EncryptErrorReasons.PublicKeyNotFound) diff --git a/packages/gun-utils/src/utils.ts b/packages/gun-utils/src/utils.ts index ca0357ddc92d..4ca9392d60c8 100644 --- a/packages/gun-utils/src/utils.ts +++ b/packages/gun-utils/src/utils.ts @@ -2,6 +2,7 @@ import { EventIterator } from 'event-iterator' import { getGunInstance, OnCloseEvent } from './instance' function getGunNodeFromPath(path: string[]) { + // eslint-disable-next-line unicorn/no-array-reduce const resultNode = path.reduce((gun, path) => gun.get(path as never), getGunInstance()) return resultNode } diff --git a/packages/icons/general/Plugin.tsx b/packages/icons/general/Plugin.tsx new file mode 100644 index 000000000000..98c5b861bc0e --- /dev/null +++ b/packages/icons/general/Plugin.tsx @@ -0,0 +1,12 @@ +import { createIcon } from '../utils' + +export const PluginIcon = createIcon( + 'PluginIcon', + + + , + '0 0 19 18', +) diff --git a/packages/icons/general/Provider.tsx b/packages/icons/general/Provider.tsx new file mode 100644 index 000000000000..3524f1decfe2 --- /dev/null +++ b/packages/icons/general/Provider.tsx @@ -0,0 +1,14 @@ +import { createIcon } from '../utils' + +export const ProviderByIcon = createIcon( + 'ProviderByIcon', + + + , + '0 0 17 16', +) diff --git a/packages/icons/general/index.ts b/packages/icons/general/index.ts index d30eda75b638..24df574b958f 100644 --- a/packages/icons/general/index.ts +++ b/packages/icons/general/index.ts @@ -116,6 +116,8 @@ export * from './MindsRound' export * from './CircleLoading' export * from './AddUser' export * from './PopupRestore' +export * from './Provider' +export * from './Plugin' export * from './Direct' export * from './SearchEmpty' export * from './WarningTriangle' diff --git a/packages/icons/plugins/CyberConnect.tsx b/packages/icons/plugins/CyberConnect.tsx index d2dada033e36..6aafbe3351fe 100644 --- a/packages/icons/plugins/CyberConnect.tsx +++ b/packages/icons/plugins/CyberConnect.tsx @@ -15,7 +15,7 @@ export const CyberConnectIcon: typeof SvgIcon = createPaletteAwareIcon( fillRule="evenodd" clipRule="evenodd" d="M20.8194 14.1385L29.6618 23.2918C29.768 23.4001 29.8525 23.529 29.9105 23.6712C29.9685 23.8133 29.9989 23.9659 30 24.1202C30.001 24.2745 29.9727 24.4275 29.9165 24.5705C29.8604 24.7135 29.7777 24.8436 29.673 24.9535L29.0242 25.6228C28.8418 25.8206 28.601 25.9497 28.3397 25.9897C28.1672 26.0136 27.9917 25.9958 27.8269 25.9377C27.6621 25.8797 27.5126 25.7829 27.3899 25.6551L16.5922 14.4778C16.4045 14.2838 16.2556 14.0534 16.1539 13.7997C16.0523 13.5461 16 13.2742 16 12.9996C16 12.725 16.0523 12.4531 16.1539 12.1994C16.2556 11.9458 16.4045 11.7153 16.5922 11.5213L27.3899 0.344062C27.4951 0.234991 27.62 0.148459 27.7576 0.0894208C27.8951 0.0303825 28.0425 0 28.1914 0C28.3403 0 28.4877 0.0303825 28.6253 0.0894208C28.7628 0.148459 28.8878 0.234991 28.993 0.344062L29.6618 1.04798C29.7672 1.15687 29.8508 1.2862 29.9078 1.42856C29.9648 1.57093 29.9942 1.72354 29.9942 1.87767C29.9942 2.03179 29.9648 2.1844 29.9078 2.32677C29.8508 2.46914 29.7672 2.59848 29.6618 2.70738L20.8194 11.8629C20.5294 12.1655 20.3667 12.5745 20.3667 13.0007C20.3667 13.427 20.5294 13.8359 20.8194 14.1385ZM9.18017 11.8726L0.341223 2.71698C0.233969 2.60853 0.148579 2.47911 0.0900066 2.33622C0.0314345 2.19333 0.000846678 2.03982 1.73677e-05 1.88459C-0.000811943 1.72936 0.0281358 1.5755 0.0851776 1.43194C0.142219 1.28839 0.226225 1.158 0.332313 1.04833L0.978629 0.376712C1.16268 0.180155 1.40373 0.0512975 1.66506 0.00975571C1.83749 -0.0133774 2.01275 0.00476068 2.17734 0.0627778C2.34194 0.120795 2.49149 0.217133 2.61448 0.344398L13.408 11.5218C13.5957 11.7158 13.7445 11.9463 13.8461 12.1999C13.9477 12.4536 14 12.7255 14 13.0001C14 13.2747 13.9477 13.5466 13.8461 13.8002C13.7445 14.0539 13.5957 14.2843 13.408 14.4783L2.61448 25.6557C2.50913 25.7649 2.38405 25.8515 2.24639 25.9105C2.10872 25.9696 1.96117 26 1.81215 26C1.66314 26 1.51558 25.9696 1.37792 25.9105C1.24025 25.8515 1.11517 25.7649 1.00983 25.6557L0.341223 24.9634C0.235829 24.8543 0.152224 24.7247 0.0951825 24.5822C0.038141 24.4396 0.00878436 24.2868 0.00878436 24.1325C0.00878436 23.9782 0.038141 23.8254 0.0951825 23.6828C0.152224 23.5402 0.235829 23.4107 0.341223 23.3016L9.18017 14.1483C9.47006 13.8457 9.63272 13.4367 9.63272 13.0104C9.63272 12.5842 9.47006 12.1752 9.18017 11.8726Z" - fill="white" + fill="black" />, undefined, '-8 -10 44 44', diff --git a/packages/icons/plugins/MarketsClaim.tsx b/packages/icons/plugins/MarketsClaim.tsx index f94a2736ade3..1e82b939ee43 100644 --- a/packages/icons/plugins/MarketsClaim.tsx +++ b/packages/icons/plugins/MarketsClaim.tsx @@ -1,7 +1,6 @@ import { createIcon } from '../utils' -import type { SvgIcon } from '@mui/material' -export const MarketsClaimIcon: typeof SvgIcon = createIcon( +export const MarketsClaimIcon = createIcon( 'Markets', <> diff --git a/packages/icons/plugins/NFTAvatar.tsx b/packages/icons/plugins/NFTAvatar.tsx new file mode 100644 index 000000000000..4ba11fbe2ed0 --- /dev/null +++ b/packages/icons/plugins/NFTAvatar.tsx @@ -0,0 +1,29 @@ +import { createIcon } from '../utils' +import type { SvgIcon } from '@mui/material' + +export const NFTAvatarIcon: typeof SvgIcon = createIcon( + 'nft_avatar', + + + + + + + + + + , + '0 0 37 36', +) diff --git a/packages/icons/plugins/connect.tsx b/packages/icons/plugins/connect.tsx new file mode 100644 index 000000000000..291662e4980f --- /dev/null +++ b/packages/icons/plugins/connect.tsx @@ -0,0 +1,31 @@ +import { createIcon } from '@masknet/icons' +export const PluginWalletConnectIcon = createIcon( + 'PluginWalletConnectIcon', + + + + + + , + '0 0 17 16', +) diff --git a/packages/icons/plugins/index.ts b/packages/icons/plugins/index.ts index 996563638215..3915a4d0166c 100644 --- a/packages/icons/plugins/index.ts +++ b/packages/icons/plugins/index.ts @@ -24,3 +24,6 @@ export * from './DHEDGEIcon' export * from './PoolTogetherIcon' export * from './SecurityChecker' export * from './ArtBlocks' +export * from './NFTAvatar' +export * from './connect' +export * from './shared' diff --git a/packages/icons/plugins/shared.tsx b/packages/icons/plugins/shared.tsx new file mode 100644 index 000000000000..fa880e3d3136 --- /dev/null +++ b/packages/icons/plugins/shared.tsx @@ -0,0 +1,20 @@ +import { createIcon } from '@masknet/icons' +export const SharedIcon = createIcon( + 'SharedIcon', + + + + + + + + + + , + '0 0 19 18', +) diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 32446804934f..43da926be902 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -299,10 +299,10 @@ export async function updatePersonaDB( else nextLinkedProfiles = nextRecord.linkedProfiles } if (howToMerge.explicitUndefinedField === 'ignore') { - for (const _key in nextRecord) { - const key = _key as keyof typeof nextRecord + const keys = Object.keys(nextRecord) as Array + for (const key of keys) { if (nextRecord[key] === undefined) { - delete nextRecord[key as keyof typeof nextRecord] + delete nextRecord[key] } } } diff --git a/packages/mask/background/services/identity/profile/update.ts b/packages/mask/background/services/identity/profile/update.ts index c82f7a8f5792..b3dc946d59a0 100644 --- a/packages/mask/background/services/identity/profile/update.ts +++ b/packages/mask/background/services/identity/profile/update.ts @@ -58,15 +58,12 @@ export async function resolveUnknownLegacyIdentity(identifier: ProfileIdentifier const unknown = ProfileIdentifier.of(identifier.network, '$unknown').unwrap() const self = ProfileIdentifier.of(identifier.network, '$self').unwrap() - const r = await queryProfilesDB({ identifiers: [unknown, self] }) - if (!r.length) return - const final = { - ...r.reduce((p, c) => ({ ...p, ...c })), - identifier, - } + const records = await queryProfilesDB({ identifiers: [unknown, self] }) + if (!records.length) return + const finalRecord: ProfileRecord = Object.assign({}, ...records, { identifier }) try { await consistentPersonaDBWriteAccess(async (t) => { - await createProfileDB(final, t) + await createProfileDB(finalRecord, t) await deleteProfileDB(unknown, t).catch(() => {}) await deleteProfileDB(self, t).catch(() => {}) }) diff --git a/packages/mask/package.json b/packages/mask/package.json index 8192e03b4fa3..3d892fb25cb3 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -57,6 +57,7 @@ "@types/json-stable-stringify": "^1.0.34", "@types/json2csv": "^5.0.3", "@types/node": "^17.0.31", + "@types/react-avatar-editor": "^10.3.6", "@types/react-highlight-words": "^0.16.4", "@types/react-router-dom": "^5.3.3", "@types/react-virtualized-auto-sizer": "^1.0.1", @@ -100,6 +101,7 @@ "pure-react-carousel": "^1.28.1", "promievent": "^0.1.4", "protobufjs": "^6.11.2", + "react-avatar-editor": "^12.0.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-draggable": "^4.4.4", diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 5b896d697ce7..a568b8f03316 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -21,7 +21,6 @@ "load": "Load", "load_all": "Load All", "no_data": "No Data", - "tip": "Tip", "tags": "Tags", "contract": "Contract", "plugins": "Plugins", @@ -128,10 +127,6 @@ "user_guide_tip_2": "Connect and switch your wallet.", "user_guide_tip_3": "Browse the Web 3 footprints of your Twitter friends, enjoy the freedom of Web 3.", "user_guide_tip_4": "Click here to have a quick start.", - "create_persona": "Create persona", - "connect_persona": "Connect persona", - "please_create_persona": "Please create persona", - "please_connect_persona": "Please connect persona", "mask_network": "Mask Network", "import": "Import", "no_search_result": "No result", @@ -191,7 +186,6 @@ "wallet_search_no_result": "No results.", "wallet_confirm_with_password": "Confirm with password", "wallet_airdrop_nft_unclaimed_title": "NFT Airdrop Unclaimed:", - "plugin_not_enabled": "{{plugin}} (Not Enabled)", "plugin_external_unknown_plugin": "New unknown Mask plugins found. Do you want to load them?", "plugin_external_loader_search_holder": "Search for an external plugin", "plugin_external_loader_search_button": "Search for plugin", @@ -288,7 +282,7 @@ "plugin_wallet_nft_approving_all": "Unlocking {{symbol}}...", "plugin_wallet_approve_all_nft": "Unlock {{symbol}}", "plugin_wallet_approve_all_nft_successfully": "Unlock {{symbol}} successfully", - "plugin_wallet_connect_a_wallet": "Connect a Wallet", + "plugin_wallet_connect_a_wallet": "Connect wallet", "plugin_wallet_confirm_risk_warning": "Confirm Risk Warning", "plugin_wallet_no_gas_fee": "No Gas Fee", "plugin_wallet_update_gas_fee": "Updating Gas Fee…", @@ -308,18 +302,26 @@ "plugin_wallet_select_provider_dialog_title": "Connect Wallet", "plugin_wallet_token_unlock": "Exact Unlock", "plugin_wallet_token_infinite_unlock": "Infinite Unlock", - "plugin_wallet_switch_network": "Switch to {{network}} Network", "plugin_wallet_switch_network_under_going": "Switching to {{network}} Network…", "plugin_wallet_not_available_on": "Not available on {{network}} Network.", "plugin_wallet_connect_wallet": "Connect Wallet", - "plugin_wallet_connect_wallet_tip": "Please connect to a wallet.", "plugin_wallet_fail_to_sign": "Failed to sign password.", + "plugin_wallet_connect_dialog_title": "WalletConnect", + "plugin_wallet_return_mobile_wallet_options": "Return to Mobile Wallet Options", + "plugin_wallet_view_qr_code": "View QR Code", + "plugin_wallet_switch_network": "Switch to {{network}}", + "plugin_wallet_connect_tips": "You’re connected to a walletConnect\nwallet. Please switch network in that wallet, \nor switch to another wallet.", + "plugin_wallet_connect_network": "Connect {{network}} wallet", + "plugin_wallet_connect_network_under_going": "Connect {{network}} wallet", + "plugin_wallet_name_placeholder": "Enter 1-12 characters", + "plugin_nft_avatar_recommend_feature_description": "Set your NFT as profile picture with exclusive aura", "plugin_red_packet_display_name": "Plugin: Lucky Drop", + "application_hint": "Socialize and show off your NFTs. People can bid,buy, view your valuable NFTs without leaving Twitter.", "plugin_red_packet_claimed": "Claimed", "plugin_red_packet_erc20_tab_title": "Token", "plugin_red_packet_erc721_tab_title": "Collectibles", "plugin_red_packet_name": "Lucky Drop", - "plugin_red_packet_recommend_feature_description": "Surpirse your social friends with Lucky Drop", + "plugin_red_packet_recommend_feature_description": "Astonish your friends with a surprise gift", "plugin_red_packet_description": "Gift crypto or NFTs to any users, first come, first served.", "plugin_red_packet_erc721_insufficient_balance": "Insufficient Balance", "plugin_red_packet_details": "Lucky Drop Details", @@ -573,14 +575,14 @@ "plugin_ito_list_table_got": "Balance", "plugin_ito_list_button_send": "Send", "plugin_ito_withdraw": "Withdraw", - "plugin_ito_qualification_loading": "Verify qualification…", - "plugin_ito_qualification_failed": "The wallet address isn't on the list.", "plugin_ito_withdrawn": "Withdrawn", "plugin_ito_your_swapped_amount": "You swapped {{amount}} {{symbol}}", "plugin_ito_your_claimed_amount": "You claimed {{amount}} {{symbol}}", "plugin_ito_your_refund_amount": "return {{amount}} {{symbol}}.", "plugin_ito_unlock_in_advance": "Unlock in Advance", "plugin_ito_swapped_status": "{{remain}} / {{total}} {{token}} Swapped", + "plugin_ito_qualification_loading": "Verify qualification…", + "plugin_ito_qualification_failed": "The wallet address isn't on the list.", "plugin_ito_congratulations": "Congratulations!", "plugin_ito_out_of_stock_hit": "Better luck next time", "plugin_ito_claim_success_share": "I just attended @{{user}}'s #ITO with @{{account}} to swap ${{symbol}}. Install mask.io and start your own Initial Twitter Offering! \n {{link}}", @@ -782,7 +784,6 @@ "plugin_dhedge_buy_token": "GET {{symbol}}", "plugin_dhedge_enter_an_amount": "Enter an amount", "plugin_dhedge_insufficient_balance": "Insufficient {{symbol}} balance", - "plugin_dhedge_loading": "Loading...", "plugin_dhedge_pool_not_found": "Invalid pool address.", "plugin_dhedge_smt_wrong": "Something went wrong!", "plugin_pooltogether_tab_pools": "Pools", @@ -881,7 +882,8 @@ "plugin_furucombo_rewards": "Rewards", "plugin_furucombo_invest": "Invest", "plugin_furucombo_pool_not_found": "Invalid pool address.", - "plugin_furucombo_smt_wrong": "Something went wrong!", + "plugin_furucombo_load_failed": "Load failed", + "plugin_furucombo_reload": "Reload", "plugin_furucombo_head_pools": "Pools", "plugin_furucombo_head_action": "Action", "plugin_pets_dialog_title": "Non-Fungible Friends", @@ -1019,14 +1021,13 @@ "popups_backup_password": "Backup Password", "popups_rename_error_tip": "Maximum length is {{length}} characters long.", "nft_wallet_label": "Wallet", - "plugin_profile_no_wallets": "Connect your wallet here.
This section will be shown to your encrypted friends.
The display content includes digital art, donations, badges
and other public information on chain.", "plugin_vcent_last_offer_at": "LATEST OFFER at", "nft_input_address_label": "Please input contract address", "nft_input_tokenid_label": "Please input token ID", - "nft_owner_hint": "This NFT does not exist or does not belong to you.", + "nft_owner_hint": "The contract address is incorrect or the collection does not belong to you", "nft_add_dialog_title": "Add Collectible", "nft_add_button_label": "Add", - "nft_avatar": "NFT Avatar", + "nft_avatar": "NFT PFP", "web3_tab_hint": "No address found.", "plugin_artblocks_not_active": "This project is no more active", "plugin_artblocks_completed": "Completed", @@ -1064,5 +1065,9 @@ "plugin_transak_name": "Fiat On-Ramp", "plugin_transak_description": "Fiat On-Ramp Aggregator on Twitter. Buy crypto in 60+ countries with Transak support.", "plugin_valuables_name": "Valuables", - "plugin_valuables_description": "Buy & sell tweets autographed by their original creators." + "plugin_valuables_description": "Buy & sell tweets autographed by their original creators.", + "popups_wallet_unsupported_network": "Unsupported network type", + "plugin_default_title": "Default", + "plugin_provider_by": "Provided by", + "plugin_enables": "Enable plugins" } diff --git a/packages/mask/shared-ui/locales/ja-JP.json b/packages/mask/shared-ui/locales/ja-JP.json index e93e29c4057c..179543ea1c1b 100644 --- a/packages/mask/shared-ui/locales/ja-JP.json +++ b/packages/mask/shared-ui/locales/ja-JP.json @@ -230,8 +230,6 @@ "plugin_ito_list_table_got": "バランス", "plugin_ito_list_button_send": "送る", "plugin_ito_withdraw": "取り消す", - "plugin_ito_qualification_loading": "資格認証中…", - "plugin_ito_qualification_failed": "ウォレットアドレスがリストにありません。", "plugin_ito_withdrawn": "取り消しました", "plugin_ito_your_swapped_amount": "あなたは {{amount}} {{symbol}} を獲得しました", "plugin_ito_your_claimed_amount": "あなたは {{amount}} {{symbol}} を請求しました", @@ -368,7 +366,6 @@ "plugin_dhedge_buy_token": "{{symbol}} を得る", "plugin_dhedge_enter_an_amount": "量を入力", "plugin_dhedge_insufficient_balance": "{{symbol}} の残高が不十分です", - "plugin_dhedge_loading": "ロード中…", "plugin_dhedge_pool_not_found": "無効なプールアドレスです!", "plugin_dhedge_smt_wrong": "何か間違いが発生しました!" } diff --git a/packages/mask/shared-ui/locales/ko-KR.json b/packages/mask/shared-ui/locales/ko-KR.json index e62f09bc4cef..9841390b0b3a 100644 --- a/packages/mask/shared-ui/locales/ko-KR.json +++ b/packages/mask/shared-ui/locales/ko-KR.json @@ -19,7 +19,6 @@ "load": "로드", "load_all": "모두 로드하기", "no_data": "데이터 없음", - "tip": "팁", "tags": "태그", "contract": "컨트랙트", "initializing": "초기화 중...", @@ -77,8 +76,6 @@ "user_guide_tip_connected": "해당 계정에 성공적으로 연결되었습니다.", "user_guide_tip_1": "클릭하여 메인 기능을 확인하세요.", "user_guide_tip_2": "여기서 월렛을 연결하세요. 여기서 네트워크나 월렛을 바꿀 수 있습니다.", - "create_persona": "페르소나 만들기", - "connect_persona": "페로소나 연결하기", "mask_network": "Mask Network", "import": "불러오기", "no_search_result": "결과 없음", @@ -200,7 +197,7 @@ "plugin_wallet_switch_network_under_going": "{{network}} 네트워크로 바꾸는 중...", "plugin_wallet_not_available_on": "{{network}} 에서는 사용할 수 없습니다.", "plugin_wallet_connect_wallet": "월렛 연결", - "plugin_wallet_connect_wallet_tip": "월렛을 연결하세요.", + "plugin_wallet_name_placeholder": "1-12 자 입력하세요", "plugin_wallet_fail_to_sign": "비밀번호 사인 실패합니다.", "plugin_red_packet_display_name": "플러그인: 빨간 백", "plugin_red_packet_erc20_tab_title": "토큰", @@ -415,8 +412,6 @@ "plugin_ito_list_table_got": "잔액", "plugin_ito_list_button_send": "발송", "plugin_ito_withdraw": "철회", - "plugin_ito_qualification_loading": "자격 인증...", - "plugin_ito_qualification_failed": "이 월렛 주소는 리스트에 없습니다.", "plugin_ito_withdrawn": "철회됨", "plugin_ito_your_swapped_amount": "{{amount}} {{symbol}} 스왑되었습니다.", "plugin_ito_your_claimed_amount": "{{amount}} {{symbol}} 수령되었습니다.", @@ -575,7 +570,6 @@ "plugin_dhedge_buy_token": "{{symbol}} 획득", "plugin_dhedge_enter_an_amount": "수액 입력", "plugin_dhedge_insufficient_balance": "{{symbol}} 잔액 부족", - "plugin_dhedge_loading": "로딩 중", "plugin_dhedge_pool_not_found": "무효한 풀 주소!", "plugin_dhedge_smt_wrong": "오류가 발생합니다.", "plugin_pooltogether_tab_pools": "풀(Pools)", diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index 0d941a058607..de7a1f01bf1f 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -7,6 +7,8 @@ "debug_metadata_put_metadata": "crwdns10061:0crwdne10061:0", "edit": "crwdns10063:0crwdne10063:0", "clear": "crwdns10065:0crwdne10065:0", + "connect": "crwdns16576:0crwdne16576:0", + "back": "crwdns16578:0crwdne16578:0", "more": "crwdns10067:0crwdne10067:0", "approve": "crwdns10069:0crwdne10069:0", "address": "crwdns10071:0crwdne10071:0", @@ -19,9 +21,10 @@ "load": "crwdns10089:0crwdne10089:0", "load_all": "crwdns10093:0crwdne10093:0", "no_data": "crwdns10097:0crwdne10097:0", - "tip": "crwdns10105:0crwdne10105:0", "tags": "crwdns10107:0crwdne10107:0", "contract": "crwdns10109:0crwdne10109:0", + "plugins": "crwdns16580:0crwdne16580:0", + "persona_required": "crwdns16582:0crwdne16582:0", "initializing": "crwdns10111:0crwdne10111:0", "redirect_alert": "crwdns10113:0crwdne10113:0", "typed_message_text_alert": "crwdns10117:0crwdne10117:0", @@ -48,6 +51,19 @@ "auto_paste_failed_snackbar": "crwdns4005:0crwdne4005:0", "auto_paste_failed_snackbar_action": "crwdns4007:0crwdne4007:0", "auto_paste_failed_snackbar_action_close": "crwdns9363:0crwdne9363:0", + "compose_encrypt_method_text": "crwdns16584:0crwdne16584:0", + "compose_encrypt_method_text_sub_title": "crwdns16586:0crwdne16586:0", + "compose_encrypt_method_image": "crwdns16588:0crwdne16588:0", + "compose_encrypt_method_image_sub_title": "crwdns16590:0crwdne16590:0", + "compose_no_local_key": "crwdns16592:0crwdne16592:0", + "compose_encrypt_visible_to_all": "crwdns16594:0crwdne16594:0", + "compose_encrypt_visible_to_all_sub": "crwdns16596:0crwdne16596:0", + "compose_encrypt_visible_to_private": "crwdns16598:0crwdne16598:0", + "compose_encrypt_visible_to_private_sub": "crwdns16600:0crwdne16600:0", + "compose_shared_friends": "crwdns16602:0crwdne16602:0", + "compose_encrypt_visible_to_share": "crwdns16606:0crwdne16606:0", + "compose_encrypt_visible_to_share_sub": "crwdns16608:0crwdne16608:0", + "compose_encrypt_share_dialog_empty": "crwdns16610:0crwdne16610:0", "automation_request_click_post_button": "crwdns4009:0crwdne4009:0", "try_again": "crwdns8125:0crwdne8125:0", "ok": "crwdns12997:0crwdne12997:0", @@ -134,7 +150,10 @@ "post_dialog__button": "crwdns4257:0crwdne4257:0", "post_dialog__placeholder": "crwdns4265:0crwdne4265:0", "post_dialog__title": "crwdns4275:0crwdne4275:0", + "post_dialog_visible_to": "crwdns16612:0crwdne16612:0", + "post_dialog_encryption_method": "crwdns16614:0crwdne16614:0", "post_dialog_enable_paste_auto": "crwdns10129:0crwdne10129:0", + "post_dialog_share_with_input_placeholder": "crwdns16616:0crwdne16616:0", "post_modal_hint__button": "crwdns4277:0crwdne4277:0", "hide": "crwdns4293:0crwdne4293:0", "reset": "crwdns4301:0crwdne4301:0", @@ -147,6 +166,7 @@ "select_none": "crwdns4335:0crwdne4335:0", "all_friends": "crwdns4337:0crwdne4337:0", "select_specific_friends_dialog__title": "crwdns4341:0crwdne4341:0", + "select_friends_dialog_persona_connect": "crwdns16618:0crwdne16618:0", "service_decryption_failed": "crwdns4343:0crwdne4343:0", "service_username_invalid": "crwdns4359:0crwdne4359:0", "save": "crwdns10439:0crwdne10439:0", @@ -168,7 +188,6 @@ "wallet_search_no_result": "crwdns8139:0crwdne8139:0", "wallet_confirm_with_password": "crwdns10141:0crwdne10141:0", "wallet_airdrop_nft_unclaimed_title": "crwdns8093:0crwdne8093:0", - "plugin_not_enabled": "crwdns10811:0{{plugin}}crwdne10811:0", "plugin_external_unknown_plugin": "crwdns10143:0crwdne10143:0", "plugin_external_loader_search_holder": "crwdns10145:0crwdne10145:0", "plugin_external_loader_search_button": "crwdns10147:0crwdne10147:0", @@ -288,7 +307,7 @@ "plugin_wallet_switch_network_under_going": "crwdns4689:0{{network}}crwdne4689:0", "plugin_wallet_not_available_on": "crwdns7913:0{{network}}crwdne7913:0", "plugin_wallet_connect_wallet": "crwdns4693:0crwdne4693:0", - "plugin_wallet_connect_wallet_tip": "crwdns4695:0crwdne4695:0", + "plugin_wallet_name_placeholder": "crwdns4705:0crwdne4705:0", "plugin_wallet_fail_to_sign": "crwdns4707:0crwdne4707:0", "plugin_red_packet_display_name": "crwdns4711:0crwdne4711:0", "plugin_red_packet_claimed": "crwdns10205:0crwdne10205:0", @@ -549,8 +568,6 @@ "plugin_ito_list_table_got": "crwdns5131:0crwdne5131:0", "plugin_ito_list_button_send": "crwdns5133:0crwdne5133:0", "plugin_ito_withdraw": "crwdns5135:0crwdne5135:0", - "plugin_ito_qualification_loading": "crwdns5137:0crwdne5137:0", - "plugin_ito_qualification_failed": "crwdns5139:0crwdne5139:0", "plugin_ito_withdrawn": "crwdns5141:0crwdne5141:0", "plugin_ito_your_swapped_amount": "crwdns5143:0{{amount}}crwdnd5143:0{{symbol}}crwdne5143:0", "plugin_ito_your_claimed_amount": "crwdns5145:0{{amount}}crwdnd5145:0{{symbol}}crwdne5145:0", @@ -758,7 +775,6 @@ "plugin_dhedge_buy_token": "crwdns5423:0{{symbol}}crwdne5423:0", "plugin_dhedge_enter_an_amount": "crwdns5425:0crwdne5425:0", "plugin_dhedge_insufficient_balance": "crwdns5427:0{{symbol}}crwdne5427:0", - "plugin_dhedge_loading": "crwdns5429:0crwdne5429:0", "plugin_dhedge_pool_not_found": "crwdns5431:0crwdne5431:0", "plugin_dhedge_smt_wrong": "crwdns5433:0crwdne5433:0", "plugin_pooltogether_tab_pools": "crwdns5435:0crwdne5435:0", @@ -890,8 +906,6 @@ "popups_social_account": "crwdns16328:0crwdne16328:0", "popups_connected_wallets": "crwdns16330:0crwdne16330:0", "popups_loading": "crwdns16332:0crwdne16332:0", - "popups_wallet_disconnect_success": "crwdns16334:0crwdne16334:0", - "popups_wallet_disconnect_failed": "crwdns16336:0crwdne16336:0", "popups_connect_your_web2_social_accounts": "crwdns16338:0crwdne16338:0", "popups_connect_social_account": "crwdns16340:0crwdne16340:0", "popups_disconnect_success": "crwdns16342:0crwdne16342:0", @@ -994,7 +1008,6 @@ "popups_backup_password": "crwdns8985:0crwdne8985:0", "popups_rename_error_tip": "crwdns8987:0{{length}}crwdne8987:0", "nft_wallet_label": "crwdns9205:0crwdne9205:0", - "plugin_profile_no_wallets": "crwdns9327:0crwdne9327:0", "plugin_vcent_last_offer_at": "crwdns10295:0crwdne10295:0", "nft_input_address_label": "crwdns9193:0crwdne9193:0", "nft_input_tokenid_label": "crwdns9195:0crwdne9195:0", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index a70027a7272b..6922041dec0c 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -19,7 +19,6 @@ "load": "加载", "load_all": "加载全部", "no_data": "暂无数据", - "tip": "小费", "tags": "标签", "contract": "合约", "initializing": "初始化中...", @@ -161,7 +160,6 @@ "wallet_search_no_result": "没有任何结果。", "wallet_confirm_with_password": "使用密码确认", "wallet_airdrop_nft_unclaimed_title": "未认领的NFT Airdrop:", - "plugin_not_enabled": "{{plugin}} (未启用)", "plugin_external_unknown_plugin": "发现新的未知Mask插件,您想要加载它们吗?", "plugin_external_loader_search_holder": "查找外部插件", "plugin_external_loader_search_button": "查找插件", @@ -273,7 +271,7 @@ "plugin_wallet_switch_network_under_going": "正在切换到 {{network}} 网络…", "plugin_wallet_not_available_on": "在 {{network}} 网络上不可用。", "plugin_wallet_connect_wallet": "连接钱包", - "plugin_wallet_connect_wallet_tip": "请连接钱包", + "plugin_wallet_name_placeholder": "输入1-12 个字符", "plugin_wallet_fail_to_sign": "无法签名密码。", "plugin_red_packet_display_name": "插件:红包", "plugin_red_packet_claimed": "已认领", @@ -515,8 +513,6 @@ "plugin_ito_list_table_got": "余额", "plugin_ito_list_button_send": "发送", "plugin_ito_withdraw": "提取", - "plugin_ito_qualification_loading": "资格认证中…", - "plugin_ito_qualification_failed": "钱包地址不在列表内。", "plugin_ito_withdrawn": "已提取", "plugin_ito_your_swapped_amount": "你兑换了 {{amount}} {{symbol}}", "plugin_ito_your_claimed_amount": "你认领了 {{amount}} {{symbol}}", @@ -703,7 +699,6 @@ "plugin_dhedge_buy_token": "获得 {{symbol}}", "plugin_dhedge_enter_an_amount": "输入数额", "plugin_dhedge_insufficient_balance": "{{symbol}} 余额不足", - "plugin_dhedge_loading": "加载中...", "plugin_dhedge_pool_not_found": "无效的池子地址。", "plugin_dhedge_smt_wrong": "发生了一些错误!", "plugin_pooltogether_tab_pools": "池子", @@ -827,7 +822,6 @@ "popups_backup_password": "备份密码", "popups_rename_error_tip": "最大长度为 {{length}} 个字符。", "nft_wallet_label": "钱包", - "plugin_profile_no_wallets": "在此连接您的钱包。
此部分将显示给您的加密朋友。
显示内容包括数字艺术、捐赠、徽章
和其他链上的公开信息。", "plugin_vcent_last_offer_at": "最新的OFFER为", "nft_input_address_label": "请输入合约地址", "nft_input_tokenid_label": "请输入Token ID", diff --git a/packages/mask/shared-ui/locales/zh-TW.json b/packages/mask/shared-ui/locales/zh-TW.json index c3e24953ee5e..11a6411dd67c 100644 --- a/packages/mask/shared-ui/locales/zh-TW.json +++ b/packages/mask/shared-ui/locales/zh-TW.json @@ -184,7 +184,6 @@ "plugin_wallet_switch_network_under_going": "切換至 {{network}} 中", "plugin_wallet_not_available_on": "{{network}} 不可用", "plugin_wallet_connect_wallet": "連接錢包", - "plugin_wallet_connect_wallet_tip": "請連接你的錢包", "plugin_red_packet_display_name": "插件:紅包", "plugin_red_packet_claimed": "已認領", "plugin_red_packet_erc20_tab_title": "代幣", @@ -361,8 +360,6 @@ "plugin_ito_list_table_got": "餘額", "plugin_ito_list_button_send": "送出", "plugin_ito_withdraw": "提取", - "plugin_ito_qualification_loading": "驗證資格…", - "plugin_ito_qualification_failed": "錢包地址不在清單上。", "plugin_ito_withdrawn": "已提取", "plugin_ito_your_swapped_amount": "你交換了 {{amount}} {{symbol}}", "plugin_ito_your_claimed_amount": "你認領了 {{amount}} {{symbol}}", @@ -515,7 +512,6 @@ "plugin_dhedge_buy_token": "取得 {{symbol}}", "plugin_dhedge_enter_an_amount": "輸入一個金額", "plugin_dhedge_insufficient_balance": "{{symbol}} 餘額不足", - "plugin_dhedge_loading": "讀取中…", "plugin_dhedge_pool_not_found": "錯誤的池地址!", "plugin_dhedge_smt_wrong": "出錯了!" } diff --git a/packages/mask/src/UIRoot.tsx b/packages/mask/src/UIRoot.tsx index bdfc6f4258b1..a7ab2e658145 100644 --- a/packages/mask/src/UIRoot.tsx +++ b/packages/mask/src/UIRoot.tsx @@ -14,6 +14,7 @@ import { MaskThemeProvider } from '@masknet/theme' const identity = (jsx: React.ReactNode) => jsx as JSX.Element function compose(init: React.ReactNode, ...f: ((children: React.ReactNode) => JSX.Element)[]) { + // eslint-disable-next-line unicorn/no-array-reduce return f.reduceRight((prev, curr) => curr(prev), <>{init}) } 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/ProfileTabContent.tsx b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx index 33a19efd8fbd..2d27ba3a6b20 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx @@ -9,7 +9,6 @@ import { ConcealableTabs } from '@masknet/shared' import { EMPTY_LIST, NextIDPlatform } from '@masknet/shared-base' import { makeStyles, useStylesExtends } from '@masknet/theme' import { NetworkPluginID } from '@masknet/web3-shared-base' -import type { ChainId } from '@masknet/web3-shared-evm' import { Box, CircularProgress, Typography } from '@mui/material' import { first } from 'lodash-unified' import { useEffect, useMemo, useState } from 'react' 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/ApplicationBoard.tsx b/packages/mask/src/components/shared/ApplicationBoard.tsx index 5a9e796f3e2e..e6bfc4203480 100644 --- a/packages/mask/src/components/shared/ApplicationBoard.tsx +++ b/packages/mask/src/components/shared/ApplicationBoard.tsx @@ -3,7 +3,7 @@ import { makeStyles, getMaskColor } from '@masknet/theme' import { Typography } from '@mui/material' import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra/content-script' import { useCurrentWeb3NetworkPluginID, useAccount, useChainId } from '@masknet/plugin-infra/web3' -import { EMPTY_LIST, CrossIsolationMessages, formatPersonaPublicKey } from '@masknet/shared-base' +import { formatPersonaPublicKey } from '@masknet/shared-base' import { NetworkPluginID } from '@masknet/web3-shared-base' import { getCurrentSNSNetwork } from '../../social-network-adaptor/utils' import { activatedSocialNetworkUI } from '../../social-network' @@ -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/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 f076c43407a6..6c2fa22c5cb3 100644 --- a/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/ConnectedWallets/index.tsx @@ -11,13 +11,16 @@ import type { ConnectedWalletInfo } from '../type' import { NextIDProof } from '@masknet/web3-providers' import Service from '../../../../service' import { usePopupCustomSnackbar } from '@masknet/theme' -import { isSameAddress } from '@masknet/web3-shared-base' +import { isSameAddress, NetworkPluginID } from '@masknet/web3-shared-base' +import { useLocation, useNavigate } from 'react-router-dom' const ConnectedWallets = memo(() => { const { t } = useI18N() - const chainId = useChainId() + const chainId = useChainId(NetworkPluginID.PLUGIN_EVM) const { NameService } = useWeb3State() const wallets = useWallets() + const navigate = useNavigate() + const location = useLocation() const { proofs, currentPersona, refreshProofs, fetchProofsLoading } = PersonaContext.useContainer() const { showSnackbar } = usePopupCustomSnackbar() @@ -28,7 +31,7 @@ const ConnectedWallets = memo(() => { const results = await Promise.all( proofs.map(async (x, index) => { if (x.platform === NextIDPlatform.Ethereum) { - const domain = await NameService?.reverse?.(x.identity) + const domain = await NameService?.reverse?.(NetworkPluginID.PLUGIN_EVM, x.identity) if (domain) return { @@ -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/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/ActionBar.tsx index bf7b768b6e59..cee4272c97d9 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 7f6f25549d9d..c2d8aa396690 100644 --- a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/Collectible.tsx @@ -1,16 +1,17 @@ import { useState } from 'react' -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 { makeStyles } from '@masknet/theme' import { NetworkPluginID } from '@masknet/web3-shared-base' -import { formatWeiToEther } from '@masknet/web3-shared-evm' import { useChainId } from '@masknet/plugin-infra/web3' import { useI18N } from '../../../utils' import { CollectionView } from './CollectionView' import { DetailsView } from './DetailsView' +import { ChainId, formatWeiToEther } 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 { @@ -55,6 +56,7 @@ const useStyles = makeStyles()((theme) => { interface CollectibleProps { projectId: string + chainId?: ChainId } export function Collectible(props: CollectibleProps) { @@ -131,7 +133,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..68967394799d --- /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 { isSameAddress } from '@masknet/web3-shared-base' +import type { SelectTokenInfo, TokenInfo } from '../types' +import { PersonaPage } from './PersonaPage' +import { DialogContent } from '@mui/material' +import { useI18N } from '../locales/i18n_generated' +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..b56dc0df2e86 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTListDialog.tsx @@ -0,0 +1,281 @@ +import { makeStyles, useCustomSnackbar } from '@masknet/theme' +import type { ChainId, SchemaType } 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 { useAccount, useCurrentWeb3NetworkPluginID } from '@masknet/plugin-infra/web3' +import { NFTWalletConnect } from './WalletConnect' +import { isSameAddress, NetworkPluginID, NonFungibleToken } from '@masknet/web3-shared-base' + +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?: NonFungibleToken, tokenInfo?: TokenInfo) { + if (!token && !tokenInfo) return false + return isSameAddress(token?.address, tokenInfo?.address) && token?.tokenId === tokenInfo?.tokenId +} +interface NFTListDialogProps { + onNext: () => void + tokenInfo?: TokenInfo + wallets?: BindingProof[] + onSelected: (info: SelectTokenInfo) => void +} + +export function NFTListDialog(props: NFTListDialogProps) { + return null + // 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: NonFungibleToken) => { + // setSelectedToken(token) + // } + + // const onSave = useCallback(async () => { + // if (!selectedToken?.metadata?.imageURL) return + // setDisabled(true) + // try { + // const image = await downloadUrl(selectedToken.metadata.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: NonFungibleToken) => { + // 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..5f2f89f8712f --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/NFTListPage.tsx @@ -0,0 +1,115 @@ +import { useImageChecker } from '@masknet/plugin-infra/web3' +import { makeStyles } from '@masknet/theme' +import type { NonFungibleToken } from '@masknet/web3-shared-base' +import { ChainId, createERC721Token, SchemaType } 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: NonFungibleToken[] + onSelect?: (token: NonFungibleToken) => void + children?: React.ReactElement +} + +export function NFTListPage(props: NFTListPageProps) { + const { classes } = useStyles() + const { onSelect, chainId, tokenInfo, tokens, children } = props + const [selectedToken, setSelectedToken] = useState | undefined>( + tokenInfo + ? createERC721Token( + chainId, + tokenInfo.address, + tokenInfo.tokenId, + { + chainId, + name: '', + symbol: '', + }, + {}, + tokenInfo.tokenId, + ) + : undefined, + ) + + const onChange = (token: NonFungibleToken) => { + if (!token) return + setSelectedToken(token) + onSelect?.(token) + } + + return ( + <> + + + {children ?? + tokens.map((token: NonFungibleToken, i) => ( + onChange(token)} + /> + ))} + + + + ) +} + +interface NFTImageCollectibleAvatarProps { + token: NonFungibleToken + onChange: (token: NonFungibleToken) => void + selectedToken?: NonFungibleToken +} + +function NFTImageCollectibleAvatar({ token, onChange, selectedToken }: NFTImageCollectibleAvatarProps) { + const { classes } = useStyles() + const { value: isImageToken, loading } = useImageChecker(token.metadata?.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..888ed61e31cf --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Application/WalletConnect.tsx @@ -0,0 +1,117 @@ +import { WalletMessages } from '@masknet/plugin-wallet' +import { useRemoteControlledDialog } from '@masknet/shared-base-ui' +import { makeStyles, useStylesExtends } from '@masknet/theme' +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 { providerResolver } from '@masknet/web3-shared-evm' +import { NetworkPluginID } from '@masknet/web3-shared-base' +import { useProviderType } 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 t = useI18N() + const providerType = useProviderType(NetworkPluginID.PLUGIN_EVM) + + const { setDialog: openSelectProviderDialog } = useRemoteControlledDialog( + WalletMessages.events.selectProviderDialogUpdated, + ) + return ( + + + + + {t.application_dialog_title()} + + + + {t.provider_by()} + + + Mask Network + + {providerType ? ( + + + + ) : null} + + + + + + + ) +} + +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..61bcf1145c65 --- /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 { 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 { + 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' +import { isSameAddress, NetworkPluginID } from '@masknet/web3-shared-base' +import { explorerResolver } from '@masknet/web3-shared-evm' + +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 }) + 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 { Others } = useWeb3State() + const { isETH, address, verify = false } = props + + const { classes } = useWalletUIStyles() + const chainId = useChainId(NetworkPluginID.PLUGIN_EVM) + const currentPluginId = useCurrentWeb3NetworkPluginID() + const networkDescriptor = useNetworkDescriptor(isETH ? NetworkPluginID.PLUGIN_EVM : currentPluginId, chainId) + const { value: domain } = useReverseAddress(NetworkPluginID.PLUGIN_EVM, address) + 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 4e01f2f92cdf..3fbc987443d4 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx @@ -1,10 +1,9 @@ import { makeStyles } from '@masknet/theme' -import { isSameAddress } from '@masknet/web3-shared-base' -import { ERC721TokenDetailed } from '@masknet/web3-shared-evm' -import { Button, DialogContent, Typography } from '@mui/material' +import { isSameAddress, NonFungibleToken } from '@masknet/web3-shared-base' +import type { ChainId, SchemaType } 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 { useAccount } from '@masknet/plugin-infra/web3' @@ -20,6 +19,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': { @@ -30,18 +36,21 @@ const useStyles = makeStyles()((theme) => ({ }, })) export interface AddNFTProps { + account?: string onClose: () => void - onAddClick: (token: ERC721TokenDetailed) => void + chainId?: ChainId + onAddClick?: (token: NonFungibleToken) => void open: boolean + title?: string } export function AddNFT(props: AddNFTProps) { - const { onClose, open, onAddClick } = props + const { onClose, open, onAddClick, title, chainId, account } = props const { t } = useI18N() const { classes } = useStyles() const [address, setAddress] = useState('') const [tokenId, setTokenId] = useState('') const [message, setMessage] = useState('') - const account = useAccount(NetworkPluginID.PLUGIN_EVM) + const _account = useAccount(NetworkPluginID.PLUGIN_EVM) const onClick = useCallback(async () => { if (!address) { @@ -53,14 +62,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'))) @@ -81,19 +94,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 8a4f046edd03..262ffffe653c 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTImage.tsx @@ -2,11 +2,12 @@ import classNames from 'classnames' import { makeStyles } from '@masknet/theme' import { isSameAddress, NonFungibleToken } from '@masknet/web3-shared-base' import type { ChainId, SchemaType } from '@masknet/web3-shared-evm' +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', @@ -14,10 +15,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, @@ -26,26 +28,27 @@ 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}`, }, })) interface NFTImageProps { - token: NonFungibleToken - selectedToken?: NonFungibleToken - onChange: (token: NonFungibleToken) => void + showBadge?: boolean + token: NonFungibleToken + selectedToken?: NonFungibleToken + onChange: (token: NonFungibleToken) => void } -function isSameNFT(a: NonFungibleToken, b?: NonFungibleToken) { +function isSameNFT(a: NonFungibleToken, b?: NonFungibleToken) { return isSameAddress(a.address, b?.address) && a.chainId === b?.chainId && a.tokenId === b?.tokenId } export function NFTImage(props: NFTImageProps) { - const { token, onChange, selectedToken } = props + const { token, onChange, selectedToken, showBadge = false } = props const { classes } = useStyles() return ( @@ -55,6 +58,7 @@ export function NFTImage(props: NFTImageProps) { src={token.metadata?.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 76d257ab3af6..b8f63413f182 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..0c7620043325 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/hooks/useTokenOwner.ts @@ -0,0 +1,40 @@ +import { useAsyncRetry } from 'react-use' +import { useERC721TokenContract } from '@masknet/plugin-infra/web3-evm' +import { isSameAddress } from '@masknet/web3-shared-base' +import { ChainId, safeNonPayableTransactionCall } from '@masknet/web3-shared-evm' +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(chainId ?? ChainId.Mainnet, address) + 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 { 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..9d7fd8c95bb8 --- /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": "Cannot 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/languages.ts b/packages/mask/src/plugins/Avatar/locales/languages.ts new file mode 100644 index 000000000000..db91c03aa516 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/locales/languages.ts @@ -0,0 +1,19 @@ +// 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' +export const languages = { + en: en_US, +} +// @ts-ignore +if (import.meta.webpackHot) { + // @ts-ignore + import.meta.webpackHot.accept(['./en-US.json'], () => + globalThis.dispatchEvent?.( + new CustomEvent('MASK_I18N_HMR', { + detail: ['com.maskbook.avatar', { en: en_US }], + }), + ), + ) +} 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 f0e2af67a0b3..2be07b976d9c 100644 --- a/packages/mask/src/plugins/Avatar/utils/index.ts +++ b/packages/mask/src/plugins/Avatar/utils/index.ts @@ -1,45 +1,101 @@ -import BigNumber from 'bignumber.js' -import { isNull } from 'lodash-unified' -import { ChainId, NonFungibleAssetProvider, formatBalance, SchemaType } from '@masknet/web3-shared-evm' -// import { EVM_RPC } from '@masknet/plugin-evm/src/messages' +import { first, isNull } from 'lodash-unified' +import { ChainId, NonFungibleAssetProvider, formatBalance, createContract } from '@masknet/web3-shared-evm' +// import { EVM_RPC } from '../../EVM/messages' import Services from '../../../extension/service' -import { getOrderUnitPrice } from '@masknet/web3-providers' -import { NonFungibleTokenEvent, ZERO } from '@masknet/web3-shared-base' +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?: NonFungibleTokenEvent | 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) { - return null - // const asset = await EVM_RPC.getAsset({ - // address, - // tokenId, - // chainId: ChainId.Mainnet, - // provider: NonFungibleAssetProvider.OPENSEA, - // }) - // const amount = - // getOrderUnitPrice( - // asset?.desktopOrder?.current_price, - // asset?.desktopOrder?.payment_token_contract?.decimals ?? 0, - // asset?.desktopOrder?.quantity ?? '1', - // ) ?? - // getLastSalePrice(asset?.last_sale) ?? - // ZERO - // return { - // amount: new BigNumber(amount).toFixed(), - // name: asset?.name ?? '', - // symbol: asset?.desktopOrder?.payment_token_contract?.symbol ?? 'ETH', - // image: asset?.image_url ?? '', - // owner: asset?.top_ownerships[0].owner.address ?? '', - // slug: asset?.slug ?? '', - // } + const asset = await EVM_RPC.getAsset({ + address, + tokenId, + chainId: ChainId.Mainnet, + provider: NonFungibleAssetProvider.OPENSEA, + }) + const amount = + getOrderUnitPrice( + asset?.desktopOrder?.current_price, + asset?.desktopOrder?.payment_token_contract?.decimals ?? 0, + asset?.desktopOrder?.quantity ?? '1', + ) ?? + getLastSalePrice(asset?.last_sale) ?? + ZERO + return { + amount: new BigNumber(amount).toFixed(), + name: asset?.name ?? '', + symbol: asset?.desktopOrder?.payment_token_contract?.symbol ?? 'ETH', + image: asset?.image_url ?? '', + owner: asset?.top_ownerships?.[0]?.owner.address ?? '', + slug: asset?.slug ?? '', + } } -export async function createNFT(address: string, tokenId: string) { - return null - // 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 { @@ -93,3 +149,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..dd7245fd93a8 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx @@ -7,16 +7,22 @@ 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/plugin-infra/web3' +import { NetworkPluginID } from '@masknet/web3-shared-base' 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 +34,7 @@ export function ActionBar(props: ActionBarProps) { const { classes } = useStyles() const { asset, assetOrder } = CollectibleState.useContainer() const assets = asset.value + const chainId = useChainId(NetworkPluginID.PLUGIN_EVM) const { open: openCheckoutDialog, @@ -43,53 +50,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/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 da3a8eaab350..30686a9ff24c 100644 --- a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/Collectible.tsx @@ -17,6 +17,7 @@ import { Markdown } from '../../Snapshot/SNSAdaptor/Markdown' import { ActionBar } from './ActionBar' import { useChainId } from '@masknet/plugin-infra/web3' import { NetworkPluginID } from '@masknet/web3-shared-base' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' const useStyles = makeStyles()((theme) => { return { @@ -78,7 +79,7 @@ export function Collectible(props: CollectibleProps) { const { t } = useI18N() const { classes } = useStyles() const chainId = useChainId(NetworkPluginID.PLUGIN_EVM) - 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 @@ -266,7 +267,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/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/FindTruman.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx index 9ab793fd4f4a..21f220bdc08d 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,11 @@ 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/plugin-infra/web3' +import { NetworkPluginID } from '@masknet/web3-shared-base' const useStyles = makeStyles()((theme) => { return { @@ -137,6 +142,7 @@ export function getPostTypeTitle(t: FindTrumanI18nFunction, postType: PostType) export function FindTruman(props: FindTrumanProps) { const { classes } = useStyles() const { address, t } = useContext(FindTrumanContext) + const chainId = useChainId(NetworkPluginID.PLUGIN_EVM) const { postType, clueId, @@ -182,74 +188,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()} + + ) : ( + + )} + +