diff --git a/cspell.json b/cspell.json index 167a544b3a2c..b35a64aa8c62 100644 --- a/cspell.json +++ b/cspell.json @@ -347,6 +347,7 @@ "nftred", "NFTX", "noamount", + "nonfungiblefriends", "northcutt", "notistack", "nounset", diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 7d73699903af..797f7947ce19 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -886,11 +886,13 @@ "plugin_furucombo_head_pools": "Pools", "plugin_furucombo_head_action": "Action", "plugin_pets_dialog_title": "Non-Fungible Friends", + "plugin_pets_dialog_title_share": "Successful", "plugin_pets_dialog_contract": "NFT Contract", "plugin_pets_dialog_token": "Token ID", "plugin_pets_dialog_msg": "Greeting message", "plugin_pets_dialog_msg_optional": "Optional, 100 characters max.", "plugin_pets_dialog_btn": "Confirm", + "plugin_pets_dialog_btn_share": "Share", "plugin_pets_dialog_preview": "Preview", "plugin_pets_dialog_created": "Created by MintTeam", "plugin_pets_dialog_powered": "Powered by RSS3", diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx index 7a18fda8274f..c41972abf26a 100644 --- a/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx +++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx @@ -1,346 +1,51 @@ -import { useEffect, useState, useMemo, ReactNode } from 'react' -import { useAsync } from 'react-use' -import { useRemoteControlledDialog, useValueRef } from '@masknet/shared' -import { isSameAddress } from '@masknet/web3-shared-evm' -import { makeStyles, useStylesExtends, useCustomSnackbar } from '@masknet/theme' -import { - TextField, - Typography, - Box, - DialogContent, - Grid, - MenuItem, - Snackbar, - Autocomplete, - FormControlLabel, - Checkbox, -} from '@mui/material' -import { LoadingButton } from '@mui/lab' +import { useState } from 'react' +import { useAsync, useTimeout } from 'react-use' +import type { Constant } from '@masknet/web3-shared-evm/constants/utils' +import { useRemoteControlledDialog } from '@masknet/shared' +import { DialogContent } from '@mui/material' import { PluginPetMessages, PluginPetRPC } from '../messages' import { InjectedDialog } from '../../../components/shared/InjectedDialog' -import { initMeta, initCollection, GLB3DIcon } from '../constants' -import { PreviewBox } from './PreviewBox' -import { PetMetaDB, FilterContract, OwnerERC721TokenInfo, ImageType } from '../types' -import { useUser, useNFTs, useNFTsExtra } from '../hooks' import { useI18N } from '../../../utils' -import { ShadowRootPopper } from '../../../utils/shadow-root/ShadowRootComponents' -import { ImageLoader } from './ImageLoader' -import type { Constant } from '@masknet/web3-shared-evm/constants/utils' -import { petShowSettings } from '../settings' +import { PetShareDialog } from './PetShareDialog' +import { PetSetDialog } from './PetSetDialog' -const useStyles = makeStyles()((theme) => ({ - desBox: { - display: 'flex', - justifyContent: 'space-between', - marginTop: theme.spacing(3), - }, - des: { - color: '#7b8192', - fontSize: '12px', - }, - input: { - margin: theme.spacing(2, 0, 0), - }, - inputOption: { - margin: theme.spacing(4, 0, 0), - }, - inputBorder: { - borderRadius: theme.spacing(1), - padding: theme.spacing(1), - }, - inputArea: { - borderRadius: theme.spacing(1), - padding: theme.spacing(2), - }, - menuItem: { - width: '100%', - }, - btn: { - margin: theme.spacing(8, 0, 2), - }, - thumbnail: { - width: 25, - height: 25, - objectFit: 'cover', - margin: theme.spacing(0, 1, 0, 0), - display: 'inline-block', - borderRadius: 4, - }, - glbIcon: { - width: 15, - height: 18, - marginLeft: theme.spacing(1), - }, - itemFix: { - display: 'flex', - alignItems: 'center', - width: '100%', - }, - itemTxt: { - flex: 1, - marginLeft: theme.spacing(0.5), - }, - prevBox: { - margin: theme.spacing(2, 0, 0), - border: '1px dashed #ccc', - borderRadius: 4, - height: 'calc(100% - 16px)', - boxSizing: 'border-box', - padding: 4, - }, - boxPaper: { - backgroundColor: theme.palette.mode === 'dark' ? '#1B1E38' : '#FFFFFF', - marginBottom: 10, - boxShadow: theme.palette.mode === 'dark' ? '0 0 5px #FFFFFF' : '0 0 5px #CCCCCC', - }, -})) +enum PetFriendNFTStep { + SetFriendNFT = 'set', + ShareFriendNFT = 'share', +} export function PetDialog() { const { t } = useI18N() - const classes = useStylesExtends(useStyles(), {}) - const { showSnackbar } = useCustomSnackbar() const { open, closeDialog } = useRemoteControlledDialog(PluginPetMessages.events.essayDialogUpdated, () => {}) + const [step, setStep] = useState(PetFriendNFTStep.SetFriendNFT) const [configNFTs, setConfigNFTs] = useState | undefined>(undefined) - const [loading, setLoading] = useState(false) - const checked = useValueRef(petShowSettings) - - const user = useUser() - const nfts = useNFTs(user, configNFTs) - const extraData = useNFTsExtra(configNFTs) - const [collection, setCollection] = useState(initCollection) - const [isCollectionsError, setCollectionsError] = useState(false) - - const [metaData, setMetaData] = useState(initMeta) - const [isImageError, setImageError] = useState(false) - const [isTipShow, setTipShow] = useState(false) - const [holderChange, setHolderChange] = useState(true) - const [tokenInfoSelect, setTokenInfoSelect] = useState(null) - const [inputTokenName, setInputTokenName] = useState('') + const [isReady, cancel] = useTimeout(500) useAsync(async () => { setConfigNFTs(await PluginPetRPC.getConfigEssay()) }, []) - useEffect(() => { - if (open) return - setMetaData(initMeta) - setCollection(initCollection) - setTokenInfoSelect(null) - setInputTokenName('') - }, [open]) + const handleSetDialogClose = () => setStep(PetFriendNFTStep.ShareFriendNFT) - let timer: NodeJS.Timeout | null = null - const closeDialogHandle = () => { - setTipShow(true) + const handleClose = () => { closeDialog() - if (timer !== null) clearTimeout(timer) - timer = setTimeout(() => { - setTipShow(false) - }, 2000) - PluginPetMessages.events.setResult.sendToAll(Math.random()) - } - - const saveHandle = async () => { - if (!collection.name) { - setCollectionsError(true) - return - } - if (!metaData.image) { - setImageError(true) - return - } - setLoading(true) - const chosenToken = collection.tokens.find((item) => item.mediaUrl === metaData.image) - const meta = { - ...metaData, - userId: user.userId, - contract: collection.contract, - tokenId: chosenToken?.tokenId ?? '', - } - try { - await PluginPetRPC.setUserAddress(user) - await PluginPetRPC.saveEssay(user.address, meta, user.userId) - closeDialogHandle() - } catch { - showSnackbar(t('plugin_pets_dialog_fail'), { variant: 'error' }) - } finally { - setLoading(false) - } - } - - const onCollectionChange = (v: string) => { - const matched = nfts.find((item) => item.name === v) - if (matched) { - setCollection(matched) - setTokenInfoSelect(null) - setInputTokenName('') - setMetaData({ - ...metaData, - userId: user.userId, - tokenId: '', - image: '', - }) - } - setCollectionsError(false) - } - - const onImageChange = (v: OwnerERC721TokenInfo | null) => { - setTokenInfoSelect(v) - setInputTokenName(v?.name ?? '') - setMetaData({ - ...metaData, - userId: user.userId, - tokenId: v?.tokenId ?? '', - image: v?.mediaUrl ?? '', - type: v?.glbSupport ? ImageType.GLB : ImageType.NORMAL, - }) - setImageError(false) - } - - const setMsgValueCheck = (v: string) => { - if (v.length <= 100) { - setMetaData({ ...metaData, word: v }) - } - } - - const imageChose = useMemo(() => { - if (!metaData.image) return '' - const imageChosen = collection.tokens.find((item) => item.tokenId === metaData.tokenId) - return imageChosen?.mediaUrl - }, [metaData.image, collection.tokens]) - - const renderImg = (address: string) => { - const matched = extraData.find((item) => isSameAddress(item.address, address)) - return + isReady() ? setStep(PetFriendNFTStep.SetFriendNFT) : cancel() } - const paperComponent = (children: ReactNode | undefined) => {children} - - const nftsRender = useMemo(() => { - return ( - onCollectionChange(newValue?.name ?? '')} - getOptionLabel={(option) => option.name} - PopperComponent={ShadowRootPopper} - PaperComponent={({ children }) => paperComponent(children)} - renderOption={(props, option) => ( - - - {renderImg(option.contract)} - {option.name} - - - )} - renderInput={(params) => ( - - )} - /> - ) - }, [nfts, extraData]) - - const tokensRender = useMemo(() => { - return ( - onImageChange(newValue)} - getOptionLabel={(option) => option.name ?? ''} - PaperComponent={({ children }) => paperComponent(children)} - PopperComponent={ShadowRootPopper} - renderOption={(props, option) => ( - - {!option.glbSupport ? : null} - {option.name} - {option.glbSupport ? : null} - - )} - renderInput={(params) => ( - - )} - /> - ) - }, [collection.tokens, tokenInfoSelect]) - return ( - <> - - - - - - - - {nftsRender} - {tokensRender} - setMsgValueCheck(e.target.value)} - onBlur={() => setHolderChange(true)} - onFocus={() => setHolderChange(false)} - /> - - - (petShowSettings.value = e.target.checked)} /> - } - label={t('plugin_pets_dialog_check_title')} - sx={{ marginTop: '4px' }} - /> - - {t('plugin_pets_dialog_btn')} - - - {t('plugin_pets_dialog_created')} - {t('plugin_pets_dialog_powered')} - - - - - + + + {step === PetFriendNFTStep.SetFriendNFT ? ( + + ) : ( + + )} + + ) } diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx new file mode 100644 index 000000000000..d56c28726f8c --- /dev/null +++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PetSetDialog.tsx @@ -0,0 +1,328 @@ +import { useState, useMemo, ReactNode } from 'react' +import { useTimeout } from 'react-use' +import { isSameAddress } from '@masknet/web3-shared-evm' +import { makeStyles, useStylesExtends, useCustomSnackbar } from '@masknet/theme' +import { useValueRef } from '@masknet/shared' +import { + TextField, + Typography, + Box, + Grid, + MenuItem, + Snackbar, + Autocomplete, + FormControlLabel, + Checkbox, +} from '@mui/material' +import { LoadingButton } from '@mui/lab' +import type { Constant } from '@masknet/web3-shared-evm/constants/utils' +import { PluginPetMessages, PluginPetRPC } from '../messages' +import { initMeta, initCollection, GLB3DIcon } from '../constants' +import { PreviewBox } from './PreviewBox' +import { PetMetaDB, FilterContract, OwnerERC721TokenInfo, ImageType } from '../types' +import { useUser, useNFTs, useNFTsExtra } from '../hooks' +import { useI18N } from '../../../utils' +import { ShadowRootPopper } from '../../../utils/shadow-root/ShadowRootComponents' +import { ImageLoader } from './ImageLoader' +import { petShowSettings } from '../settings' + +const useStyles = makeStyles()((theme) => ({ + desBox: { + display: 'flex', + justifyContent: 'space-between', + marginTop: theme.spacing(3), + }, + des: { + color: '#7b8192', + fontSize: '12px', + }, + input: { + margin: theme.spacing(2, 0, 0), + }, + inputOption: { + margin: theme.spacing(4, 0, 0), + }, + inputBorder: { + borderRadius: theme.spacing(1), + padding: theme.spacing(1), + }, + inputArea: { + borderRadius: theme.spacing(1), + padding: theme.spacing(2), + }, + menuItem: { + width: '100%', + }, + btn: { + margin: theme.spacing(8, 0, 2), + }, + thumbnail: { + width: 25, + height: 25, + objectFit: 'cover', + margin: theme.spacing(0, 1, 0, 0), + display: 'inline-block', + borderRadius: 4, + }, + glbIcon: { + width: 15, + height: 18, + marginLeft: theme.spacing(1), + }, + itemFix: { + display: 'flex', + alignItems: 'center', + width: '100%', + }, + itemTxt: { + flex: 1, + marginLeft: theme.spacing(0.5), + }, + prevBox: { + margin: theme.spacing(2, 0, 0), + border: '1px dashed #ccc', + borderRadius: 4, + height: 'calc(100% - 16px)', + boxSizing: 'border-box', + padding: 4, + }, + boxPaper: { + backgroundColor: theme.palette.mode === 'dark' ? '#1B1E38' : '#FFFFFF', + marginBottom: 10, + boxShadow: theme.palette.mode === 'dark' ? '0 0 5px #FFFFFF' : '0 0 5px #CCCCCC', + }, +})) + +interface PetSetDialogProps { + configNFTs: Record | undefined + onClose: () => void +} + +export function PetSetDialog({ configNFTs, onClose }: PetSetDialogProps) { + const { t } = useI18N() + const classes = useStylesExtends(useStyles(), {}) + const { showSnackbar } = useCustomSnackbar() + const [loading, setLoading] = useState(false) + const checked = useValueRef(petShowSettings) + const [isReady, cancel] = useTimeout(2000) + + const user = useUser() + const nfts = useNFTs(user, configNFTs) + const extraData = useNFTsExtra(configNFTs) + const [collection, setCollection] = useState(initCollection) + const [isCollectionsError, setIsCollectionsError] = useState(false) + + const [metaData, setMetaData] = useState(initMeta) + const [isImageError, setIsImageError] = useState(false) + const [isTipShow, setIsTipShow] = useState(false) + const [holderChange, setHolderChange] = useState(true) + const [tokenInfoSelect, setTokenInfoSelect] = useState(null) + const [inputTokenName, setInputTokenName] = useState('') + + const closeDialogHandle = () => { + setIsTipShow(true) + onClose() + isReady() ? setIsTipShow(false) : cancel() + PluginPetMessages.events.setResult.sendToAll(Math.random()) + } + + const saveHandle = async () => { + if (!collection.name) { + setIsCollectionsError(true) + return + } + if (!metaData.image) { + setIsImageError(true) + return + } + setLoading(true) + const chosenToken = collection.tokens.find((item) => item.mediaUrl === metaData.image) + const meta = { + ...metaData, + userId: user.userId, + contract: collection.contract, + tokenId: chosenToken?.tokenId ?? '', + } + try { + await PluginPetRPC.setUserAddress(user) + await PluginPetRPC.saveEssay(user.address, meta, user.userId) + closeDialogHandle() + } catch { + showSnackbar(t('plugin_pets_dialog_fail'), { variant: 'error' }) + } finally { + setLoading(false) + } + } + + const onCollectionChange = (v: string) => { + const matched = nfts.find((item) => item.name === v) + if (matched) { + setCollection(matched) + setTokenInfoSelect(null) + setInputTokenName('') + setMetaData({ + ...metaData, + userId: user.userId, + tokenId: '', + image: '', + }) + } + setIsCollectionsError(false) + } + + const onImageChange = (v: OwnerERC721TokenInfo | null) => { + setTokenInfoSelect(v) + setInputTokenName(v?.name ?? '') + setMetaData({ + ...metaData, + userId: user.userId, + tokenId: v?.tokenId ?? '', + image: v?.mediaUrl ?? '', + type: v?.glbSupport ? ImageType.GLB : ImageType.NORMAL, + }) + setIsImageError(false) + } + + const setMsgValueCheck = (v: string) => { + if (v.length <= 100) { + setMetaData({ ...metaData, word: v }) + } + } + + const imageChose = useMemo(() => { + if (!metaData.image) return '' + const imageChosen = collection.tokens.find((item) => item.tokenId === metaData.tokenId) + return imageChosen?.mediaUrl + }, [metaData.image, collection.tokens]) + + const renderImg = (address: string) => { + const matched = extraData.find((item) => isSameAddress(item.address, address)) + return + } + + const paperComponent = (children: ReactNode | undefined) => {children} + + const nftsRender = useMemo(() => { + return ( + onCollectionChange(newValue?.name ?? '')} + getOptionLabel={(option) => option.name} + PopperComponent={ShadowRootPopper} + PaperComponent={({ children }) => paperComponent(children)} + renderOption={(props, option) => ( + + + {renderImg(option.contract)} + {option.name} + + + )} + renderInput={(params) => ( + + )} + /> + ) + }, [nfts, extraData]) + + const tokensRender = useMemo(() => { + return ( + onImageChange(newValue)} + getOptionLabel={(option) => option.name ?? ''} + PaperComponent={({ children }) => paperComponent(children)} + PopperComponent={ShadowRootPopper} + renderOption={(props, option) => ( + + {!option.glbSupport ? : null} + {option.name} + {option.glbSupport ? : null} + + )} + renderInput={(params) => ( + + )} + /> + ) + }, [collection.tokens, tokenInfoSelect]) + + return ( + <> + + + + + + + {nftsRender} + {tokensRender} + setMsgValueCheck(e.target.value)} + onBlur={() => setHolderChange(true)} + onFocus={() => setHolderChange(false)} + /> + + + (petShowSettings.value = e.target.checked)} /> + } + label={t('plugin_pets_dialog_check_title')} + sx={{ marginTop: '4px' }} + /> + + {t('plugin_pets_dialog_btn')} + + + {t('plugin_pets_dialog_created')} + {t('plugin_pets_dialog_powered')} + + + + + ) +} diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/PetShareDialog.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PetShareDialog.tsx new file mode 100644 index 000000000000..c717d2599246 --- /dev/null +++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PetShareDialog.tsx @@ -0,0 +1,53 @@ +import { useCallback } from 'react' +import { makeStyles } from '@masknet/theme' +import { Button, Typography, Box } from '@mui/material' +import { activatedSocialNetworkUI } from '../../../social-network' +import { useI18N } from '../../../utils' +import { Share_Twitter_TXT } from '../constants' + +const useStyles = makeStyles()((theme) => ({ + root: { + margin: theme.spacing(0, 2.5), + }, + shareNotice: { + color: '#222', + fontSize: '16px', + fontFamily: 'TwitterChirp', + lineHeight: '16px', + marginTop: theme.spacing(2), + }, + shareButton: { + width: '100%', + margin: theme.spacing(4, 0, 3), + }, +})) + +interface PetSetDialogProps { + onClose: () => void +} + +export function PetShareDialog({ onClose }: PetSetDialogProps) { + const { t } = useI18N() + const { classes } = useStyles() + const shareLink = activatedSocialNetworkUI.utils.getShareLinkURL?.('') + + const onShareClick = useCallback(() => { + if (shareLink) { + const search = '?text=' + Share_Twitter_TXT + shareLink.search = search + shareLink.href = shareLink.origin + shareLink.pathname + shareLink.search + const share = shareLink.toString() + window.open(share, '_blank', 'noopener noreferrer') + } + onClose() + }, [shareLink]) + + return ( + + {t('plugin_pets_dialog_success')} + + + ) +} diff --git a/packages/mask/src/plugins/Pets/assets/punk2d.png b/packages/mask/src/plugins/Pets/assets/punk2d.png index 6df747db6bf0..097162e6d70f 100644 Binary files a/packages/mask/src/plugins/Pets/assets/punk2d.png and b/packages/mask/src/plugins/Pets/assets/punk2d.png differ diff --git a/packages/mask/src/plugins/Pets/constants.ts b/packages/mask/src/plugins/Pets/constants.ts index ca7a2719a584..9e16706aabc9 100644 --- a/packages/mask/src/plugins/Pets/constants.ts +++ b/packages/mask/src/plugins/Pets/constants.ts @@ -6,7 +6,8 @@ export const PetsPluginID = 'com.maskbook.pets' export const TWITTER = 'twitter.com' export const MASK_TWITTER = 'realMaskNetwork' export const NFTS_CONFIG_ADDRESS = '0x500cF2ea6755ea35eAC8727A70D997ecc3d26258' -export const DEFAULT_SET_WORD = 'Click the Mask icon to the left and set up your Non-Fungible Friend in "NFTs" tab!' +export const DEFAULT_SET_WORD = + 'Click the wallet icon on the left side of the page and set up your Non-Fungible Friend in "NFTs" tab!' export const DEFAULT_PUNK_MASK_WORD = "I'm CryptoPunk #6128... in 3D & with a body! Voyagers, welcome to the uncharted waters of WEB3!" @@ -34,6 +35,11 @@ export const Punk3D = { }), } +export const Share_Twitter = 'https://twitter.com/realMaskNetwork/status/1486648872558424064' +export const Share_Twitter_TXT = encodeURIComponent( + `I just set up my Non-Fungible Friend with @realMaskNetwork (powered by @MintTeamNFT). Visit my profile to check it out! Install Mask Network extension from mask.io and set yours.\n #mask_io #nonfungiblefriends\n${Share_Twitter}`, +) + export const GLB3DIcon = new URL('./assets/glb3D.png', import.meta.url).toString() export const CloseIcon = new URL('./assets/close.png', import.meta.url).toString() export const DragIcon = new URL('./assets/drag.png', import.meta.url).toString() diff --git a/packages/mask/src/plugins/Pets/hooks/useEssay.ts b/packages/mask/src/plugins/Pets/hooks/useEssay.ts index 95ef0c3ef469..3b123a876b94 100644 --- a/packages/mask/src/plugins/Pets/hooks/useEssay.ts +++ b/packages/mask/src/plugins/Pets/hooks/useEssay.ts @@ -4,6 +4,7 @@ import { useAsync } from 'react-use' import { PluginPetRPC } from '../messages' import type { User } from '../types' import { Punk3D, DEFAULT_SET_WORD, MASK_TWITTER, DEFAULT_PUNK_MASK_WORD, PunkIcon } from './../constants' +import { useUser } from './useUser' export function useEssay(user: User, refresh?: number) { return useAsync(async () => { @@ -15,16 +16,19 @@ export function useEssay(user: User, refresh?: number) { export function useDefaultEssay(user: User) { const [essayMeta, setEssayMeta] = useState(undefined) + const profileUser = useUser() useEffect(() => { if (user?.userId || user?.userId !== '$unknown') { + const isProfile = user.userId === profileUser.userId + const isMASK = user.userId === MASK_TWITTER setEssayMeta({ - image: user.userId === MASK_TWITTER ? Punk3D.url : PunkIcon, - word: user.userId === MASK_TWITTER ? DEFAULT_PUNK_MASK_WORD : DEFAULT_SET_WORD, - type: user.userId === MASK_TWITTER ? ImageType.GLB : ImageType.NORMAL, + image: isMASK ? Punk3D.url : isProfile ? PunkIcon : '', + word: isMASK ? DEFAULT_PUNK_MASK_WORD : DEFAULT_SET_WORD, + type: isMASK ? ImageType.GLB : ImageType.NORMAL, }) } else { setEssayMeta(undefined) } - }, [user]) + }, [user, profileUser]) return essayMeta }