diff --git a/cspell.json b/cspell.json index 45a3e25326ad..be7a2a2b16e4 100644 --- a/cspell.json +++ b/cspell.json @@ -161,6 +161,7 @@ "perma", "pids", "plusplus", + "polygonscan", "pooltogether", "popc", "popper", diff --git a/packages/icons/brands/EtherScan.svg b/packages/icons/brands/EtherScan.svg new file mode 100644 index 000000000000..0537689e1b9f --- /dev/null +++ b/packages/icons/brands/EtherScan.svg @@ -0,0 +1 @@ + diff --git a/packages/icons/brands/PolygonScan.svg b/packages/icons/brands/PolygonScan.svg new file mode 100644 index 000000000000..b2b865eca1c4 --- /dev/null +++ b/packages/icons/brands/PolygonScan.svg @@ -0,0 +1 @@ + diff --git a/packages/icons/general/ArrowDrop.svg b/packages/icons/general/ArrowDrop.svg index 3a70b8204d6d..5bf9d7999b78 100644 --- a/packages/icons/general/ArrowDrop.svg +++ b/packages/icons/general/ArrowDrop.svg @@ -2,7 +2,7 @@ diff --git a/packages/icons/general/Connect.svg b/packages/icons/general/Connect.svg new file mode 100644 index 000000000000..eb4616e6121f --- /dev/null +++ b/packages/icons/general/Connect.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/packages/icons/general/DoubleArrowUp.svg b/packages/icons/general/DoubleArrowUp.svg new file mode 100644 index 000000000000..00d9920f1671 --- /dev/null +++ b/packages/icons/general/DoubleArrowUp.svg @@ -0,0 +1 @@ + diff --git a/packages/icons/general/Edit2.svg b/packages/icons/general/Edit2.svg index 360e0bab4476..ab2ebb5fe72d 100644 --- a/packages/icons/general/Edit2.svg +++ b/packages/icons/general/Edit2.svg @@ -1,6 +1,5 @@ - + - + - \ No newline at end of file diff --git a/packages/icons/general/Identity.svg b/packages/icons/general/Identity.svg new file mode 100644 index 000000000000..0f76a7100010 --- /dev/null +++ b/packages/icons/general/Identity.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/icons/general/NextIdPersonaVerified.svg b/packages/icons/general/NextIdPersonaVerified.svg index c354730ed815..5b712d60bbc5 100644 --- a/packages/icons/general/NextIdPersonaVerified.svg +++ b/packages/icons/general/NextIdPersonaVerified.svg @@ -2,7 +2,7 @@ - \ No newline at end of file diff --git a/packages/icons/general/Selected.dark.svg b/packages/icons/general/Selected.dark.svg index a1eda20e6b09..98cb38f5c74c 100644 --- a/packages/icons/general/Selected.dark.svg +++ b/packages/icons/general/Selected.dark.svg @@ -1,7 +1,7 @@ - + - \ No newline at end of file diff --git a/packages/icons/general/Selected.light.svg b/packages/icons/general/Selected.light.svg index 7830ae3984b8..784b42115f8e 100644 --- a/packages/icons/general/Selected.light.svg +++ b/packages/icons/general/Selected.light.svg @@ -1,7 +1,7 @@ - + - + x.persona === persona?.identifier.publicKeyAsHex.toLowerCase()), } - }, [isOwnerIdentity, identity.identifier?.toText()]) + }, [isOwnerIdentity, identity.identifier?.userId]) } diff --git a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx index c8c1e869991f..35fd88205d38 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx @@ -1,28 +1,30 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useUpdateEffect } from 'react-use' -import { first } from 'lodash-unified' +import { first, uniqBy } from 'lodash-unified' import { createInjectHooksRenderer, PluginId, useActivatedPluginsSNSAdaptor, + useIsMinimalMode, usePluginI18NField, } from '@masknet/plugin-infra/content-script' import { useSocialAddressListAll, useAvailablePlugins } from '@masknet/plugin-infra/web3' -import { ConcealableTabs } from '@masknet/shared' +import { AddressItem } from '@masknet/shared' import { CrossIsolationMessages, EMPTY_LIST } from '@masknet/shared-base' -import { makeStyles, useStylesExtends } from '@masknet/theme' -import { Box, CircularProgress } from '@mui/material' +import { makeStyles, MaskTabList, ShadowRootMenu, useStylesExtends, useTabs } from '@masknet/theme' +import { Box, Button, CircularProgress, Link, MenuItem, Tab, Typography } from '@mui/material' import { Icons } from '@masknet/icons' -import { SocialAddressType } from '@masknet/web3-shared-base' +import { isSameAddress, NetworkPluginID, SocialAddress, SocialAddressType } from '@masknet/web3-shared-base' import { activatedSocialNetworkUI } from '../../social-network' import { isTwitter } from '../../social-network-adaptor/twitter.com/base' -import { MaskMessages } from '../../utils' +import { MaskMessages, useI18N } from '../../utils' import { useLocationChange } from '../../utils/hooks/useLocationChange' import { useCurrentVisitingIdentity, useCurrentVisitingSocialIdentity, useIsCurrentVisitingOwnerIdentity, } from '../DataSource/useActivatedUI' +import { TabContext } from '@mui/lab' function getTabContent(tabId?: string) { return createInjectHooksRenderer(useActivatedPluginsSNSAdaptor.visibility.useAnyMode, (x) => { @@ -33,14 +35,106 @@ function getTabContent(tabId?: string) { const useStyles = makeStyles()((theme) => ({ root: {}, + container: { + 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(69, 163, 251, 0.2) 100%), #FFFFFF;', + padding: '16px 16px 0 16px', + }, + title: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '16px', + }, + walletItem: { + display: 'flex', + alignItems: 'center', + fontSize: 18, + fontWeight: 700, + }, + menuItem: { + display: 'flex', + alignItems: 'center', + flexGrow: 1, + justifyContent: 'space-between', + }, + addressMenu: { + maxHeight: 192, + width: 248, + backgroundColor: theme.palette.maskColor.bottom, + }, + addressItem: { + display: 'flex', + alignItems: 'center', + }, + link: { + cursor: 'pointer', + marginTop: 2, + zIndex: 1, + '&:hover': { + textDecoration: 'none', + }, + }, + settingLink: { + cursor: 'pointer', + marginTop: 4, + zIndex: 1, + '&:hover': { + textDecoration: 'none', + }, + }, + linkIcon: { + color: theme.palette.maskColor.second, + fontSize: '20px', + margin: '4px 2px 0 2px', + }, content: { position: 'relative', - padding: theme.spacing(2, 1), }, - settingIcon: { - cursor: 'pointer', - color: theme.palette.maskColor.main, - margin: '0 6px', + walletButton: { + padding: 0, + fontSize: '18px', + minWidth: 0, + background: 'transparent', + '&:hover': { + background: 'none', + }, + }, + settingItem: { + display: 'flex', + alignItems: 'center', + }, + tabs: { + display: 'flex', + position: 'relative', + }, + addressLabel: { + color: theme.palette.maskColor.dark, + fontSize: 18, + fontWeight: 700, + }, + arrowDropIcon: { + color: theme.palette.maskColor.dark, + }, + verifiedIcon: { + color: theme.palette.maskColor.success, + }, + selectedIcon: { + color: theme.palette.maskColor.primary, + }, + gearIcon: { + color: theme.palette.maskColor.dark, + }, + linkOutIcon: { + color: theme.palette.maskColor.secondaryDark, + }, + mainLinkIcon: { + margin: '0px 2px', + color: theme.palette.maskColor.secondaryDark, + }, + secondLinkIcon: { + margin: '4px 2px 0 2px', + color: theme.palette.maskColor.secondaryDark, }, })) @@ -49,25 +143,49 @@ export interface ProfileTabContentProps extends withClasses<'text' | 'button' | export function ProfileTabContent(props: ProfileTabContentProps) { const classes = useStylesExtends(useStyles(), props) + const { t } = useI18N() const translate = usePluginI18NField() const [hidden, setHidden] = useState(true) - const [selectedTab, setSelectedTab] = useState() + const [selectedAddress, setSelectedAddress] = useState | undefined>() + const [anchorEl, setAnchorEl] = useState(null) const isOwnerIdentity = useIsCurrentVisitingOwnerIdentity() const currentVisitingIdentity = useCurrentVisitingIdentity() const currentVisitingUserId = currentVisitingIdentity.identifier?.userId - const { value: socialAddressList = EMPTY_LIST, loading: loadingSocialAddressList } = useSocialAddressListAll( - currentVisitingIdentity, - [SocialAddressType.NEXT_ID], - ) + const { value: currentVisitingSocialIdentity, loading: loadingCurrentVisitingSocialIdentity } = + useCurrentVisitingSocialIdentity() + + const { + value: socialAddressList = EMPTY_LIST, + loading: loadingSocialAddressList, + retry: retrySocialAddress, + } = useSocialAddressListAll(currentVisitingSocialIdentity) + + useEffect(() => { + return MaskMessages.events.ownProofChanged.on(() => { + retrySocialAddress() + }) + }, [retrySocialAddress]) + + useEffect(() => { + const sortedList = socialAddressList.slice(0).sort((a, z) => { + if (a.type === SocialAddressType.NEXT_ID) return -1 + if (z.type === SocialAddressType.NEXT_ID) return 1 + return 0 + }) + setSelectedAddress(first(sortedList)) + }, [socialAddressList]) const activatedPlugins = useActivatedPluginsSNSAdaptor('any') const displayPlugins = useAvailablePlugins(activatedPlugins, (plugins) => { return plugins .flatMap((x) => x.ProfileTabs?.map((y) => ({ ...y, pluginID: x.ID })) ?? EMPTY_LIST) - .filter((x) => x.Utils?.shouldDisplay?.(currentVisitingIdentity, socialAddressList) ?? true) + .filter((x) => { + const shouldDisplay = x.Utils?.shouldDisplay?.(currentVisitingIdentity, selectedAddress) ?? true + return x.pluginID !== PluginId.NextID && shouldDisplay + }) .sort((a, z) => { // order those tabs from next id first if (a.pluginID === PluginId.NextID) return -1 @@ -93,32 +211,32 @@ export function ProfileTabContent(props: ProfileTabContentProps) { label: typeof x.label === 'string' ? x.label : translate(x.pluginID, x.label), })) - const selectedTabId = selectedTab ?? first(tabs)?.id + const [currentTab, onChange] = useTabs(first(tabs)?.id ?? PluginId.Collectible, ...tabs.map((tab) => tab.id)) - const { value: currentVisitingSocialIdentity, loading: loadingCurrentVisitingSocialIdentity } = - useCurrentVisitingSocialIdentity() + const isWeb3ProfileDisable = useIsMinimalMode(PluginId.Web3Profile) const showNextID = !!( isTwitter(activatedSocialNetworkUI) && - isOwnerIdentity && - (socialAddressList.length === 0 || !currentVisitingSocialIdentity?.hasBinding) + (isWeb3ProfileDisable || + (isOwnerIdentity && !currentVisitingSocialIdentity?.hasBinding) || + (isOwnerIdentity && + socialAddressList.findIndex((address) => address.type === SocialAddressType.NEXT_ID)) === -1) ) - const handleOpenDialog = () => { - CrossIsolationMessages.events.requestWeb3ProfileDialog.sendToAll({ - open: true, - }) - } - const Component = getTabContent( - showNextID ? displayPlugins?.find((tab) => tab?.pluginID === PluginId.NextID)?.ID : selectedTabId, - ) - const Utils = displayPlugins.find((x) => x.ID === selectedTabId)?.Utils + + const componentTabId = showNextID ? `${PluginId.NextID}_tabContent` : currentTab + + const component = useMemo(() => { + const Component = getTabContent(componentTabId) + + return + }, [componentTabId, currentVisitingSocialIdentity?.publicKey, selectedAddress]) useLocationChange(() => { - setSelectedTab(undefined) + onChange(undefined, first(tabs)?.id) }) useUpdateEffect(() => { - setSelectedTab(undefined) + onChange(undefined, first(tabs)?.id) }, [currentVisitingUserId]) useEffect(() => { @@ -133,6 +251,15 @@ export function ProfileTabContent(props: ProfileTabContentProps) { }) }, [currentVisitingUserId]) + const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) + const onSelect = (option: SocialAddress) => { + setSelectedAddress(option) + } + const handleOpenDialog = () => { + CrossIsolationMessages.events.requestWeb3ProfileDialog.sendToAll({ + open: true, + }) + } if (hidden) return null if (!currentVisitingUserId || loadingSocialAddressList || loadingCurrentVisitingSocialIdentity) @@ -150,24 +277,112 @@ export function ProfileTabContent(props: ProfileTabContentProps) { return (
- {tabs.length > 0 && !showNextID && ( -
- - tabs={tabs} - selectedId={selectedTabId} - onChange={setSelectedTab} - tail={ - isOwnerIdentity && - } - /> -
- )} -
- Utils?.filter?.(x) ?? true).sort(Utils?.sorter)} - /> +
+ {tabs.length > 0 && !showNextID && ( +
+
+
+ + setAnchorEl(null)}> + {uniqBy(socialAddressList ?? [], (x) => x.address.toLowerCase()).map((x) => { + return ( + onSelect(x)}> +
+
+ + {x?.type === SocialAddressType.NEXT_ID && ( + + )} +
+ {isSameAddress(selectedAddress?.address, x.address) && ( + + )} +
+
+ ) + })} +
+
+
+ theme.palette.maskColor.secondaryDark}> + {t('powered_by')} + + theme.palette.maskColor.dark}> + {t('mask_network')} + + {isOwnerIdentity ? ( + + ) : ( + + + + )} +
+
+
+ + + {tabs.map((tab) => ( + + ))} + + +
+
+ )}
+
{component}
) } diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx index acaf58fd4f7d..382a5e0a80c5 100644 --- a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx @@ -4,13 +4,14 @@ import { NFTCardStyledAssetPlayer } from '@masknet/shared' import { ActionsBarNFT } from '../ActionsBarNFT' import type { NonFungibleToken, SourceType, Wallet } from '@masknet/web3-shared-base' import type { Web3Helper } from '@masknet/plugin-infra/src/entry-web3' +import { resolveOpenSeaLink } from '@masknet/web3-shared-evm' const useStyles = makeStyles()((theme) => ({ root: { display: 'flex', alignItems: 'center', justifyContent: 'center', - borderRadius: 4, + borderRadius: '8px 8px 0 0', position: 'absolute', zIndex: 1, backgroundColor: theme.palette.mode === 'light' ? '#F7F9FA' : '#2F3336', @@ -67,7 +68,11 @@ export function CollectibleCard(props: CollectibleCardProps) { const { classes } = useStyles() return ( - +
{readonly || !wallet ? null : ( @@ -83,6 +88,7 @@ export function CollectibleCard(props: CollectibleCardProps) { loadingFailImage: classes.loadingFailImage, wrapper: classes.wrapper, }} + showNetwork /> diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx index 93e23394b872..bdee51d876af 100644 --- a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx @@ -6,14 +6,12 @@ import { NonFungibleAsset, NonFungibleTokenCollection, SocialAddress, - SocialAddressType, SourceType, Wallet, } from '@masknet/web3-shared-base' import { Box, Button, Stack, styled, Typography } from '@mui/material' import { LoadingBase, makeStyles, useStylesExtends } from '@masknet/theme' -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' -import { ElementAnchor, RetryHint, ReversedAddress } from '@masknet/shared' +import { ElementAnchor, RetryHint } from '@masknet/shared' import { EMPTY_LIST } from '@masknet/shared-base' import type { IdentityResolved } from '@masknet/plugin-infra' import { useNonFungibleAssets, useTrustedNonFungibleTokens, Web3Helper } from '@masknet/plugin-infra/web3' @@ -74,6 +72,7 @@ const useStyles = makeStyles()((theme) => ({ description: { background: theme.palette.mode === 'light' ? '#F7F9FA' : '#2F3336', alignSelf: 'stretch', + borderRadius: '0 0 8px 8px', }, name: { whiteSpace: 'nowrap', @@ -222,12 +221,10 @@ export function CollectibleList(props: CollectibleListProps) { export function CollectionList({ addressName, - onSelectAddress, persona, visitingProfile, }: { addressName: SocialAddress - onSelectAddress: (event: React.MouseEvent) => void persona?: string visitingProfile?: IdentityResolved }) { @@ -291,55 +288,15 @@ export function CollectionList({ if ((done && !allCollectibles.length) || !account) return ( - <> - {addressName && ( - - - - - - )} - - - {t('dashboard_no_collectible_found')} - - - + + + {t('no_NFTs_found')} + + ) return ( - - - - - - - + @@ -404,7 +361,12 @@ export function CollectionList({ key={i} alignItems="center" justifyContent="center" - sx={{ marginTop: '8px', marginBottom: '12px', minWidth: 30, maxHeight: 24 }}> + sx={{ + marginTop: '8px', + marginBottom: '12px', + minWidth: 30, + maxHeight: 24, + }}> ({ +const useStyles = makeStyles()((theme) => ({ container: { background: 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%), linear-gradient(90deg, rgba(98, 126, 234, 0.2) 0%, rgba(59, 153, 252, 0.2) 100%)', @@ -57,6 +57,7 @@ const useStyles = makeStyles()(() => ({ arrow: { fontSize: 20, transition: 'all 300ms', + color: theme.palette.maskColor.secondaryDark, }, })) diff --git a/packages/mask/src/extension/popups/pages/Wallet/components/WalletHeader/UI.tsx b/packages/mask/src/extension/popups/pages/Wallet/components/WalletHeader/UI.tsx index 034c0a584a63..98ded1c11a1b 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/components/WalletHeader/UI.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/components/WalletHeader/UI.tsx @@ -61,6 +61,7 @@ const useStyles = makeStyles()((theme) => ({ arrow: { fontSize: 20, transition: 'all 300ms', + color: theme.palette.maskColor.secondaryDark, }, colorChainICon: { borderRadius: '999px!important', diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx index 2cf2157dc603..193309373a82 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx @@ -1,94 +1,22 @@ -import { useState } from 'react' -import { first, uniqBy } from 'lodash-unified' -import { ReversedAddress } from '@masknet/shared' -import { getMaskColor, makeStyles, ShadowRootMenu } from '@masknet/theme' -import { MenuItem } from '@mui/material' -import { SocialAddress, NetworkPluginID, SocialIdentity, SocialAddressType } from '@masknet/web3-shared-base' +import type { SocialAddress, NetworkPluginID, SocialIdentity } from '@masknet/web3-shared-base' import { CollectionList } from '../../../extension/options-page/DashboardComponents/CollectibleList' -import { EMPTY_LIST } from '@masknet/shared-base' import { useCurrentVisitingProfile } from '../hooks/useContext' -const useStyles = makeStyles()((theme) => ({ - root: { - position: 'relative', - }, - text: { - paddingTop: 36, - paddingBottom: 36, - '& > p': { - color: getMaskColor(theme).textPrimary, - }, - }, - note: { - padding: `0 ${theme.spacing(1)}`, - textAlign: 'right', - }, - icon: { - color: getMaskColor(theme).textPrimary, - }, - iconContainer: { - display: 'inherit', - }, - tipList: { - listStyleType: 'decimal', - paddingLeft: 16, - }, - button: { - border: `1px solid ${theme.palette.text.primary} !important`, - color: `${theme.palette.text.primary} !important`, - borderRadius: 9999, - background: 'transparent', - '&:hover': { - background: 'rgba(15, 20, 25, 0.1)', - }, - }, -})) - export interface NFTPageProps { identity?: SocialIdentity - socialAddressList?: Array> + socialAddress?: SocialAddress } -export function NFTPage({ socialAddressList, identity }: NFTPageProps) { - const { classes } = useStyles() - const [anchorEl, setAnchorEl] = useState(null) - - const [selectedAddress, setSelectedAddress] = useState(first(socialAddressList)) - const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) - const onClose = () => setAnchorEl(null) - const onSelect = (option: SocialAddress) => { - setSelectedAddress(option) - onClose() - } +export function NFTPage({ socialAddress, identity }: NFTPageProps) { const currentVisitingProfile = useCurrentVisitingProfile() - if (!selectedAddress) return null + if (!socialAddress) return null return ( -
- - {uniqBy(socialAddressList ?? EMPTY_LIST, (x) => x.address.toLowerCase()).map((x) => { - return ( - onSelect(x)}> - {x.type === SocialAddressType.ADDRESS || x.type === SocialAddressType.KV ? ( - - ) : ( - x.label - )} - - ) - })} - - -
+ ) } diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx index 247a64086ad7..3275dc9cc289 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx @@ -40,9 +40,6 @@ const sns: Plugin.SNSAdaptor.Definition = { TabContent: NFTPage, }, Utils: { - shouldDisplay: (identity, socialAddressList) => { - return !!socialAddressList?.length - }, sorter: (a, z) => { if (a.type === SocialAddressType.ENS) return -1 if (z.type === SocialAddressType.ENS) return 1 diff --git a/packages/mask/src/plugins/NextID/components/NextIdPage.tsx b/packages/mask/src/plugins/NextID/components/NextIdPage.tsx index 76f310fe6fca..b3a75bf2bbac 100644 --- a/packages/mask/src/plugins/NextID/components/NextIdPage.tsx +++ b/packages/mask/src/plugins/NextID/components/NextIdPage.tsx @@ -1,4 +1,3 @@ -import { Icons } from '@masknet/icons' import { PluginId, useIsMinimalMode } from '@masknet/plugin-infra/content-script' import { useChainId } from '@masknet/plugin-infra/web3' import { NextIDPlatform, PopupRoutes, EMPTY_LIST } from '@masknet/shared-base' @@ -16,6 +15,7 @@ import { useI18N } from '../locales' import { BindDialog } from './BindDialog' import type { LiveSelector } from '@dimensiondev/holoflows-kit' import { searchAllProfileTabSelector } from '../../../social-network-adaptor/twitter.com/utils/selector' +import { Icons } from '@masknet/icons' const useStyles = makeStyles()((theme) => ({ tip: { @@ -60,9 +60,12 @@ const useStyles = makeStyles()((theme) => ({ }, container: { 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: '16px', - padding: '14px', + '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(45, 41, 253, 0.2) 100%), #FFFFFF;', + padding: '14px 14px 16px 14px ', + height: '166px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', }, verifyIntro: { fontSize: '14px', @@ -86,34 +89,45 @@ const useStyles = makeStyles()((theme) => ({ backgroundColor: theme.palette.background.default, height: '196px', }, - walletIcon: { - fontSize: 18, - marginRight: 8, - }, web3Icon: { marginRight: 6, marginTop: 2, }, + walletIcon: { + marginRight: 8, + color: theme.palette.maskColor.white, + }, item1: { - color: '#767f8d', - fontSize: '14', + color: theme.palette.maskColor.secondaryDark, + fontSize: '14px', fontWeight: 400, }, item2: { - color: '#07101B', - fontSize: '14', + color: theme.palette.maskColor.dark, + fontSize: '14px', fontWeight: 500, marginLeft: '2px', }, button: { borderRadius: '99px', - backgroundColor: '#07101b', + backgroundColor: theme.palette.maskColor.dark, color: '#fff', + marginTop: 'auto', ':hover': { color: 'fff', - backgroundColor: '#07101b', + backgroundColor: theme.palette.maskColor.dark, }, }, + content: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '14px', + fontWeight: 400, + }, + linkOutIcon: { + color: theme.palette.maskColor.secondaryDark, + }, })) interface NextIdPageProps { @@ -127,6 +141,7 @@ export const TAB_SELECTOR: { [key: string]: LiveSelector } = export function NextIdPage({ persona }: NextIdPageProps) { const t = useI18N() const { classes } = useStyles() + const currentProfileIdentifier = useLastRecognizedIdentity() const visitingPersonaIdentifier = useCurrentVisitingIdentity() const personaConnectStatus = usePersonaConnectStatus() @@ -140,8 +155,15 @@ export function NextIdPage({ persona }: NextIdPageProps) { const personaActionButton = useMemo(() => { if (!personaConnectStatus.action) return null const button = personaConnectStatus.hasPersona ? t.connect_persona() : t.create_persona() + const icon = personaConnectStatus.hasPersona ? ( + + ) : ( + + ) + return ( ) @@ -163,6 +185,19 @@ export function NextIdPage({ persona }: NextIdPageProps) { ) }, [publicKeyAsHex, visitingPersonaIdentifier, isVerified]) + const description = useMemo(() => { + if (personaConnectStatus.action && !personaConnectStatus.hasPersona) { + return t.create_persona_intro() + } + if (!isOwn) { + return t.others_lack_wallet() + } + if (isAccountVerified) { + return t.add_wallet_intro() + } + return '' + }, [personaConnectStatus, isOwn, isAccountVerified, t]) + const isWeb3ProfileDisable = useIsMinimalMode(PluginId.Web3Profile) const { @@ -187,12 +222,14 @@ export function NextIdPage({ persona }: NextIdPageProps) { const handleAddWallets = () => { Services.Helper.openPopupWindow(PopupRoutes.ConnectedWallets, { - chainId, internal: true, }) } - const getButton = () => { + const getButton = useMemo(() => { + if (!isOwn) { + return + } if (isWeb3ProfileDisable) { return ( ) } return ( ) - } + }, [isWeb3ProfileDisable, personaActionButton, isOwn, isAccountVerified, t]) if (loadingBindings || loadingPersona || loadingVerifyInfo) { return ( @@ -255,18 +292,14 @@ export function NextIdPage({ persona }: NextIdPageProps) { href="https://mask.io/" width="22px" height="22px" - style={{ alignSelf: 'center' }}> - + style={{ alignSelf: 'center', marginLeft: '4px' }}> +
-
-
-
-
-
- - {getButton()} + {description} + + {getButton} {openBindDialog && currentPersona && isOwn && ( diff --git a/packages/mask/src/plugins/NextID/locales/en-US.json b/packages/mask/src/plugins/NextID/locales/en-US.json index 4e20f01487e9..07af9078bb34 100644 --- a/packages/mask/src/plugins/NextID/locales/en-US.json +++ b/packages/mask/src/plugins/NextID/locales/en-US.json @@ -54,5 +54,8 @@ "send_specific_tip_successfully": "Sent {{amount}} {{name}} tip successfully.", "search": "Search", "web3_profile": "Web3 Profile", - "mask_network": "Mask Network" + "mask_network": "Mask Network", + "create_persona_intro": "Please create your persona to use Web3 Profile.", + "add_wallet_intro": "In the Web3 tab, you can show your wallet addresses for NFT collections, donation records, and other on-chain feeds to friends who have also installed the Mask extension.", + "others_lack_wallet": "The user has not set this." } diff --git a/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json b/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json deleted file mode 100644 index e0d8c3ff3ca3..000000000000 --- a/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "day_one": "crwdns18212:0crwdne18212:0", - "day_other": "crwdns18214:0crwdne18214:0", - "hour_one": "crwdns18216:0crwdne18216:0", - "hour_other": "crwdns18218:0crwdne18218:0", - "ago": "crwdns18220:0crwdne18220:0", - "no_data": "crwdns18222:0crwdne18222:0" -} diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index 2b53fde50fe2..c2a1b1137df8 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -597,14 +597,14 @@ export namespace Plugin.SNSAdaptor { */ TabContent: InjectUI<{ identity?: SocialIdentity - socialAddressList?: Array> + socialAddress?: SocialAddress }> } Utils?: { /** * If it returns false, this tab will not be displayed. */ - shouldDisplay?(identity?: SocialIdentity, addressNames?: Array>): boolean + shouldDisplay?(identity?: SocialIdentity, addressName?: SocialAddress): boolean /** * Filter social address. */ diff --git a/packages/plugin-infra/src/web3-state/Identity.ts b/packages/plugin-infra/src/web3-state/Identity.ts index a5fd3a91d89b..a692ae445ed9 100644 --- a/packages/plugin-infra/src/web3-state/Identity.ts +++ b/packages/plugin-infra/src/web3-state/Identity.ts @@ -34,7 +34,9 @@ export class IdentityServiceState implements Web3SocialIdentityState { if (fromCache) return fromCache const fromRemote = this.getFromRemote(identity) - this.cache.set(ID, fromRemote) + if (!identity.isOwner) { + this.cache.set(ID, fromRemote) + } return fromRemote } diff --git a/packages/plugin-infra/src/web3/useSocialAddressListAll.ts b/packages/plugin-infra/src/web3/useSocialAddressListAll.ts index 301d77e1d569..7b6134513b45 100644 --- a/packages/plugin-infra/src/web3/useSocialAddressListAll.ts +++ b/packages/plugin-infra/src/web3/useSocialAddressListAll.ts @@ -26,18 +26,20 @@ export function useSocialAddressListAll( return useAsyncRetry(async () => { const userId = identity?.identifier?.userId - if (!userId || userId === '$unknown') return EMPTY_LIST - + if (!userId || userId === '$unknown' || !identity?.publicKey) return EMPTY_LIST let cached = addressCache.get(userId) + if (!cached) { cached = Promise.allSettled( [EVM_IdentityService, SolanaIdentityService].map((x) => x?.lookup(identity) ?? []), ) - addressCache.set(userId, cached) + if (!identity.isOwner) { + addressCache.set(userId, cached) + } } const allSettled = await cached const listOfAddress = allSettled.flatMap((x) => (x.status === 'fulfilled' ? x.value : [])) const sorted = sorter && listOfAddress.length ? listOfAddress.sort(sorter) : listOfAddress return includes?.length ? sorted.filter((x) => includes.includes(x.type)) : sorted - }, [identity, sorter, includes?.join(), EVM_IdentityService?.lookup, SolanaIdentityService?.lookup]) + }, [identity?.publicKey, sorter, includes?.join(), EVM_IdentityService?.lookup, SolanaIdentityService?.lookup]) } diff --git a/packages/plugins/EVM/src/state/IdentityService.ts b/packages/plugins/EVM/src/state/IdentityService.ts index 3c8589a35bf0..b515720236c1 100644 --- a/packages/plugins/EVM/src/state/IdentityService.ts +++ b/packages/plugins/EVM/src/state/IdentityService.ts @@ -36,17 +36,15 @@ function getNextIDPlatform() { return NextIDPlatform.Twitter } -async function getWalletAddressesFromNextID(userId: string) { - if (!userId) return EMPTY_LIST +async function getWalletAddressesFromNextID(userId?: string, publicKey?: string) { + if (!userId || !publicKey) return EMPTY_LIST const bindings = await NextIDProof.queryAllExistedBindingsByPlatform(getNextIDPlatform(), userId) - for (const binding of bindings) { - const identities = binding.proofs - .filter((x) => x.platform === NextIDPlatform.Ethereum && isValidAddress(x.identity)) - .map((y) => y.identity) - if (identities.length) return identities - } - return EMPTY_LIST + const binding = bindings.find((binding) => binding.persona.toLowerCase() === publicKey.toLowerCase()) + return ( + binding?.proofs.filter((x) => x.platform === NextIDPlatform.Ethereum && isValidAddress(x.identity)) ?? + EMPTY_LIST + ) } export class IdentityService extends IdentityServiceState { @@ -75,10 +73,10 @@ export class IdentityService extends IdentityServiceState { } /** Read a social address from NextID. */ - private async getSocialAddressFromNextID({ identifier }: SocialIdentity) { - const listOfAddress = await getWalletAddressesFromNextID(identifier?.userId ?? '') + private async getSocialAddressFromNextID({ identifier, publicKey }: SocialIdentity) { + const listOfAddress = await getWalletAddressesFromNextID(identifier?.userId, publicKey) return listOfAddress - .map((x) => this.createSocialAddress(SocialAddressType.NEXT_ID, x)) + .map((x) => this.createSocialAddress(SocialAddressType.NEXT_ID, x.identity)) .filter(Boolean) as Array> } diff --git a/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx b/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx index b130d507010a..d4dfb814d09f 100644 --- a/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx +++ b/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx @@ -15,7 +15,7 @@ export enum SecurityMessageLevel { Safe = 'Safe', } -export const Center = memo(({ children }) => ( +export const Center = memo(({ children }: { children: ReactNode }) => ( {children} diff --git a/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/SecurityPanel.tsx b/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/SecurityPanel.tsx index ebf6a5106085..8c2b900d2772 100644 --- a/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/SecurityPanel.tsx +++ b/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/SecurityPanel.tsx @@ -13,6 +13,7 @@ import type { TokenAPI } from '@masknet/web3-providers' import { Icons } from '@masknet/icons' import type { ChainId, SchemaType } from '@masknet/web3-shared-evm' import { formatCurrency, FungibleToken } from '@masknet/web3-shared-base' +import type { SecurityMessage } from '../rules' interface TokenCardProps { tokenSecurity: TokenSecurity @@ -209,7 +210,7 @@ export const SecurityPanel = memo(({ tokenSecurity, tokenInfo, t {makeMessageList.map((x, i) => ( - + ))} {(!makeMessageList.length || securityMessageLevel === SecurityMessageLevel.Safe) && ( ({ - button: { - border: `1px solid ${theme.palette.text.primary} !important`, - color: `${theme.palette.text.primary} !important`, - borderRadius: 4, - background: 'transparent', - '&:hover': { - background: 'rgba(15, 20, 25, 0.1)', - }, - }, -})) +import { FeedPage } from './pages/FeedPage' +import { useMemo } from 'react' export enum TabCardType { Donation = 1, Footprint = 2, + Feed = 3, } export interface TabCardProps { - persona?: string type: TabCardType - socialAddressList?: Array> + socialAddress?: SocialAddress + publicKey?: string } -export function TabCard({ type, socialAddressList, persona }: TabCardProps) { - const t = useI18N() - const { classes } = useStyles() - - const [selectedAddress, setSelectedAddress] = useState(first(socialAddressList)) - +export function TabCard({ type, socialAddress, publicKey }: TabCardProps) { const { value: donations = EMPTY_LIST, loading: loadingDonations } = useDonations( - formatEthereumAddress(selectedAddress?.address ?? ZERO_ADDRESS), + formatEthereumAddress(socialAddress?.address ?? ZERO_ADDRESS), ) const { value: footprints = EMPTY_LIST, loading: loadingFootprints } = useFootprints( - formatEthereumAddress(selectedAddress?.address ?? ZERO_ADDRESS), + formatEthereumAddress(socialAddress?.address ?? ZERO_ADDRESS), ) const currentVisitingProfile = useCurrentVisitingProfile() - const [anchorEl, setAnchorEl] = useState(null) - const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) - const onClose = () => setAnchorEl(null) - const onSelect = (option: SocialAddress) => { - setSelectedAddress(option) - onClose() - } - - const { value: kvValue } = useKV(persona) + const { value: kvValue } = useKV(publicKey) const unHiddenDonations = useCollectionFilter( kvValue?.proofs ?? EMPTY_LIST, - donations, CollectionType.Donations, + donations, currentVisitingProfile, - selectedAddress, + socialAddress, ) const unHiddenFootprints = useCollectionFilter( (kvValue as KVType)?.proofs, - footprints, CollectionType.Footprints, + footprints, currentVisitingProfile, - selectedAddress, + socialAddress, ) - if (!selectedAddress) return null + const page = useMemo(() => { + if (!socialAddress) return null + if (type === TabCardType.Donation) { + return + } + if (type === TabCardType.Footprint) { + return + } + if (type === TabCardType.Feed) { + return + } + return null + }, [type, socialAddress, publicKey, unHiddenDonations, unHiddenFootprints]) - const isDonation = type === TabCardType.Donation + if (!socialAddress) return null - const summary = - isDonation && !loadingDonations ? ( - - {t.total_grants({ - count: donations.length.toString(), - })} - - ) : null - - return ( - <> - -
-
{summary}
-
- - setAnchorEl(null)}> - {uniqBy(socialAddressList ?? [], (x) => x.address.toLowerCase()).map((x) => { - return ( - onSelect(x)}> - {x?.type === SocialAddressType.KV || x?.type === SocialAddressType.ADDRESS ? ( - - ) : ( - x.label - )} - - ) - })} - -
-
- {isDonation ? ( - - ) : ( - - )} - - ) + return page } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx index b012d9304989..6f384b508357 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx @@ -1,17 +1,18 @@ -import { makeStyles, MaskColorVar } from '@masknet/theme' +import { useReverseAddress, useWeb3State } from '@masknet/plugin-infra/web3' +import { makeStyles } from '@masknet/theme' +import type { RSS3BaseAPI } from '@masknet/web3-providers' +import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' import { Typography } from '@mui/material' import classnames from 'classnames' -import { HTMLProps, Fragment } from 'react' +import formatDateTime from 'date-fns/format' +import type { HTMLProps } from 'react' +import { RSS3_DEFAULT_IMAGE } from '../../constants' import { useI18N } from '../../locales' export interface DonationCardProps extends HTMLProps { - imageUrl: string - name: string - contribCount: number - contribDetails: Array<{ - token: string - amount: string - }> + donation: RSS3BaseAPI.Collection + address: SocialAddress + onSelect: () => void } const useStyles = makeStyles()((theme) => ({ @@ -19,85 +20,85 @@ const useStyles = makeStyles()((theme) => ({ borderRadius: 8, display: 'flex', flexDirection: 'row', - backgroundColor: MaskColorVar.twitterBg, - padding: theme.spacing(1), flexGrow: 1, alignItems: 'stretch', + padding: 3, + cursor: 'pointer', }, cover: { flexShrink: 1, - height: 90, - width: 90, + height: 126, + width: 126, borderRadius: 8, objectFit: 'cover', }, - title: { - color: theme.palette.text.primary, - fontSize: 16, + date: { + color: theme.palette.maskColor.main, + fontSize: 14, + fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }, info: { - flexGrow: 1, - marginLeft: theme.spacing(1), + marginTop: 15, + marginLeft: '12px', fontSize: 16, - display: 'flex', - overflow: 'hidden', - flexDirection: 'column', - justifyContent: 'space-around', - fontFamily: '-apple-system,system-ui,sans-serif', }, infoRow: { - whiteSpace: 'nowrap', + marginBottom: 8, overflow: 'hidden', textOverflow: 'ellipsis', }, - infoLabel: { - color: theme.palette.text.primary, + activity: { + fontSize: 14, + fontWeight: 400, + fontColor: theme.palette.maskColor.main, }, - infoValue: { - color: theme.palette.text.secondary, + fontColor: { + color: theme.palette.maskColor.primary, + }, + tokenInfoColor: { + color: theme.palette.maskColor.main, }, })) -export const DonationCard = ({ - imageUrl, - name, - contribCount, - contribDetails, - className, - ...rest -}: DonationCardProps) => { +export const DonationCard = ({ donation, address, onSelect, className, ...rest }: DonationCardProps) => { const { classes } = useStyles() const t = useI18N() + const { value: domain } = useReverseAddress(address.networkSupporterPluginID, address.address) + const { Others } = useWeb3State(address.networkSupporterPluginID) + const reversedAddress = + !domain || !Others?.formatDomainName + ? Others?.formatAddress?.(address.address, 5) ?? address.address + : Others.formatDomainName(domain) + + const date = donation.timestamp ? formatDateTime(new Date(donation.timestamp), 'MMM dd, yyyy') : '--' + return ( -
- {name} -
-
- - {name} +
+ {donation.title +
+
+ + {date} + +
+
+ + {reversedAddress}{' '} + {t.contributed()}{' '} + {donation.tokenAmount?.toString()} + {donation.tokenSymbol ?? 'ETH'}{' '} + {t.to()}{' '} + {donation.title} -
-
- {contribCount} - {t.contribution({ count: contribCount })} -
-
- {contribDetails.map((contrib, i) => ( - - {contrib.amount} - {contrib.token} - - ))} -
-
+
+
) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/FeedCard.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/FeedCard.tsx new file mode 100644 index 000000000000..9e497798b61b --- /dev/null +++ b/packages/plugins/RSS3/src/SNSAdaptor/components/FeedCard.tsx @@ -0,0 +1,252 @@ +import { NFTCardStyledAssetPlayer, ReversedAddress, TokenIcon } from '@masknet/shared' +import { makeStyles } from '@masknet/theme' +import { Alchemy_EVM, RSS3BaseAPI } from '@masknet/web3-providers' +import { isSameAddress, NetworkPluginID } from '@masknet/web3-shared-base' +import { ChainId, formatTokenId, isZeroAddress, resolveIPFSLinkFromURL, ZERO_ADDRESS } from '@masknet/web3-shared-evm' +import { Box, Typography, Card } from '@mui/material' +import { useMemo } from 'react' +import { useI18N } from '../../locales' +import { useAsyncRetry } from 'react-use' +import formatDistanceToNow from 'date-fns/formatDistanceToNow' + +const useStyles = makeStyles()((theme) => ({ + wrapper: { + display: 'flex', + justifyContent: 'space-between', + marginBottom: 16, + cursor: 'pointer', + }, + img: { + width: '64px !important', + height: '64px !important', + borderRadius: '8px', + objectFit: 'cover', + }, + collection: { + borderLeft: `4px solid ${theme.palette.maskColor.line}`, + paddingLeft: 12, + marginTop: 12, + marginLeft: 8, + }, + time: { + color: theme.palette.maskColor.third, + marginLeft: 10, + }, + + summary: { + textOverflow: 'ellipsis', + maxWidth: '400px', + overflow: 'hidden', + whiteSpace: 'nowrap', + color: theme.palette.maskColor.main, + }, + defaultImage: { + background: theme.palette.maskColor.modalTitleBg, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 12, + }, + loadingFailImage: { + minHeight: '0 !important', + maxWidth: 'none', + width: 64, + height: 64, + }, + action: { + color: theme.palette.maskColor.main, + }, +})) + +export const ChainID = { + ethereum: ChainId.Mainnet, + polygon: ChainId.Matic, + bnb: ChainId.BSC, +} + +enum TAG { + NFT = 'NFT', + Token = 'Token', + POAP = 'POAP', + Gitcoin = 'Gitcoin', + Mirror = 'Mirror Entry', + ETH = 'ETH', +} + +export interface FeedCardProps { + feed: RSS3BaseAPI.Web3Feed + address?: string + onSelect: (feed: RSS3BaseAPI.Web3Feed) => void +} + +export function FeedCard({ feed, address, onSelect }: FeedCardProps) { + const { classes } = useStyles() + const t = useI18N() + + const { value: NFTMetadata } = useAsyncRetry(async () => { + if ((feed.title && feed.summary) || !feed.metadata?.collection_address) return + + const res = await Alchemy_EVM.getAsset(feed.metadata?.collection_address, feed.metadata?.token_id ?? '', { + chainId: ChainID[feed.metadata?.network ?? 'ethereum'], + }) + return res + }, [feed.metadata?.collection_address]) + + const action = useMemo(() => { + if (!feed) return + if (feed.tags?.includes(TAG.NFT)) { + if (isSameAddress(feed.metadata?.from, address)) { + return ( + + {t.sent_an_NFT_to()}{' '} + + + ) + } + if (isZeroAddress(feed.metadata?.from)) { + return t.minted_an_NFT() + } + if (isSameAddress(feed.metadata?.to, address)) { + return ( + + {t.acquired_an_NFT_from()}{' '} + + + ) + } + } + if (feed.tags?.includes(TAG.Token) || feed.tags?.includes(TAG.ETH)) { + if (isSameAddress(feed.metadata?.from, address)) { + return ( + + {t.sent_to()}{' '} + + + ) + } + if (isSameAddress(feed.metadata?.to, address)) { + return ( + + {t.received_from()}{' '} + + + ) + } + } + if (feed.tags?.includes(TAG.Gitcoin)) { + if (isSameAddress(feed.metadata?.from, address)) { + return t.donated() + } + if (isSameAddress(feed.metadata?.to, address)) { + return t.received_donation_from() + } + } + if (isSameAddress(feed.metadata?.from, address)) { + return t.received() + } + return t.sent() + }, [address, feed]) + + const logo = useMemo(() => { + if (feed.tags?.includes(TAG.NFT)) { + return ( + + attachment?.type === 'preview')?.address || + '', + )} + tokenId={feed.metadata?.token_id} + classes={{ + loadingFailImage: classes.loadingFailImage, + wrapper: classes.img, + iframe: classes.img, + }} + /> + + ) + } + if (feed.tags.includes(TAG.Token) || feed.tags.includes(TAG.ETH)) { + return ( + + ) + } + if (feed.tags.includes(TAG.Gitcoin)) { + return ( + attachment?.type === 'logo')?.address} + /> + ) + } + return null + }, [feed]) + + return ( + + onSelect({ + ...feed, + title: + feed.title || + NFTMetadata?.metadata?.name || + NFTMetadata?.collection?.name || + NFTMetadata?.contract?.name || + `#${feed.metadata?.token_id}`, + summary: feed.summary || NFTMetadata?.metadata?.description || NFTMetadata?.collection?.description, + imageURL: resolveIPFSLinkFromURL( + NFTMetadata?.metadata?.imageURL || + feed.attachments?.find((attachment) => attachment?.type === 'preview')?.address || + feed.attachments?.find((attachment) => attachment?.type === 'logo')?.address || + '', + ), + traits: NFTMetadata?.traits, + }) + }> +
+ + + {action} + {' '} + + {formatDistanceToNow(new Date(feed.date_updated))} {t.ago()} + + + + + {feed.title || + NFTMetadata?.metadata?.name || + NFTMetadata?.collection?.name || + NFTMetadata?.contract?.name || + ''} + + + {feed.summary || NFTMetadata?.metadata?.description || NFTMetadata?.collection?.description} || + {formatTokenId(feed.metadata?.token_id ?? '0x00')} + + +
+ {logo} +
+ ) +} diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx index 98a6f16245fc..9cdc0f85e06b 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx @@ -1,65 +1,67 @@ import { Typography } from '@mui/material' -import EventRoundedIcon from '@mui/icons-material/EventRounded' -import LocationOnRoundedIcon from '@mui/icons-material/LocationOnRounded' import fromUnixTime from 'date-fns/fromUnixTime' -import { ImageHolder } from './ImageHolder' +import formatDateTime from 'date-fns/format' +import { makeStyles } from '@masknet/theme' import { useI18N } from '../../locales' +import { RSS3_DEFAULT_IMAGE } from '../../constants' +import type { RSS3BaseAPI } from '@masknet/web3-providers' + +const useStyles = makeStyles()((theme) => ({ + card: { + display: 'flex', + padding: 3, + marginBottom: 16, + cursor: 'pointer', + }, + cover: { + flexShrink: 1, + height: 126, + width: 126, + borderRadius: 8, + objectFit: 'cover', + }, + content: { + marginLeft: 12, + marginTop: 15, + }, + infoRow: { + marginBottom: 8, + fontSize: 14, + fontWeight: 400, + color: theme.palette.maskColor.main, + }, +})) const formatDate = (ts: string): string => { return fromUnixTime(Number.parseInt(ts, 16)).toLocaleDateString('en-US') } export interface FootprintProps { - imageUrl: string - startDate: string | undefined - endDate: string | undefined - city: string | undefined - country: string | undefined username: string - activity: string + footprint: RSS3BaseAPI.Collection + onSelect: () => void } -export const FootprintCard = ({ imageUrl, startDate, endDate, city, country, activity }: FootprintProps) => { +export const FootprintCard = ({ footprint, onSelect }: FootprintProps) => { const t = useI18N() - // Calc display date - let displayDate: string - if (startDate && endDate) { - displayDate = formatDate(startDate) - if (endDate !== startDate) { - displayDate += ` ~ ${formatDate(endDate)}` - } - } else { - displayDate = t.no_activity_time() - } + const { classes } = useStyles() - // Calc location - const location = city || country || 'Metaverse' + const date = footprint.timestamp + ? formatDateTime(new Date(footprint.timestamp), 'MMM dd, yyyy') + : t.no_activity_time() return ( -
+
- + {t.inactive_project()}
-
-
- - - {displayDate} - -
-
- - - {location} - -
-
- - {t.attended()} - - - {activity} - -
+
+ {date} + @ {footprint.location} + {footprint.title}
) diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx index ddf44b0daf33..8e634d3011ef 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx @@ -6,6 +6,7 @@ import { useI18N } from '../../locales' interface Props { loading?: boolean empty?: boolean + description?: string } const useStyles = makeStyles()((theme) => ({ @@ -17,7 +18,7 @@ const useStyles = makeStyles()((theme) => ({ }, })) -export const StatusBox: FC = ({ loading, empty }) => { +export const StatusBox: FC = ({ loading, empty, description }) => { const { classes } = useStyles() const t = useI18N() if (loading) { @@ -31,7 +32,7 @@ export const StatusBox: FC = ({ loading, empty }) => { if (empty) { return ( - {t.no_data()} + {description} ) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts index e47b9be71ea2..3d8171ac9492 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts +++ b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts @@ -1,13 +1,14 @@ import { IdentityResolved, PluginId } from '@masknet/plugin-infra' import { NextIDPlatform } from '@masknet/shared-base' +import type { RSS3BaseAPI } from '@masknet/web3-providers' import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' import { useMemo } from 'react' -import type { CollectionType, GeneralAsset, Proof } from '../../types' +import type { CollectionType, Proof } from '../../types' export const useCollectionFilter = ( hiddenInfo: Proof[], - collections: GeneralAsset[], type: CollectionType, + collections?: RSS3BaseAPI.Collection[], currentVisitingProfile?: IdentityResolved, address?: SocialAddress, ) => { diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts index 8f94c6bc52ba..42dadff84ae4 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts +++ b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts @@ -1,12 +1,10 @@ import { useAsync } from 'react-use' import type { AsyncState } from 'react-use/lib/useAsync' -import { EMPTY_LIST } from '@masknet/shared-base' -import { PluginProfileRPC } from '../../messages' -import type { GeneralAsset } from '../../types' +import { RSS3, RSS3BaseAPI } from '@masknet/web3-providers' -export function useDonations(address: string): AsyncState { +export function useDonations(address: string): AsyncState { return useAsync(async () => { - const response = await PluginProfileRPC.getDonations(address) - return response.status ? response.assets : EMPTY_LIST + const response = await RSS3.getDonations(address) + return response }, [address]) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useFootprints.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useFootprints.ts index 587db827ed99..c0e09690d6c1 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useFootprints.ts +++ b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useFootprints.ts @@ -1,11 +1,10 @@ +import { RSS3, RSS3BaseAPI } from '@masknet/web3-providers' import { useAsync } from 'react-use' import type { AsyncState } from 'react-use/lib/useAsync' -import { PluginProfileRPC } from '../../messages' -import type { GeneralAsset } from '../../types' -export function useFootprints(address: string): AsyncState { +export function useFootprints(address: string): AsyncState { return useAsync(async () => { - const response = await PluginProfileRPC.getFootprints(address) - return response.status ? response.assets : [] + const response = await RSS3.getFootprints(address) + return response }, [address]) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useRss3Profile.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useRss3Profile.ts index a5a18a027142..3aefc7de148e 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useRss3Profile.ts +++ b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useRss3Profile.ts @@ -3,7 +3,7 @@ import type { AsyncState } from 'react-use/lib/useAsync' import { PluginProfileRPC } from '../../messages' import type { RSS3Profile } from '../../types' -export function useRss3Profile(address: string): AsyncState { +export function useRSS3Profile(address: string): AsyncState { return useAsync(async () => { if (!address) return null return PluginProfileRPC.getRSS3ProfileByAddress(address) diff --git a/packages/plugins/RSS3/src/SNSAdaptor/index.tsx b/packages/plugins/RSS3/src/SNSAdaptor/index.tsx index a9f5ba15d8b6..3a630bf34441 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/index.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/index.tsx @@ -1,18 +1,12 @@ import type { Plugin } from '@masknet/plugin-infra' -import { NetworkPluginID, SocialAddress, SocialAddressType, SocialIdentity } from '@masknet/web3-shared-base' +import { NetworkPluginID, SocialAddress, SocialIdentity } from '@masknet/web3-shared-base' import { base } from '../base' import { PLUGIN_ID } from '../constants' import { setupContext } from './context' import { TabCard, TabCardType } from './TabCard' -function sorter(a: SocialAddress, z: SocialAddress) { - if (a.type === SocialAddressType.RSS3) return -1 - if (z.type === SocialAddressType.RSS3) return 1 - return 0 -} - -function shouldDisplay(identity?: SocialIdentity, addressNames?: Array>) { - return !!addressNames?.some((x) => x.networkSupporterPluginID === NetworkPluginID.PLUGIN_EVM) +function shouldDisplay(identity?: SocialIdentity, addressName?: SocialAddress) { + return addressName?.networkSupporterPluginID === NetworkPluginID.PLUGIN_EVM } const sns: Plugin.SNSAdaptor.Definition = { @@ -26,18 +20,17 @@ const sns: Plugin.SNSAdaptor.Definition = { label: 'Donations', priority: 1, UI: { - TabContent: ({ socialAddressList = [], identity }) => { + TabContent: ({ socialAddress, identity }) => { return ( ) }, }, Utils: { - sorter, shouldDisplay, }, }, @@ -46,18 +39,36 @@ const sns: Plugin.SNSAdaptor.Definition = { label: 'Footprints', priority: 2, UI: { - TabContent: ({ socialAddressList = [], identity }) => { + TabContent: ({ socialAddress, identity }) => { return ( + ) + }, + }, + Utils: { + shouldDisplay, + }, + }, + { + ID: `${PLUGIN_ID}_feed`, + label: 'Feed', + priority: 3, + UI: { + TabContent: ({ socialAddress, identity }) => { + return ( + ) }, }, Utils: { - sorter, shouldDisplay, }, }, diff --git a/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx b/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx index 4ac47ed9d81e..56f7dc90621a 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx @@ -1,21 +1,14 @@ +import { CollectionDetailCard } from '@masknet/shared' +import { EMPTY_LIST } from '@masknet/shared-base' import { makeStyles } from '@masknet/theme' -import { List, ListItem } from '@mui/material' -import urlcat from 'urlcat' -import { RSS3_DEFAULT_IMAGE } from '../../constants' +import type { RSS3BaseAPI } from '@masknet/web3-providers' +import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' +import { Box, List, ListItem } from '@mui/material' +import { useState } from 'react' +import { CollectionType } from '../../constants' import { useI18N } from '../../locales' -import type { GeneralAsset, GeneralAssetWithTags } from '../../types' import { DonationCard, StatusBox } from '../components' -const getDonationLink = (label: string, donation: GeneralAssetWithTags) => { - const { platform, identity, id, type } = donation - return urlcat(`https://${label}.bio/singlegitcoin/:platform/:identity/:id/:type`, { - platform, - identity, - id, - type: type.replaceAll('-', '.'), - }) -} - const useStyles = makeStyles()((theme) => ({ statusBox: { display: 'flex', @@ -25,7 +18,7 @@ const useStyles = makeStyles()((theme) => ({ }, list: { display: 'grid', - gridTemplateColumns: 'repeat(2, 1fr)', + gridTemplateColumns: 'repeat(1, 1fr)', gridGap: theme.spacing(2), }, listItem: { @@ -48,31 +41,47 @@ const useStyles = makeStyles()((theme) => ({ })) export interface DonationPageProps { - donations?: GeneralAsset[] + donations?: RSS3BaseAPI.Collection[] loading?: boolean - addressLabel: string + address: SocialAddress } -export function DonationPage({ donations = [], loading, addressLabel }: DonationPageProps) { +export function DonationPage({ donations = EMPTY_LIST, loading, address }: DonationPageProps) { const { classes } = useStyles() const t = useI18N() + const [selectedDonation, setSelectedDonation] = useState() + if (loading || !donations.length) { - return + return } return ( - - {donations.map((donation) => ( - - - - ))} - + + + {donations.map((donation) => ( + + setSelectedDonation(donation)} + className={classes.donationCard} + donation={donation} + address={address} + /> + + ))} + + setSelectedDonation(undefined)} + img={selectedDonation?.imageURL} + title={selectedDonation?.title} + referenceURL={selectedDonation?.actions?.[0]?.related_urls?.[0]} + description={selectedDonation?.description} + type={CollectionType.donations} + time={selectedDonation?.timestamp} + tokenSymbol={selectedDonation?.tokenSymbol} + tokenAmount={selectedDonation?.tokenAmount?.toString()} + hash={selectedDonation?.hash} + /> + ) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/pages/FeedPage.tsx b/packages/plugins/RSS3/src/SNSAdaptor/pages/FeedPage.tsx new file mode 100644 index 000000000000..f5351dd4b3e4 --- /dev/null +++ b/packages/plugins/RSS3/src/SNSAdaptor/pages/FeedPage.tsx @@ -0,0 +1,53 @@ +import { CollectionDetailCard } from '@masknet/shared' +import { RSS3, RSS3BaseAPI } from '@masknet/web3-providers' +import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' +import { useState } from 'react' +import { useAsyncRetry } from 'react-use' +import { FeedCard } from '../components/FeedCard' +import { StatusBox } from '../components/StatusBox' +import { useI18N } from '../../locales' +import { CollectionType } from '../../constants' + +export interface FeedPageProps { + socialAddress?: SocialAddress +} + +export function FeedPage({ socialAddress }: FeedPageProps) { + const t = useI18N() + const [selectedFeed, setSelectedFeed] = useState() + const { value: feed, loading } = useAsyncRetry(async () => { + if (!socialAddress?.address) return + return RSS3.getWeb3Feed(socialAddress?.address) + }, [socialAddress]) + + if (!socialAddress) return null + if (loading || !feed?.list?.length) { + return + } + + return ( +
+ {feed?.list?.map((info) => { + return ( + setSelectedFeed(feed)} + feed={info} + address={socialAddress?.address} + /> + ) + })} + setSelectedFeed(undefined)} + img={selectedFeed?.imageURL} + title={selectedFeed?.title} + relatedURLs={selectedFeed?.related_urls} + description={selectedFeed?.summary} + metadata={selectedFeed?.metadata} + traits={selectedFeed?.traits} + type={CollectionType.feeds} + /> +
+ ) +} diff --git a/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx b/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx index a3c70b2b3b0d..bdc603ab3001 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx @@ -1,61 +1,55 @@ -import { makeStyles } from '@masknet/theme' -import urlcat from 'urlcat' -import { RSS3_DEFAULT_IMAGE } from '../../constants' -import type { GeneralAsset, GeneralAssetWithTags } from '../../types' +import { CollectionDetailCard } from '@masknet/shared' +import type { RSS3BaseAPI } from '@masknet/web3-providers' +import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' +import { Box } from '@mui/material' +import { useState } from 'react' import { FootprintCard, StatusBox } from '../components' -import { useRss3Profile } from '../hooks' - -const useStyles = makeStyles()((theme) => ({ - address: { - color: theme.palette.primary.main, - }, - link: { - '&:hover': { - textDecoration: 'none', - }, - }, -})) - -const getFootprintLink = (label: string, footprint: GeneralAssetWithTags) => { - const { platform, identity, id, type } = footprint - return urlcat(`https://${label}.bio/singlefootprint/:platform/:identity/:id/:type`, { - platform, - identity, - id, - type: type.replaceAll('-', '.'), - }) -} +import { useRSS3Profile } from '../hooks' +import { useI18N } from '../../locales' +import { EMPTY_LIST } from '@masknet/shared-base' +import { CollectionType } from '../../constants' export interface FootprintPageProps { - footprints?: GeneralAsset[] + footprints?: RSS3BaseAPI.Collection[] loading?: boolean - addressLabel: string - address?: string + address: SocialAddress } -export function FootprintPage({ footprints = [], address, loading, addressLabel }: FootprintPageProps) { - const { classes } = useStyles() - const { value: profile } = useRss3Profile(address || '') +export function FootprintPage({ footprints = EMPTY_LIST, address, loading }: FootprintPageProps) { + const { value: profile } = useRSS3Profile(address.address || '') const username = profile?.name + const t = useI18N() + + const [selectedFootprint, setSelectedFootprint] = useState() + if (loading || !footprints.length) { - return + return } return ( -
- {footprints.map((footprint) => ( - - ))} -
+ +
+ {footprints.map((footprint) => ( + setSelectedFootprint(footprint)} + username={username ?? ''} + footprint={footprint} + /> + ))} +
+ setSelectedFootprint(undefined)} + img={selectedFootprint?.imageURL} + title={selectedFootprint?.title} + referenceURL={selectedFootprint?.actions?.[0]?.related_urls?.[0]} + description={selectedFootprint?.description} + type={CollectionType.footprints} + time={selectedFootprint?.timestamp} + location={selectedFootprint?.location} + /> +
) } diff --git a/packages/plugins/RSS3/src/constants.ts b/packages/plugins/RSS3/src/constants.ts index d393e73c3ca7..015589d74584 100644 --- a/packages/plugins/RSS3/src/constants.ts +++ b/packages/plugins/RSS3/src/constants.ts @@ -8,3 +8,9 @@ export const PLUGIN_NAME = 'RSS3' /* cspell:disable-next-line */ export const RSS3_DEFAULT_IMAGE = resolveIPFSLink('QmVFq9qimnudPcs6QkQv8ZVEsvwD3aqETHWtS5yXgdbYY5') + +export enum CollectionType { + donations = 'Donations', + footprints = 'Footprints', + feeds = 'Feeds', +} diff --git a/packages/plugins/RSS3/src/locales/en-US.json b/packages/plugins/RSS3/src/locales/en-US.json index 757d0d9437f0..5f9ee37377e9 100644 --- a/packages/plugins/RSS3/src/locales/en-US.json +++ b/packages/plugins/RSS3/src/locales/en-US.json @@ -2,8 +2,24 @@ "inactive_project": "Inactive Project", "no_activity_time": "No activity time", "attended": "attended", - "no_data": "No data", + "no_data": "No {{collection}} found.", + "no_NFT_found": "No NFTs found.", + "no_Feed_found": "No Feeds found.", + "no_Donation_found": "No Donations found.", + "no_Footprint_found": "No Footprints found.", "total_grants": "Total {{count}} Grants", "contribution": "Contribution", - "contribution_other": "Contributions" + "contribution_other": "Contributions", + "contributed": "contributed", + "to": "to", + "ago": "ago", + "sent_an_NFT_to": "sent an NFT to", + "minted_an_NFT": "minted an NFT", + "acquired_an_NFT_from": "acquired an NFT from", + "sent_to": "sent to", + "received_from": "received from", + "donated": "donated", + "received_donation_from": "received donation from", + "received": "received", + "sent": "sent" } diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/components/CollectionList.tsx b/packages/plugins/Web3Profile/src/SNSAdaptor/components/CollectionList.tsx index 0c174f22ff1c..a8bf874f0925 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/components/CollectionList.tsx +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/components/CollectionList.tsx @@ -11,9 +11,10 @@ interface CollectionListProps extends withClasses void size?: number + showNetwork?: boolean } export function CollectionList(props: CollectionListProps) { - const { collections, onList, size = 64 } = props + const { collections, onList, size = 64, showNetwork = false } = props const classes = useStylesExtends(useStyles(), props) return ( @@ -24,13 +25,14 @@ export function CollectionList(props: CollectionListProps) { className={classes.collectionWrap} onClick={() => onList?.(collection.key)}> ({ flexDirection: 'column', '::-webkit-scrollbar': { backgroundColor: 'transparent', - width: 20, + width: 5, }, '::-webkit-scrollbar-thumb': { borderRadius: '20px', @@ -56,7 +56,6 @@ const useStyles = makeStyles()((theme) => ({ }, walletIcon: { marginRight: '8px', - fontSize: 16, }, emptyItem: { marginTop: 'calc(50% - 104px)', @@ -133,7 +132,7 @@ export function ImageManagement(props: ImageManagementProps) { {!hasConnectedWallets && ( diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx index 81a80bc2f8c7..4358e636752d 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx @@ -9,6 +9,8 @@ import { ChainId, explorerResolver, NETWORK_DESCRIPTORS } from '@masknet/web3-sh import { NetworkPluginID } from '@masknet/web3-shared-base' import { Empty } from './Empty' import { CollectionList } from './CollectionList' +import { useMemo, useState } from 'react' +import { EMPTY_LIST } from '@masknet/shared-base' const useStyles = makeStyles()((theme) => { return { @@ -32,10 +34,14 @@ const useStyles = makeStyles()((theme) => { display: 'flex', // overflow: 'hidden', padding: 0, - flexDirection: 'column', + width: 126, + height: 126, borderRadius: 12, userSelect: 'none', lineHeight: 0, + '&:nth-last-child(-n+4)': { + marginBottom: 0, + }, }, link: { cursor: 'pointer', @@ -62,17 +68,38 @@ const useStyles = makeStyles()((theme) => { }, list: { gridRowGap: 16, - gridColumnGap: 14, + gridColumnGap: 20, display: 'grid', justifyItems: 'center', gridTemplateColumns: 'repeat(4, 1fr)', - paddingBottom: '20px', }, listBox: { display: 'flex', flexWrap: 'wrap', - height: 298, - overflow: 'hidden', + minHeight: 298, + justifyContent: 'center', + }, + loadIcon: { + width: 82, + height: 32, + borderRadius: 99, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.palette.maskColor.thirdMain, + fontSize: 12, + fontWeight: 700, + marginTop: 4, + cursor: 'pointer', + }, + arrowUp: { + cursor: 'pointer', + color: theme.palette.maskColor.second, + marginRight: 10, + }, + rightIcons: { + display: 'flex', + alignItems: 'center', }, } }) @@ -84,15 +111,49 @@ export interface WalletAssetsCardProps extends withClasses { collectionList?: CollectionTypes[] } +const enum LOAD_STATUS { + Unnecessary = 1, + Necessary = 2, + Finish = 3, +} + export function WalletAssetsCard(props: WalletAssetsCardProps) { const { address, onSetting, collectionList } = props const t = useI18N() const classes = useStylesExtends(useStyles(), props) const chainId = ChainId.Mainnet + + const [loadStatus, setLoadStatus] = useState( + collectionList && collectionList?.filter((collection) => !collection?.hidden)?.length > 8 + ? LOAD_STATUS.Necessary + : LOAD_STATUS.Unnecessary, + ) + const { Others } = useWeb3State(address?.platform ?? NetworkPluginID.PLUGIN_EVM) const iconURL = NETWORK_DESCRIPTORS.find((network) => network?.chainId === ChainId.Mainnet)?.icon + const collections = useMemo(() => { + const filterCollections = collectionList?.filter((collection) => !collection?.hidden) + if (!filterCollections || filterCollections?.length === 0) { + return EMPTY_LIST + } + if (filterCollections?.length > 8 && loadStatus !== LOAD_STATUS.Finish) { + return filterCollections?.slice(0, 8) + } + return filterCollections + }, [loadStatus, collectionList]) + + const loadIcon = useMemo(() => { + if (loadStatus === LOAD_STATUS.Necessary) + return ( + setLoadStatus(LOAD_STATUS.Finish)} className={classes.loadIcon}> + {t.load_more()} + + ) + return null + }, [loadStatus, setLoadStatus]) + const { value: domain } = useReverseAddress(NetworkPluginID.PLUGIN_EVM, address?.address) return ( @@ -111,7 +172,16 @@ export function WalletAssetsCard(props: WalletAssetsCardProps) {
- +
+ {loadStatus === LOAD_STATUS.Finish && ( + setLoadStatus(LOAD_STATUS.Necessary)} + /> + )} + +
{collectionList && collectionList?.filter((collection) => !collection?.hidden)?.length > 0 ? ( @@ -119,8 +189,10 @@ export function WalletAssetsCard(props: WalletAssetsCardProps) { !collection?.hidden)?.slice(0, 8)} + collections={collections} + showNetwork /> + {loadIcon} ) : ( diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletSetting.tsx b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletSetting.tsx index fc0772282438..d3f9177a9d19 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletSetting.tsx +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletSetting.tsx @@ -102,7 +102,6 @@ const useStyles = makeStyles()((theme) => ({ }, walletIcon: { marginRight: '8px', - fontSize: 16, }, })) @@ -279,7 +278,7 @@ const WalletSetting = memo( ) : ( diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/types.ts b/packages/plugins/Web3Profile/src/SNSAdaptor/types.ts index 80c6e4464e92..7df32ca76cba 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/types.ts +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/types.ts @@ -1,5 +1,6 @@ import type { BindingProof, NextIDPlatform, ProfileInformation } from '@masknet/shared-base' import type { NetworkPluginID } from '@masknet/web3-shared-base' +import type { ChainId } from '@masknet/web3-shared-evm' export interface GeneralAsset { platform: string @@ -53,6 +54,7 @@ export interface CollectionTypes { iconURL?: string hidden?: boolean name?: string + chainId?: ChainId } export interface Collection { diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/utils.ts b/packages/plugins/Web3Profile/src/SNSAdaptor/utils.ts index 955c6f1d9ace..527174c1612c 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/utils.ts +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/utils.ts @@ -192,6 +192,7 @@ export const getNFTList = async (walletList: WalletTypes[]) => { tokenId: asset.tokenId, iconURL: asset?.metadata?.imageURL, name: asset?.metadata?.name, + chainId: ChainId.Mainnet, })), } } else { @@ -218,6 +219,7 @@ export const getNFTList_Polygon = async (walletList: WalletTypes[]) => { tokenId: asset.tokenId, iconURL: asset?.metadata?.imageURL, name: asset?.metadata?.name, + chainId: ChainId.Matic, })), } } else { diff --git a/packages/plugins/Web3Profile/src/locales/en-US.json b/packages/plugins/Web3Profile/src/locales/en-US.json index f2adcfa8b843..4b4e50f1a808 100644 --- a/packages/plugins/Web3Profile/src/locales/en-US.json +++ b/packages/plugins/Web3Profile/src/locales/en-US.json @@ -53,5 +53,6 @@ "wallet_setting_hint": "Toggle the button to manage wallet display settings.", "no_authenticated_wallet": "That hasn't been authenticated yet.", "no_items_found": "No Items found.", - "account_empty": "Please verify this persona to set your Web3 profile." + "account_empty": "Please verify this persona to set your Web3 profile.", + "load_more": "Load More" } diff --git a/packages/shared/src/UI/components/AddressItem/index.tsx b/packages/shared/src/UI/components/AddressItem/index.tsx new file mode 100644 index 000000000000..987dcfe72551 --- /dev/null +++ b/packages/shared/src/UI/components/AddressItem/index.tsx @@ -0,0 +1,63 @@ +import { makeStyles } from '@masknet/theme' +import { Link, Typography, TypographyProps } from '@mui/material' +import type { NetworkPluginID, SocialAddress } from '@masknet/web3-shared-base' +import { ReversedAddress } from '../../..' +import { Icons } from '@masknet/icons' +import { useWeb3State } from '@masknet/plugin-infra/web3' + +const useStyles = makeStyles()((theme) => ({ + link: { + cursor: 'pointer', + marginTop: 2, + zIndex: 1, + '&:hover': { + textDecoration: 'none', + }, + }, + linkIcon: { + color: theme.palette.maskColor.second, + margin: '0px 2px 0 2px', + }, +})) + +export interface AddressItemProps { + identityAddress?: SocialAddress + reverse?: boolean + TypographyProps?: TypographyProps + iconProps?: string +} + +export function AddressItem({ + identityAddress, + reverse = true, + TypographyProps = { fontSize: '14px', fontWeight: 700 }, + iconProps, +}: AddressItemProps) { + const { classes } = useStyles() + const { Others } = useWeb3State(identityAddress?.networkSupporterPluginID) + + if (!identityAddress) return null + + return ( + <> + + {reverse ? ( + + ) : ( + {identityAddress.label} + )} + + + + + + ) +} diff --git a/packages/shared/src/UI/components/AssetPlayer/index.tsx b/packages/shared/src/UI/components/AssetPlayer/index.tsx index 603abf193f77..3a99d77bd1ce 100644 --- a/packages/shared/src/UI/components/AssetPlayer/index.tsx +++ b/packages/shared/src/UI/components/AssetPlayer/index.tsx @@ -8,6 +8,7 @@ import { makeStyles, useStylesExtends } from '@masknet/theme' import { Box } from '@mui/material' import { GeneratedIconProps, Icons } from '@masknet/icons' import type { Web3Helper } from '@masknet/plugin-infra/web3' +import { ImageIcon } from '@masknet/shared' interface ERC721TokenQuery { contractAddress: string @@ -38,12 +39,19 @@ interface AssetPlayerProps fallbackResourceLoader?: JSX.Element setERC721TokenName?: (name: string) => void setSourceType?: (type: string) => void + showNetwork?: boolean + networkIcon?: URL | string } const useStyles = makeStyles()({ hidden: { position: 'absolute', visibility: 'hidden', }, + networkIcon: { + position: 'absolute', + top: 6, + right: 6, + }, }) enum AssetPlayerState { @@ -55,7 +63,7 @@ enum AssetPlayerState { export const AssetPlayer = memo((props) => { const ref = useRef(null) - const { url, type, options, iconProps, isFixedIframeSize = true } = props + const { url, type, options, iconProps, isFixedIframeSize = true, showNetwork = false, networkIcon } = props const classes = useStylesExtends(useStyles(), props) const [hidden, setHidden] = useState(Boolean(props.renderTimeout)) const { RPC_URLS } = getRPCConstants(props.erc721Token?.chainId) @@ -188,7 +196,7 @@ export const AssetPlayer = memo((props) => { ) return ( - <> + ((props) => { : props.loadingIcon ?? } {IframeResizerMemo} - + {showNetwork && } + ) }) diff --git a/packages/shared/src/UI/components/CollectionDetailCard/index.tsx b/packages/shared/src/UI/components/CollectionDetailCard/index.tsx new file mode 100644 index 000000000000..1c0e56fe00c5 --- /dev/null +++ b/packages/shared/src/UI/components/CollectionDetailCard/index.tsx @@ -0,0 +1,281 @@ +import { memo, ReactNode } from 'react' +import { makeStyles } from '@masknet/theme' +import { InjectedDialog } from '../../../contexts' +import { useSharedI18N } from '../../../locales' +import { Box, Card, DialogContent, Link, Typography } from '@mui/material' +import type { RSS3BaseAPI } from '@masknet/web3-providers' +import { Icons } from '@masknet/icons' +import { ChainId, explorerResolver, ZERO_ADDRESS } from '@masknet/web3-shared-evm' +import { NFTCardStyledAssetPlayer } from '@masknet/shared' +import { EMPTY_LIST } from '@masknet/shared-base' +import formatDistanceToNow from 'date-fns/formatDistanceToNow' + +interface CollectionDetailCardProps { + img?: string + open: boolean + title?: string + referenceURL?: string + description?: string + onClose: () => void + date?: string + location?: string + relatedURLs?: string[] + metadata?: RSS3BaseAPI.Metadata + traits?: Array<{ + type: string + value: string + }> + type: CollectionType + time?: string + tokenAmount?: string + tokenSymbol?: string + hash?: string +} +const useStyles = makeStyles()((theme) => ({ + img: { + flexShrink: 1, + height: '300px !important', + width: '300px !important', + borderRadius: 8, + objectFit: 'cover', + }, + loadingFailImage: { + minHeight: '0 !important', + maxWidth: 'none', + width: 300, + height: 300, + }, + flexItem: { + display: 'flex', + width: '100%', + justifyContent: 'center', + }, + dayBox: { + display: 'flex', + alignItems: 'center', + color: theme.palette.maskColor.highlight, + }, + linkBox: { + display: 'flex', + width: 36, + height: 36, + borderRadius: 12, + backgroundColor: theme.palette.maskColor.highlight, + justifyContent: 'center', + alignItems: 'center', + marginLeft: 24, + }, + link: { + color: theme.palette.maskColor.highlight, + }, + txItem: { + display: 'flex', + justifyContent: 'space-between', + }, + donationAmount: { + fontSize: 16, + fontWeight: 400, + display: 'flex', + alignItems: 'center', + }, + threeLine: { + display: '-webkit-box', + '-webkit-line-clamp': '3', + height: 60, + fontSize: 14, + fontWeight: 400, + width: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + '-webkit-box-orient': 'vertical', + }, + themeColor: { + color: theme.palette.maskColor.highlight, + }, + linkLogo: { + width: 24, + height: 24, + }, + icons: { + margin: '16px 0 16px 0', + display: 'flex', + alignItems: 'center', + }, + traitsBox: { + marginTop: 16, + gridRowGap: 16, + gridColumnGap: 20, + display: 'grid', + gridTemplateColumns: 'repeat(3, 170px)', + }, + traitItem: { + backgroundColor: theme.palette.maskColor.bg, + borderRadius: 8, + padding: 12, + }, + traitValue: { + fontSize: 14, + fontWeight: 700, + color: theme.palette.maskColor.main, + }, + secondText: { + fontSize: 14, + fontWeight: 400, + color: theme.palette.maskColor.second, + }, + singleRow: { + maxWidth: 400, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }, + linkOutIcon: { + color: theme.palette.mode === 'light' ? 'white' : 'black', + }, +})) + +const ChainID = { + ethereum: ChainId.Mainnet, + polygon: ChainId.Matic, + bnb: ChainId.BSC, +} + +export enum CollectionType { + donations = 'Donations', + footprints = 'Footprints', + feeds = 'Feeds', +} + +export const CollectionDetailCard = memo( + ({ + img, + open, + onClose, + title, + referenceURL, + metadata, + description, + date, + location, + relatedURLs = EMPTY_LIST, + traits, + type, + time, + tokenAmount, + tokenSymbol, + hash, + }) => { + const t = useSharedI18N() + const { classes } = useStyles() + + const icons = relatedURLs.map((url) => { + let icon: ReactNode = null + if (url.includes('etherscan.io')) { + icon = + } else if (url.includes('polygonscan.com/tx')) { + icon = + } else if (url.includes('polygonscan.com/token')) { + icon = + } else if (url.includes('opensea.io')) { + icon = + } else if (url.includes('gitcoin.co')) { + icon = + } + return icon ? ( + + {icon} + + ) : null + }) + + return ( + + + + + + + + + + {title} + + {icons.length > 0 &&
{icons}
} + + + + {referenceURL} + + + {date && ( + + {date} + + )} + {location && ( + + @ + {location} + + )} + + {t.description()} + +
{description}
+ + {type === CollectionType.donations ? ( + <> + + {t.contributions()} + + + {1} + + + ) : null} + {type === CollectionType.donations && ( +
+ + {tokenAmount} {tokenSymbol} + +
+ {formatDistanceToNow(new Date(time ?? 0))} {t.ago()} + + + +
+
+ )} + {traits && traits.length > 0 && ( + + {t.properties()} + + )} + {traits && traits.length > 0 && ( + + {traits?.map((trait) => ( +
+ {trait.type} + {trait.value} +
+ ))} +
+ )} +
+
+ ) + }, +) diff --git a/packages/shared/src/UI/components/ConcealableTabs/index.tsx b/packages/shared/src/UI/components/ConcealableTabs/index.tsx deleted file mode 100644 index a0791cad0c60..000000000000 --- a/packages/shared/src/UI/components/ConcealableTabs/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { Icons } from '@masknet/icons' -import { makeStyles } from '@masknet/theme' -import { Button } from '@mui/material' -import classnames from 'classnames' -import { throttle } from 'lodash-unified' -import { HTMLProps, ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' - -const TAB_WIDTH = 126 -const useStyles = makeStyles()((theme) => ({ - container: { - display: 'flex', - position: 'relative', - backgroundColor: theme.palette.background.default, - '&::after': { - content: '""', - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - height: 1, - backgroundColor: theme.palette.divider, - zIndex: 0, - }, - }, - track: { - flexGrow: 1, - display: 'flex', - overflow: 'auto', - 'scrollbar-width': 'none', - '&::-webkit-scrollbar': { - display: 'none', - }, - }, - button: { - height: 35, - minWidth: TAB_WIDTH, - padding: theme.spacing(0, 2.5), - borderRadius: 0, - flexShrink: 0, - border: '1px solid transparent', - }, - normal: { - boxSizing: 'border-box', - color: `${theme.palette.text.secondary} !important`, - backgroundColor: theme.palette.background.default, - border: '1px solid transparent', - '&:hover': { - color: `${theme.palette.text.primary} !important`, - backgroundColor: theme.palette.background.default, - }, - }, - selected: { - color: `${theme.palette.text.primary} !important`, - backgroundColor: theme.palette.background.paper, - border: '1px solid', - borderColor: theme.palette.background.paper, - borderBottomColor: theme.palette.background.paper, - '&:hover': { - borderBottomColor: theme.palette.background.paper, - backgroundColor: theme.palette.background.paper, - }, - position: 'relative', - zIndex: 10, - '&::after': { - content: '""', - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - height: 1, - backgroundColor: theme.palette.background.paper, - }, - }, - controllers: { - display: 'flex', - flexGrow: 0, - alignItems: 'center', - borderLeft: `1px solid ${theme.palette.divider}`, - }, - controller: { - display: 'flex', - minWidth: 35, - color: theme.palette.text.primary, - border: 'none', - width: 35, - height: 35, - borderRadius: 0, - boxSizing: 'border-box', - alignItems: 'center', - justifyContent: 'center', - '&:hover': { - border: 'none !important', - borderBottomColor: theme.palette.divider, - color: `${theme.palette.text.primary} !important`, - backgroundColor: theme.palette.background.paper, - }, - '&[disabled]': { - backgroundColor: theme.palette.background.default, - }, - }, -})) - -interface TabOption { - id: T - label: string -} - -export interface ConcealableTabsProps extends Omit, 'onChange'> { - tabs: Array> - selectedId?: T - onChange?(id: T): void - tail?: ReactNode -} - -export function ConcealableTabs({ - className, - tabs, - selectedId, - tail, - onChange, - ...rest -}: ConcealableTabsProps) { - const { classes } = useStyles() - const [overflow, setOverflow] = useState(false) - - const trackRef = useRef(null) - const [reachedLeftEdge, setReachedLeftEdge] = useState(false) - const [reachedRightEdge, setReachedRightEdge] = useState(false) - - useLayoutEffect(() => { - const tabList = trackRef.current - if (!tabList) return - const isWider = tabList.scrollWidth > tabList.offsetWidth - setOverflow(isWider) - - if (!isWider) return - const detectScrollStatus = throttle(() => { - const reachedRight = tabList.scrollWidth - tabList.offsetWidth <= tabList.scrollLeft - const reachedLeft = tabList.scrollLeft === 0 - setReachedRightEdge(reachedRight) - setReachedLeftEdge(reachedLeft) - }, 100) - - detectScrollStatus() - tabList.addEventListener('scroll', detectScrollStatus) - return () => { - tabList.removeEventListener('scroll', detectScrollStatus) - } - }, []) - - useEffect(() => { - if (selectedId === undefined && tabs.length) { - onChange?.(tabs[0].id) - } - }, [selectedId, tabs.map((x) => x.id).join(), onChange]) - - const slide = useCallback((toLeft: boolean) => { - const tabList = trackRef.current - if (!tabList) return - const scrolled = Math.round(tabList.scrollLeft / TAB_WIDTH) - tabList.scrollTo({ left: TAB_WIDTH * (scrolled + (toLeft ? 1 : -1)), behavior: 'smooth' }) - }, []) - - return ( -
-
- {tabs.map((tab) => ( - - ))} -
- {overflow || tail ? ( -
- {overflow ? ( - <> - - - - ) : null} - {tail} -
- ) : null} -
- ) -} diff --git a/packages/shared/src/UI/components/NFTCard/index.tsx b/packages/shared/src/UI/components/NFTCard/index.tsx index a466aa269736..b2c96c6b43e9 100644 --- a/packages/shared/src/UI/components/NFTCard/index.tsx +++ b/packages/shared/src/UI/components/NFTCard/index.tsx @@ -1,8 +1,8 @@ import { Icons } from '@masknet/icons' -import { useImageChecker } from '@masknet/shared' +import { ImageIcon, useImageChecker } from '@masknet/shared' import { makeStyles, ShadowRootTooltip } from '@masknet/theme' import { isSameAddress, NetworkPluginID, NonFungibleToken } from '@masknet/web3-shared-base' -import type { ChainId, SchemaType } from '@masknet/web3-shared-evm' +import { ChainId, NETWORK_DESCRIPTORS, SchemaType } from '@masknet/web3-shared-evm' import { Box, Skeleton } from '@mui/material' import classNames from 'classnames' @@ -81,6 +81,11 @@ const useStyles = makeStyles<{ networkPluginID: NetworkPluginID }>()((theme, pro maskIcon: { fontSize: 30, }, + networkIcon: { + position: 'absolute', + top: 6, + right: 6, + }, })) interface NFTImageCollectibleAvatarProps { @@ -89,6 +94,7 @@ interface NFTImageCollectibleAvatarProps { selectedToken?: NonFungibleToken pluginId: NetworkPluginID size?: number + showNetwork?: boolean } export function NFTImageCollectibleAvatar({ @@ -97,6 +103,7 @@ export function NFTImageCollectibleAvatar({ selectedToken, pluginId, size = 126, + showNetwork = false, }: NFTImageCollectibleAvatarProps) { const { classes } = useStyles({ networkPluginID: pluginId }) const { value: isImageToken, loading } = useImageChecker(token.metadata?.imageURL) @@ -121,6 +128,7 @@ export function NFTImageCollectibleAvatar({ token={token} selectedToken={selectedToken} onChange={onChange} + showNetwork={showNetwork} /> ) : ( @@ -138,6 +146,7 @@ interface NFTImageProps { selectedToken?: NonFungibleToken onChange?: (token: NonFungibleToken) => void size?: number + showNetwork?: boolean } function isSameNFT( @@ -154,8 +163,9 @@ function isSameNFT( } export function NFTImage(props: NFTImageProps) { - const { token, onChange, selectedToken, showBadge = false, pluginId, size = 126 } = props + const { token, onChange, selectedToken, showBadge = false, pluginId, size = 126, showNetwork = false } = props const { classes } = useStyles({ networkPluginID: pluginId }) + const iconURL = NETWORK_DESCRIPTORS.find((network) => network?.chainId === token.chainId)?.icon return ( @@ -169,6 +179,8 @@ export function NFTImage(props: NFTImageProps) { isSameNFT(pluginId, token, selectedToken) ? classes.itemSelected : '', )} /> + {showNetwork && } + {showBadge && isSameNFT(pluginId, token, selectedToken) ? ( ) : null} diff --git a/packages/shared/src/UI/components/NFTCardStyledAssetPlayer/index.tsx b/packages/shared/src/UI/components/NFTCardStyledAssetPlayer/index.tsx index 9982f6ff5e44..28b446cb9078 100644 --- a/packages/shared/src/UI/components/NFTCardStyledAssetPlayer/index.tsx +++ b/packages/shared/src/UI/components/NFTCardStyledAssetPlayer/index.tsx @@ -5,6 +5,8 @@ import { AssetPlayer } from '../AssetPlayer' import { useNonFungibleToken, Web3Helper } from '@masknet/plugin-infra/web3' import { NetworkPluginID } from '@masknet/web3-shared-base' import { useImageChecker } from '../../../hooks' +import { NETWORK_DESCRIPTORS } from '@masknet/web3-shared-evm' +import { ImageIcon } from '../ImageIcon' import { Image } from '../Image' const useStyles = makeStyles()((theme) => ({ @@ -30,6 +32,12 @@ const useStyles = makeStyles()((theme) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', + position: 'relative', + }, + networkIcon: { + position: 'absolute', + top: 6, + right: 6, }, })) @@ -44,6 +52,7 @@ interface Props extends withClasses<'loadingFailImage' | 'iframe' | 'wrapper' | isNative?: boolean setERC721TokenName?: (name: string) => void setSourceType?: (type: string) => void + showNetwork?: boolean } const assetPlayerFallbackImageDark = new URL('./nft_token_fallback_dark.png', import.meta.url) @@ -61,6 +70,7 @@ export function NFTCardStyledAssetPlayer(props: Props) { setERC721TokenName, renderOrder, setSourceType, + showNetwork = false, } = props const classes = useStylesExtends(useStyles(), props) const theme = useTheme() @@ -80,13 +90,17 @@ export function NFTCardStyledAssetPlayer(props: Props) { const fallbackImageURL = theme.palette.mode === 'dark' ? assetPlayerFallbackImageDark : assetPlayerFallbackImageLight + const networkIcon = NETWORK_DESCRIPTORS.find((network) => network?.chainId === chainId)?.icon + return isImageToken || isNative ? (
+ {showNetwork && }
) : ( ) diff --git a/packages/shared/src/UI/components/ReversedAddress/index.tsx b/packages/shared/src/UI/components/ReversedAddress/index.tsx index 5431612c4113..874e55918c24 100644 --- a/packages/shared/src/UI/components/ReversedAddress/index.tsx +++ b/packages/shared/src/UI/components/ReversedAddress/index.tsx @@ -1,19 +1,25 @@ import { memo } from 'react' import type { NetworkPluginID } from '@masknet/web3-shared-base' import { useReverseAddress, useWeb3State } from '@masknet/plugin-infra/web3' +import { Typography, TypographyProps } from '@mui/material' export interface ReverseAddressProps { address: string pluginId?: NetworkPluginID domainSize?: number size?: number + TypographyProps?: TypographyProps + isInline?: boolean } -export const ReversedAddress = memo(({ address, pluginId, domainSize, size = 5 }) => { - const { value: domain } = useReverseAddress(pluginId, address) - const { Others } = useWeb3State(pluginId) +export const ReversedAddress = memo( + ({ address, pluginId, domainSize, size = 5, TypographyProps = { fontSize: '14px', fontWeight: 700 } }) => { + const { value: domain } = useReverseAddress(pluginId, address) + const { Others } = useWeb3State(pluginId) - if (!domain || !Others?.formatDomainName) return <>{Others?.formatAddress?.(address, size) ?? address} + if (!domain || !Others?.formatDomainName) + return {Others?.formatAddress?.(address, size) ?? address} - return <>{Others.formatDomainName(domain, domainSize)} -}) + return {Others?.formatDomainName(domain, domainSize)} + }, +) diff --git a/packages/shared/src/UI/components/TokenSecurity/components/RiskCard.tsx b/packages/shared/src/UI/components/TokenSecurity/components/RiskCard.tsx index 62071b3981d1..0dc0ef48a6a0 100644 --- a/packages/shared/src/UI/components/TokenSecurity/components/RiskCard.tsx +++ b/packages/shared/src/UI/components/TokenSecurity/components/RiskCard.tsx @@ -51,6 +51,7 @@ export const RiskCard = memo(({ info, tokenSecurity }) => { fee: '', percentage: '', distance: '', + count: 0, })} titleColor={DefineMapping[info.level].titleColor} description={t[info.messageKey]({ @@ -61,6 +62,7 @@ export const RiskCard = memo(({ info, tokenSecurity }) => { fee: '', percentage: '', distance: '', + count: 0, })} /> ) diff --git a/packages/shared/src/UI/components/index.ts b/packages/shared/src/UI/components/index.ts index 32a582dc5205..7a343857e2e1 100644 --- a/packages/shared/src/UI/components/index.ts +++ b/packages/shared/src/UI/components/index.ts @@ -2,7 +2,6 @@ export * from './AddressViewer' export * from './ApplicationEntry' export * from './AssetPlayer' export * from './ChainIcon' -export * from './ConcealableTabs' export * from './FungibleTokenList' export * from './I18NextProviderHMR' export * from './ImageIcon' @@ -23,4 +22,6 @@ export * from './Linking' export * from './LoadRetry' export * from './NFTCard' export * from './TokenSecurity' +export * from './CollectionDetailCard' export * from './Image' +export * from './AddressItem' diff --git a/packages/shared/src/locales/en-US.json b/packages/shared/src/locales/en-US.json index dff481cfe823..4f767c757ee6 100644 --- a/packages/shared/src/locales/en-US.json +++ b/packages/shared/src/locales/en-US.json @@ -32,6 +32,8 @@ "load_retry": "Reload", "powered_by": "Powered by", "go_plus": "GO+", + "rss3": "RSS3", + "mask_network": "Mask Network", "high_risk": "High Risk", "low_risk": "Low Risk", "medium_risk": "Medium Risk", @@ -41,7 +43,16 @@ "token_info": "Token info", "more_details": "More Details", "more": "More", + "details": "Details", "unnamed": "Unnamed", + "contributions": "Contributions", + "description": "Description", + "day_one": "day", + "day_other": "days", + "hour_one": "hour", + "hour_other": "hours", + "ago": "ago", + "properties": "Properties", "security_detection": "Security Detection", "risky_items": "{{quantity}} Risky factors", "attention_items": "{{quantity}} Attention factors", diff --git a/packages/web3-providers/src/rss3/constants.ts b/packages/web3-providers/src/rss3/constants.ts index 83c1f8cf33d1..61c79f8ce233 100644 --- a/packages/web3-providers/src/rss3/constants.ts +++ b/packages/web3-providers/src/rss3/constants.ts @@ -1,12 +1,31 @@ +import { NetworkPluginID } from '@masknet/web3-shared-base' + export const RSS3_ENDPOINT = 'https://hub.pass3.me' export const NEW_RSS3_ENDPOINT = 'https://pregod.rss3.dev/v1.1.0/notes/' +export const RSS3_FEED_ENDPOINT = 'https://pregod.rss3.dev/v0.4.0/' + +export const NETWORK_PLUGIN = { + [NetworkPluginID.PLUGIN_EVM]: 'ethereum', + [NetworkPluginID.PLUGIN_FLOW]: 'flow', + [NetworkPluginID.PLUGIN_SOLANA]: 'solana', +} + export const CollectionType = { NFT: /Polygon.NFT|Ethereum.NFT|BSC.NFT/, donation: /Gitcoin.Donation/, footprint: /Mirror.XYZ|xDai.POAP/, } +export enum TAG { + NFT = 'NFT', + Token = 'Token', + POAP = 'POAP', + Gitcoin = 'Gitcoin', + Mirror = 'Mirror Entry', + ETH = 'ETH', +} + export enum NETWORK { ethereum = 'ethereum', ethereum_classic = 'ethereum_classic', diff --git a/packages/web3-providers/src/rss3/index.ts b/packages/web3-providers/src/rss3/index.ts index e2bef17e60ac..af72f145a201 100644 --- a/packages/web3-providers/src/rss3/index.ts +++ b/packages/web3-providers/src/rss3/index.ts @@ -1,10 +1,18 @@ import urlcat from 'urlcat' import RSS3 from 'rss3-next' import { ChainId, SchemaType } from '@masknet/web3-shared-evm' -import { NEW_RSS3_ENDPOINT, RSS3_ENDPOINT, TAG, TYPE } from './constants' +import { NETWORK_PLUGIN, NEW_RSS3_ENDPOINT, RSS3_ENDPOINT, RSS3_FEED_ENDPOINT, TAG, TYPE } from './constants' import { NonFungibleTokenAPI, RSS3BaseAPI } from '../types' import { fetchJSON } from '../helpers' -import { createIndicator, createPageable, HubOptions, TokenType } from '@masknet/web3-shared-base' +import { + createIndicator, + createPageable, + HubOptions, + leftShift, + NetworkPluginID, + TokenType, + ZERO, +} from '@masknet/web3-shared-base' import { first } from 'lodash-unified' export class RSS3API implements RSS3BaseAPI.Provider, NonFungibleTokenAPI.Provider { @@ -106,6 +114,19 @@ export class RSS3API implements RSS3BaseAPI.Provider, NonFungibleTokenAPI.Provid .filter((x) => x.chainId === chainId) return createPageable(data, createIndicator(indicator)) } + + async getWeb3Feed(address: string, type?: RSS3BaseAPI.FeedType, networkPluginId = NetworkPluginID.PLUGIN_EVM) { + if (!address) return + const url = urlcat(RSS3_FEED_ENDPOINT, 'account::address@:network/notes', { + address, + network: NETWORK_PLUGIN[networkPluginId], + limit: 100, + exclude_tags: TAG.POAP, + latest: false, + }) + const res = fetchJSON(url + '&tags=Gitcoin&tags=POAP&tags=NFT&tags=Donation') + return res + } } const getIdFromDonationURL = (url?: string) => { @@ -118,9 +139,20 @@ const createCollection = (collectionResponse: RSS3BaseAPI.CollectionResponse[]): const firstAction = first(collection.actions) return { ...collection, - title: firstAction?.metadata?.title, + title: firstAction?.metadata?.title || firstAction?.metadata?.name, id: firstAction?.metadata?.id ?? getIdFromDonationURL(firstAction?.related_urls?.[0]) ?? collection?.hash, imageURL: firstAction?.metadata?.logo ?? firstAction?.metadata?.image, + description: firstAction?.metadata?.description, + tokenAmount: collection.actions?.reduce((pre, cur) => { + return pre.plus( + leftShift(cur?.metadata?.token?.value || '0', cur?.metadata?.token?.decimals).toFixed(8), + ) + }, ZERO), + tokenSymbol: firstAction?.metadata?.token?.symbol, + location: + firstAction?.metadata?.attributes?.find((trait) => trait.trait_type === 'city')?.value || + firstAction?.metadata?.attributes?.find((trait) => trait.trait_type === 'country')?.value || + 'Metaverse', } }) } diff --git a/packages/web3-providers/src/types/RSS3.ts b/packages/web3-providers/src/types/RSS3.ts index 9862b5d9386a..c2527a3c3155 100644 --- a/packages/web3-providers/src/types/RSS3.ts +++ b/packages/web3-providers/src/types/RSS3.ts @@ -1,3 +1,5 @@ +import type { NetworkPluginID } from '@masknet/web3-shared-base' +import type BigNumber from 'bignumber.js' import type RSS3 from 'rss3-next' export namespace RSS3BaseAPI { @@ -41,7 +43,7 @@ export namespace RSS3BaseAPI { } export interface Token { - name: string + name?: string image?: string value?: string symbol?: string @@ -56,6 +58,12 @@ export namespace RSS3BaseAPI { platform?: PLATFORM description?: string image?: string + attributes?: Array<{ + value: string + trait_type: string + }> + standard?: string + name?: string } export interface Action { @@ -131,20 +139,83 @@ export namespace RSS3BaseAPI { id: string imageURL?: string title?: string + description?: string + actions?: Action[] + tokenAmount?: BigNumber.Value + tokenSymbol?: string + location: string } + export type FeedType = 'Token' | 'Donation' | 'NFT' + export enum AssetType { GitcoinDonation = 'Gitcoin-Donation', POAP = 'POAP', NFT = 'NFT', } + export type Tag = 'NFT' | 'Token' | 'POAP' | 'Gitcoin' | 'Mirror Entry' | 'ETH' + export interface NameInfo { rnsName: string ensName: string | null address: string } + export interface Metadata { + collection_address?: string + collection_name?: string + contract_type?: string + from?: string + log_index?: string + network?: 'polygon' | 'ethereum' | 'bnb' + proof?: string + to?: string + token_id?: string + token_standard?: string + token_symbol?: string + token_address?: string + } + + export interface Attachment { + address?: string + mime_type?: string + size_in_bytes?: string + type?: string + } + + export interface Web3Feed { + attachments?: Attachment[] + authors: string[] + /* cspell:disable-next-line */ + backlinks: string + date_created: string + date_updated: string + identifier: string + links: string + related_urls?: string[] + // this field works different from API doc + source: string + tags: Tag[] + summary?: string + title?: string + metadata?: Metadata + imageURL?: string + traits?: Array<{ + type: string + value: string + }> + } + + export interface Web3FeedResponse { + version: string + date_updated: string + identifier: string + identifier_next?: string + total: string + list: Web3Feed[] + } + export interface Provider { createRSS3(address: string): RSS3 getFileData(rss3: RSS3, address: string, key: string): Promise @@ -153,5 +224,10 @@ export namespace RSS3BaseAPI { getFootprints(address: string): Promise getNameInfo(id: string): Promise getProfileInfo(address: string): Promise + getWeb3Feed( + address: string, + type?: FeedType, + networkPluginId?: NetworkPluginID, + ): Promise } } diff --git a/packages/web3-shared/base/src/specs/index.ts b/packages/web3-shared/base/src/specs/index.ts index d0d681de22a3..fcdb579f242e 100644 --- a/packages/web3-shared/base/src/specs/index.ts +++ b/packages/web3-shared/base/src/specs/index.ts @@ -130,6 +130,8 @@ export interface SocialIdentity { hasBinding?: boolean /** The public key of persona in hex */ publicKey?: string + /** Is own user account identity */ + isOwner?: boolean } export interface SocialAddress {