diff --git a/cspell.json b/cspell.json index 3b35105fa48d..566f31e16a1b 100644 --- a/cspell.json +++ b/cspell.json @@ -243,7 +243,11 @@ "solana", "Görli", "resizer", - "Pageable" + "Pageable", + "metaverse", + "hexlify", + "contribs", + "contrib" ], "ignoreWords": [ "cryptopunks", @@ -346,7 +350,9 @@ "AICANFT", "CANFT", "Kodav", - "koda" + "koda", + "poap", + "irss" ], "ignoreRegExpList": ["/@servie/"], "overrides": [ diff --git a/package.json b/package.json index a988409f237d..9e8b39828041 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "preinstall": "npx only-allow pnpm", "postinstall": "patch-package", "prepare": "husky install", - "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js" + "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", + "spellcheck": "cspell lint --no-must-find-files" }, "lint-staged": { "*": "prettier --write --ignore-unknown", diff --git a/packages/mask/package.json b/packages/mask/package.json index dff57ae3c59b..869f88efcfea 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -54,6 +54,7 @@ "@uniswap/sdk-core": "^3.0.1", "@uniswap/v2-sdk": "3.0.0-alpha.2", "@uniswap/v3-sdk": "^3.3.2", + "@unstoppabledomains/resolution": "^5.0.1", "@walletconnect/client": "^1.2.2", "anchorme": "^2.1.2", "arweave": "1.10.13", diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 1829ed2630cc..9414678a06c1 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -18,7 +18,6 @@ "sign": "Sign", "reload": "Reload", "load": "Load", - "donation": "Donation", "load_all": "Load All", "supported_by": "Supported by", "no_data": "No Data", @@ -1010,5 +1009,15 @@ "plugin_profile_binding_rule3": "Twitter ID + {{suffix}} is ENS.", "plugin_profile_binding_rule4": "Address used to sign the avatar.", "plugin_profile_current_display_of": "Current display of {{type}}: ", + "plugin_profile_loading": "Loading...", + "plugin_profile_error_no_address": "Failed to find any valid address.", + "plugin_profile_address_name_ens": "ENS", + "plugin_profile_address_name_uns": "UNS", + "plugin_profile_address_name_dns": "DNS", + "plugin_profile_address_name_rns": "RNS", + "plugin_profile_address_name_address": "address", + "plugin_profile_rss3_inactive_project": "Inactive Project", + "plugin_profile_rss3_no_activity_time": "No activity time", + "plugin_profile_rss3_attended": "Attended", "plugin_vcent_last_offer_at": "LATEST OFFER at" } diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index d567a580cdb2..5a16b3fd3625 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -18,7 +18,6 @@ "sign": "crwdns10085:0crwdne10085:0", "reload": "crwdns10087:0crwdne10087:0", "load": "crwdns10089:0crwdne10089:0", - "donation": "crwdns10091:0crwdne10091:0", "load_all": "crwdns10093:0crwdne10093:0", "supported_by": "crwdns10095:0crwdne10095:0", "no_data": "crwdns10097:0crwdne10097:0", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index c183b9a9141c..cb5d41212706 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -18,7 +18,6 @@ "sign": "签名", "reload": "重新加载", "load": "加载", - "donation": "捐赠", "load_all": "加载全部", "supported_by": "技术支持:", "no_data": "暂无数据", @@ -888,10 +887,10 @@ "nft_avatar": "NFT 头像", "plugin_profile_no_wallets": "在此连接您的钱包。
此部分将显示给您的加密朋友。
显示内容包括数字艺术、捐赠、徽章
和其他链上的公开信息。", "plugin_profile_binding_rules_title": "Web3 按如下规则展示NFTs:", - "plugin_profile_binding_rule1": "您的Twitter 昵称为ENS。", - "plugin_profile_binding_rule2": "您的Twitter bio 包含 ENS(例如,vitalik.eth) 或有效地址。", - "plugin_profile_binding_rule3": "您的Twitter ID + {{suffix}} 是ENS。", - "plugin_profile_binding_rule4": "用于签署NFT 头像的地址。", - "plugin_profile_current_display_of": "当前显示 {{type}}: ", - "plugin_vcent_last_offer_at": "最新的OFFER为" + "plugin_profile_binding_rule1": "您的 Twitter 昵称为ENS。", + "plugin_profile_binding_rule2": "您的 Twitter bio 包含 ENS(例如,vitalik.eth) 或有效地址。", + "plugin_profile_binding_rule3": "您的 Twitter ID + {{suffix}} 是ENS。", + "plugin_profile_binding_rule4": "用于签署 NFT 头像的地址。", + "plugin_profile_current_display_of": "当前显示 {{type}}:", + "plugin_vcent_last_offer_at": "最新的 Offer 为" } diff --git a/packages/mask/src/database/Persona/types.ts b/packages/mask/src/database/Persona/types.ts index 94011842b08e..216b16cd2edd 100644 --- a/packages/mask/src/database/Persona/types.ts +++ b/packages/mask/src/database/Persona/types.ts @@ -6,6 +6,7 @@ export interface Profile extends Readonly { diff --git a/packages/mask/src/manifest.json b/packages/mask/src/manifest.json index d46e6a243549..f9fc4ff9aef6 100644 --- a/packages/mask/src/manifest.json +++ b/packages/mask/src/manifest.json @@ -10,6 +10,6 @@ "browser_action": { "default_popup": "popups.html" }, "homepage_url": "https://mask.io", "description": "The portal to the new & open Internet. Send encrypted message and decentralized Apps right on top of social networks.", - "web_accessible_resources": ["js/*", "*.png"], + "web_accessible_resources": ["js/*", "*.png", "*.css"], "content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self';" } diff --git a/packages/mask/src/plugins/Avatar/Services/index.ts b/packages/mask/src/plugins/Avatar/Services/index.ts index d5528e006f8f..de7511f874c1 100644 --- a/packages/mask/src/plugins/Avatar/Services/index.ts +++ b/packages/mask/src/plugins/Avatar/Services/index.ts @@ -28,7 +28,7 @@ export async function saveNFTAvatar(address: string, nft: AvatarMetaDB) { export async function getAddress(userId: string) { const address = await getUserAddress(userId) - return address + return (address ?? '') as string } export { getNFTContractVerifiedFromJSON } from './verified' diff --git a/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts b/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts index 9ced59fe96b5..6d0df27138a4 100644 --- a/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts +++ b/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts @@ -3,15 +3,13 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn' import { PluginNFTAvatarRPC } from '../messages' const cache = new Map>() + export function useUserOwnerAddress(userId: string): AsyncState { return useAsync(async () => { if (!userId) return - let f = cache.get(userId) - if (!f) { - f = PluginNFTAvatarRPC.getAddress(userId) - cache.set(userId, f) + if (!cache.has(userId)) { + cache.set(userId, PluginNFTAvatarRPC.getAddress(userId)) } - const address = await f - return address - }, [userId, cache]) + return cache.get(userId) + }, [userId]) } diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx index 6647a61e0e22..ffc81be94d1a 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx @@ -1,8 +1,8 @@ +import { useState } from 'react' import IframeResizer from 'iframe-resizer-react' import { useTheme } from '@mui/material' -import type { Dao_Payload } from './hooks/useDao' -import { useState } from 'react' import type { ProfileIdentifier } from '@masknet/shared-base' +import type { Dao_Payload } from './hooks' interface DAOPageProps { payload: Dao_Payload | undefined diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx index 02f1553eff8d..2393ada10c2e 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx @@ -1,11 +1,68 @@ -import { Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { CircularProgress, Link } from '@mui/material' +import urlcat from 'urlcat' +import type { GeneralAssetWithTags } from '../types' +import { RSS3_DEFAULT_IMAGE } from '../constants' +import { DonationCard } from './components' +import { useDonations } from './hooks' import { useI18N } from '../../../utils' -interface DonationPageProps {} + +const getDonationLink = (address: string, donation: GeneralAssetWithTags) => { + const { platform, identity, id, type } = donation + return urlcat('https://rss3.bio/:address/singlegitcoin/:platform/:identity/:id/:type', { + address, + platform, + identity, + id, + type, + }) +} + +const useStyles = makeStyles()((theme) => ({ + address: { + color: theme.palette.primary.main, + }, + link: { + '&:hover': { + textDecoration: 'none', + }, + }, +})) + +export interface DonationPageProps { + address: string +} + export function DonationPage(props: DonationPageProps) { + const { address } = props + const { classes } = useStyles() + const { donations, loading } = useDonations(address) const { t } = useI18N() + + if (loading) { + return ( +
+ +
+ ) + } return ( - - {t('donation')} - +
+ {donations.map((donation) => ( + + + + ))} +
) } diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx index 9e96f2171a51..8af942239e14 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx @@ -1,18 +1,21 @@ import { useEffect, useMemo, useState } from 'react' +import { useUpdateEffect } from 'react-use' +import { Box, Typography } from '@mui/material' +import { unreachable } from '@dimensiondev/kit' import { makeStyles, useStylesExtends } from '@masknet/theme' - -import { MaskMessages } from '../../../utils' +import { AddressNameType, useAddressNames } from '@masknet/web3-shared-evm' +import { MaskMessages, useI18N } from '../../../utils' import { useLocationChange } from '../../../utils/hooks/useLocationChange' -import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' import { WalletsPage } from './WalletsPage' import { NFTPage } from './NFTPage' import { DonationPage } from './DonationsPage' +import { FootprintPage } from './FootprintPage' import { DAOPage } from './DAOPage' import { PageTags } from '../types' -import { unreachable } from '@dimensiondev/kit' import { PageTag } from './PageTag' -import { useDao } from './hooks/useDao' -import { useUpdateEffect } from 'react-use' +import { AddressViewer } from './components/AddressViewer' +import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' +import { useDao } from './hooks' const useStyles = makeStyles()((theme) => ({ root: { @@ -25,55 +28,124 @@ const useStyles = makeStyles()((theme) => ({ tags: { padding: theme.spacing(2), }, + metadata: {}, content: { position: 'relative', padding: theme.spacing(1), }, })) -interface EnhancedProfilePageProps extends withClasses<'text' | 'button'> {} +export interface EnhancedProfilePageProps extends withClasses<'text' | 'button'> {} export function EnhancedProfilePage(props: EnhancedProfilePageProps) { - const [show, setShow] = useState(false) + const { t } = useI18N() const classes = useStylesExtends(useStyles(), props) + + //#region identity const identity = useCurrentVisitingIdentity() + const { value: addressNames, loading: loadingAddressNames } = useAddressNames(identity) + //#endregion + + const [hidden, setHidden] = useState(true) const { value: daoPayload } = useDao(identity.identifier) const [currentTag, setCurrentTag] = useState(PageTags.NFTTag) + useLocationChange(() => { + setCurrentTag(PageTags.NFTTag) MaskMessages.events.profileNFTsTabUpdated.sendToLocal('reset') }) useEffect(() => { return MaskMessages.events.profileNFTsPageUpdated.on((data) => { - setShow(data.show) + setHidden(!data.show) }) - }, []) + }, [identity]) + + useUpdateEffect(() => { + setCurrentTag(PageTags.NFTTag) + }, [identity.identifier]) + + const addressName = useMemo(() => { + const getAddressByType = (type: AddressNameType) => addressNames?.find((x) => x.type === type) + + const addressLiteral = getAddressByType(AddressNameType.ADDRESS) + const addressENS = getAddressByType(AddressNameType.ENS) + const addressUNS = getAddressByType(AddressNameType.UNS) + const addressGUN = getAddressByType(AddressNameType.GUN) + const addressRSS3 = getAddressByType(AddressNameType.RSS3) + const addressTheGraph = getAddressByType(AddressNameType.THE_GRAPH) + + switch (currentTag) { + case PageTags.WalletTag: + return + case PageTags.NFTTag: + return addressENS || addressUNS || addressRSS3 || addressLiteral || addressGUN || addressTheGraph + case PageTags.DonationTag: + return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + case PageTags.FootprintTag: + return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + case PageTags.DAOTag: + return + default: + unreachable(currentTag) + } + }, [currentTag, addressNames]) + + const address = addressName?.resolvedAddress ?? '' const content = useMemo(() => { switch (currentTag) { case PageTags.WalletTag: return case PageTags.NFTTag: - return + return case PageTags.DonationTag: - return + return + case PageTags.FootprintTag: + return case PageTags.DAOTag: return default: unreachable(currentTag) } - }, [currentTag, daoPayload, identity.identifier]) + }, [address, currentTag, daoPayload, identity.identifier]) - useUpdateEffect(() => { - setCurrentTag(PageTags.NFTTag) - }, [identity.identifier]) + if (hidden) return null - if (!show) return null + if (loadingAddressNames) + return ( +
+ + {t('plugin_profile_loading')} + +
+ ) + + if (!addressName) + return ( +
+ + {t('plugin_profile_error_no_address')} + +
+ ) return (
+
- + +
+
+
{content}
diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx index b583f310388f..c366dca0c389 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx @@ -1,6 +1,6 @@ -import { Typography } from '@mui/material' -import classnames from 'classnames' import { ReactElement, useCallback, useEffect, useState } from 'react' +import classnames from 'classnames' +import { Typography } from '@mui/material' import { MaskMessages } from '../../../utils' export interface EnhancedProfileTabProps extends withClasses<'tab' | 'button' | 'selected'> { diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx new file mode 100644 index 000000000000..21cf9d77e585 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx @@ -0,0 +1,73 @@ +import urlcat from 'urlcat' +import { makeStyles } from '@masknet/theme' +import { CircularProgress, Link } from '@mui/material' +import type { GeneralAssetWithTags } from '../types' +import { RSS3_DEFAULT_IMAGE } from '../constants' +import { FootprintCard } from './components' +import { useFootprints, useRss3Profile } from './hooks' + +const useStyles = makeStyles()((theme) => ({ + address: { + color: theme.palette.primary.main, + }, + link: { + '&:hover': { + textDecoration: 'none', + }, + }, +})) + +const getFootprintLink = (address: string, footprint: GeneralAssetWithTags) => { + const { platform, identity, id, type } = footprint + return urlcat('https://rss3.bio/:address/singlefootprint/:platform/:identity/:id/:type', { + address, + platform, + identity, + id, + type, + }) +} + +export interface FootprintPageProps { + address: string +} + +export function FootprintPage(props: FootprintPageProps) { + const { address } = props + const { classes } = useStyles() + const { value: profile } = useRss3Profile(address) + const username = profile?.name + + const { footprints, loading } = useFootprints(address) + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+ {footprints.map((footprint) => ( + + + + ))} +
+ ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx index 65cd490a5cb5..03bf9848f2ae 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx @@ -1,26 +1,12 @@ import { getMaskColor, makeStyles, useStylesExtends } from '@masknet/theme' -import { - formatEthereumAddress, - resolveAddressLinkOnExplorer, - ChainId, - EthereumNameType, - useEthereumAddress, -} from '@masknet/web3-shared-evm' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import { Box, Link, Typography, CircularProgress, Tooltip } from '@mui/material' -import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' +import { Box, Typography } from '@mui/material' import { CollectionList } from '../../../extension/options-page/DashboardComponents/CollectibleList' import { useI18N } from '../../../utils' -import { useUserOwnerAddress } from '../../Avatar/hooks/useUserOwnerAddress' const useStyles = makeStyles()((theme) => ({ root: { position: 'relative', }, - note: { - padding: `0 ${theme.spacing(1)}`, - textAlign: 'right', - }, text: { paddingTop: 36, paddingBottom: 36, @@ -28,51 +14,18 @@ const useStyles = makeStyles()((theme) => ({ color: getMaskColor(theme).textPrimary, }, }, - icon: { - color: getMaskColor(theme).textPrimary, - }, - iconContainer: { - display: 'inherit', - }, - tipList: { - listStyleType: 'decimal', - paddingLeft: 16, - }, })) -interface NFTPageProps extends withClasses<'text' | 'button'> {} +export interface NFTPageProps extends withClasses<'text' | 'button'> { + address: string +} export function NFTPage(props: NFTPageProps) { - const classes = useStylesExtends(useStyles(), props) + const { address } = props const { t } = useI18N() - const identity = useCurrentVisitingIdentity() - const { loading: loadingENS, value } = useEthereumAddress( - identity.nickname ?? '', - identity.identifier.userId, - identity.bio ?? '', - ) - const { type, name, address } = value ?? {} - const { loading: loadingWalletGun, value: walletAddressGun } = useUserOwnerAddress(identity.identifier.userId) - - const rulesTipMap = [ - t('plugin_profile_binding_rule1'), - t('plugin_profile_binding_rule2'), - t('plugin_profile_binding_rule3', { suffix: `".eth"` }), - t('plugin_profile_binding_rule4'), - ] - - const tooltipRender = ( -
- {t('plugin_profile_binding_rules_title')} -
    - {rulesTipMap.map((item, index) => { - return
  • {item}
  • - })} -
-
- ) + const classes = useStylesExtends(useStyles(), props) - if (!address && !walletAddressGun) + if (!address) return (
@@ -80,58 +33,10 @@ export function NFTPage(props: NFTPageProps) {
) + return (
- {loadingWalletGun || loadingENS ? ( - - - - ) : ( - <> - - - - {t('plugin_profile_current_display_of', { type: type })} - - {type === EthereumNameType.DEFAULT - ? formatEthereumAddress( - (address?.length === 0 ? walletAddressGun : address) ?? '', - 4, - ) - : name} - - -
- - - -
-
-
- - - )} +
) } diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx index 17ee66423075..dcf77987e852 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx @@ -1,16 +1,18 @@ +import classNames from 'classnames' import { makeStyles } from '@masknet/theme' import { Button } from '@mui/material' -import classNames from 'classnames' import { useI18N } from '../../../utils' import { PageTags } from '../types' -import type { Dao_Payload } from './hooks/useDao' +import { useDonations, useFootprints, Dao_Payload } from './hooks' const useStyles = makeStyles()((theme) => ({ root: { - '>:nth-child(2)': { - marginLeft: theme.spacing(2), - marginRight: theme.spacing(2), - }, + display: 'flex', + gap: '8px', + flexWrap: 'wrap', + }, + hidden: { + display: 'none', }, button: { border: `1px solid ${theme.palette.text.primary} !important`, @@ -22,21 +24,41 @@ const useStyles = makeStyles()((theme) => ({ color: theme.palette.primary.main, borderRadius: 9999, }, - hidden: { - display: 'none', + connectRSS3: { + position: 'absolute', + right: '16px', + boxSizing: 'border-box', + height: 36, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 9999, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + border: '0px solid', + backgroundColor: theme.palette.primary.main, + color: 'rgb(255, 255, 255)', + cursor: 'pointer', }, })) -interface PageTagProps { +export interface PageTagProps { tag: PageTags + address: string daoPayload: Dao_Payload | undefined onChange: (tag: PageTags) => void } export function PageTag(props: PageTagProps) { const { classes } = useStyles() - const { onChange, tag, daoPayload } = props + const { onChange, tag, address, daoPayload } = props const { t } = useI18N() + + const { donations } = useDonations(address) + const { footprints } = useFootprints(address) + const hasDonations = donations.length > 0 + const hasFootprints = footprints.length > 0 + return (
- + {hasDonations ? ( + + ) : null} + {hasFootprints ? ( + + ) : null} {daoPayload ? (