diff --git a/packages/dashboard/src/API.tsx b/packages/dashboard/src/API.tsx
index 75f61c874754..53593349d6f1 100644
--- a/packages/dashboard/src/API.tsx
+++ b/packages/dashboard/src/API.tsx
@@ -20,6 +20,7 @@ export interface PluginMessages extends DashboardPluginMessages {
Wallet: typeof WalletMessages
Transak: typeof import('../../mask/dist/src/plugins/Transak/messages').PluginTransakMessages
Swap: typeof import('../../mask/dist/src/plugins/Trader/messages').PluginTraderMessages
+ Pets: typeof import('../../mask/dist/src/plugins/Pets/messages').PluginPetMessages
}
export function setService(rpc: any) {
Services = rpc
diff --git a/packages/dashboard/src/initialization/isolated_bridge.ts b/packages/dashboard/src/initialization/isolated_bridge.ts
index 697171536267..4924785e599c 100644
--- a/packages/dashboard/src/initialization/isolated_bridge.ts
+++ b/packages/dashboard/src/initialization/isolated_bridge.ts
@@ -24,7 +24,8 @@ function installPluginService() {
const Wallet = channelOf('com.maskbook.wallet')
const Transak = channelOf('com.maskbook.transak')
const Swap = channelOf('com.maskbook.trader')
- setPluginMessages({ Wallet, Transak, Swap })
+ const Pets = channelOf('com.maskbook.pets')
+ setPluginMessages({ Wallet, Transak, Swap, Pets })
setPluginServices({
Wallet: initRPCBridge(PluginMessages.Wallet.events.rpc),
Swap: initRPCBridge(PluginMessages.Swap.rpc),
diff --git a/packages/dashboard/src/locales/en-US.json b/packages/dashboard/src/locales/en-US.json
index 119b69668b6b..be4df90a9c6e 100644
--- a/packages/dashboard/src/locales/en-US.json
+++ b/packages/dashboard/src/locales/en-US.json
@@ -349,8 +349,8 @@
"labs_settings_market_trend_source": "Default Trending Source",
"labs_settings_swap": "Swap Settings",
"labs_settings_swap_network": "{{network}} Network Default Trading Source",
- "labs_pets": "LootMan by MintTeam",
- "labs_pets_desc": "Explore the endless possibilities of NFTs. Link and display your NFTs on social media in a revolutionized way.",
+ "labs_pets": "Non-Fungible Friends by Mint Team",
+ "labs_pets_desc": "Explore the endless possibilities of NFTs.",
"labs_setup_tutorial": "Setup Tutorial",
"labs_do_not_show_again": "Don't show again.",
"labs_cryptoartai": "CRYPTOART.AI",
diff --git a/packages/dashboard/src/pages/Labs/index.tsx b/packages/dashboard/src/pages/Labs/index.tsx
index eb3b78468ba1..02d16f0c1e76 100644
--- a/packages/dashboard/src/pages/Labs/index.tsx
+++ b/packages/dashboard/src/pages/Labs/index.tsx
@@ -198,6 +198,8 @@ export default function Plugins() {
const { openDialog: openSwapDialog } = useRemoteControlledDialog(PluginMessages.Swap.swapDialogUpdated)
+ const { openDialog: openEssayDialog } = useRemoteControlledDialog(PluginMessages.Pets.events.essayDialogUpdated)
+
async function onSwitch(id: string, checked: boolean) {
await Services.Settings.setPluginMinimalModeEnabled(id, !checked)
setPluginStatus({ ...pluginStatus, [id]: checked })
@@ -234,13 +236,14 @@ export default function Plugins() {
const search = new URLSearchParams(location.search)
const open = search.get('open')
const code = search.get('code')
-
if (open === 'Transak') {
openTransakDialog(code ?? '')
} else if (open === 'Swap') {
openSwapDialog()
+ } else if (open === 'Pets') {
+ openEssayDialog()
}
- }, [location.search, openTransakDialog, openSwapDialog])
+ }, [location.search, openTransakDialog, openSwapDialog, openEssayDialog])
return (
}>
diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json
index 31f92a670094..7a684617bab1 100644
--- a/packages/mask/shared-ui/locales/en-US.json
+++ b/packages/mask/shared-ui/locales/en-US.json
@@ -881,31 +881,17 @@
"plugin_furucombo_smt_wrong": "Something went wrong!",
"plugin_furucombo_head_pools": "Pools",
"plugin_furucombo_head_action": "Action",
- "plugin_pets_loot_properties": "Loot Properties:",
- "plugin_pets_get_nft_shows": "Get your NFTshow and News:",
- "plugin_pets_loot_des_0": "What’s up buddy! How’s everything going?",
- "plugin_pets_loot_des_1": "Good to see you! I've been waiting awhile, how can I help you?",
- "plugin_pets_loot_des_2": "I am the LOOTER from Mask, wanna join for fun? Line up!",
- "plugin_pets_loot_des_3": "Beep! Beep! Clicking unavailable for now... Come later!",
- "plugin_pets_loot_des_4": "Mask, Mask, I love Mask!",
- "plugin_pets_loot_des_5": "I wanna go for a ride, but code won’t let me...",
- "plugin_pets_loot_des_6": "Go talk with Suji! I wanna hangout for Worlds!",
- "plugin_pets_loot_des_7": "Did you know that you are the boss of Mask?",
- "plugin_pets_loot_des_8": "Heard the news? You guys are almost available for NFT show!",
- "plugin_pets_loot_des_9": "Don’t enjoy my company? Click the “X” on the upper right.",
- "plugin_pets_loot_des_10": "Check out my equipment by clicking “i”",
- "plugin_pets_loot_des_11": "I am Loot #3870.",
- "plugin_pets_loot_des_12": "How’s your day?",
- "plugin_pets_loot_des_13": "I wonder what the real world is like…",
- "plugin_pets_loot_des_14": "Where am I from? Metaverse.",
- "plugin_pets_loot_info_0": "Falchion",
- "plugin_pets_loot_info_1": "Chain Mail",
- "plugin_pets_loot_info_2": "Hood",
- "plugin_pets_loot_info_3": "Leather Belt",
- "plugin_pets_loot_info_4": "Chain Boots",
- "plugin_pets_loot_info_5": "Chain Gloves",
- "plugin_pets_loot_info_6": "Amulet of the Twins",
- "plugin_pets_loot_info_7": "Gold Ring",
+ "plugin_pets_dialog_title": "Non-Fungible Friends",
+ "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_preview": "Preview",
+ "plugin_pets_dialog_created": "Created by MintTeam",
+ "plugin_pets_dialog_powered": "Powered by RSS3",
+ "plugin_pets_dialog_success": "Your Non-Fungible Friend has been set up successfully",
+ "plugin_pets_dialog_fail": "Setting failed, please try later",
"popups_following_permissions": "The plugin ({{pluginName}}) (hosted on {{pluginURL}}) is going to request the following permissions:",
"popups_permissions": "Permissions",
"popups_sites": "Sites",
diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json
index 897890047018..1b88bd8c74af 100644
--- a/packages/mask/shared-ui/locales/qya-AA.json
+++ b/packages/mask/shared-ui/locales/qya-AA.json
@@ -865,29 +865,6 @@
"plugin_furucombo_head_action": "crwdns9131:0crwdne9131:0",
"plugin_pets_loot_properties": "crwdns10265:0crwdne10265:0",
"plugin_pets_get_nft_shows": "crwdns10267:0crwdne10267:0",
- "plugin_pets_loot_des_0": "crwdns9401:0crwdne9401:0",
- "plugin_pets_loot_des_1": "crwdns9403:0crwdne9403:0",
- "plugin_pets_loot_des_2": "crwdns9405:0crwdne9405:0",
- "plugin_pets_loot_des_3": "crwdns9407:0crwdne9407:0",
- "plugin_pets_loot_des_4": "crwdns9409:0crwdne9409:0",
- "plugin_pets_loot_des_5": "crwdns9411:0crwdne9411:0",
- "plugin_pets_loot_des_6": "crwdns9413:0crwdne9413:0",
- "plugin_pets_loot_des_7": "crwdns9415:0crwdne9415:0",
- "plugin_pets_loot_des_8": "crwdns9417:0crwdne9417:0",
- "plugin_pets_loot_des_9": "crwdns9419:0crwdne9419:0",
- "plugin_pets_loot_des_10": "crwdns9421:0crwdne9421:0",
- "plugin_pets_loot_des_11": "crwdns9423:0crwdne9423:0",
- "plugin_pets_loot_des_12": "crwdns9425:0crwdne9425:0",
- "plugin_pets_loot_des_13": "crwdns9427:0crwdne9427:0",
- "plugin_pets_loot_des_14": "crwdns9429:0crwdne9429:0",
- "plugin_pets_loot_info_0": "crwdns9431:0crwdne9431:0",
- "plugin_pets_loot_info_1": "crwdns9433:0crwdne9433:0",
- "plugin_pets_loot_info_2": "crwdns9435:0crwdne9435:0",
- "plugin_pets_loot_info_3": "crwdns9437:0crwdne9437:0",
- "plugin_pets_loot_info_4": "crwdns9439:0crwdne9439:0",
- "plugin_pets_loot_info_5": "crwdns9441:0crwdne9441:0",
- "plugin_pets_loot_info_6": "crwdns9443:0crwdne9443:0",
- "plugin_pets_loot_info_7": "crwdns9445:0crwdne9445:0",
"popups_following_permissions": "crwdns10271:0{{pluginName}}crwdnd10271:0{{pluginURL}}crwdne10271:0",
"popups_permissions": "crwdns10281:0crwdne10281:0",
"popups_sites": "crwdns10283:0crwdne10283:0",
diff --git a/packages/mask/src/components/shared/ApplicationBoard.tsx b/packages/mask/src/components/shared/ApplicationBoard.tsx
index fceeed9cf73d..63f2e447b7d0 100644
--- a/packages/mask/src/components/shared/ApplicationBoard.tsx
+++ b/packages/mask/src/components/shared/ApplicationBoard.tsx
@@ -9,6 +9,7 @@ import { useControlledDialog } from '../../utils/hooks/useControlledDialog'
import { RedPacketPluginID } from '../../plugins/RedPacket/constants'
import { ITO_PluginID } from '../../plugins/ITO/constants'
import { PluginTransakMessages } from '../../plugins/Transak/messages'
+import { PluginPetMessages } from '../../plugins/Pets/messages'
import { ClaimAllDialog } from '../../plugins/ITO/SNSAdaptor/ClaimAllDialog'
import { EntrySecondLevelDialog } from './EntrySecondLevelDialog'
import { NetworkTab } from './NetworkTab'
@@ -149,6 +150,10 @@ export function ApplicationBoard({ secondEntries, secondEntryChainTabs }: MaskAp
const { setDialog: setBuyDialog } = useRemoteControlledDialog(PluginTransakMessages.buyTokenDialogUpdated)
// #endregion
+ // #region pet friends
+ const { setDialog: setPetDialog } = useRemoteControlledDialog(PluginPetMessages.events.essayDialogUpdated)
+ // #endregion
+
// #region second level entry dialog
const {
open: isSecondLevelEntryDialogOpen,
@@ -284,6 +289,14 @@ export function ApplicationBoard({ secondEntries, secondEntryChainTabs }: MaskAp
undefined,
true,
),
+ createEntry(
+ 'Non-F Friends',
+ new URL('./assets/mintTeam.png', import.meta.url).toString(),
+ () => setPetDialog({ open: true }),
+ [ChainId.Mainnet],
+ currentChainId !== ChainId.Mainnet,
+ true,
+ ),
],
undefined,
),
diff --git a/packages/mask/src/components/shared/assets/mintTeam.png b/packages/mask/src/components/shared/assets/mintTeam.png
new file mode 100644
index 000000000000..8e1e462fab4b
Binary files /dev/null and b/packages/mask/src/components/shared/assets/mintTeam.png differ
diff --git a/packages/mask/src/extension/dashboard/index.tsx b/packages/mask/src/extension/dashboard/index.tsx
index f951e277033e..ccf5e5bc9fc2 100644
--- a/packages/mask/src/extension/dashboard/index.tsx
+++ b/packages/mask/src/extension/dashboard/index.tsx
@@ -4,6 +4,7 @@ import Services from '../service'
import { WalletRPC, WalletMessages } from '../../plugins/Wallet/messages'
import { PluginTransakMessages } from '../../plugins/Transak/messages'
import { PluginTraderMessages, PluginTraderRPC } from '../../plugins/Trader/messages'
+import { PluginPetMessages } from '../../plugins/Pets/messages'
import { MaskMessages } from '../../utils/messages'
import { startPluginDashboard } from '@masknet/plugin-infra'
import { createPluginHost } from '../../plugin-infra/host'
@@ -16,6 +17,7 @@ const msg: DashboardPluginMessages = {
Wallet: WalletMessages,
Swap: PluginTraderMessages,
Transak: PluginTransakMessages,
+ Pets: PluginPetMessages,
}
const rpc: DashboardPluginServices = {
Wallet: WalletRPC,
diff --git a/packages/mask/src/plugins/NextID/components/NextIdPage.tsx b/packages/mask/src/plugins/NextID/components/NextIdPage.tsx
index 4ae6a1e20b46..f1b642315a67 100644
--- a/packages/mask/src/plugins/NextID/components/NextIdPage.tsx
+++ b/packages/mask/src/plugins/NextID/components/NextIdPage.tsx
@@ -66,10 +66,10 @@ export function NextIdPage({}: NextIDPageProps) {
return Services.Helper.queryExistedBinding(currentPersona.identifier)
}
- if (!visitingPersonaIdentifier) return Promise.resolve(null)
+ if (!visitingPersonaIdentifier) return null
const visitingPersona = await Services.Identity.queryPersonaByProfile(visitingPersonaIdentifier.identifier)
- if (!visitingPersona) return Promise.resolve(null)
+ if (!visitingPersona) return null
return Services.Helper.queryExistedBinding(visitingPersona.identifier)
}, [currentPersona, count, visitingPersonaIdentifier])
diff --git a/packages/mask/src/plugins/NextID/hooks/useWalletSign.ts b/packages/mask/src/plugins/NextID/hooks/useWalletSign.ts
index e104d607eddf..9c55d608ff62 100644
--- a/packages/mask/src/plugins/NextID/hooks/useWalletSign.ts
+++ b/packages/mask/src/plugins/NextID/hooks/useWalletSign.ts
@@ -9,7 +9,7 @@ export const useWalletSign = (message?: string, address?: string) => {
const [state, fn] = useAsyncFn(
async (changed: boolean) => {
- if (changed) return Promise.resolve(undefined)
+ if (changed) return undefined
if (!address || !message) return
try {
showSnackbar(t.notify_wallet_sign(), { processing: true, message: t.notify_wallet_sign_confirm() })
diff --git a/packages/mask/src/plugins/Pets/Dashboard/index.tsx b/packages/mask/src/plugins/Pets/Dashboard/index.tsx
new file mode 100644
index 000000000000..dae4781ed9ac
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/Dashboard/index.tsx
@@ -0,0 +1,13 @@
+import type { Plugin } from '@masknet/plugin-infra'
+import { base } from '../base'
+import { PetDialog } from '../SNSAdaptor/PetDialog'
+
+const dashboard: Plugin.Dashboard.Definition = {
+ ...base,
+ init() {},
+ GlobalInjection: function Component() {
+ return
+ },
+}
+
+export default dashboard
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/Animate.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/Animate.tsx
new file mode 100644
index 000000000000..c8493f09a8c1
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/Animate.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react'
+import { makeStyles, useStylesExtends } from '@masknet/theme'
+import { useEssay, useDefaultEssay, useCurrentVisitingUser } from '../hooks'
+import { ModelNFT } from './ModelNFT'
+import { NormalNFT } from './NormalNFT'
+import { ImageType } from '../types'
+import { PluginPetMessages } from '../messages'
+
+const useStyles = makeStyles()(() => ({
+ root: {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ },
+}))
+
+const AnimatePic = () => {
+ const classes = useStylesExtends(useStyles(), {})
+
+ const [start, setStart] = useState(true)
+ const [refresh, setRefresh] = useState(0)
+
+ const visitor = useCurrentVisitingUser(refresh)
+ const visitorMeta = useEssay(visitor, refresh)
+ const defMeta = useDefaultEssay(visitor)
+ const showMeta = visitorMeta ?? defMeta
+
+ const [hidden, setHidden] = useState(false)
+ const [infoShow, setInfoShow] = useState(false)
+
+ const handleClose = () => setHidden(true)
+ const handleMouseEnter = () => setInfoShow(true)
+ const handleMouseLeave = () => setInfoShow(false)
+
+ useEffect(() => {
+ const refreshHandle = async (data: number) => setRefresh(data)
+ PluginPetMessages.events.setResult.on(refreshHandle)
+ let count = 0
+ const timer = setInterval(() => {
+ const check = count % 9 < 5
+ setStart(check)
+ count += 1
+ }, 1000)
+ return () => {
+ clearInterval(timer)
+ PluginPetMessages.events.setResult.off(refreshHandle)
+ }
+ }, [])
+ if (!visitor.userId || visitor.userId === '$unknown' || hidden || !showMeta?.image) return null
+ return (
+
+ {showMeta.type === ImageType.GLB ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default AnimatePic
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/drag.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/Drag.tsx
similarity index 76%
rename from packages/mask/src/plugins/Pets/SNSAdaptor/drag.tsx
rename to packages/mask/src/plugins/Pets/SNSAdaptor/Drag.tsx
index f1888cb53886..70ff082eceb5 100644
--- a/packages/mask/src/plugins/Pets/SNSAdaptor/drag.tsx
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/Drag.tsx
@@ -9,17 +9,28 @@ interface StateProps {
rel: any
}
-const contentWidth = 96
+const contentWidth = 150
const contentHeight = 150
const startPosition = {
x: 50,
y: 150,
}
-class Draggable extends React.PureComponent {
+
+interface DraggableProps {
+ baseWidth?: number
+ baseHeight?: number
+ moveHandle?: (x: number, y: number) => void
+}
+
+class Draggable extends React.PureComponent {
ref = React.createRef()
mouseMoveFuc = this.onMouseMove.bind(this)
mouseUpFuc = this.onMouseUp.bind(this)
+ static defaultProps = {
+ transferMsg: (x: number, y: number) => {},
+ }
+
override state: StateProps = {
pos: {
x: startPosition.x,
@@ -56,10 +67,13 @@ class Draggable extends React.PureComponent {
if (!this.state.dragging) return
this.setState({
pos: {
- x: window.innerWidth - contentWidth - (e.pageX - this.state.rel.x),
- y: window.innerHeight - contentHeight - (e.pageY - this.state.rel.y),
+ x: window.innerWidth - (this.props.baseWidth || contentWidth) - (e.pageX - this.state.rel.x),
+ y: window.innerHeight - (this.props.baseHeight || contentHeight) - (e.pageY - this.state.rel.y),
},
})
+ if (this.props.moveHandle) {
+ this.props.moveHandle(this.state.pos.x, this.state.pos.y)
+ }
e.stopPropagation()
e.preventDefault()
}
@@ -77,7 +91,6 @@ class Draggable extends React.PureComponent {
document.removeEventListener('mousemove', this.mouseMoveFuc)
document.removeEventListener('mouseup', this.mouseUpFuc)
}
-
override render() {
return (
{this.props.children || null}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/ImageLoader.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/ImageLoader.tsx
new file mode 100644
index 000000000000..2ad992cd6ae5
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/ImageLoader.tsx
@@ -0,0 +1,21 @@
+import { CircularProgress } from '@mui/material'
+import { ImgHTMLAttributes, useState, SyntheticEvent } from 'react'
+import { DefaultIcon } from '../constants'
+
+export function ImageLoader(props: ImgHTMLAttributes) {
+ const [loaded, setLoaded] = useState(false)
+ const onErrorHandle = (event: SyntheticEvent) => {
+ event.currentTarget.src = DefaultIcon
+ }
+ return (
+ <>
+
setLoaded(Boolean(props.src))}
+ onError={onErrorHandle}
+ style={{ display: loaded || !props.src ? 'block' : 'none' }}
+ />
+ {!loaded && Boolean(props.src) ? : null}
+ >
+ )
+}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/ModelNFT.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/ModelNFT.tsx
new file mode 100644
index 000000000000..031487040446
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/ModelNFT.tsx
@@ -0,0 +1,83 @@
+import { useState } from 'react'
+import { makeStyles, useStylesExtends } from '@masknet/theme'
+import { Box } from '@mui/material'
+import classNames from 'classnames'
+import Drag from './Drag'
+import ModelView from './ModelView'
+import { useStyles as boxUseStyles } from './PreviewBox'
+import { DragIcon } from '../constants'
+import type { ShowMeta } from '../types'
+
+const useStyles = makeStyles()(() => ({
+ dragContent: {
+ position: 'absolute',
+ bottom: -25,
+ right: 0,
+ },
+ dragImg: {
+ width: 15,
+ height: 15,
+ },
+ wordContent: {
+ position: 'absolute',
+ left: 12,
+ bottom: 150,
+ },
+ word: {
+ width: '152px !important',
+ maxHeight: 85,
+ },
+ glbView: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ width: '100%',
+ height: '100%',
+ },
+}))
+
+interface ModelNFTProps {
+ start: boolean
+ showMeta: ShowMeta | undefined
+}
+
+export function ModelNFT(props: ModelNFTProps) {
+ const { start, showMeta } = props
+ const classes = useStylesExtends(useStyles(), {})
+ const boxClasses = useStylesExtends(boxUseStyles(), {})
+ const [position, setPosition] = useState({ x: 50, y: 150 })
+ const moveHandle = (x: number, y: number) => {
+ setPosition({ x, y })
+ }
+
+ return (
+
+
+
+
+

+
+ {start && showMeta?.word ? (
+
+
+ {showMeta?.word}
+
+
+ ) : null}
+
+
+ )
+}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/ModelView.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/ModelView.tsx
new file mode 100644
index 000000000000..6816c115800a
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/ModelView.tsx
@@ -0,0 +1,68 @@
+import { FC, HTMLProps, useMemo } from 'react'
+import { AssetPlayer } from '@masknet/shared'
+import { makeStyles } from '@masknet/theme'
+
+interface ModelViewProps extends HTMLProps {
+ source: string
+}
+
+const useStyles = makeStyles()((theme) => ({
+ body: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ player: {
+ maxWidth: '90%',
+ maxHeight: '90%',
+ border: 'none',
+ },
+ errorPlaceholder: {
+ padding: '82px 0',
+ backgroundColor: theme.palette.background.default,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 12,
+ width: '100%',
+ },
+ loadingPlaceholder: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ padding: '74px 0',
+ },
+ loadingIcon: {
+ width: 36,
+ height: 52,
+ },
+ errorIcon: {
+ width: 36,
+ height: 36,
+ },
+ iframe: {
+ minHeight: 1,
+ minWidth: 1,
+ },
+}))
+
+const ModelView: FC = ({ source, ...rest }) => {
+ const { classes } = useStyles()
+ return useMemo(() => {
+ if (!source) return null
+ return (
+
+ )
+ }, [source])
+}
+
+export default ModelView
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/NormalNFT.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/NormalNFT.tsx
new file mode 100644
index 000000000000..c0c2b93a862c
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/NormalNFT.tsx
@@ -0,0 +1,99 @@
+import { makeStyles, useStylesExtends } from '@masknet/theme'
+import { Box } from '@mui/material'
+import { useStyles as useBoxStyles } from './PreviewBox'
+import classNames from 'classnames'
+import Drag from './Drag'
+import type { ShowMeta } from '../types'
+import { CloseIcon } from '../constants'
+
+const useStyles = makeStyles()(() => ({
+ root: {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ },
+ imgContent: {
+ zIndex: 999,
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ imgBox: {
+ width: '80%',
+ height: '80%',
+ textAlign: 'center',
+ },
+ close: {
+ width: 15,
+ height: 15,
+ cursor: 'pointer',
+ backgroundSize: 'contain',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ },
+ wordContent: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ wordBox: {
+ position: 'absolute',
+ maxWidth: 150,
+ bottom: 150,
+ },
+ dragImg: {
+ width: 15,
+ height: 15,
+ },
+}))
+
+interface NormalNFTProps {
+ start: boolean
+ infoShow: boolean
+ showMeta: ShowMeta | undefined
+ handleClose: () => void
+}
+
+export function NormalNFT(props: NormalNFTProps) {
+ const { start, infoShow, showMeta, handleClose } = props
+ const classes = useStylesExtends(useStyles(), {})
+ const boxClasses = useStylesExtends(useBoxStyles(), {})
+
+ return (
+
+ {start && showMeta?.word ? (
+
+
+ {showMeta?.word}
+
+
+ ) : null}
+
+
+

+
+
+ {infoShow ? (
+
+ ) : null}
+
+ )
+}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx
new file mode 100644
index 000000000000..659d3b629886
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PetDialog.tsx
@@ -0,0 +1,326 @@
+import { useEffect, useState, useMemo, ReactNode } from 'react'
+import { useAsync } from 'react-use'
+import { useRemoteControlledDialog } 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 } from '@mui/material'
+import { LoadingButton } from '@mui/lab'
+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'
+
+const useStyles = makeStyles()((theme) => ({
+ desBox: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ },
+ 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, 4),
+ },
+ 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',
+ },
+}))
+
+export function PetDialog() {
+ const { t } = useI18N()
+ const classes = useStylesExtends(useStyles(), {})
+ const { showSnackbar } = useCustomSnackbar()
+ const { open, closeDialog } = useRemoteControlledDialog(PluginPetMessages.events.essayDialogUpdated, () => {})
+ const [configNFTs, setConfigNFTs] = useState | undefined>(undefined)
+ const [loading, setLoading] = useState(false)
+
+ 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('')
+
+ useAsync(async () => {
+ setConfigNFTs(await PluginPetRPC.getConfigEssay())
+ }, [])
+
+ useEffect(() => {
+ if (open) return
+ setMetaData(initMeta)
+ setCollection(initCollection)
+ setTokenInfoSelect(null)
+ setInputTokenName('')
+ }, [open])
+
+ let timer: NodeJS.Timeout | null = null
+ const closeDialogHandle = () => {
+ setTipShow(true)
+ 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
+ }
+
+ 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) => (
+
+ )}
+ 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)}
+ />
+
+
+
+
+ {t('plugin_pets_dialog_btn')}
+
+
+ {t('plugin_pets_dialog_created')}
+ {t('plugin_pets_dialog_powered')}
+
+
+
+
+ >
+ )
+}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/PreviewBox.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/PreviewBox.tsx
new file mode 100644
index 000000000000..f4f3267f1d31
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/PreviewBox.tsx
@@ -0,0 +1,152 @@
+import { makeStyles, useStylesExtends } from '@masknet/theme'
+import { Typography } from '@mui/material'
+import classNames from 'classnames'
+import { useI18N } from '../../../utils'
+import type { OwnerERC721TokenInfo } from '../types'
+import ModelView from './ModelView'
+
+export const useStyles = makeStyles()((theme) => ({
+ box: {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 'calc(100% - 16px)',
+ border: '1px dashed #bbb',
+ borderRadius: '4px',
+ marginTop: '16px',
+ boxSizing: 'border-box',
+ padding: '8px',
+ overflow: 'hidden',
+ },
+ msgBox: {
+ width: '80%',
+ backgroundColor: '#fff',
+ borderRadius: '12px',
+ boxShadow: '0 0 8px #ddd',
+ opacity: 1,
+ pointerEvents: 'none',
+ transition: 'all 200ms',
+ padding: '12px',
+ fontSize: '12px',
+ lineHeight: '16px',
+ color: '#222',
+ textAlign: 'center',
+ marginBottom: '12px',
+ '&::before': {
+ content: '""',
+ width: '8px',
+ height: '8px',
+ backgroundColor: '#fff',
+ position: 'absolute',
+ bottom: '-4px',
+ left: '50%',
+ boxShadow: '3px 3px 6px #ccc',
+ transform: 'translateX(-50%) rotate(45deg)',
+ },
+
+ '@keyframes word-show': {
+ '0%': {
+ opacity: '0',
+ transform: 'scale3d(1, 1, 1)',
+ },
+ '30%': {
+ transform: 'scale3d(1.25, 0.75, 1)',
+ },
+ '40%': {
+ transform: 'scale3d(0.75, 1.25, 1)',
+ },
+ '50%': {
+ transform: 'scale3d(1.15, 0.85, 1)',
+ },
+ '65%': {
+ transform: 'scale3d(0.95, 1.05, 1)',
+ },
+ '75%': {
+ transform: 'scale3d(1.05, 0.95, 1)',
+ },
+ '100%': {
+ transform: 'scale3d(1, 1, 1)',
+ },
+ },
+ },
+ wordShow: {
+ animation: 'word-show 0.9s both;',
+ fontSize: '12px',
+ fontFamily: 'TwitterChirp',
+ lineHeight: '16px',
+ color: '#222',
+ textAlign: 'left',
+ overflowWrap: 'break-word',
+ },
+ image: {
+ borderRadius: '4px',
+ width: '100%',
+ opacity: '0',
+ transition: 'all 200ms',
+ '@keyframes image-show': {
+ '0%': {
+ opacity: '0',
+ },
+ '100%': {
+ opacity: '1',
+ },
+ },
+ animation: 'image-show 0.4s both;',
+ },
+ noData: {
+ paddingBottom: '-12px',
+ color: '#7b8192',
+ fontSize: '12px',
+ textAlign: 'center',
+ },
+ glbTransfer: {
+ position: 'absolute',
+ width: 45,
+ height: 20,
+ bottom: '-10px',
+ cursor: 'pointer',
+ },
+ glbView: {
+ width: '100%',
+ height: 150,
+ },
+}))
+
+interface Props {
+ message?: string
+ imageUrl?: string
+ tokenInfo?: OwnerERC721TokenInfo | null
+}
+
+export function PreviewBox(props: Props) {
+ const classes = useStylesExtends(useStyles(), {})
+ const { t } = useI18N()
+ return (
+
+ {props.message && (
+
+ {props.message}
+
+ )}
+ {props.imageUrl &&
+ (props.tokenInfo?.glbSupport ? (
+
+
+
+ ) : (
+

+ ))}
+ {!(props.message || props.imageUrl) && (
+
+ {t('plugin_pets_dialog_preview')}
+
+ )}
+
+ )
+}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx
deleted file mode 100644
index 22abc0b7888a..000000000000
--- a/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useEffect, useState } from 'react'
-import { makeStyles, useStylesExtends } from '@masknet/theme'
-import { getAssetAsBlobURL } from '../../../utils'
-import Drag from './drag'
-import AnimatedMessage from './animatedMsg'
-import Tip from './tooltip'
-import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI'
-import { PetsPluginID } from '../constants'
-import { useIsMinimalMode } from '@masknet/plugin-infra'
-
-const useStyles = makeStyles()(() => ({
- root: {
- position: 'fixed',
- top: 0,
- left: 0,
- },
- img: {
- zIndex: 999,
- width: '100%',
- height: '100%',
- backgroundSize: 'contain',
- },
- close: {
- width: 15,
- height: 15,
- cursor: 'pointer',
- backgroundSize: 'contain',
- position: 'absolute',
- top: 0,
- right: -10,
- },
-}))
-
-const AnimatePic = () => {
- const classes = useStylesExtends(useStyles(), {})
- const Background = getAssetAsBlobURL(new URL('../assets/loot.gif', import.meta.url))
- const Close = getAssetAsBlobURL(new URL('../assets/close.png', import.meta.url))
-
- const [show, setShow] = useState(false)
- const [infoShow, setInfoShow] = useState(false)
- const disabled = useIsMinimalMode(PetsPluginID)
-
- const identity = useCurrentVisitingIdentity()
- useEffect(() => {
- const userId = identity.identifier.userId
- const maskId = 'realMaskNetwork'
- setShow(!disabled && userId === maskId)
- }, [identity, disabled])
-
- const handleClose = () => setShow(false)
- const handleMouseEnter = () => setInfoShow(true)
- const handleMouseLeave = () => setInfoShow(false)
-
- return (
-
- {show ? (
-
-
-
- {infoShow ? (
- <>
-
-
- >
- ) : null}
-
- ) : null}
-
- )
-}
-
-export default AnimatePic
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/animatedMsg.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/animatedMsg.tsx
deleted file mode 100644
index efbec66f757e..000000000000
--- a/packages/mask/src/plugins/Pets/SNSAdaptor/animatedMsg.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React from 'react'
-import { makeStyles } from '@masknet/theme'
-import { CSSTransition } from 'react-transition-group'
-import { useI18N } from '../../../utils'
-
-const useStyles = makeStyles()(() => ({
- txt: {
- position: 'absolute',
- opacity: 0,
- top: -40,
- fontSize: '12px',
- color: '#737373',
- fontWeight: 600,
- width: '150%',
- fontFamily: 'TwitterChirp',
- },
- enter: {
- left: 150,
- opacity: 0,
- },
- enterActive: {
- left: 0,
- opacity: 1,
- transition: 'all 1500ms ease-in',
- },
- enterDone: {
- left: 0,
- opacity: 1,
- },
- exitActive: {
- left: 150,
- opacity: 0,
- transition: 'all 1500ms ease-out',
- },
-}))
-
-const AnimatedMessage = () => {
- const { t } = useI18N()
- const { classes } = useStyles()
- const [start, setStart] = React.useState(false)
- const txts = [
- t('plugin_pets_loot_des_0'),
- t('plugin_pets_loot_des_1'),
- t('plugin_pets_loot_des_2'),
- t('plugin_pets_loot_des_3'),
- t('plugin_pets_loot_des_4'),
- t('plugin_pets_loot_des_5'),
- t('plugin_pets_loot_des_6'),
- t('plugin_pets_loot_des_7'),
- t('plugin_pets_loot_des_8'),
- t('plugin_pets_loot_des_9'),
- t('plugin_pets_loot_des_10'),
- t('plugin_pets_loot_des_11'),
- t('plugin_pets_loot_des_12'),
- t('plugin_pets_loot_des_13'),
- t('plugin_pets_loot_des_14'),
- ]
-
- const [show, setShow] = React.useState(txts[0])
-
- React.useEffect(() => {
- let count = 0
- const timer = setInterval(() => {
- setStart(count % 9 < 5)
- count = count + 1
- }, 1000 * 1)
- return () => {
- clearInterval(timer)
- }
- }, [])
-
- React.useEffect(() => {
- if (start) return
- const timer = setTimeout(() => {
- const idx = Math.round(Math.random() * (txts.length - 1))
- setShow(txts[idx])
- }, 1500)
- return () => {
- clearInterval(timer)
- }
- }, [start])
-
- return (
-
- {show}
-
- )
-}
-
-export default AnimatedMessage
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/index.tsx
index 22170f37857c..9c3e1ca4d06e 100644
--- a/packages/mask/src/plugins/Pets/SNSAdaptor/index.tsx
+++ b/packages/mask/src/plugins/Pets/SNSAdaptor/index.tsx
@@ -1,12 +1,18 @@
import type { Plugin } from '@masknet/plugin-infra'
import { base } from '../base'
-import AnimatePic from './animate'
+import AnimatePic from './Animate'
+import { PetDialog } from './PetDialog'
const sns: Plugin.SNSAdaptor.Definition = {
...base,
init() {},
GlobalInjection() {
- return
+ return (
+ <>
+
+
+ >
+ )
},
}
diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/tooltip.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/tooltip.tsx
deleted file mode 100644
index 859f5e234c02..000000000000
--- a/packages/mask/src/plugins/Pets/SNSAdaptor/tooltip.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { makeStyles } from '@masknet/theme'
-import { Typography, Box } from '@mui/material'
-import { getAssetAsBlobURL, ShadowRootTooltip, useI18N } from '../../../utils'
-
-const useStyles = makeStyles()((theme) => ({
- root: {
- position: 'fixed',
- top: 0,
- left: 0,
- },
- info: {
- zIndex: 999,
- width: 15,
- height: 15,
- right: -10,
- bottom: 2,
- backgroundRepeat: 'round',
- position: 'absolute',
- },
- content: {
- backgroundColor: 'red',
- },
- title: {
- fontSize: '12px',
- color: '#737373',
- fontWeight: 600,
- },
- des: {
- fontSize: '12px',
- color: '#737373',
- fontWeight: 400,
- },
- arrow: {
- color: theme.palette.common.white,
- },
- tooltip: {
- backgroundColor: theme.palette.common.white,
- },
-}))
-
-const Tip = () => {
- const { t } = useI18N()
- const { classes } = useStyles()
- const Info = getAssetAsBlobURL(new URL('../assets/info.png', import.meta.url))
- const click = () => window.open('https://twitter.com/MintTeamNFT')
- const loots = [
- t('plugin_pets_loot_info_0'),
- t('plugin_pets_loot_info_1'),
- t('plugin_pets_loot_info_2'),
- t('plugin_pets_loot_info_3'),
- t('plugin_pets_loot_info_4'),
- t('plugin_pets_loot_info_5'),
- t('plugin_pets_loot_info_6'),
- t('plugin_pets_loot_info_7'),
- ]
-
- const titleRender = (
-
-
- {t('plugin_pets_loot_properties')}
-
- {loots.map((txt) => (
-
- {txt}
-
- ))}
-
- {t('plugin_pets_get_nft_shows')}
-
-
-
- https://twitter.com/MintTeamNFT
-
-
-
- )
- return (
-
-
-
- )
-}
-
-export default Tip
diff --git a/packages/mask/src/plugins/Pets/Services/index.ts b/packages/mask/src/plugins/Pets/Services/index.ts
new file mode 100644
index 000000000000..b1b8d4342232
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/Services/index.ts
@@ -0,0 +1,18 @@
+import { getCustomEssayFromRSS, getConfigNFTsFromRSS, saveCustomEssayToRSS } from './rss3'
+import { personalSign } from '../../../extension/background-script/EthereumService'
+import type { PetMetaDB } from '../types'
+
+export * from './storage'
+
+export async function saveEssay(address: string, petMeta: PetMetaDB, userId: string): Promise {
+ const signature = await personalSign(userId, address)
+ return saveCustomEssayToRSS(address, petMeta, signature)
+}
+
+export function getEssay(address: string): Promise {
+ return getCustomEssayFromRSS(address)
+}
+
+export function getConfigEssay(): Promise {
+ return getConfigNFTsFromRSS()
+}
diff --git a/packages/mask/src/plugins/Pets/Services/rss3.ts b/packages/mask/src/plugins/Pets/Services/rss3.ts
new file mode 100644
index 000000000000..b791720afa30
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/Services/rss3.ts
@@ -0,0 +1,36 @@
+import { RSS3 } from '@masknet/web3-providers'
+import type { Constant } from '@masknet/web3-shared-evm/constants/utils'
+import { personalSign } from '../../../extension/background-script/EthereumService'
+import type { ConfigRSSNode, EssayRSSNode, PetMetaDB } from '../types'
+import { NFTS_CONFIG_ADDRESS } from '../constants'
+
+const cache = new Map | undefined>()
+
+export async function getCustomEssayFromRSS(address: string): Promise {
+ if (!address) return
+ const rss = RSS3.createRSS3(address, async (message: string) => {
+ return personalSign(message, address)
+ })
+ const data = await RSS3.getFileData(rss, address, '_pet')
+ return data?.essay
+}
+
+export async function saveCustomEssayToRSS(address: string, essay: PetMetaDB, signature: string) {
+ if (!address) return
+ const rss = RSS3.createRSS3(address, async (message: string) => {
+ return personalSign(message, address)
+ })
+ await RSS3.setFileData(rss, address, '_pet', { address, signature, essay })
+ return essay
+}
+
+export async function getConfigNFTsFromRSS() {
+ const v = cache.get(NFTS_CONFIG_ADDRESS)
+ if (v) return v
+ const rss = RSS3.createRSS3(NFTS_CONFIG_ADDRESS, async (message: string) => {
+ return personalSign(message, NFTS_CONFIG_ADDRESS)
+ })
+ const data = await RSS3.getFileData(rss, NFTS_CONFIG_ADDRESS, '_pet_nfts')
+ cache.set(NFTS_CONFIG_ADDRESS, data?.essay)
+ return data?.essay
+}
diff --git a/packages/mask/src/plugins/Pets/Services/storage.ts b/packages/mask/src/plugins/Pets/Services/storage.ts
new file mode 100644
index 000000000000..1a8c04cbca43
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/Services/storage.ts
@@ -0,0 +1,14 @@
+import { KeyValue } from '@masknet/web3-providers'
+import { PetsPluginID } from '../constants'
+import type { User } from '../types'
+
+const storage = KeyValue.createJSON_Storage(PetsPluginID)
+
+export async function setUserAddress(user: User) {
+ await storage.set(user.userId, user.address)
+}
+
+export async function getUserAddress(userId: string) {
+ if (!userId || userId === '$unknown') return ''
+ return storage.get(userId)
+}
diff --git a/packages/mask/src/plugins/Pets/Worker/index.ts b/packages/mask/src/plugins/Pets/Worker/index.ts
index 1ec7a4b0ff6d..0b57fdb9bfed 100644
--- a/packages/mask/src/plugins/Pets/Worker/index.ts
+++ b/packages/mask/src/plugins/Pets/Worker/index.ts
@@ -1,5 +1,6 @@
import type { Plugin } from '@masknet/plugin-infra'
import { base } from '../base'
+import '../messages'
const worker: Plugin.Worker.Definition = {
...base,
diff --git a/packages/mask/src/plugins/Pets/assets/defaultIcon.png b/packages/mask/src/plugins/Pets/assets/defaultIcon.png
new file mode 100644
index 000000000000..c7a8853a00fd
Binary files /dev/null and b/packages/mask/src/plugins/Pets/assets/defaultIcon.png differ
diff --git a/packages/mask/src/plugins/Pets/assets/drag.png b/packages/mask/src/plugins/Pets/assets/drag.png
new file mode 100644
index 000000000000..41b0c1a46d1d
Binary files /dev/null and b/packages/mask/src/plugins/Pets/assets/drag.png differ
diff --git a/packages/mask/src/plugins/Pets/assets/glb3D.png b/packages/mask/src/plugins/Pets/assets/glb3D.png
new file mode 100644
index 000000000000..8325cbc6340b
Binary files /dev/null and b/packages/mask/src/plugins/Pets/assets/glb3D.png differ
diff --git a/packages/mask/src/plugins/Pets/assets/loot.gif b/packages/mask/src/plugins/Pets/assets/loot.gif
deleted file mode 100644
index 5ed6046cc8e8..000000000000
Binary files a/packages/mask/src/plugins/Pets/assets/loot.gif and /dev/null differ
diff --git a/packages/mask/src/plugins/Pets/assets/punk2d.png b/packages/mask/src/plugins/Pets/assets/punk2d.png
new file mode 100644
index 000000000000..3ea84c77dfc6
Binary files /dev/null and b/packages/mask/src/plugins/Pets/assets/punk2d.png differ
diff --git a/packages/mask/src/plugins/Pets/base.tsx b/packages/mask/src/plugins/Pets/base.tsx
index 0dcdb684d3f9..119d8bf64690 100644
--- a/packages/mask/src/plugins/Pets/base.tsx
+++ b/packages/mask/src/plugins/Pets/base.tsx
@@ -5,10 +5,9 @@ import { PETSIcon } from '../../resources/PETSIcon'
export const base: Plugin.Shared.Definition = {
ID: PetsPluginID,
icon: ,
- name: { fallback: 'NFT revolution' },
+ name: { fallback: 'Non-Fungible Friends' },
description: {
- fallback:
- 'Explore the endless possibilities of NFTs. Link and display your NFTs on social media in a revolutionized way.',
+ fallback: 'Explore the endless possibilities of NFTs.',
},
publisher: { name: { fallback: '' }, link: 'https://github.com/etouyang/' },
enableRequirement: {
diff --git a/packages/mask/src/plugins/Pets/constants.ts b/packages/mask/src/plugins/Pets/constants.ts
index 5895bf8d0300..ca7a2719a584 100644
--- a/packages/mask/src/plugins/Pets/constants.ts
+++ b/packages/mask/src/plugins/Pets/constants.ts
@@ -1 +1,41 @@
+import { mediaViewerUrl } from '@masknet/shared'
+import urlcat from 'urlcat'
+import { ImageType } from './types'
+
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_PUNK_MASK_WORD =
+ "I'm CryptoPunk #6128... in 3D & with a body! Voyagers, welcome to the uncharted waters of WEB3!"
+
+export const initMeta = {
+ userId: '',
+ tokenId: '',
+ contract: '',
+ word: '',
+ image: '',
+ type: ImageType.NORMAL,
+}
+
+export const initCollection = {
+ name: '',
+ contract: '',
+ tokens: [],
+}
+
+export const Punk3D = {
+ contract: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
+ tokenId: '6128',
+ url: urlcat(mediaViewerUrl, {
+ url: 'https://gateway.pinata.cloud/ipfs/QmZjfo1zKTfQZjqs4CaZJ7pQDZHrUBaozre8Z71c7ZXGMc',
+ type: 'model/gltf-binary',
+ }),
+}
+
+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()
+export const DefaultIcon = new URL('./assets/defaultIcon.png', import.meta.url).toString()
+export const PunkIcon = new URL('./assets/punk2d.png', import.meta.url).toString()
diff --git a/packages/mask/src/plugins/Pets/hooks/index.ts b/packages/mask/src/plugins/Pets/hooks/index.ts
new file mode 100644
index 000000000000..d640128d48a6
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/hooks/index.ts
@@ -0,0 +1,3 @@
+export * from './useEssay'
+export * from './useNfts'
+export * from './useUser'
diff --git a/packages/mask/src/plugins/Pets/hooks/useEssay.ts b/packages/mask/src/plugins/Pets/hooks/useEssay.ts
new file mode 100644
index 000000000000..95ef0c3ef469
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/hooks/useEssay.ts
@@ -0,0 +1,30 @@
+import { ImageType, ShowMeta } from './../types'
+import { useEffect, useState } from 'react'
+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'
+
+export function useEssay(user: User, refresh?: number) {
+ return useAsync(async () => {
+ if (!user.address) return null
+ const metaData = await PluginPetRPC.getEssay(user.address)
+ return metaData?.userId === user.userId ? metaData : null
+ }, [user, refresh]).value
+}
+
+export function useDefaultEssay(user: User) {
+ const [essayMeta, setEssayMeta] = useState(undefined)
+ useEffect(() => {
+ if (user?.userId || user?.userId !== '$unknown') {
+ 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,
+ })
+ } else {
+ setEssayMeta(undefined)
+ }
+ }, [user])
+ return essayMeta
+}
diff --git a/packages/mask/src/plugins/Pets/hooks/useNfts.ts b/packages/mask/src/plugins/Pets/hooks/useNfts.ts
new file mode 100644
index 000000000000..c0d5db3295c0
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/hooks/useNfts.ts
@@ -0,0 +1,95 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useAsync } from 'react-use'
+import { OpenSea } from '@masknet/web3-providers'
+import {
+ useChainId,
+ useCollectibles,
+ ERC721TokenDetailed,
+ isSameAddress,
+ ERC721ContractDetailed,
+ SocketState,
+} from '@masknet/web3-shared-evm'
+import { Constant, transform } from '@masknet/web3-shared-evm/constants/utils'
+import { cloneDeep, findLastIndex } from 'lodash-unified'
+import { delay } from '@masknet/shared-base'
+import type { User, FilterContract } from '../types'
+import { Punk3D } from '../constants'
+import { RaribleIPFSURL } from '../../Collectible/constants'
+
+function useInitNFTs(config: Record | undefined) {
+ return useMemo(() => {
+ if (!config) return []
+ const nftList = transform(config)()
+ return Object.keys(nftList).map((i) => {
+ const value = nftList[i as keyof typeof nftList]
+ return { name: i, contract: value as string, tokens: [] }
+ })
+ }, [config])
+}
+
+export function useNFTs(user: User | undefined, configNFTs: Record | undefined) {
+ const initContracts = useInitNFTs(configNFTs)
+ const [nfts, setNfts] = useState(initContracts)
+ const chainId = useChainId()
+ const [fetchTotal, setFetchTotal] = useState([])
+ const { data: collectibles, state } = useCollectibles(user?.address ?? '', chainId)
+ useEffect(() => {
+ if (!initContracts.length) return
+ const tempNFTs: FilterContract[] = cloneDeep(initContracts)
+ if (collectibles.length && (state === SocketState.done || state === SocketState.sent)) {
+ const total = [...fetchTotal, ...collectibles]
+ setFetchTotal(total)
+ for (const NFT of total) {
+ const sameNFT = tempNFTs.find((temp) => isSameAddress(temp.contract, NFT.contractDetailed.address))
+ if (!sameNFT) continue
+ const isPunk =
+ isSameAddress(NFT.contractDetailed.address, Punk3D.contract) && NFT.tokenId === Punk3D.tokenId
+ if (isPunk) {
+ NFT.info.mediaUrl = Punk3D.url
+ }
+ const glbSupport = NFT.info.mediaUrl?.endsWith('.glb') || isPunk
+ if (NFT.info.mediaUrl?.includes('ipfs://')) {
+ NFT.info.mediaUrl = NFT.info.mediaUrl.replace('ipfs://', RaribleIPFSURL)
+ }
+ const item = { ...NFT.info, tokenId: NFT.tokenId, glbSupport }
+ const sameTokenIndex = findLastIndex(sameNFT.tokens, (v) => v.tokenId === NFT.tokenId)
+ if (sameTokenIndex === -1) {
+ sameNFT.tokens.push(item)
+ } else {
+ sameNFT.tokens[sameTokenIndex] = item
+ }
+ }
+ }
+ setNfts(tempNFTs)
+ return () => {}
+ }, [JSON.stringify(user), JSON.stringify(collectibles), state, JSON.stringify(initContracts)])
+ return nfts
+}
+
+export function useNFTsExtra(configNFTs: Record | undefined) {
+ const initContracts = useInitNFTs(configNFTs)
+ const [retry, setRetry] = useState(0)
+ const chainId = useChainId()
+ const [extra, setExtra] = useState([])
+ useAsync(async () => {
+ if (!initContracts.length) return
+ if (retry > 2) return
+ let requests = []
+ if (!extra.length) {
+ requests = initContracts.map((nft) => OpenSea.getContract(nft.contract, chainId))
+ } else {
+ // openSea api request should not immediately
+ await delay(3000)
+ requests = extra.map((nft, index) => {
+ if (nft.symbol && nft.name !== 'Unknown Token') {
+ return Promise.resolve(nft)
+ }
+ return OpenSea.getContract(initContracts[index].contract, chainId)
+ })
+ }
+ const lists: ERC721ContractDetailed[] = await Promise.all(requests)
+ setExtra(lists)
+ setRetry(retry + 1)
+ }, [retry, JSON.stringify(initContracts)])
+ return extra
+}
diff --git a/packages/mask/src/plugins/Pets/hooks/useUser.ts b/packages/mask/src/plugins/Pets/hooks/useUser.ts
new file mode 100644
index 000000000000..14f60ca2dd6a
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/hooks/useUser.ts
@@ -0,0 +1,35 @@
+import { useAsync } from 'react-use'
+import { useEffect, useState } from 'react'
+import { useWallet } from '@masknet/web3-shared-evm'
+import type { User } from '../types'
+import { useCurrentVisitingIdentity, useLastRecognizedIdentity } from '../../../components/DataSource/useActivatedUI'
+import { PluginPetRPC } from '../messages'
+
+export function useUser() {
+ const [user, setUser] = useState({ userId: '', address: '' })
+ const wallet = useWallet()
+ const whoAmI = useLastRecognizedIdentity()
+ useEffect(() => {
+ if (!(wallet?.address && whoAmI?.identifier?.userId)) return
+ setUser({ userId: whoAmI.identifier.userId, address: wallet?.address })
+ }, [wallet, whoAmI])
+ return user
+}
+
+export function useCurrentVisitingUser(flag?: number) {
+ const [user, setUser] = useState({ userId: '', address: '' })
+ const identity = useCurrentVisitingIdentity()
+ useAsync(async () => {
+ let address = ''
+ try {
+ const response = await PluginPetRPC.getUserAddress(identity.identifier.userId ?? '')
+ if (response) address = response as string
+ } finally {
+ setUser({
+ userId: identity.identifier.userId ?? '',
+ address: address,
+ })
+ }
+ }, [identity, flag])
+ return user
+}
diff --git a/packages/mask/src/plugins/Pets/index.ts b/packages/mask/src/plugins/Pets/index.ts
index eb51ba0afe25..3f47413092cc 100644
--- a/packages/mask/src/plugins/Pets/index.ts
+++ b/packages/mask/src/plugins/Pets/index.ts
@@ -13,4 +13,9 @@ registerPlugin({
hotModuleReload: (hot) =>
import.meta.webpackHot && import.meta.webpackHot.accept('./Worker', () => hot(import('./Worker'))),
},
+ Dashboard: {
+ load: () => import('./Dashboard'),
+ hotModuleReload: (hot) =>
+ import.meta.webpackHot && import.meta.webpackHot.accept('./Dashboard', () => hot(import('./Dashboard'))),
+ },
})
diff --git a/packages/mask/src/plugins/Pets/messages.ts b/packages/mask/src/plugins/Pets/messages.ts
new file mode 100644
index 000000000000..35f27cb1a10e
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/messages.ts
@@ -0,0 +1,19 @@
+import { createPluginMessage, PluginMessageEmitter, createPluginRPC } from '@masknet/plugin-infra'
+import { serializer } from '@masknet/shared-base'
+import { PetsPluginID } from './constants'
+import type { PetsDialogEvent } from './types'
+export interface PetMessage {
+ /**
+ * Pets essay set dialog
+ */
+ essayDialogUpdated: PetsDialogEvent
+ setResult: number
+ rpc: unknown
+}
+
+if (import.meta.webpackHot) import.meta.webpackHot.accept()
+export const PluginPetMessages: { events: PluginMessageEmitter } = {
+ events: createPluginMessage(PetsPluginID, serializer),
+}
+
+export const PluginPetRPC = createPluginRPC(PetsPluginID, () => import('./Services'), PluginPetMessages.events.rpc)
diff --git a/packages/mask/src/plugins/Pets/types.ts b/packages/mask/src/plugins/Pets/types.ts
new file mode 100644
index 000000000000..44759423b244
--- /dev/null
+++ b/packages/mask/src/plugins/Pets/types.ts
@@ -0,0 +1,51 @@
+import type { ERC721TokenInfo } from '@masknet/web3-shared-evm'
+import type { Constant } from '@masknet/web3-shared-evm/constants/utils'
+
+export interface PetsDialogEvent {
+ open: boolean
+}
+
+export interface EssayRSSNode {
+ address: string
+ signature: string
+ essay: PetMetaDB
+}
+
+export interface ConfigRSSNode {
+ address: string
+ signature: string
+ essay: Record
+}
+
+export enum ImageType {
+ NORMAL = 'normal',
+ GLB = 'glb',
+}
+
+export interface PetMetaDB extends ShowMeta {
+ userId: string
+ tokenId: string
+ contract: string
+}
+
+export interface ShowMeta {
+ image: string
+ word: string
+ type: ImageType
+}
+
+export interface User {
+ userId: string
+ address: string
+}
+
+export interface OwnerERC721TokenInfo extends ERC721TokenInfo {
+ tokenId: string
+ glbSupport: boolean
+}
+
+export interface FilterContract {
+ name: string
+ contract: string
+ tokens: OwnerERC721TokenInfo[]
+}
diff --git a/packages/shared/src/UI/components/AssetPlayer/index.tsx b/packages/shared/src/UI/components/AssetPlayer/index.tsx
index b8a0f8163d90..bc1c0de39947 100644
--- a/packages/shared/src/UI/components/AssetPlayer/index.tsx
+++ b/packages/shared/src/UI/components/AssetPlayer/index.tsx
@@ -32,6 +32,7 @@ interface AssetPlayerProps
renderTimeout?: number
iconProps?: SvgIconProps
fallbackImage?: URL
+ showIframeFromInit?: boolean
setERC721TokenName?: (name: string) => void
setSourceType?: (type: string) => void
}
@@ -89,7 +90,6 @@ export const AssetPlayer = memo(({ url, type, options, iconPro
}
}, [url, JSON.stringify(erc721Token), type, JSON.stringify(options), playerState])
// endregion
-
type ERC721TokenNameMsg = { message: { type: 'name'; name: string } | { type: 'sourceType'; name: string } }
// #region resource loaded error
const onMessage = useCallback(
@@ -131,7 +131,14 @@ export const AssetPlayer = memo(({ url, type, options, iconPro
setPlayerState(AssetPlayerState.INIT)
setIframe()
}}
- className={playerState !== AssetPlayerState.NORMAL ? classes.hidden : classes.iframe}
+ className={
+ ![
+ AssetPlayerState.NORMAL,
+ ...(props.showIframeFromInit ? [AssetPlayerState.INIT] : []),
+ ].includes(playerState)
+ ? classes.hidden
+ : classes.iframe
+ }
onResized={({ type }) => {
if (type === 'init' || playerState === AssetPlayerState.NORMAL) return
setPlayerState(AssetPlayerState.NORMAL)
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index c16747306f50..a552a39b2942 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -10,6 +10,7 @@ export interface DashboardPluginMessages {
Wallet: unknown
Transak: unknown
Swap: unknown
+ Pets: unknown
}
export interface DashboardPluginServices {