Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<RainbowBox width={avatarSize} height={avatarSize}>
<svg className={classes.root} width={avatarSize} height={avatarSize} viewBox={`0 0 ${R} ${R}`} id={id}>
<RainbowBox>
<svg
className={classes.root}
width="100%"
height="100%"
viewBox={`0 0 ${avatarSize} ${avatarSize}`}
id={id}>
<defs>
<path
id={`${id}-path`}
fill="none"
stroke="none"
strokeWidth="0"
d={`M${x1} ${y1} A${path_r} ${path_r} 0 1 1 ${x2} ${y2}`}
d={`M${x1} ${y1} A${path_r} ${path_r} 0 1 1 ${x2} ${y1}`}
/>
<linearGradient id={`${id}-gradient`} x1="0%" y1="0%" x2="100%" y2="0">
<stop offset="0%" stopColor="#00f8ff" />
Expand All @@ -49,23 +52,16 @@ export function NFTAvatarRing(props: NFTAvatarRingProps) {
</linearGradient>
</defs>

<circle
cx={R / 2}
cy={R / 2}
r={r + strokeWidth / 2}
fill="none"
stroke={stroke}
strokeWidth={strokeWidth}
/>
<circle cx={R} cy={R} r={R - strokeWidth / 2} fill="none" stroke={stroke} strokeWidth={strokeWidth} />
<pattern id={`${id}-pattern`} x="0" y="0" width="300%" height="100%" patternUnits="userSpaceOnUse">
<circle cx={R / 2} cy={R / 2} r={R / 2} fill={`url(#${id}-gradient)`}>
<circle cx={R} cy={R} r={R} fill={`url(#${id}-gradient)`}>
<animateTransform
attributeName="transform"
type="rotate"
dur="10s"
repeatCount="indefinite"
from={`0 ${R / 2} ${R / 2}`}
to={`360 ${R / 2} ${R / 2}`}
from={`0 ${R} ${R}`}
to={`360 ${R} ${R}`}
/>
</circle>
</pattern>
Expand Down
58 changes: 28 additions & 30 deletions packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
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<StyleProps>()((theme, { width, height, radius = '100%' }) => ({
root: {
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',
Expand All @@ -49,8 +47,8 @@ const useStyles = makeStyles<StyleProps>()((theme, { width, height, radius = '10
}))

interface RainbowBoxProps extends withClasses<'root'> {
width: number
height: number
width?: number
height?: number
radius?: string
children?: React.ReactNode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<NFTAvatarInTwitter />)
}

interface StyleProps {
width: number
size: number
}

const useStyles = makeStyles<StyleProps>()((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',
Expand All @@ -63,25 +41,37 @@ const useStyles = makeStyles<StyleProps>()((theme, props) => ({
}))

function NFTAvatarInTwitter() {
const rainBowElement = useRef<Element | null>()
const borderElement = useRef<Element | null>()
const identity = useCurrentVisitingIdentity()
const wallet = useWallet()
const { value: _avatar } = useNFTAvatar(identity.identifier.userId)
const [avatar, setAvatar] = useState<AvatarMetaDB | undefined>()
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<NFTAvatarEvent>()
const onUpdate = (data: NFTAvatarEvent) => {
setNFTEvent(data)
}

useEffect(() => {
useAsync(async () => {
if (!wallet || !NFTAvatar) return

if (!NFTEvent?.address || !NFTEvent?.tokenId) {
Expand All @@ -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])

Expand All @@ -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 ? (
<NFTBadge
avatar={avatar}
size={size}
Expand Down
Loading