diff --git a/.i18n-codegen.json b/.i18n-codegen.json index d97c47ff540f..e3882327b8bb 100644 --- a/.i18n-codegen.json +++ b/.i18n-codegen.json @@ -359,6 +359,17 @@ "trans": "Translate", "sourceMap": "inline" } + }, + { + "input": "./packages/mask/src/plugins/Web3Feed/locales/en-US.json", + "output": "./packages/mask/src/plugins/Web3Feed/locales/i18n_generated", + "parser": { "type": "i18next", "contextSeparator": "$", "pluralSeparator": "_" }, + "generator": { + "type": "i18next/react-hooks", + "hooks": "useI18N", + "namespace": "com.maskbook.web3-feed", + "trans": "Translate" + } } ] } diff --git a/cspell.json b/cspell.json index 2efeb3f8b3a7..d2dd760ab577 100644 --- a/cspell.json +++ b/cspell.json @@ -160,6 +160,7 @@ "perma", "pids", "plusplus", + "polygonscan", "pooltogether", "popc", "popper", diff --git a/packages/dashboard/src/pages/Personas/components/PersonaLine/index.tsx b/packages/dashboard/src/pages/Personas/components/PersonaLine/index.tsx index 3a5f9819252c..d75e63af3baa 100644 --- a/packages/dashboard/src/pages/Personas/components/PersonaLine/index.tsx +++ b/packages/dashboard/src/pages/Personas/components/PersonaLine/index.tsx @@ -13,7 +13,7 @@ import { } from '@masknet/shared-base' import { LoadingAnimation, SOCIAL_MEDIA_ICON_MAPPING } from '@masknet/shared' import { PersonaContext } from '../../hooks/usePersonaContext' -import { NextIdPersonaWarningIcon, NextIdPersonaVerifiedIcon } from '@masknet/icons' +import { NextIdPersonaWarning, NextIdPersonaVerified } from '@masknet/icons' const useStyles = makeStyles()((theme) => ({ icon: { @@ -143,9 +143,9 @@ export const ConnectedPersonaLine = memo( {proof.loading ? ( ) : isProved?.is_valid ? ( - + ) : ( - + )} )} 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/RSS3.svg b/packages/icons/general/RSS3.svg new file mode 100644 index 000000000000..0a526f9f859c --- /dev/null +++ b/packages/icons/general/RSS3.svg @@ -0,0 +1,6 @@ + + + + + + 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 @@ - + + + + + , + + + + + , + undefined, + '0 0 16 16', +) diff --git a/packages/icons/plugins/WalletUnderTabs.svg b/packages/icons/plugins/WalletUnderTabs.svg index 01949d149918..adfe652e2ebe 100644 --- a/packages/icons/plugins/WalletUnderTabs.svg +++ b/packages/icons/plugins/WalletUnderTabs.svg @@ -1,21 +1,25 @@ ({ 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', + }, + addressItem: { + display: 'flex', + alignItems: 'center', + }, + link: { + cursor: 'pointer', + marginTop: 2, + 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', }, })) @@ -51,11 +103,12 @@ export interface ProfileTabContentProps extends withClasses<'text' | 'button' | export function ProfileTabContent(props: ProfileTabContentProps) { const classes = useStylesExtends(useStyles(), props) - const { t } = useI18N() + const t = useSharedI18N() const translate = usePluginI18NField() const [hidden, setHidden] = useState(true) - const [selectedTab, setSelectedTab] = useState() + const [selectedAddress, setSelectedAddress] = useState | undefined>() + const [anchorEl, setAnchorEl] = useState(null) const currentIdentity = useLastRecognizedIdentity() const identity = useCurrentVisitingIdentity() @@ -100,6 +153,7 @@ export function ProfileTabContent(props: ProfileTabContentProps) { const addressList = useMemo(() => { if (!wallets?.length || (!isOwn && socialAddressList?.length)) { + setSelectedAddress(first(socialAddressList)) return socialAddressList } const addresses = wallets.map((proof) => { @@ -110,7 +164,9 @@ export function ProfileTabContent(props: ProfileTabContentProps) { address: proof?.identity, } }) - return [...socialAddressList, ...addresses] + const addressList = [...addresses, ...socialAddressList] + setSelectedAddress(first(addressList)) + return addressList }, [socialAddressList, wallets?.map((x) => x.identity).join(), isOwn]) const activatedPlugins = useActivatedPluginsSNSAdaptor('any') @@ -119,8 +175,8 @@ export function ProfileTabContent(props: ProfileTabContentProps) { const displayPlugins = useMemo(() => { return availablePlugins .flatMap((x) => x.ProfileTabs?.map((y) => ({ ...y, pluginID: x.ID })) ?? EMPTY_LIST) - .filter((z) => z.Utils?.shouldDisplay?.(identity, addressList) ?? true) - }, [identity, availablePlugins.map((x) => x.ID).join(), addressList.map((x) => x.address).join()]) + .filter((z) => z.Utils?.shouldDisplay?.(identity, selectedAddress) ?? true) + }, [identity, availablePlugins.map((x) => x.ID).join(), selectedAddress]) const tabs = displayPlugins .sort((a, z) => { @@ -148,44 +204,31 @@ 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.NextID, ...tabs.map((tab) => tab.id)) + const showNextID = isTwitter(activatedSocialNetworkUI) && - ((isOwn && addressList?.length === 0) || isWeb3ProfileDisable || (isOwn && !isCurrentConnectedPersonaBind)) + ((isOwn && addressList?.length === 0) || + isWeb3ProfileDisable || + (isOwn && !isCurrentConnectedPersonaBind) || + (isOwn && !wallets?.length) || + !addressList?.length) const componentTabId = showNextID ? displayPlugins?.find((tab) => tab?.pluginID === PluginId.NextID)?.ID - : selectedTabId + : currentTab - const handleOpenDialog = () => { - CrossIsolationMessages.events.requestWeb3ProfileDialog.sendToAll({ - open: true, - }) - } const component = useMemo(() => { const Component = getTabContent(componentTabId) - const Utils = displayPlugins.find((x) => x.ID === selectedTabId)?.Utils - return ( - Utils?.filter?.(x) ?? true).sort(Utils?.sorter)} - /> - ) - }, [ - componentTabId, - personaPublicKey, - displayPlugins.map((x) => x.ID).join(), - personaList.join(), - addressList.map((x) => x.address).join(), - ]) + return + }, [componentTabId, personaPublicKey, selectedAddress]) useLocationChange(() => { - setSelectedTab(undefined) + onChange(undefined, first(tabs)?.id) }) useUpdateEffect(() => { - setSelectedTab(undefined) + onChange(undefined, first(tabs)?.id) }, [identity.identifier?.userId]) useEffect(() => { @@ -199,10 +242,22 @@ export function ProfileTabContent(props: ProfileTabContentProps) { setHidden(!data.show) }) }, [identity.identifier?.userId]) + const onClose = () => setAnchorEl(null) + const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) + const onSelect = (option: SocialAddress) => { + setSelectedAddress(option) + onClose() + } + const handleOpenDialog = () => { + CrossIsolationMessages.events.requestWeb3ProfileDialog.sendToAll({ + open: true, + }) + } if (hidden) return null - if (!identity.identifier?.userId || loadingSocialAddressList || loadingPersonaList) + // loadingSocialAddress + if (!identity.identifier?.userId || loadingPersonaList) return (
{tabs.length > 0 && !showNextID && ( - - tabs={tabs} - selectedId={selectedTabId} - onChange={setSelectedTab} - tail={isOwn && } - /> +
+
+
+ + setAnchorEl(null)}> + {uniqBy(addressList ?? [], (x) => x.address.toLowerCase()).map((x) => { + return ( + onSelect(x)}> +
+
+ {x?.type === SocialAddressType.KV || + x?.type === SocialAddressType.ADDRESS ? ( + + ) : ( + + {x.label} + + )} + + + + {x?.type === SocialAddressType.KV && ( + + )} +
+ {isSameAddress(selectedAddress?.address, x.address) && ( + + )} +
+
+ ) + })} +
+
+
+ + {t.powered_by()} + + + {t.mask_network()} + + +
+
+
+ + + {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..8ae81d75b63b 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_nft_at_current_address')} + + ) 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, + }}> ({ @@ -75,11 +75,7 @@ export const PersonaHeaderUI = memo(
- {avatar ? ( - - ) : ( - - )} + {avatar ? : }
{formatPersonaName(nickname)} @@ -87,8 +83,9 @@ export const PersonaHeaderUI = memo(
-
diff --git a/packages/mask/src/extension/popups/pages/Wallet/SwitchWallet/WalletItem.tsx b/packages/mask/src/extension/popups/pages/Wallet/SwitchWallet/WalletItem.tsx index 0d55714551cc..9784bc9aa164 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/SwitchWallet/WalletItem.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/SwitchWallet/WalletItem.tsx @@ -105,7 +105,9 @@ export const WalletItem = memo(({ wallet, onClick, isSelected } ({Others.formatDomainName(domain)}) ) : null} - {isHovering ? : null} + {isHovering ? ( + + ) : null} 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 10983f7de484..354cf9d41865 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 @@ -115,6 +115,7 @@ export const WalletHeaderUI = memo( {!disabled ? ( ) : null} @@ -145,6 +146,7 @@ export const WalletHeaderUI = memo( {!disabled ? ( ) : null} diff --git a/packages/mask/src/extension/popups/pages/Wallet/components/WalletInfo/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/components/WalletInfo/index.tsx index 856f829b5747..0a44d9942b3b 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/components/WalletInfo/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/components/WalletInfo/index.tsx @@ -109,7 +109,8 @@ export const WalletInfoUI = memo(
- {name} + {name}{' '} + {domain && formatDomainName ? ( {formatDomainName(domain)} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx index 905141d5f620..5082937311d0 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/NFTPage.tsx @@ -1,95 +1,17 @@ -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 { persona?: string identity?: SocialIdentity - socialAddressList?: Array> + socialAddress?: SocialAddress } -export function NFTPage({ socialAddressList, persona }: 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, persona }: 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 - )} - - ) - })} - - -
- ) + return } diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx index e2bcc6c39153..c18e6f378c91 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 6b6e3fa39bca..3ea099eda404 100644 --- a/packages/mask/src/plugins/NextID/components/NextIdPage.tsx +++ b/packages/mask/src/plugins/NextID/components/NextIdPage.tsx @@ -1,4 +1,4 @@ -import { NewLinkOutIcon, PluginIcon, Verified, WalletUnderTabsIcon, Web3ProfileIcon } from '@masknet/icons' +import { Plugin, WalletUnderTabs, Web3Profile, Connect, Identity, LinkOut } 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' @@ -59,9 +59,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', @@ -85,22 +88,18 @@ const useStyles = makeStyles()((theme) => ({ backgroundColor: theme.palette.background.default, height: '196px', }, - walletIcon: { - fontSize: 18, - marginRight: 8, - }, web3Icon: { marginRight: 6, marginTop: 2, }, item1: { color: '#767f8d', - fontSize: '14', + fontSize: '14px', fontWeight: 400, }, item2: { color: '#07101B', - fontSize: '14', + fontSize: '14px', fontWeight: 500, marginLeft: '2px', }, @@ -108,11 +107,19 @@ const useStyles = makeStyles()((theme) => ({ borderRadius: '99px', backgroundColor: '#07101b', color: '#fff', + marginTop: 'auto', ':hover': { color: 'fff', backgroundColor: '#07101b', }, }, + content: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '14px', + fontWeight: 400, + }, })) interface NextIdPageProps { @@ -122,6 +129,8 @@ interface NextIdPageProps { export function NextIdPage({ persona }: NextIdPageProps) { const t = useI18N() const { classes } = useStyles() + + const [description, setDescription] = useState('') const currentProfileIdentifier = useLastRecognizedIdentity() const visitingPersonaIdentifier = useCurrentVisitingIdentity() const personaConnectStatus = usePersonaConnectStatus() @@ -135,8 +144,16 @@ export function NextIdPage({ persona }: NextIdPageProps) { const personaActionButton = useMemo(() => { if (!personaConnectStatus.action) return null const button = personaConnectStatus.hasPersona ? t.connect_persona() : t.create_persona() + setDescription(personaConnectStatus.hasPersona ? '' : t.create_persona_intro()) + const icon = personaConnectStatus.hasPersona ? ( + + ) : ( + + ) + return ( ) @@ -187,11 +204,15 @@ export function NextIdPage({ persona }: NextIdPageProps) { }) } - const getButton = () => { + const getButton = useMemo(() => { + if (!isOwn) { + setDescription(t.others_lack_wallet()) + return + } if (isWeb3ProfileDisable) { return ( ) @@ -199,21 +220,22 @@ export function NextIdPage({ persona }: NextIdPageProps) { if (personaActionButton && isOwn) { return personaActionButton } - if (!isAccountVerified) { + if (!isAccountVerified && isOwn) { return ( ) } + setDescription(t.add_wallet_intro()) return ( ) - } + }, [isWeb3ProfileDisable, personaActionButton, isOwn, isAccountVerified, t]) if (loadingBindings || loadingPersona || loadingVerifyInfo) { return ( @@ -234,7 +256,7 @@ export function NextIdPage({ persona }: NextIdPageProps) {
- + {t.web3_profile()} @@ -250,18 +272,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/Pets/SNSAdaptor/PetSetDialog.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx index 0fc502401116..87f3d8761487 100644 --- a/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx +++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx @@ -29,7 +29,7 @@ import { petShowSettings } from '../settings' import { ChainBoundary } from '../../../web3/UI/ChainBoundary' import { useWeb3Connection } from '@masknet/plugin-infra/web3' import { saveCustomEssayToRSS } from '../Services/rss3' -import { RSS3Icon } from '../assets/rss3' +import { Rss3 } from '@masknet/icons' import ActionButton from '../../../extension/options-page/DashboardComponents/ActionButton' const useStyles = makeStyles()((theme) => ({ @@ -376,7 +376,7 @@ export function PetSetDialog({ configNFTs, onClose }: PetSetDialogProps) { RSS3 - + diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/PriceChanged.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/PriceChanged.tsx index 9e710730e5c2..1a9e14d18d14 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/PriceChanged.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/PriceChanged.tsx @@ -31,8 +31,8 @@ export function PriceChanged(props: PriceChangedProps) { if (props.amount === 0) return null return ( - {props.amount > 0 ? : null} - {props.amount < 0 ? : null} + {props.amount > 0 ? : null} + {props.amount < 0 ? : null} 0 ? colors?.success : colors?.danger}> {props.amount.toFixed(2)}% diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx index 4d13767a1e7e..37c6c94a375f 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx @@ -248,7 +248,7 @@ export function TrendingViewDeck(props: TrendingViewDeckProps) { sx={{ padding: 0 }} size="small" onClick={() => setCoinMenuOpen((v) => !v)}> - + )} - {selected && } + {selected && }
{Others?.chainResolver.chainName(network.chainId)} diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx index 22cbf7e66bba..9fcdec0f5963 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/TransactionSnackbar/index.tsx @@ -111,7 +111,7 @@ export function TransactionSnackbar({ pluginID }: Tra {progress.status === TransactionStatusType.SUCCEED ? computed.successfulDescription ?? computed.description : computed.description}{' '} - + ), }, diff --git a/packages/mask/src/plugins/Web3Feed/SNSAdaptor/FeedCard.tsx b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/FeedCard.tsx new file mode 100644 index 000000000000..1f3891b4f273 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/FeedCard.tsx @@ -0,0 +1,223 @@ +import { NFTCardStyledAssetPlayer, TokenIcon } from '@masknet/shared' +import { makeStyles } from '@masknet/theme' +import { Alchemy_EVM, RSS3BaseAPI } from '@masknet/web3-providers' +import { NetworkPluginID } from '@masknet/web3-shared-base' +import { resolveIPFSLinkFromURL, ZERO_ADDRESS } from '@masknet/web3-shared-evm' +import { Box, Typography, Card } from '@mui/material' +import differenceInCalendarDays from 'date-fns/differenceInDays' +import differenceInCalendarHours from 'date-fns/differenceInHours' +import { useMemo } from 'react' +import { ChainID } from '../constants' +import { ReversedAddress } from './ReversedAddress' +import { useI18N } from '../locales' +import { useAsyncRetry } from 'react-use' + +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', + '-webkit-line-clamp': '1', + maxWidth: '400px', + overflow: 'hidden', + display: '-webkit-box', + '-webkit-box-orient': 'vertical', + }, + defaultImage: { + background: theme.palette.maskColor.modalTitleBg, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 12, + }, + loadingFailImage: { + minHeight: '0 !important', + maxWidth: 'none', + width: 64, + height: 64, + }, +})) + +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('NFT')) { + if (feed.metadata?.from?.toLowerCase() === address) { + return ( + + sent a NFT to + + ) + } + if (feed.metadata?.from === ZERO_ADDRESS) { + return 'minted a NFT' + } + if (feed.metadata?.to?.toLowerCase() === address) { + return ( + + acquire a NFT from + + ) + } + } + if (feed.tags?.includes('Token') || feed.tags?.includes('ETH')) { + if (feed.metadata?.from?.toLowerCase() === address) { + return ( + + sent to + + ) + } + if (feed.metadata?.to?.toLowerCase() === address) { + return ( + + received from + + ) + } + } + if (feed.tags?.includes('Gitcoin')) { + if (feed.metadata?.from?.toLowerCase() === address) { + return 'donated' + } + if (feed.metadata?.to?.toLowerCase() === address) { + return 'received donation from' + } + } + if (feed.metadata?.from?.toLowerCase() === address) { + return 'received' + } + return 'sent' + }, [address, feed]) + + const logo = useMemo(() => { + if (feed.tags?.includes('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('Token') || feed.tags.includes('ETH')) { + return ( + + ) + } + if (feed.tags.includes('Gitcoin')) { + return ( + attachment?.type === 'logo')?.address} + /> + ) + } + return null + }, [feed]) + + const time = useMemo(() => { + const days = differenceInCalendarDays(new Date(), new Date(feed.date_updated)) + const hours = differenceInCalendarHours(new Date(), new Date(feed.date_updated)) + return [ + days > 0 ? `${days} ${t.day({ count: days })} ` : '', + hours > 0 ? `${hours} ${t.day({ count: hours })} ` : '', + t.ago(), + ].join('') + }, [feed.date_updated, t]) + 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} {time} + + + {feed.title || + NFTMetadata?.metadata?.name || + NFTMetadata?.collection?.name || + NFTMetadata?.contract?.name || + ''} + + + {feed.summary || NFTMetadata?.metadata?.description || NFTMetadata?.collection?.description} || + `#${feed.metadata?.token_id}` + + +
+ {logo} +
+ ) +} diff --git a/packages/mask/src/plugins/Web3Feed/SNSAdaptor/ReversedAddress.tsx b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/ReversedAddress.tsx new file mode 100644 index 000000000000..c292af15c4ad --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/ReversedAddress.tsx @@ -0,0 +1,26 @@ +import { memo } from 'react' +import type { NetworkPluginID } from '@masknet/web3-shared-base' +import { useReverseAddress, useWeb3State } from '@masknet/plugin-infra/web3' +import { ZERO_ADDRESS } from '@masknet/web3-shared-evm' + +interface ReverseAddressProps { + address?: string + pluginId?: NetworkPluginID + domainSize?: number + size?: number + fontSize?: string + fontWeight?: number +} + +export const ReversedAddress = memo( + ({ address = ZERO_ADDRESS, pluginId, domainSize, size = 5, fontSize = '14px', fontWeight = 400 }) => { + const { value: domain } = useReverseAddress(pluginId, address) + const { Others } = useWeb3State(pluginId) + if (address === ZERO_ADDRESS) return null + + if (!domain || !Others?.formatDomainName) + return {Others?.formatAddress?.(address, size) ?? address} + + return {Others.formatDomainName(domain, domainSize)} + }, +) diff --git a/packages/mask/src/plugins/Web3Feed/SNSAdaptor/StatusBox.tsx b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/StatusBox.tsx new file mode 100644 index 000000000000..f7c27973fcb3 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/StatusBox.tsx @@ -0,0 +1,39 @@ +import { makeStyles } from '@masknet/theme' +import { Box, CircularProgress, Typography } from '@mui/material' +import type { FC } from 'react' +import { useI18N } from '../locales' + +interface Props { + loading?: boolean + empty?: boolean +} + +const useStyles = makeStyles()((theme) => ({ + statusBox: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: theme.spacing(6), + }, +})) + +export const StatusBox: FC = ({ loading, empty }) => { + const { classes } = useStyles() + const t = useI18N() + if (loading) { + return ( + + + + ) + } + + if (empty) { + return ( + + {t.no_data()} + + ) + } + return null +} diff --git a/packages/mask/src/plugins/Web3Feed/SNSAdaptor/Web3FeedPage.tsx b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/Web3FeedPage.tsx new file mode 100644 index 000000000000..31ff671c3cbd --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/Web3FeedPage.tsx @@ -0,0 +1,50 @@ +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 './FeedCard' +import { StatusBox } from './StatusBox' + +export interface Web3FeedPageProps { + persona?: string + socialAddress?: SocialAddress +} + +export function Web3FeedPage({ socialAddress, persona }: Web3FeedPageProps) { + 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} + /> +
+ ) +} diff --git a/packages/mask/src/plugins/Web3Feed/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/index.tsx new file mode 100644 index 000000000000..e97baea9383f --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/SNSAdaptor/index.tsx @@ -0,0 +1,23 @@ +import type { Plugin } from '@masknet/plugin-infra/content-script' +import { base } from '../base' +import { PLUGIN_ID } from '../constants' +import { Web3FeedPage } from './Web3FeedPage' + +const sns: Plugin.SNSAdaptor.Definition = { + ...base, + init(signal, context) {}, + ProfileTabs: [ + { + ID: `${PLUGIN_ID}_web3Feed`, + label: 'Web3Feed', + priority: 4, + UI: { + TabContent: ({ socialAddress, persona }) => { + return + }, + }, + }, + ], +} + +export default sns diff --git a/packages/mask/src/plugins/Web3Feed/Worker/index.ts b/packages/mask/src/plugins/Web3Feed/Worker/index.ts new file mode 100644 index 000000000000..2da0563bd149 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/Worker/index.ts @@ -0,0 +1,8 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { base } from '../base' + +const worker: Plugin.Worker.Definition = { + ...base, + init(signal) {}, +} +export default worker diff --git a/packages/mask/src/plugins/Web3Feed/base.ts b/packages/mask/src/plugins/Web3Feed/base.ts new file mode 100644 index 000000000000..6fdbd9edc841 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/base.ts @@ -0,0 +1,26 @@ +import { PLUGIN_ID } from './constants' +import { languages } from './locales/languages' +import { Plugin, CurrentSNSNetwork } from '@masknet/plugin-infra' + +export const base: Plugin.Shared.Definition = { + ID: PLUGIN_ID, + name: { fallback: 'Web3Feed' }, + description: { + fallback: 'web3 user collection feed', + }, + publisher: { name: { fallback: 'Mask Network' }, link: 'https://mask.io/' }, + enableRequirement: { + architecture: { app: true, web: true }, + networks: { + type: 'opt-in', + networks: { + [CurrentSNSNetwork.Twitter]: true, + [CurrentSNSNetwork.Facebook]: false, + [CurrentSNSNetwork.Instagram]: false, + }, + }, + target: 'stable', + }, + + i18n: languages, +} diff --git a/packages/mask/src/plugins/Web3Feed/constants.ts b/packages/mask/src/plugins/Web3Feed/constants.ts new file mode 100644 index 000000000000..4b7cb0aac151 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/constants.ts @@ -0,0 +1,10 @@ +import { PluginId } from '@masknet/plugin-infra' +import { ChainId } from '@masknet/web3-shared-evm' +export const PLUGIN_ID = PluginId.Web3Feed +export const PLUGIN_NAME = 'Web3 Feed' +export const PLUGIN_DESCRIPTION = 'web3 user collection feed' +export const ChainID = { + ethereum: ChainId.Mainnet, + polygon: ChainId.Matic, + bnb: ChainId.BSC, +} diff --git a/packages/mask/src/plugins/Web3Feed/index.ts b/packages/mask/src/plugins/Web3Feed/index.ts new file mode 100644 index 000000000000..eb51ba0afe25 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/index.ts @@ -0,0 +1,16 @@ +import { registerPlugin } from '@masknet/plugin-infra' +import { base } from './base' + +registerPlugin({ + ...base, + SNSAdaptor: { + load: () => import('./SNSAdaptor'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./SNSAdaptor', () => hot(import('./SNSAdaptor'))), + }, + Worker: { + load: () => import('./Worker'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./Worker', () => hot(import('./Worker'))), + }, +}) diff --git a/packages/mask/src/plugins/Web3Feed/locales/en-US.json b/packages/mask/src/plugins/Web3Feed/locales/en-US.json new file mode 100644 index 000000000000..213a84e43bb3 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/en-US.json @@ -0,0 +1,8 @@ +{ + "day_one": "days", + "day_other": "days", + "hour_one": "hour", + "hour_other": "hours", + "ago": "ago", + "no_data": "No feed at the current address" +} diff --git a/packages/mask/src/plugins/Web3Feed/locales/index.ts b/packages/mask/src/plugins/Web3Feed/locales/index.ts new file mode 100644 index 000000000000..d6ead60252e4 --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/index.ts @@ -0,0 +1,6 @@ +// This file is auto generated. DO NOT EDIT +// Run `npx gulp sync-languages` to regenerate. +// Default fallback language in a family of languages are chosen by the alphabet order +// To overwrite this, please overwrite packages/scripts/src/locale-kit-next/index.ts + +export * from './i18n_generated' diff --git a/packages/mask/src/plugins/Web3Feed/locales/ja-JP.json b/packages/mask/src/plugins/Web3Feed/locales/ja-JP.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/ja-JP.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Web3Feed/locales/ko-KR.json b/packages/mask/src/plugins/Web3Feed/locales/ko-KR.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/ko-KR.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Web3Feed/locales/languages.ts b/packages/mask/src/plugins/Web3Feed/locales/languages.ts new file mode 100644 index 000000000000..143dc822172a --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/languages.ts @@ -0,0 +1,34 @@ +// This file is auto generated. DO NOT EDIT +// Run `npx gulp sync-languages` to regenerate. +// Default fallback language in a family of languages are chosen by the alphabet order +// To overwrite this, please overwrite packages/scripts/src/locale-kit-next/index.ts +import en_US from './en-US.json' +import ja_JP from './ja-JP.json' +import ko_KR from './ko-KR.json' +import qya_AA from './qya-AA.json' +import zh_CN from './zh-CN.json' +import zh_TW from './zh-TW.json' +export const languages = { + en: en_US, + ja: ja_JP, + ko: ko_KR, + qy: qya_AA, + 'zh-CN': zh_CN, + zh: zh_TW, +} +// @ts-ignore +if (import.meta.webpackHot) { + // @ts-ignore + import.meta.webpackHot.accept( + ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'], + () => + globalThis.dispatchEvent?.( + new CustomEvent('MASK_I18N_HMR', { + detail: [ + 'org.findtruman', + { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }, + ], + }), + ), + ) +} diff --git a/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json b/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/qya-AA.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Web3Feed/locales/zh-CN.json b/packages/mask/src/plugins/Web3Feed/locales/zh-CN.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/zh-CN.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/plugins/Web3Feed/locales/zh-TW.json b/packages/mask/src/plugins/Web3Feed/locales/zh-TW.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/mask/src/plugins/Web3Feed/locales/zh-TW.json @@ -0,0 +1 @@ +{} diff --git a/packages/mask/src/tsconfig.json b/packages/mask/src/tsconfig.json index fd9627fe22ca..40eacb07444b 100644 --- a/packages/mask/src/tsconfig.json +++ b/packages/mask/src/tsconfig.json @@ -31,6 +31,13 @@ { "path": "../../plugins/Wallet" }, { "path": "../../plugins/DAO" }, { "path": "../../plugins/FileService" }, + { "path": "../../plugins/RSS3" }, + { "path": "../../plugins/Web3Profile" }, + { "path": "../../plugins/example" }, + { "path": "../../plugins/Debugger" }, + { "path": "../../plugins/CyberConnect" }, + { "path": "../../plugins/CrossChainBridge" }, + { "path": "../../plugins/GoPlusSecurity" }, { "path": "../../plugins/EVM" }, { "path": "../../plugins/Flow" }, { "path": "../../plugins/Solana" }, diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index de1241da56ee..c8387a3f3bdf 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -593,14 +593,14 @@ export namespace Plugin.SNSAdaptor { TabContent: InjectUI<{ identity?: SocialIdentity persona?: string - 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. */ @@ -960,6 +960,7 @@ export enum PluginId { Referral = 'com.maskbook.referral', Web3Profile = 'io.mask.web3-profile', ScamSniffer = 'io.scamsniffer.mask-plugin', + Web3Feed = 'io.mask.web3-feed', // @masknet/scripts: insert-here } /** diff --git a/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx b/packages/plugins/GoPlusSecurity/src/SNSAdaptor/components/Common.tsx index 403cd516e158..c9263481a078 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 12a067f6c7d9..7b0b595440be 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 { DefaultTokenIcon, LinkOutIcon } 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 type { RSS3BaseAPI } from '@masknet/web3-providers' export enum TabCardType { Donation = 1, @@ -34,30 +16,17 @@ export enum TabCardType { export interface TabCardProps { persona?: string type: TabCardType - socialAddressList?: Array> + socialAddress?: SocialAddress } -export function TabCard({ type, socialAddressList, persona }: TabCardProps) { - const t = useI18N() - const { classes } = useStyles() - - const [selectedAddress, setSelectedAddress] = useState(first(socialAddressList)) - +export function TabCard({ type, socialAddress, persona }: 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 unHiddenDonations = useCollectionFilter( @@ -65,92 +34,31 @@ export function TabCard({ type, socialAddressList, persona }: TabCardProps) { donations, CollectionType.Donations, currentVisitingProfile, - selectedAddress, + socialAddress, ) const unHiddenFootprints = useCollectionFilter( (kvValue as KVType)?.proofs, footprints, CollectionType.Footprints, currentVisitingProfile, - selectedAddress, + socialAddress, ) - if (!selectedAddress) return null + if (!socialAddress) return null const isDonation = type === TabCardType.Donation - 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 isDonation ? ( + + ) : ( + ) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/DonationCard.tsx index b012d9304989..6c25db404fec 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.Donation + address: SocialAddress + onSelect: () => void } const useStyles = makeStyles()((theme) => ({ @@ -19,85 +20,81 @@ 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, }, })) -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.detail?.txs?.[0] + ? formatDateTime(new Date(Number(donation.detail?.txs?.[0]?.timeStamp) * 1000), 'MMM dd, yyyy') + : '--' return ( -
- {name} -
-
- - {name} +
+ {donation.detail?.grant?.title +
+
+ + {date} + +
+
+ + {reversedAddress} {t.contributed()}{' '} + {donation.detail?.txs?.[0]?.formatedAmount} + {donation.detail?.txs?.[0]?.symbol} {t.to()}{' '} + {donation.detail?.grant?.title} -
-
- {contribCount} - {t.contribution({ count: contribCount })} -
-
- {contribDetails.map((contrib, i) => ( - - {contrib.amount} - {contrib.token} - - ))} -
-
+
+
) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx index 98a6f16245fc..29cbc20d8e4b 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/components/FootprintCard.tsx @@ -1,65 +1,68 @@ 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, + fontColor: 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.Footprint + 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.detail?.end_date + ? formatDateTime(new Date(footprint.detail?.end_date), 'MMM dd, yyyy') + : t.no_activity_time() + const location = footprint.detail.city || footprint.detail.country || 'Metaverse' return ( -
+
- + {t.inactive_project()}
-
-
- - - {displayDate} - -
-
- - - {location} - -
-
- - {t.attended()} - - - {activity} - -
+
+ {date} + @ {location} + {footprint.detail?.name || ''}
) diff --git a/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx b/packages/plugins/RSS3/src/SNSAdaptor/components/StatusBox.tsx index ddf44b0daf33..799221f4975a 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 + collection?: 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, collection = 'Donation' }) => { const { classes } = useStyles() const t = useI18N() if (loading) { @@ -31,7 +32,7 @@ export const StatusBox: FC = ({ loading, empty }) => { if (empty) { return ( - {t.no_data()} + {t.no_data({ collection })} ) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts index e47b9be71ea2..acd02b370fba 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts +++ b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useCollectionFilter.ts @@ -1,12 +1,13 @@ 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 { CollectionType, Proof } from '../../types' export const useCollectionFilter = ( hiddenInfo: Proof[], - collections: GeneralAsset[], + collections: RSS3BaseAPI.Donation[] | RSS3BaseAPI.Footprint[], type: CollectionType, currentVisitingProfile?: IdentityResolved, address?: SocialAddress, @@ -21,6 +22,13 @@ export const useCollectionFilter = ( ) const hiddenList = proof?.content?.[PluginId.Web3Profile]?.unListedCollections?.[address?.address?.toLowerCase()]?.[type] ?? [] - return collections?.filter((collection) => hiddenList?.findIndex((url) => url === collection?.id) === -1) + if (type === CollectionType.Donations) { + return (collections as RSS3BaseAPI.Donation[])?.filter( + (collection: { id: string }) => hiddenList?.findIndex((url) => url === collection?.id) === -1, + ) + } + return (collections as RSS3BaseAPI.Footprint[])?.filter( + (collection: { id: string }) => hiddenList?.findIndex((url) => url === collection?.id) === -1, + ) }, [address, currentVisitingProfile?.identifier?.userId, type, hiddenInfo?.length, collections?.length]) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts b/packages/plugins/RSS3/src/SNSAdaptor/hooks/useDonations.ts index 8f94c6bc52ba..196afd5738e7 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..ed0776223d0c 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/index.tsx b/packages/plugins/RSS3/src/SNSAdaptor/index.tsx index 1d3e40b7a429..bb5b20b125d6 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,14 +20,11 @@ const sns: Plugin.SNSAdaptor.Definition = { label: 'Donations', priority: 1, UI: { - TabContent: ({ socialAddressList = [], persona }) => { - return ( - - ) + TabContent: ({ socialAddress, persona }) => { + return }, }, Utils: { - sorter, shouldDisplay, }, }, @@ -42,14 +33,11 @@ const sns: Plugin.SNSAdaptor.Definition = { label: 'Footprints', priority: 2, UI: { - TabContent: ({ socialAddressList = [], persona }) => { - return ( - - ) + TabContent: ({ socialAddress, persona }) => { + 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..a3c233511887 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/pages/DonationsPage.tsx @@ -1,21 +1,12 @@ +import { CollectionDetailCard } from '@masknet/shared' 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 { 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 +16,7 @@ const useStyles = makeStyles()((theme) => ({ }, list: { display: 'grid', - gridTemplateColumns: 'repeat(2, 1fr)', + gridTemplateColumns: 'repeat(1, 1fr)', gridGap: theme.spacing(2), }, listItem: { @@ -48,31 +39,43 @@ const useStyles = makeStyles()((theme) => ({ })) export interface DonationPageProps { - donations?: GeneralAsset[] + donations?: RSS3BaseAPI.Donation[] loading?: boolean - addressLabel: string + address: SocialAddress } -export function DonationPage({ donations = [], loading, addressLabel }: DonationPageProps) { +export function DonationPage({ donations = [], 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?.detail?.grant?.logo} + title={selectedDonation?.detail?.grant?.title} + referenceUrl={selectedDonation?.detail?.grant?.reference_url} + description={selectedDonation?.detail?.grant?.description} + contributions={selectedDonation?.detail?.txs} + /> + ) } diff --git a/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx b/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx index a3c70b2b3b0d..4fafdb2009ee 100644 --- a/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx +++ b/packages/plugins/RSS3/src/SNSAdaptor/pages/FootprintPage.tsx @@ -1,61 +1,49 @@ -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('-', '.'), - }) -} - export interface FootprintPageProps { - footprints?: GeneralAsset[] + footprints?: RSS3BaseAPI.Footprint[] 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 = [], address, loading }: FootprintPageProps) { + const { value: profile } = useRss3Profile(address.address || '') const username = profile?.name + 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?.detail?.image_url} + title={selectedFootprint?.detail?.name} + referenceUrl={selectedFootprint?.detail?.event_url} + description={selectedFootprint?.detail?.description} + date={selectedFootprint?.detail?.end_date} + location={selectedFootprint?.detail?.city || selectedFootprint?.detail?.country || 'Metaverse'} + /> +
) } diff --git a/packages/plugins/RSS3/src/locales/en-US.json b/packages/plugins/RSS3/src/locales/en-US.json index 757d0d9437f0..38525dd9adba 100644 --- a/packages/plugins/RSS3/src/locales/en-US.json +++ b/packages/plugins/RSS3/src/locales/en-US.json @@ -2,8 +2,10 @@ "inactive_project": "Inactive Project", "no_activity_time": "No activity time", "attended": "attended", - "no_data": "No data", + "no_data": "No {{collection}} at the current address", "total_grants": "Total {{count}} Grants", "contribution": "Contribution", - "contribution_other": "Contributions" + "contribution_other": "Contributions", + "contributed": "contributed", + "to": "to" } 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', diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx index 8d40f343fb80..95e4c84481a8 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/components/WalletAssets.tsx @@ -1,5 +1,5 @@ import { Card, Typography, Link, Box } from '@mui/material' -import { Edit2Icon, LinkOutIcon } from '@masknet/icons' +import { Edit2Icon, LinkOutIcon, DoubleArrowUp } from '@masknet/icons' import { makeStyles, useStylesExtends } from '@masknet/theme' import { useI18N } from '../../locales' import { ImageIcon } from './ImageIcon' @@ -9,6 +9,7 @@ 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' const useStyles = makeStyles()((theme) => { return { @@ -32,10 +33,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 +67,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 +110,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 [] + } + 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 +171,16 @@ export function WalletAssetsCard(props: WalletAssetsCardProps) {
- +
+ {loadStatus === LOAD_STATUS.Finish && ( + setLoadStatus(LOAD_STATUS.Necessary)} + /> + )} + +
{collectionList && collectionList?.filter((collection) => !collection?.hidden)?.length > 0 ? ( @@ -119,8 +188,10 @@ export function WalletAssetsCard(props: WalletAssetsCardProps) { !collection?.hidden)?.slice(0, 8)} + collections={collections} + showNetwork /> + {loadIcon}
) : ( diff --git a/packages/plugins/Web3Profile/src/SNSAdaptor/index.tsx b/packages/plugins/Web3Profile/src/SNSAdaptor/index.tsx index fe8a8c066cd3..7b05193c71ee 100644 --- a/packages/plugins/Web3Profile/src/SNSAdaptor/index.tsx +++ b/packages/plugins/Web3Profile/src/SNSAdaptor/index.tsx @@ -1,6 +1,6 @@ import type { Plugin } from '@masknet/plugin-infra' import { ApplicationEntry } from '@masknet/shared' -import { Web3ProfileIcon } from '@masknet/icons' +import { Web3Profile } from '@masknet/icons' import { base } from '../base' import { Web3ProfileDialog } from './components/Web3ProfileDialog' import { setupContext } from './context' @@ -18,7 +18,7 @@ const sns: Plugin.SNSAdaptor.Definition = { }, ApplicationEntries: [ (() => { - const icon = + const icon = const name = { i18nKey: '__plugin_name', fallback: 'Web3 Profile' } const recommendFeature = { description: , 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 d5d6b05aac12..e2111d02ca17 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..ed19267b6d46 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/AssetPlayer/index.tsx b/packages/shared/src/UI/components/AssetPlayer/index.tsx index 8b28e14476a1..25b952719be6 100644 --- a/packages/shared/src/UI/components/AssetPlayer/index.tsx +++ b/packages/shared/src/UI/components/AssetPlayer/index.tsx @@ -38,6 +38,7 @@ interface AssetPlayerProps fallbackResourceLoader?: JSX.Element setERC721TokenName?: (name: string) => void setSourceType?: (type: string) => void + showNetwork?: boolean } const useStyles = makeStyles()({ hidden: { @@ -55,7 +56,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 } = props const classes = useStylesExtends(useStyles(), props) const [hidden, setHidden] = useState(Boolean(props.renderTimeout)) const { RPC_URLS } = getRPCConstants(props.erc721Token?.chainId) @@ -188,7 +189,7 @@ export const AssetPlayer = memo((props) => { ) return ( - <> + ((props) => { : props.loadingIcon ?? } {IframeResizerMemo} - + ) }) diff --git a/packages/shared/src/UI/components/CollectionDetailCard/assets/etherscan.png b/packages/shared/src/UI/components/CollectionDetailCard/assets/etherscan.png new file mode 100644 index 000000000000..7561d51c5f7f Binary files /dev/null and b/packages/shared/src/UI/components/CollectionDetailCard/assets/etherscan.png differ 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..b2536c289043 --- /dev/null +++ b/packages/shared/src/UI/components/CollectionDetailCard/index.tsx @@ -0,0 +1,263 @@ +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 differenceInCalendarDays from 'date-fns/differenceInDays' +import differenceInCalendarHours from 'date-fns/differenceInHours' +import { Gitcoin, LinkOut, OpenSeaColoredIcon, PolygonScan, EtherScan } from '@masknet/icons' +import { ChainId, explorerResolver } from '@masknet/web3-shared-evm' +import { NFTCardStyledAssetPlayer } from '@masknet/shared' +import { EMPTY_LIST } from '@masknet/shared-base' + +interface CollectionDetailCardProps { + img?: string + open: boolean + title?: string + referenceUrl?: string + description?: string + contributions?: RSS3BaseAPI.DonationTx[] + onClose: () => void + date?: string + location?: string + relatedURLs?: string[] + metadata?: RSS3BaseAPI.Metadata + traits?: Array<{ + type: string + value: 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, + }, +})) + +const ChainID = { + ethereum: ChainId.Mainnet, + polygon: ChainId.Matic, + bnb: ChainId.BSC, +} + +export const CollectionDetailCard = memo( + ({ + img, + open, + onClose, + title, + referenceUrl, + metadata, + description, + contributions = EMPTY_LIST, + date, + location, + relatedURLs = EMPTY_LIST, + traits, + }) => { + 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}
+ + + {referenceUrl} + + {date && ( + + {date} + + )} + {location && ( + + @ + {location} + + )} + + {t.description()} + +
+ {description} +
+ + {contributions.length ? ( + <> + + {t.contributions()} + + + {contributions.length} + + + ) : null} + {contributions.map((contribution) => { + const days = differenceInCalendarDays(Date.now(), Number(contribution.timeStamp) * 1000) + const hours = differenceInCalendarHours(Date.now(), Number(contribution.timeStamp) * 1000) + return ( +
+ + {contribution.formatedAmount} {contribution.symbol} + +
+ {days > 0 ? `${days} ${t.day({ count: days })} ` : ''} + {hours > 0 ? `${hours} ${t.day({ count: hours })} ` : ''} + {t.ago()} + + + +
+
+ ) + })} + {traits && ( + + {t.properties()} + + )} + + {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 405928a12ca6..000000000000 --- a/packages/shared/src/UI/components/ConcealableTabs/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { LeftArrowIcon, RightArrowIcon } 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 241ea284d505..549219c934a8 100644 --- a/packages/shared/src/UI/components/NFTCard/index.tsx +++ b/packages/shared/src/UI/components/NFTCard/index.tsx @@ -1,8 +1,8 @@ import { MaskAvatarIcon, SelectedIcon } 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..8d7435aa5b37 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,6 +90,8 @@ 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 && }
) : ( (({ address, pluginId, domainSize, size = 5 }) => { - const { value: domain } = useReverseAddress(pluginId, address) - const { Others } = useWeb3State(pluginId) +export const ReversedAddress = memo( + ({ address, pluginId, domainSize, size = 5, 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..de17c3b9a517 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,5 @@ export * from './Linking' export * from './LoadRetry' export * from './NFTCard' export * from './TokenSecurity' +export * from './CollectionDetailCard' export * from './Image' 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 3bf8bc91b322..dbda73f602ec 100644 --- a/packages/web3-providers/src/rss3/constants.ts +++ b/packages/web3-providers/src/rss3/constants.ts @@ -1,4 +1,13 @@ +import { NetworkPluginID } from '@masknet/web3-shared-base' + export const RSS3_ENDPOINT = 'https://hub.pass3.me' +export const RSS3_FEED_ENDPOINT = 'https://pregod.rss3.dev/v0.4.0/' + +export const PLATFORM = { + [NetworkPluginID.PLUGIN_EVM]: 'ethereum', + [NetworkPluginID.PLUGIN_FLOW]: 'flow', + [NetworkPluginID.PLUGIN_SOLANA]: 'solana', +} export const NEW_RSS3_ENDPOINT = 'https://prenode.rss3.dev' export const CollectionType = { diff --git a/packages/web3-providers/src/rss3/index.ts b/packages/web3-providers/src/rss3/index.ts index 8005b0e45425..730156c6c6ee 100644 --- a/packages/web3-providers/src/rss3/index.ts +++ b/packages/web3-providers/src/rss3/index.ts @@ -1,10 +1,10 @@ import urlcat from 'urlcat' import RSS3 from 'rss3-next' import { ChainId, SchemaType } from '@masknet/web3-shared-evm' -import { CollectionType, NEW_RSS3_ENDPOINT, RSS3_ENDPOINT } from './constants' +import { PLATFORM, RSS3_ENDPOINT, CollectionType, NEW_RSS3_ENDPOINT, RSS3_FEED_ENDPOINT } 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, NetworkPluginID, TokenType } from '@masknet/web3-shared-base' export class RSS3API implements RSS3BaseAPI.Provider, NonFungibleTokenAPI.Provider { createRSS3( @@ -109,4 +109,15 @@ export class RSS3API implements RSS3BaseAPI.Provider, NonFungibleTokenAPI.Provid .filter((x) => x.chainId === chainId) return createPageable(data, createIndicator(indicator)) } + + async getWeb3Feed( + address: string, + { networkPluginId = NetworkPluginID.PLUGIN_EVM }: HubOptions = {}, + type?: RSS3BaseAPI.FeedType, + ) { + if (!address) return + const url = `${RSS3_FEED_ENDPOINT}account:${address}@${PLATFORM[networkPluginId]}/notes?limit=100&exclude_tags=POAP&tags=Gitcoin&tags=POAP&tags=NFT&tags=Donation&latest=false` + const res = fetchJSON(url) + return res + } } diff --git a/packages/web3-providers/src/types/RSS3.ts b/packages/web3-providers/src/types/RSS3.ts index 9b0f3250f276..62122d682267 100644 --- a/packages/web3-providers/src/types/RSS3.ts +++ b/packages/web3-providers/src/types/RSS3.ts @@ -128,18 +128,76 @@ export namespace RSS3BaseAPI { detail: FootprintType } + export type FeedType = 'Token' | 'Donation' | 'NFT' + export enum AssetType { GitcoinDonation = 'Gitcoin-Donation', POAP = 'POAP', NFT = 'NFT', } + export type Tags = '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 Attachments { + address?: string + mime_type?: string + size_in_bytes?: string + type?: string + } + + export interface Web3Feed { + attachments?: Attachments[] + 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: Tags[] + 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