diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx index 135aac2d5b2a..bd20c64ce021 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx @@ -20,24 +20,27 @@ export function NFTAvatarRing(props: NFTAvatarRingProps) { const { stroke, strokeWidth, fontSize, text, width, id } = props const avatarSize = width + 3 - const R = width + 2 * strokeWidth - 4 - const r = R / 2 - strokeWidth - const path_r = R / 2 - strokeWidth + fontSize / 2 - const x1 = R / 2 - path_r / 2 - const y1 = R / 2 + Math.sqrt(Math.pow(path_r, 2) - Math.pow(path_r / 2, 2)) - const x2 = R / 2 + path_r / 2 - const y2 = y1 + const R = avatarSize / 2 + const path_r = R - strokeWidth + fontSize / 2 + const x1 = R - path_r / 2 + const y1 = R + Math.sqrt(Math.pow(path_r, 2) - Math.pow(path_r / 2, 2)) + const x2 = R + path_r / 2 return ( - - + + @@ -49,23 +52,16 @@ export function NFTAvatarRing(props: NFTAvatarRingProps) { - + - + diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx index 4b6a18089f03..25c39d6cd47d 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx @@ -1,35 +1,33 @@ import { keyframes, makeStyles, useStylesExtends } from '@masknet/theme' +import type { Keyframes } from '@emotion/serialize' -const rainbowBorderKeyFrames = keyframes` -0%,to { - border-color: #00f8ff; - box-shadow: 0 5px 15px rgba(0,248,255,.4),0 10px 30px rgba(37,41,46,.2); -} - -20% { - border-color: #a4ff00; - box-shadow: 0 5px 15px rgba(164,255,0,.4),0 10px 30px rgba(37,41,46,.2); -} - -40% { - border-color: #f7275e; - box-shadow: 0 5px 15px rgba(247,39,94,.4),0 10px 30px rgba(37,41,46,.2) -} - -60% { - border-color: #ffd300; - box-shadow: 0 5px 15px rgba(255,211,0,.4),0 10px 30px rgba(37,41,46,.2) -} - -80% { - border-color: #ff8a00; - box-shadow: 0 5px 15px rgba(255,138,0,.4),0 10px 30px rgba(37,41,46,.2) -} +export const rainbowBorderKeyFrames: Keyframes = keyframes` + 0%, + to { + border-color: #00f8ff; + box-shadow: 0 5px 15px rgba(0, 248, 255, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + } + 20% { + border-color: #a4ff00; + box-shadow: 0 5px 15px rgba(164, 255, 0, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + } + 40% { + border-color: #f7275e; + box-shadow: 0 5px 15px rgba(247, 39, 94, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + } + 60% { + border-color: #ffd300; + box-shadow: 0 5px 15px rgba(255, 211, 0, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + } + 80% { + border-color: #ff8a00; + box-shadow: 0 5px 15px rgba(255, 138, 0, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + } ` interface StyleProps { - width: number - height: number + width?: number + height?: number radius?: string } const useStyles = makeStyles()((theme, { width, height, radius = '100%' }) => ({ @@ -37,7 +35,7 @@ const useStyles = makeStyles()((theme, { width, height, radius = '10 animation: `${rainbowBorderKeyFrames} 6s linear infinite`, width, height, - boxShadow: 'box-shadow: 0 5px 15px rgb(0 248 255 / 40%), 0 10px 30px rgb(37 41 46 / 20%);', + boxShadow: '0 5px 15px rgba(0, 248, 255, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2)', transition: '.125s ease', borderRadius: radius, display: 'flex', @@ -49,8 +47,8 @@ const useStyles = makeStyles()((theme, { width, height, radius = '10 })) interface RainbowBoxProps extends withClasses<'root'> { - width: number - height: number + width?: number + height?: number radius?: string children?: React.ReactNode } diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx index 404bd4a2f186..e56fed2ab7a4 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx @@ -2,55 +2,33 @@ import { createReactRootShadowed, MaskMessages, NFTAvatarEvent, startWatch } fro import { searchTwitterAvatarLinkSelector, searchTwitterAvatarSelector } from '../../utils/selector' import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' import { makeStyles } from '@masknet/theme' -import { useState, useEffect } from 'react' - +import { useState, useEffect, useMemo, useRef } from 'react' import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI' -import { useWallet } from '@masknet/web3-shared-evm' +import { resolveOpenSeaLink, useWallet } from '@masknet/web3-shared-evm' import type { AvatarMetaDB } from '../../../../plugins/Avatar/types' import { useNFTAvatar } from '../../../../plugins/Avatar/hooks' import { getAvatarId } from '../../utils/user' import { PluginNFTAvatarRPC } from '../../../../plugins/Avatar/messages' import { NFTBadge } from '../../../../plugins/Avatar/SNSAdaptor/NFTBadge' import { NFTAvatar } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatar' +import { useAsync, useLocation, useUpdateEffect, useWindowSize } from 'react-use' +import { rainbowBorderKeyFrames } from '../../../../plugins/Avatar/SNSAdaptor/RainbowBox' +import { trim } from 'lodash-unified' -const offset = 10 export function injectNFTAvatarInTwitter(signal: AbortSignal) { const watcher = new MutationObserverWatcher(searchTwitterAvatarSelector()) startWatch(watcher, signal) createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() } -interface StyleProps { - width: number - size: number -} - -const useStyles = makeStyles()((theme, props) => ({ +const useStyles = makeStyles()(() => ({ root: { position: 'absolute', - bottom: '-10px !important', textAlign: 'center', color: 'white', - minWidth: 134, zIndex: 2, - - left: -1 * props.width + offset / 2, - top: -1 * props.width + offset / 2, - width: props.size, - height: props.size, - - [`@media (max-width: ${theme.breakpoints.values.sm}px)`]: { - left: -27, - top: -1 * props.width, - }, - }, - update: { - position: 'absolute', - bottom: '-10px !important', - left: 55, - textAlign: 'center', - color: 'white', - minWidth: 134, + width: '100%', + height: '100%', }, text: { fontSize: '20px !important', @@ -63,25 +41,37 @@ const useStyles = makeStyles()((theme, props) => ({ })) function NFTAvatarInTwitter() { + const rainBowElement = useRef() + const borderElement = useRef() const identity = useCurrentVisitingIdentity() const wallet = useWallet() const { value: _avatar } = useNFTAvatar(identity.identifier.userId) const [avatar, setAvatar] = useState() - const ele = searchTwitterAvatarLinkSelector().evaluate() - let size = 170 - const width = 15 - if (ele) { - const style = window.getComputedStyle(ele) - size = Number(style.width.replace('px', '') ?? 0) - Number(style.borderWidth.replace('px', '') ?? 0) - offset - } - const { classes } = useStyles({ size: size + width * 2, width }) + const windowSize = useWindowSize() + const location = useLocation() + + const showAvatar = useMemo( + () => getAvatarId(identity.avatar ?? '') === avatar?.avatarId && avatar.avatarId, + [avatar?.avatarId, identity.avatar], + ) + + const size = useMemo(() => { + const ele = searchTwitterAvatarSelector().evaluate() + if (ele) { + const style = window.getComputedStyle(ele) + return Number.parseInt(style.width.replace('px', '') ?? 0, 10) + } + return 0 + }, [windowSize]) + + const { classes } = useStyles() const [NFTEvent, setNFTEvent] = useState() const onUpdate = (data: NFTAvatarEvent) => { setNFTEvent(data) } - useEffect(() => { + useAsync(async () => { if (!wallet || !NFTAvatar) return if (!NFTEvent?.address || !NFTEvent?.tokenId) { @@ -95,20 +85,21 @@ function NFTAvatarInTwitter() { return } - PluginNFTAvatarRPC.saveNFTAvatar(wallet.address, { + const avatar = await PluginNFTAvatarRPC.saveNFTAvatar(wallet.address, { ...NFTEvent, avatarId: getAvatarId(identity.avatar ?? ''), - } as AvatarMetaDB).then((avatar: AvatarMetaDB | undefined) => { - setAvatar(avatar) - MaskMessages.events.NFTAvatarTimelineUpdated.sendToAll( - avatar ?? { - userId: identity.identifier.userId, - avatarId: getAvatarId(identity.avatar ?? ''), - address: '', - tokenId: '', - }, - ) - }) + } as AvatarMetaDB) + + setAvatar(avatar) + MaskMessages.events.NFTAvatarTimelineUpdated.sendToAll( + avatar ?? { + userId: identity.identifier.userId, + avatarId: getAvatarId(identity.avatar ?? ''), + address: '', + tokenId: '', + }, + ) + setNFTEvent(undefined) }, [identity.avatar]) @@ -121,29 +112,86 @@ function NFTAvatarInTwitter() { }, [onUpdate]) useEffect(() => { - if (!avatar || !avatar.avatarId) return - if (getAvatarId(identity.avatar ?? '') !== avatar.avatarId) return - const avatarDom = searchTwitterAvatarSelector().evaluate()?.parentElement - if (avatarDom) { - avatarDom.style.marginBottom = '10px' - avatarDom.style.overflow = 'unset' + const linkDom = searchTwitterAvatarLinkSelector().evaluate() + + if (linkDom?.firstElementChild && linkDom.childNodes.length === 4) { + const linkParentDom = linkDom.closest('div') + + if (linkParentDom) linkParentDom.style.overflow = 'visible' + + // create rainbow shadow border + if (linkDom.lastElementChild?.tagName !== 'STYLE') { + borderElement.current = linkDom.firstElementChild + + // remove useless border + linkDom.removeChild(linkDom.firstElementChild) + const style = document.createElement('style') + style.innerText = ` + ${rainbowBorderKeyFrames.styles} + + .rainbowBorder { + animation: ${rainbowBorderKeyFrames.name} 6s linear infinite; + box-shadow: 0 5px 15px rgba(0, 248, 255, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2); + transition: .125s ease; + border: 2px solid #00f8ff; + } + ` + rainBowElement.current = linkDom.firstElementChild + if (showAvatar) linkDom.firstElementChild.classList.add('rainbowBorder') + linkDom.appendChild(style) + } } + }, [location.pathname]) - const backgroundImgDom = searchTwitterAvatarSelector().evaluate()?.firstChild?.nextSibling?.firstChild - ?.firstChild as HTMLElement - if (backgroundImgDom) { - backgroundImgDom.style.borderRadius = '100%' + useEffect(() => { + const linkDom = searchTwitterAvatarLinkSelector().evaluate() + if (showAvatar) { + if (borderElement.current && linkDom?.firstElementChild === borderElement.current) { + linkDom?.removeChild(linkDom.firstElementChild) + } + + rainBowElement.current?.classList.add('rainbowBorder') + } else { + // recovery Twitter profile avatar style + if (borderElement.current && linkDom?.firstElementChild !== borderElement.current) { + linkDom?.insertBefore(borderElement.current, linkDom.firstChild) + } + + rainBowElement.current?.classList.remove('rainbowBorder') } - }, [identity, avatar, searchTwitterAvatarSelector, searchTwitterAvatarSelector]) + }, [showAvatar]) + + useUpdateEffect(() => { + if ( + location.pathname && + location.pathname.split('/').length === 2 && + trim(location.pathname, '/') !== identity.identifier.userId + ) { + setAvatar(undefined) + } + }, [location, identity]) + + useUpdateEffect(() => { + const linkParentDom = searchTwitterAvatarLinkSelector().evaluate()?.closest('div') - if (!avatar) return null + if (!avatar || !linkParentDom) return + + const handler = () => { + window.open(resolveOpenSeaLink(avatar.address, avatar.tokenId), '_blank') + } + + linkParentDom.addEventListener('click', handler) + + return () => { + linkParentDom.removeEventListener('click', handler) + } + }, [avatar]) - const avatarParent = searchTwitterAvatarSelector().closest(2).evaluate() as HTMLElement - if (avatarParent) avatarParent.style.clipPath = 'unset' + if (!avatar || !size) return null return ( <> - {getAvatarId(identity.avatar ?? '') === avatar.avatarId && avatar.avatarId ? ( + {showAvatar ? ( export const searchForegroundColorSelector: () => LiveSelector = () => querySelector('[data-testid="primaryColumn"] > div > div > div > div > div > div > div > div > div > div') export const searchNewTweetButtonSelector: () => LiveSelector = () => { - const q = querySelector('[data-testid="FloatingActionButtons_Tweet_Button"') + const q = querySelector('[data-testid="FloatingActionButtons_Tweet_Button"]') if (q.evaluate()) return q return querySelector('[data-testid="SideNav_NewTweet_Button"]') } @@ -227,17 +227,14 @@ export const searchProfessionalButtonSelector = () => //#endregion //#region avatar selector - export const searchTwitterAvatarLinkSelector: () => LiveSelector = () => querySelector('[data-testid="UserProfileHeader_Items"]').closest(2).querySelector('div a') -export const searchTwitterAvatarSelector = () => searchTwitterAvatarLinkSelector().querySelector('div') +export const searchTwitterAvatarSelector = () => + querySelector('[data-testid="UserProfileHeader_Items"]').closest(2).querySelector('img').closest(1) //#endregion //#region twitter avatar -export const searchAccountSwitherButtonSelector = () => - querySelector('[data-testid="SideNav_AccountSwitcher_Button"]') - export const searchUseCellSelector = () => querySelector('[data-testid="UserCell"]') //#endregion