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) => ( + + + {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)} + /> + + + + + {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 {