| null = null
const e = new no_xray_Event('change', {
bubbles: true,
@@ -36,4 +54,8 @@ export function hookInputUploadOnce(...[format, fileName, fileArray]: InternalEv
deleteProperty(this, 'files')
}, 200)
}
+
+ if (triggerOnActiveElementNow && document.activeElement instanceof HTMLInputElement) {
+ document.activeElement?.click()
+ }
}
diff --git a/packages/injected-script/sdk/index.ts b/packages/injected-script/sdk/index.ts
index 83a3e8a655d7..5c0b8e0d8afa 100644
--- a/packages/injected-script/sdk/index.ts
+++ b/packages/injected-script/sdk/index.ts
@@ -20,8 +20,13 @@ export function pasteInstagram(url: string) {
export function inputText(text: string) {
sendEvent('input', text)
}
-export function hookInputUploadOnce(format: string, fileName: string, image: Uint8Array) {
- sendEvent('hookInputUploadOnce', format, fileName, Array.from(image))
+export function hookInputUploadOnce(
+ format: string,
+ fileName: string,
+ image: Uint8Array,
+ triggerOnActiveElementNow = false,
+) {
+ sendEvent('hookInputUploadOnce', format, fileName, Array.from(image), triggerOnActiveElementNow)
}
document.addEventListener(CustomEventId, (e) => {
diff --git a/packages/injected-script/shared/index.ts b/packages/injected-script/shared/index.ts
index 7a6afd44f728..ab67094bf185 100644
--- a/packages/injected-script/shared/index.ts
+++ b/packages/injected-script/shared/index.ts
@@ -17,7 +17,7 @@ export interface InternalEvents {
* How to use:
* Call this event, then invoke the file selector (SNS). It will invoke click on some input, then let's replace with the result.
*/
- hookInputUploadOnce: [format: string, fileName: string, file: number[]]
+ hookInputUploadOnce: [format: string, fileName: string, file: number[], triggerOnActiveElementNow: boolean]
// #region Eth inpage provider bridge
/** Request the bridge to listen on an event. */
diff --git a/packages/mask/shared/kv-storage.ts b/packages/mask/shared/kv-storage.ts
index 17ca63c7e079..a77cc4915102 100644
--- a/packages/mask/shared/kv-storage.ts
+++ b/packages/mask/shared/kv-storage.ts
@@ -12,6 +12,12 @@ const createInMemoryKVStorage = createKVStorageHost(inMemoryBackend, MaskMessage
export const InMemoryStorages = {
Plugin: createInMemoryKVStorage('plugin', {}),
+ FacebookNFTEventOnMobile: createInMemoryKVStorage('FacebookNFTEventOnMobile', {
+ userId: '',
+ avatarId: '',
+ address: '',
+ tokenId: '',
+ }),
}
export const PersistentStorages = {
Plugin: createPersistentKVStorage('plugin', {}),
diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx
index 033f2076367f..81801277e8c4 100644
--- a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx
+++ b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx
@@ -480,8 +480,6 @@ function SetupGuideUI(props: SetupGuideUIProps) {
let content = t('setup_guide_say_hello_content')
if (ui.networkIdentifier === 'twitter.com') {
content += t('setup_guide_say_hello_follow', { account: '@realMaskNetwork' })
- } else if (ui.networkIdentifier === 'facebook.com') {
- content += t('setup_guide_say_hello_follow', { account: '@masknetwork' })
}
ui.automation.maskCompositionDialog?.open?.(makeTypedMessageText(content), {
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx
index 15204fd6bb6d..93a23fbbab07 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarButton.tsx
@@ -30,6 +30,7 @@ const useStyles = makeStyles()((theme) => ({
interface NFTAvatarButtonProps extends withClasses<'root'> {
onClick: () => void
+ showSetting?: boolean
}
export function NFTAvatarButton(props: NFTAvatarButtonProps) {
@@ -40,7 +41,9 @@ export function NFTAvatarButton(props: NFTAvatarButtonProps) {
return (
- 🔥{t('nft_avatar')}
+ 🔥
+ {t('nft_avatar')}
+ {props.showSetting ? : null}
)
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx
index e57002a35d68..db84a0b25882 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatarRing.tsx
@@ -5,6 +5,16 @@ const useStyles = makeStyles()((theme) => ({
root: {
overflow: 'unset',
},
+ container: {
+ boxShadow: '0 5px 15px rgba(0, 248, 255, 0.4), 0 10px 30px rgba(37, 41, 46, 0.2)',
+ transition: 'none',
+ borderRadius: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ lineHeight: 0,
+ border: '2px solid #00f8ff',
+ },
}))
interface NFTAvatarRingProps {
stroke: string
@@ -14,11 +24,12 @@ interface NFTAvatarRingProps {
price: string
width: number
id: string
+ hasRainbow?: boolean
}
export function NFTAvatarRing(props: NFTAvatarRingProps) {
const { classes } = useStyles()
- const { stroke, strokeWidth, fontSize, text, width, id, price } = props
+ const { stroke, strokeWidth, fontSize, text, width, id, price, hasRainbow = true } = props
const avatarSize = width + 1
const R = avatarSize / 2
@@ -26,76 +37,65 @@ export function NFTAvatarRing(props: NFTAvatarRingProps) {
const x1 = R - path_r
const y1 = R
const x2 = R + path_r
- return (
-
-
-
+
+
+
+ {price}
+
+
+
+
)
+
+ return hasRainbow ? {svgNode} : {svgNode}
}
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx
index 05f8a7302545..126fe3933140 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadge.tsx
@@ -22,10 +22,11 @@ interface NFTBadgeProps extends withClasses<'root' | 'text' | 'icon'> {
avatar: AvatarMetaDB
size?: number
width?: number
+ hasRainbow?: boolean
}
export function NFTBadge(props: NFTBadgeProps) {
- const { avatar, size = 140 } = props
+ const { avatar, size = 140, hasRainbow } = props
const classes = useStylesExtends(useStyles(), props)
const { value = { amount: '0', symbol: 'ETH', name: '', slug: '' }, loading } = useNFT(
@@ -48,6 +49,7 @@ export function NFTBadge(props: NFTBadgeProps) {
width={size}
strokeWidth={14}
stroke="black"
+ hasRainbow={hasRainbow}
fontSize={9}
text={
loading
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTimeline.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTimeline.tsx
index f26e9d6ae0e9..c0fc9965f087 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTimeline.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTimeline.tsx
@@ -4,17 +4,19 @@ import { MaskMessages } from '../../../utils'
import { useNFTAvatar } from '../hooks'
import type { AvatarMetaDB } from '../types'
import { RainbowBox } from './RainbowBox'
+import type { RSS3_KEY_SNS } from '../constants'
interface NFTBadgeTimelineProps extends withClasses<'root'> {
userId: string
avatarId: string
width: number
height: number
+ snsKey: RSS3_KEY_SNS
}
export function NFTBadgeTimeline(props: NFTBadgeTimelineProps) {
- const { userId, avatarId, width, height } = props
- const { loading, value: _avatar } = useNFTAvatar(userId)
+ const { userId, avatarId, width, height, snsKey } = props
+ const { loading, value: _avatar } = useNFTAvatar(userId, snsKey)
const [avatar, setAvatar] = useState()
const [avatarId_, setAvatarId_] = useState('')
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTweet.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTweet.tsx
index 20f5806671d1..be86dee58c6a 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTweet.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTBadgeTweet.tsx
@@ -1,6 +1,7 @@
import { makeStyles } from '@masknet/theme'
import { useLastRecognizedIdentity } from '../../../components/DataSource/useActivatedUI'
import { NFTBadgeTimeline } from './NFTBadgeTimeline'
+import type { RSS3_KEY_SNS } from '../constants'
const useStyles = makeStyles()((theme) => ({
root: {},
@@ -10,12 +11,21 @@ interface NFTBadgeTweetProps {
avatarId: string
width: number
height: number
+ snsKey: RSS3_KEY_SNS
}
export function NFTBadgeTweet(props: NFTBadgeTweetProps) {
- const { avatarId, width, height } = props
+ const { avatarId, width, height, snsKey } = props
const identity = useLastRecognizedIdentity()
if (!identity?.identifier.userId) return null
- return
+ return (
+
+ )
}
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTContractImage.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTContractImage.tsx
index d52c67c7f283..1847b1f378fb 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTContractImage.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTContractImage.tsx
@@ -2,6 +2,7 @@ import { makeStyles } from '@masknet/theme'
import { CircularProgress } from '@mui/material'
import { useNFTAvatar } from '../hooks'
import { useNFTVerified } from '../hooks/useNFTVerified'
+import type { RSS3_KEY_SNS } from '../constants'
const useStyles = makeStyles()((theme) => ({
root: {},
@@ -13,13 +14,14 @@ const useStyles = makeStyles()((theme) => ({
interface NFTContractImageProps {
userId: string
+ snsKey: RSS3_KEY_SNS
}
export function NFTContractImage(props: NFTContractImageProps) {
- const { userId } = props
+ const { userId, snsKey } = props
const { classes } = useStyles()
- const { loading: loadingNFT, value: NFTAvatar } = useNFTAvatar(userId)
+ const { loading: loadingNFT, value: NFTAvatar } = useNFTAvatar(userId, snsKey)
const { loading, value: contract } = useNFTVerified(NFTAvatar?.address ?? '')
return (
diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx
index dd7dd0244d46..51a71109b6fd 100644
--- a/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx
+++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/RainbowBox.tsx
@@ -31,7 +31,6 @@ interface StyleProps {
}
const useStyles = makeStyles()((theme, { width, height, radius = '100%' }) => ({
root: {
- transform: 'scale(0.97)',
animation: `${rainbowBorderKeyFrames} 6s linear infinite`,
width,
height,
diff --git a/packages/mask/src/plugins/Avatar/Services/index.ts b/packages/mask/src/plugins/Avatar/Services/index.ts
index 9a6bc3470c8f..fbbfef876698 100644
--- a/packages/mask/src/plugins/Avatar/Services/index.ts
+++ b/packages/mask/src/plugins/Avatar/Services/index.ts
@@ -3,16 +3,19 @@ import type { AvatarMetaDB } from '../types'
import { getNFTAvatarFromJSON } from './db'
import { getUserAddress, setUserAddress } from './bind'
import { getNFTAvatarFromRSS, saveNFTAvatarToRSS } from './rss3'
+import type { RSS3_KEY_SNS } from '../constants'
export async function getNFTAvatar(
userId: string,
network: string,
+ snsKey: RSS3_KEY_SNS,
networkPluginId?: NetworkPluginID,
chainId?: number,
) {
const address = await getUserAddress(userId, network, networkPluginId, chainId)
+
if (address) {
- return getNFTAvatarFromRSS(userId, address)
+ return getNFTAvatarFromRSS(userId, address, snsKey)
}
return getNFTAvatarFromJSON(userId)
}
@@ -21,11 +24,12 @@ export async function saveNFTAvatar(
address: string,
nft: AvatarMetaDB,
network: string,
+ snsKey: RSS3_KEY_SNS,
networkPluginId?: NetworkPluginID,
chainId?: number,
) {
try {
- const avatar = await saveNFTAvatarToRSS(address, nft, '')
+ const avatar = await saveNFTAvatarToRSS(address, nft, '', snsKey)
await setUserAddress(nft.userId, address, network, networkPluginId, chainId)
return avatar
} catch (error) {
diff --git a/packages/mask/src/plugins/Avatar/Services/rss3.ts b/packages/mask/src/plugins/Avatar/Services/rss3.ts
index 5f3a5aa2388d..f07d4c727d25 100644
--- a/packages/mask/src/plugins/Avatar/Services/rss3.ts
+++ b/packages/mask/src/plugins/Avatar/Services/rss3.ts
@@ -2,6 +2,7 @@ import { personalSign } from '../../../extension/background-script/EthereumServi
import type { AvatarMetaDB } from '../types'
import addSeconds from 'date-fns/addSeconds'
import { RSS3 } from '@masknet/web3-providers'
+import type { RSS3_KEY_SNS } from '../constants'
interface NFTRSSNode {
signature: string
@@ -10,35 +11,40 @@ interface NFTRSSNode {
const cache = new Map, number]>()
-export async function getNFTAvatarFromRSS(userId: string, address: string) {
- let v = cache.get(address)
+export async function getNFTAvatarFromRSS(userId: string, address: string, snsKey: RSS3_KEY_SNS) {
+ const key = `${address}, ${userId}, ${snsKey}`
+ let v = cache.get(key)
if (!v || Date.now() > v[1]) {
- cache.set(address, [_getNFTAvatarFromRSS(userId, address), addSeconds(Date.now(), 60).getTime()])
+ cache.set(key, [_getNFTAvatarFromRSS(userId, address, snsKey), addSeconds(Date.now(), 60).getTime()])
}
- v = cache.get(address)
+ v = cache.get(key)
const result = await v?.[0]
return result?.nft
}
-async function _getNFTAvatarFromRSS(userId: string, address: string): Promise {
+async function _getNFTAvatarFromRSS(
+ userId: string,
+ address: string,
+ snsKey: RSS3_KEY_SNS,
+): Promise {
const rss = RSS3.createRSS3(address, async (message: string) => {
return personalSign(message, address)
})
- const nfts = await RSS3.getFileData>(rss, address, '_nfts')
+ const nfts = await RSS3.getFileData>(rss, address, snsKey)
if (nfts) {
return nfts[userId]
}
return RSS3.getFileData(rss, address, '_nft')
}
-export async function saveNFTAvatarToRSS(address: string, nft: AvatarMetaDB, signature: string) {
+export async function saveNFTAvatarToRSS(address: string, nft: AvatarMetaDB, signature: string, snsKey: RSS3_KEY_SNS) {
const rss = RSS3.createRSS3(address, async (message: string) => {
return personalSign(message, address)
})
- let _nfts = await RSS3.getFileData>(rss, address, '_nfts')
+ let _nfts = await RSS3.getFileData>(rss, address, snsKey)
if (!_nfts) {
_nfts = {
[nft.userId]: { signature, nft },
@@ -50,7 +56,7 @@ export async function saveNFTAvatarToRSS(address: string, nft: AvatarMetaDB, sig
}
}
- await RSS3.setFileData(rss, address, '_nfts', _nfts)
+ await RSS3.setFileData(rss, address, snsKey, _nfts)
// clear cache
if (cache.has(address)) cache.delete(address)
diff --git a/packages/mask/src/plugins/Avatar/constants.ts b/packages/mask/src/plugins/Avatar/constants.ts
index ec2a44085c2a..40558eea6cdb 100644
--- a/packages/mask/src/plugins/Avatar/constants.ts
+++ b/packages/mask/src/plugins/Avatar/constants.ts
@@ -8,3 +8,8 @@ export const NFT_CONTRACT_JSON_VERIFIED_SERVER = 'https://configuration.r2d2.to/
export const PLUGIN_ID = PluginId.Avatar
export const PLUGIN_NAME = 'Avatar'
export const PLUGIN_DESCRIPTION = 'NFT Avatar'
+
+export enum RSS3_KEY_SNS {
+ TWITTER = '_nfts',
+ FACEBOOK = '_facebook_nfts',
+}
diff --git a/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts b/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts
index 75d21fdef836..dfc223cefc4e 100644
--- a/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts
+++ b/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts
@@ -3,10 +3,11 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn'
import { activatedSocialNetworkUI } from '../../../social-network'
import { PluginNFTAvatarRPC } from '../messages'
import type { AvatarMetaDB } from '../types'
+import type { RSS3_KEY_SNS } from '../constants'
-export function useNFTAvatar(userId: string): AsyncState {
+export function useNFTAvatar(userId: string, snsKey: RSS3_KEY_SNS): AsyncState {
return useAsync(async () => {
if (!userId || userId === '$unknown') return
- return PluginNFTAvatarRPC.getNFTAvatar(userId, activatedSocialNetworkUI.networkIdentifier)
+ return PluginNFTAvatarRPC.getNFTAvatar(userId, activatedSocialNetworkUI.networkIdentifier, snsKey)
}, [userId, activatedSocialNetworkUI.networkIdentifier])
}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/collecting/identity.ts b/packages/mask/src/social-network-adaptor/facebook.com/collecting/identity.ts
index a439e6dc25fe..38bb967fe7cf 100644
--- a/packages/mask/src/social-network-adaptor/facebook.com/collecting/identity.ts
+++ b/packages/mask/src/social-network-adaptor/facebook.com/collecting/identity.ts
@@ -1,9 +1,11 @@
import { LiveSelector, MutationObserverWatcher, ValueRef } from '@dimensiondev/holoflows-kit'
-import type { SocialNetworkUI } from '../../../social-network/types'
-import { creator } from '../../../social-network/utils'
+
+import { creator, SocialNetworkUI } from '../../../social-network'
import { getProfileIdentifierAtFacebook, getUserID } from '../utils/getProfileIdentifier'
import { isMobileFacebook } from '../utils/isMobile'
-import { ProfileIdentifier } from '@masknet/shared-base'
+import { delay, ProfileIdentifier } from '@masknet/shared-base'
+import { searchAvatarSelector, searchUserIdOnMobileSelector } from '../utils/selector'
+import { getAvatar, getBioDescription, getFacebookId, getNickName } from '../utils/user'
export const IdentityProviderFacebook: SocialNetworkUI.CollectingCapabilities.IdentityResolveProvider = {
hasDeprecatedPlaceholderName: true,
@@ -36,6 +38,57 @@ function resolveLastRecognizedIdentityFacebookInner(ref: ValueRef, signal
.then((id) => id && assign({ ...ref.value, identifier: new ProfileIdentifier('facebook.com', id) }))
}
+function resolveCurrentVisitingIdentityInner(
+ ref: SocialNetworkUI.CollectingCapabilities.IdentityResolveProvider['recognized'],
+ cancel: AbortSignal,
+) {
+ const selector = isMobileFacebook ? searchUserIdOnMobileSelector() : searchAvatarSelector()
+
+ const assign = async () => {
+ await delay(500)
+ const nickname = getNickName()
+ const bio = getBioDescription()
+ const handle = getFacebookId()
+
+ const avatar = getAvatar()
+
+ ref.value = {
+ identifier: handle ? new ProfileIdentifier('facebook.com', handle) : ProfileIdentifier.unknown,
+ nickname,
+ avatar,
+ bio,
+ }
+ }
+
+ const createWatcher = (selector: LiveSelector) => {
+ const watcher = new MutationObserverWatcher(selector)
+ .addListener('onAdd', () => assign())
+ .addListener('onChange', () => assign())
+ .startWatch({
+ childList: true,
+ subtree: true,
+ attributes: true,
+ })
+ window.addEventListener('locationchange', assign)
+ cancel.addEventListener('abort', () => {
+ window.removeEventListener('locationchange', assign)
+ watcher.stopWatch()
+ })
+ }
+
+ assign()
+
+ createWatcher(selector)
+}
+
+export const CurrentVisitingIdentityProviderFacebook: SocialNetworkUI.CollectingCapabilities.IdentityResolveProvider = {
+ hasDeprecatedPlaceholderName: false,
+ recognized: creator.EmptyIdentityResolveProviderState(),
+ start(cancel) {
+ resolveCurrentVisitingIdentityInner(this.recognized, cancel)
+ },
+}
+
// #region LS
// Try to resolve my identities
const myUsernameLiveSelectorPC = new LiveSelector()
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/injection/Composition.tsx b/packages/mask/src/social-network-adaptor/facebook.com/injection/Composition.tsx
index e3c84e8f8d0a..7ebdf51ce00b 100644
--- a/packages/mask/src/social-network-adaptor/facebook.com/injection/Composition.tsx
+++ b/packages/mask/src/social-network-adaptor/facebook.com/injection/Composition.tsx
@@ -37,16 +37,13 @@ if (isMobileFacebook) {
.querySelector('[id="toolbarLabel"]')
.closest(1)
.querySelector('div:nth-child(2) > div:nth-child(4)')
- } else
+ } else {
composeBox = new LiveSelector()
- .querySelector(
- '[role="dialog"] form > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child > div:last-child > div:first-child',
+ .querySelectorAll(
+ '[role="dialog"] form > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child > div:last-child > div:first-child > div:last-child > div > div',
)
- .querySelectorAll('[role="button"][tabindex="0"], [role="button"][tabindex="-1"]')
- .map((x) => x.parentElement)
.at(-2)
- .map((x) => x.parentElement)
- .map((x) => x.parentElement)
+ }
}
export function injectCompositionFacebook(signal: AbortSignal) {
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarEditProfile.tsx b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarEditProfile.tsx
new file mode 100644
index 000000000000..1280efe19771
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarEditProfile.tsx
@@ -0,0 +1,76 @@
+import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
+import { searchFacebookEditProfileSelector, searchFacebookProfileSettingButtonSelector } from '../../utils/selector'
+import { createReactRootShadowed, startWatch } from '../../../../utils'
+import { useLayoutEffect, useState } from 'react'
+import { useLocationChange } from '../../../../utils/hooks/useLocationChange'
+import { NFTAvatarButton } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatarButton'
+import { makeStyles } from '@masknet/theme'
+
+export function injectOpenNFTAvatarEditProfileButton(signal: AbortSignal) {
+ const watcher = new MutationObserverWatcher(searchFacebookProfileSettingButtonSelector())
+ startWatch(watcher, signal)
+ createReactRootShadowed(watcher.firstDOMProxy.beforeShadow, { signal }).render(
+ ,
+ )
+}
+
+interface StyleProps {
+ minHeight: number
+ fontSize: number
+ marginTop: number
+ backgroundColor?: string
+}
+
+const useStyles = makeStyles()((theme, props) => ({
+ root: {
+ minHeight: props.minHeight,
+ fontSize: props.fontSize,
+ marginTop: props.marginTop,
+ backgroundColor: '#fff!important',
+ marginRight: theme.spacing(0.5),
+ marginLeft: theme.spacing(1.25),
+ borderRadius: '6px !important',
+ border: 'none !important',
+ color: '#050505!important',
+ },
+}))
+
+function OpenNFTAvatarEditProfileButtonInFaceBook() {
+ const [style, setStyle] = useState({ minHeight: 36, fontSize: 15, marginTop: 6 })
+ const onClick = () => {
+ const editDom = searchFacebookEditProfileSelector().evaluate()
+ editDom?.click()
+ }
+
+ const setStyleWithSelector = () => {
+ const editDom = searchFacebookProfileSettingButtonSelector().evaluate()
+
+ if (!editDom) return
+
+ const buttonDom = editDom.querySelector('a > div')
+
+ if (!buttonDom) return
+
+ const editCss = window.getComputedStyle(editDom)
+ const buttonCss = window.getComputedStyle(buttonDom)
+
+ setStyle({
+ fontSize: Number(editCss.fontSize.replace('px', '')),
+ marginTop: Number(editCss.paddingTop.replace('px', '')),
+ minHeight: 36,
+ backgroundColor: buttonCss.backgroundColor,
+ })
+ }
+
+ useLayoutEffect(() => {
+ setStyleWithSelector()
+ }, [])
+
+ useLocationChange(() => {
+ setStyleWithSelector()
+ })
+
+ const { classes } = useStyles(style)
+
+ return
+}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInFacebook.tsx b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInFacebook.tsx
new file mode 100644
index 000000000000..32178c234dbb
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInFacebook.tsx
@@ -0,0 +1,184 @@
+import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
+import { searchFacebookAvatarOnMobileSelector, searchFacebookAvatarSelector } from '../../utils/selector'
+import { createReactRootShadowed, MaskMessages, startWatch } from '../../../../utils'
+import { useEffect, useMemo, useState } from 'react'
+import type { NFTAvatarEvent } from '@masknet/shared-base'
+import { max, pickBy } from 'lodash-unified'
+import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI'
+import { useAsync, useLocation, useWindowSize } from 'react-use'
+import { useWallet } from '@masknet/plugin-infra'
+import type { AvatarMetaDB } from '../../../../plugins/Avatar/types'
+import { PluginNFTAvatarRPC } from '../../../../plugins/Avatar/messages'
+import { getAvatarId } from '../../utils/user'
+import { useNFTAvatar } from '../../../../plugins/Avatar/hooks'
+import { NFTBadge } from '../../../../plugins/Avatar/SNSAdaptor/NFTBadge'
+import { makeStyles } from '@masknet/theme'
+import { isMobileFacebook } from '../../utils/isMobile'
+import { InMemoryStorages } from '../../../../../shared'
+import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants'
+
+export function injectNFTAvatarInFacebook(signal: AbortSignal) {
+ const watcher = new MutationObserverWatcher(searchFacebookAvatarSelector())
+ if (!isMobileFacebook) {
+ startWatch(watcher, signal)
+ createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render()
+ return
+ }
+
+ // mobile
+ const mobileWatcher = new MutationObserverWatcher(searchFacebookAvatarOnMobileSelector())
+ startWatch(mobileWatcher, signal)
+ createReactRootShadowed(mobileWatcher.firstDOMProxy.afterShadow, { signal }).render()
+}
+
+const useStyles = makeStyles()(() => ({
+ root: {
+ position: 'absolute',
+ textAlign: 'center',
+ color: 'white',
+ width: '100%',
+ height: '100%',
+ top: 0,
+ left: 0,
+ },
+ text: {
+ fontSize: '20px !important',
+ fontWeight: 700,
+ },
+ icon: {
+ width: '19px !important',
+ height: '19px !important',
+ },
+}))
+
+const clearStorages = () => {
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.userId.setValue('')
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.address.setValue('')
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.tokenId.setValue('')
+}
+
+function NFTAvatarInFacebook() {
+ const { classes } = useStyles()
+ const wallet = useWallet()
+ const [avatar, setAvatar] = useState()
+ const identity = useCurrentVisitingIdentity()
+ const location = useLocation()
+ const { value: _avatar } = useNFTAvatar(identity.identifier.userId, RSS3_KEY_SNS.FACEBOOK)
+
+ const [NFTEvent, setNFTEvent] = useState()
+
+ const windowSize = useWindowSize()
+ const showAvatar = useMemo(() => {
+ if (isMobileFacebook) {
+ const node = searchFacebookAvatarOnMobileSelector().closest(1).evaluate()
+
+ if (node) {
+ node.style.position = 'relative'
+ }
+ }
+ return getAvatarId(identity.avatar ?? '') === avatar?.avatarId
+ }, [avatar?.avatarId, identity.avatar, isMobileFacebook])
+
+ const size = useMemo(() => {
+ const ele = isMobileFacebook
+ ? searchFacebookAvatarOnMobileSelector().evaluate()
+ : searchFacebookAvatarSelector().evaluate()
+ if (ele) {
+ const style = window.getComputedStyle(ele)
+ return max([148, Number.parseInt(style.width.replace('px', '') ?? 0, 10)])
+ }
+ return 0
+ }, [windowSize, isMobileFacebook, avatar])
+
+ useEffect(() => {
+ return MaskMessages.events.NFTAvatarUpdated.on((data) =>
+ setNFTEvent((prev) => {
+ if (!prev) return data
+ return { ...prev, ...pickBy(data, (item) => !!item) }
+ }),
+ )
+ }, [])
+
+ // Because of the mobile upload step, need to use memory storage to store NFTEven
+ useAsync(async () => {
+ const storages = InMemoryStorages.FacebookNFTEventOnMobile.storage
+
+ if (!wallet) return
+ if (NFTEvent?.address && NFTEvent?.tokenId && NFTEvent?.avatarId) {
+ try {
+ const avatarInfo = await PluginNFTAvatarRPC.saveNFTAvatar(
+ wallet.address,
+ { ...NFTEvent, avatarId: getAvatarId(identity.avatar ?? '') } as AvatarMetaDB,
+ identity.identifier.network,
+ RSS3_KEY_SNS.FACEBOOK,
+ )
+ if (!avatarInfo) {
+ setNFTEvent(undefined)
+ setAvatar(undefined)
+ window.alert('Sorry, failed to save NFT Avatar. Please set again.')
+ return
+ }
+
+ setAvatar(avatarInfo)
+
+ setNFTEvent(undefined)
+ } catch (error: any) {
+ setNFTEvent(undefined)
+ setAvatar(undefined)
+ window.alert(error.message)
+ return
+ }
+ } else if (storages.address.value && storages.userId.value && storages.tokenId.value) {
+ try {
+ const avatarInfo = await PluginNFTAvatarRPC.saveNFTAvatar(
+ wallet.address,
+ {
+ userId: storages.userId.value,
+ tokenId: storages.tokenId.value,
+ address: storages.address.value,
+ avatarId: getAvatarId(identity.avatar ?? ''),
+ } as AvatarMetaDB,
+ identity.identifier.network,
+ RSS3_KEY_SNS.FACEBOOK,
+ )
+ if (!avatarInfo) {
+ clearStorages()
+ setAvatar(undefined)
+ window.alert('Sorry, failed to save NFT Avatar. Please set again.')
+ return
+ }
+ setAvatar(avatarInfo)
+ clearStorages()
+ } catch (error: any) {
+ clearStorages()
+ setAvatar(undefined)
+ window.alert(error.message)
+ return
+ }
+ }
+ }, [identity.avatar])
+
+ useEffect(() => setAvatar(_avatar), [_avatar, location])
+
+ // #region clear white border
+ useEffect(() => {
+ const node = searchFacebookAvatarSelector().closest(3).evaluate()
+ if (!node) return
+ if (showAvatar) {
+ node.setAttribute('style', 'padding: 0px')
+ } else {
+ node.removeAttribute('style')
+ }
+ }, [showAvatar])
+ // #endregion
+
+ if (!avatar || !size || !showAvatar) return null
+
+ return (
+
+ )
+}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInTimeline.tsx b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInTimeline.tsx
new file mode 100644
index 000000000000..0f20cd7d4fdd
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/NFTAvatarInTimeline.tsx
@@ -0,0 +1,75 @@
+import { DOMProxy, LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
+import { createReactRootShadowed, startWatch } from '../../../../utils'
+import { getInjectNodeInfo } from '../../utils/avatar'
+import { searchFaceBookPostAvatarOnMobileSelector, searchFaceBookPostAvatarSelector } from '../../utils/selector'
+import { Flags } from '../../../../../shared'
+import { NFTBadgeTimeline } from '../../../../plugins/Avatar/SNSAdaptor/NFTBadgeTimeline'
+import { isMobileFacebook } from '../../utils/isMobile'
+import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants'
+
+function getFacebookId(element: HTMLElement | SVGElement) {
+ const node = (isMobileFacebook ? element.firstChild : element.parentNode?.parentNode) as HTMLLinkElement
+ if (!node) return ''
+ const url = new URL(node.href)
+
+ if (url.pathname === '/profile.php' && url.searchParams.get('id'))
+ return url.searchParams.get(isMobileFacebook ? 'lst' : 'id')
+
+ return url.pathname.replace('/', '')
+}
+
+function _(selector: () => LiveSelector, signal: AbortSignal) {
+ startWatch(
+ new MutationObserverWatcher(selector()).useForeach((element, key) => {
+ let remove = () => {}
+
+ const run = async () => {
+ const facebookId = getFacebookId(element)
+
+ if (!facebookId) return
+
+ const info = getInjectNodeInfo(element)
+
+ if (!info) return
+
+ const proxy = DOMProxy({ afterShadowRootInit: { mode: Flags.using_ShadowDOM_attach_mode } })
+ proxy.realCurrent = info.element
+
+ const root = createReactRootShadowed(proxy.afterShadow, { signal })
+
+ root.render(
+
+
+
,
+ )
+
+ remove = root.destory
+ }
+
+ run()
+
+ return {
+ onNodeMutation: run,
+ onTargetChanged: run,
+ onRemove: () => remove(),
+ }
+ }),
+ signal,
+ )
+}
+
+export async function injectUserNFTAvatarAtFacebook(signal: AbortSignal) {
+ _(isMobileFacebook ? searchFaceBookPostAvatarOnMobileSelector : searchFaceBookPostAvatarSelector, signal)
+}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/ProfileNFTAvatar.tsx b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/ProfileNFTAvatar.tsx
new file mode 100644
index 000000000000..81fd1b9818fd
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/injection/NFT/ProfileNFTAvatar.tsx
@@ -0,0 +1,158 @@
+import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
+import { makeStyles } from '@masknet/theme'
+import { createReactRootShadowed, MaskMessages, startWatch } from '../../../../utils'
+import { NFTAvatar } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatar'
+import { blobToArrayBuffer } from '@dimensiondev/kit'
+import { hookInputUploadOnce } from '@masknet/injected-script'
+import {
+ searchFacebookAvatarListSelector,
+ searchFacebookAvatarMobileListSelector,
+ searchFacebookAvatarOpenFilesOnMobileSelector,
+ searchFacebookAvatarOpenFilesSelector,
+ searchFacebookConfirmAvatarImageSelector,
+ searchFacebookSaveAvatarButtonSelector,
+} from '../../utils/selector'
+import { useCallback, useEffect } from 'react'
+import { toPNG } from '../../../../plugins/Avatar/utils'
+import type { ERC721TokenDetailed } from '@masknet/web3-shared-evm'
+import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI'
+import { getAvatarId } from '../../utils/user'
+import { isMobileFacebook } from '../../utils/isMobile'
+import { InMemoryStorages } from '../../../../../shared'
+
+export async function injectProfileNFTAvatarInFaceBook(signal: AbortSignal) {
+ if (!isMobileFacebook) {
+ // The first step in setting an avatar
+ const watcher = new MutationObserverWatcher(searchFacebookAvatarListSelector())
+ startWatch(watcher, signal)
+ createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render()
+
+ // The second step in setting an avatar
+ const saveButtonWatcher = new MutationObserverWatcher(searchFacebookSaveAvatarButtonSelector()).useForeach(
+ (node, key, proxy) => {
+ const root = createReactRootShadowed(proxy.afterShadow, { signal })
+ root.render()
+ return () => root.destory()
+ },
+ )
+
+ startWatch(saveButtonWatcher, signal)
+ }
+ const watcher = new MutationObserverWatcher(searchFacebookAvatarMobileListSelector())
+ startWatch(watcher, signal)
+ createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render()
+}
+
+const useStyles = makeStyles()((theme) => ({
+ root: {
+ padding: '8px 0',
+ margin: '0 16px',
+ },
+}))
+
+async function changeImageToActiveElements(image: File | Blob): Promise {
+ const imageBuffer = await blobToArrayBuffer(image)
+ hookInputUploadOnce('image/png', 'avatar.png', new Uint8Array(imageBuffer))
+ searchFacebookAvatarOpenFilesSelector().evaluate()?.click()
+}
+
+function NFTAvatarInFacebookFirstStep() {
+ const { classes } = useStyles()
+
+ const identity = useCurrentVisitingIdentity()
+
+ const onChange = useCallback(
+ async (token: ERC721TokenDetailed) => {
+ if (!token.info.imageURL) return
+ const image = await toPNG(token.info.imageURL)
+ if (!image) return
+
+ await changeImageToActiveElements(image)
+
+ MaskMessages.events.NFTAvatarUpdated.sendToLocal({
+ userId: identity.identifier.userId,
+ avatarId: '',
+ address: token.contractDetailed.address,
+ tokenId: token.tokenId,
+ })
+ },
+ [identity],
+ )
+
+ return
+}
+
+function NFTAvatarInFacebookSecondStep() {
+ useEffect(() => {
+ const save = searchFacebookSaveAvatarButtonSelector().evaluate().at(0)
+ if (!save) return
+ const handler = () => {
+ const image = searchFacebookConfirmAvatarImageSelector().evaluate()
+ if (!image) return
+
+ const imageURL = image.getAttribute('src')
+ if (!imageURL) return
+
+ const avatarId = getAvatarId(imageURL)
+ if (avatarId) {
+ MaskMessages.events.NFTAvatarUpdated.sendToLocal({
+ userId: '',
+ address: '',
+ tokenId: '',
+ avatarId,
+ })
+ }
+ }
+
+ save.addEventListener('click', handler)
+
+ return () => save.removeEventListener('click', handler)
+ }, [])
+ return null
+}
+
+async function changeImageToActiveElementsOnMobile(image: File | Blob): Promise {
+ const imageBuffer = await blobToArrayBuffer(image)
+
+ const input = searchFacebookAvatarOpenFilesOnMobileSelector().evaluate()
+
+ if (input) {
+ input.style.visibility = 'unset'
+ input.focus()
+ hookInputUploadOnce('image/png', 'avatar.png', new Uint8Array(imageBuffer), true)
+ input.style.visibility = 'hidden'
+
+ const file = new File([image], 'avatar.png', { type: 'image/png', lastModified: Date.now() })
+ const container = new DataTransfer()
+ container.items.add(file)
+ input.files = container.files
+ }
+}
+
+const useMobileStyles = makeStyles()({
+ root: {
+ backgroundColor: '#ffffff',
+ },
+})
+
+function NFTAvatarListInFaceBookMobile() {
+ const { classes } = useMobileStyles()
+ const identity = useCurrentVisitingIdentity()
+
+ const onChange = useCallback(
+ async (token: ERC721TokenDetailed) => {
+ if (!token.info.imageURL) return
+ const image = await toPNG(token.info.imageURL)
+ if (!image) return
+
+ await changeImageToActiveElementsOnMobile(image)
+
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.userId.setValue(identity.identifier.userId)
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.address.setValue(token.contractDetailed.address)
+ InMemoryStorages.FacebookNFTEventOnMobile.storage.tokenId.setValue(token.tokenId)
+ },
+ [identity],
+ )
+
+ return
+}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts b/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts
index 8d81c39aab86..ee64a7ce474e 100644
--- a/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts
+++ b/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts
@@ -5,7 +5,7 @@ import { facebookShared } from './shared'
import { getProfilePageUrlAtFacebook } from './utils/parse-username'
import { taskOpenComposeBoxFacebook } from './automation/openComposeBox'
import { pasteTextToCompositionFacebook } from './automation/pasteTextToComposition'
-import { IdentityProviderFacebook } from './collecting/identity'
+import { CurrentVisitingIdentityProviderFacebook, IdentityProviderFacebook } from './collecting/identity'
import { InitAutonomousStateFriends } from '../../social-network/defaults/state/InitFriends'
import { InitAutonomousStateProfiles } from '../../social-network/defaults/state/InitProfiles'
import { injectCompositionFacebook } from './injection/Composition'
@@ -25,6 +25,10 @@ import { makeStyles } from '@masknet/theme'
import { ProfileIdentifier } from '@masknet/shared-base'
import { globalUIState } from '../../social-network'
import { injectToolboxHintAtFacebook as injectToolboxAtFacebook } from './injection/Toolbar'
+import { injectProfileNFTAvatarInFaceBook } from './injection/NFT/ProfileNFTAvatar'
+import { injectNFTAvatarInFacebook } from './injection/NFT/NFTAvatarInFacebook'
+import { injectUserNFTAvatarAtFacebook } from './injection/NFT/NFTAvatarInTimeline'
+import { injectOpenNFTAvatarEditProfileButton } from './injection/NFT/NFTAvatarEditProfile'
const useInjectedDialogClassesOverwriteFacebook = makeStyles()((theme) => {
const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)`
@@ -129,6 +133,7 @@ const facebookUI: SocialNetworkUI.Definition = {
},
collecting: {
identityProvider: IdentityProviderFacebook,
+ currentVisitingIdentityProvider: CurrentVisitingIdentityProviderFacebook,
postsProvider: PostProviderFacebook,
},
customization: {
@@ -178,6 +183,10 @@ const facebookUI: SocialNetworkUI.Definition = {
},
),
},
+ userAvatar: injectUserNFTAvatarAtFacebook,
+ enhancedProfileNFTAvatar: injectProfileNFTAvatarInFaceBook,
+ profileAvatar: injectNFTAvatarInFacebook,
+ openNFTAvatar: injectOpenNFTAvatarEditProfileButton,
postInspector: injectPostInspectorFacebook,
pageInspector: injectPageInspectorDefault(),
setupWizard: createTaskStartSetupGuideDefault(),
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/utils/avatar.ts b/packages/mask/src/social-network-adaptor/facebook.com/utils/avatar.ts
new file mode 100644
index 000000000000..37cfab754c0f
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/utils/avatar.ts
@@ -0,0 +1,37 @@
+import { getAvatarId } from './user'
+import { isMobileFacebook } from './isMobile'
+
+export function getInjectNodeInfo(element: HTMLElement | SVGElement) {
+ if (!isMobileFacebook) {
+ const imgEle = element.querySelector('image')
+ if (!imgEle) return
+
+ const nftDom = imgEle.parentNode?.parentNode as HTMLElement
+
+ const width = Number(window.getComputedStyle(nftDom).width.replace('px', '') ?? 0)
+ const height = Number(window.getComputedStyle(nftDom).height.replace('px', '') ?? 0)
+
+ const avatarId = getAvatarId(imgEle.href.baseVal)
+
+ if (!avatarId) return
+
+ return { element: nftDom, width, height, avatarId }
+ }
+
+ const imgEle = element.querySelector('i')
+
+ if (!imgEle) return
+
+ const nftDom = imgEle.parentNode?.parentNode as HTMLElement
+
+ const containerDom = imgEle.parentNode as HTMLElement
+
+ const width = Number(window.getComputedStyle(nftDom).width.replace('px', '') ?? 0)
+ const height = Number(window.getComputedStyle(nftDom).height.replace('px', '') ?? 0)
+
+ const avatarId = getAvatarId(imgEle.style.background.match(/\(["']?(.*?)["']?\)/)?.[1] ?? '')
+
+ if (!avatarId) return
+
+ return { element: containerDom, width, height, avatarId }
+}
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/utils/getProfileIdentifier.ts b/packages/mask/src/social-network-adaptor/facebook.com/utils/getProfileIdentifier.ts
index 5b1bf6b550a2..5e36129a8491 100644
--- a/packages/mask/src/social-network-adaptor/facebook.com/utils/getProfileIdentifier.ts
+++ b/packages/mask/src/social-network-adaptor/facebook.com/utils/getProfileIdentifier.ts
@@ -2,6 +2,7 @@ import { ProfileIdentifier } from '@masknet/shared-base'
import Services from '../../../extension/service'
import type { Profile } from '../../../database'
import { getCurrentIdentifier } from '../../utils'
+import { searchUserIdOnMobileSelector } from './selector'
type link = HTMLAnchorElement | null | undefined
@@ -83,6 +84,21 @@ export function getUserID(x: string) {
const url = relative ? new URL(x, location.host) : new URL(x)
if (url.hostname !== 'www.facebook.com' && url.hostname !== 'm.facebook.com') return null
+
+ // Get the userId from the meta element
+ if (url.hostname === 'm.facebook.com') {
+ const node = searchUserIdOnMobileSelector().evaluate()
+ if (!node) return null
+
+ const href = node.getAttribute('href')
+
+ if (!href) return null
+
+ const match = href.match(/lst=(\w+)/)
+ if (!match) return null
+
+ return match[1]
+ }
if (url.pathname.endsWith('.php')) {
if (!url.search) return null
const search = new URLSearchParams(url.search)
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/facebook.com/utils/selector.ts
new file mode 100644
index 000000000000..0e2c193b6e72
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/utils/selector.ts
@@ -0,0 +1,82 @@
+import { LiveSelector } from '@dimensiondev/holoflows-kit'
+
+type E = HTMLElement
+
+const querySelector = (selector: string, singleMode = true) => {
+ const ls = new LiveSelector().querySelector(selector)
+ return (singleMode ? ls.enableSingleMode() : ls) as LiveSelector
+}
+
+const querySelectorAll = (selector: string) => {
+ return new LiveSelector().querySelectorAll(selector)
+}
+
+export const searchUserIdOnMobileSelector: () => LiveSelector = () =>
+ querySelector('div[data-sigil$="profile"] a')
+
+export const searchAvatarSelector: () => LiveSelector = () =>
+ querySelector('[role="link"] [role="img"] image, [role="button"] [role="img"] image')
+
+export const searchNickNameSelector: () => LiveSelector = () =>
+ querySelector('span[dir="auto"] div h1')
+
+export const searchNickNameSelectorOnMobile: () => LiveSelector = () => querySelector('#cover-name-root h3')
+
+export const bioDescriptionSelector = () =>
+ querySelector('span[dir="auto"] div h1').closest(7).querySelector('span[dir="auto"] > span')
+
+export const bioDescriptionSelectorOnMobile: () => LiveSelector = () => querySelector('#bio div')
+
+export const searchUserIdSelector: () => LiveSelector = () =>
+ querySelector('div[data-pagelet="ProfileTabs"] a[role="tab"]')
+
+export const searchUserIdSelectorOnMobile: () => LiveSelector = () =>
+ querySelector('#tlFeed div[data-sigil="scroll-area"] a:last-child')
+
+// #endregion facebook nft avatar
+
+export const searchFacebookAvatarListSelector = () =>
+ querySelector('[role="dialog"] > div > div > div > input[type=file] + [role="button"]')
+ .closest(3)
+ .querySelector('div')
+
+export const searchFacebookAvatarMobileListSelector = () => querySelector('#nuxChoosePhotoButton').closest(6)
+
+export const searchFacebookAvatarSelector = () =>
+ querySelector('[role="button"] svg[role="img"], [role="link"] svg[role="img"]')
+
+export const searchFacebookAvatarOnMobileSelector = () =>
+ querySelector('[data-sigil="timeline-cover"] i[aria-label$="picture"]')
+
+export const searchFaceBookPostAvatarSelector = () =>
+ new LiveSelector().querySelectorAll('[type="nested/pressable"] svg')
+
+export const searchFaceBookPostAvatarOnMobileSelector = () => querySelectorAll('[data-gt=\'{"tn":"~"}\']')
+
+export const searchFacebookAvatarOpenFilesSelector = () => querySelector('[role="dialog"] input[type=file] ~ div')
+
+export const searchFacebookAvatarOpenFilesOnMobileSelector = () =>
+ querySelector('#nuxChoosePhotoButton ~ input')
+
+export const searchFacebookSaveAvatarButtonOnMobileSelector = () => querySelector('#nuxUploadPhotoButton')
+
+export const searchFacebookAvatarContainerSelector = () =>
+ querySelector('div[data-pagelet="ProfileActions"] > div > div')
+
+export const searchFacebookProfileSettingButtonSelector = () =>
+ querySelector('[role="button"] [role="img"]').closest(10).querySelector('input[type="file"] ~ div').closest(2)
+
+export const searchFacebookEditProfileSelector = () =>
+ querySelector('[role="button"] [role="img"]').closest(1).querySelector('i[data-visualcompletion="css-img"]')
+
+export const searchFacebookSaveAvatarButtonSelector = () =>
+ new LiveSelector()
+ .querySelector('[role="dialog"] [role="slider"]')
+ .closest(7)
+ .querySelectorAll('div')
+ .map((x) => x.parentElement?.parentElement)
+ .at(-1)
+
+export const searchFacebookConfirmAvatarImageSelector = () =>
+ querySelector('[role="dialog"] [role="slider"]').closest(7).querySelector('img')
+// #region
diff --git a/packages/mask/src/social-network-adaptor/facebook.com/utils/user.ts b/packages/mask/src/social-network-adaptor/facebook.com/utils/user.ts
new file mode 100644
index 000000000000..7cdf949642de
--- /dev/null
+++ b/packages/mask/src/social-network-adaptor/facebook.com/utils/user.ts
@@ -0,0 +1,68 @@
+import {
+ bioDescriptionSelector,
+ bioDescriptionSelectorOnMobile,
+ searchAvatarSelector,
+ searchFacebookAvatarOnMobileSelector,
+ searchNickNameSelector,
+ searchNickNameSelectorOnMobile,
+ searchUserIdSelector,
+ searchUserIdSelectorOnMobile,
+} from './selector'
+import { collectNodeText } from '../../../utils'
+import { isMobileFacebook } from './isMobile'
+
+export const getNickName = () => {
+ const node = isMobileFacebook ? searchNickNameSelectorOnMobile().evaluate() : searchNickNameSelector().evaluate()
+ if (!node) return ''
+
+ return collectNodeText(node)
+}
+
+export const getAvatar = () => {
+ const node = isMobileFacebook
+ ? searchFacebookAvatarOnMobileSelector().evaluate()
+ : searchAvatarSelector().evaluate()
+ if (!node) return
+
+ const imageURL =
+ (isMobileFacebook
+ ? node.style.background.match(/\(["']?(.*?)["']?\)/)?.[1]
+ : node.getAttribute('xlink:href')) ?? ''
+
+ return imageURL.trim()
+}
+
+export const getBioDescription = () => {
+ const node = isMobileFacebook ? bioDescriptionSelectorOnMobile().evaluate() : bioDescriptionSelector().evaluate()
+
+ if (!node) return ''
+
+ // Only collect text
+ return [...node.childNodes]
+ .map((each) => {
+ if (each.nodeType === document.TEXT_NODE) return (each as Text).nodeValue || ''
+ return ''
+ })
+ .join('')
+}
+
+export const getFacebookId = () => {
+ const node = isMobileFacebook ? searchUserIdSelectorOnMobile().evaluate() : searchUserIdSelector().evaluate()
+
+ if (!node) return ''
+ const url = new URL(node.href)
+
+ if (url.pathname === '/profile.php') return url.searchParams.get(isMobileFacebook ? 'lst' : 'id')
+
+ return url.pathname.replaceAll('/', '')
+}
+
+const FACEBOOK_AVATAR_ID_MATCH = /(\w+).(?:png|jpg|gif|bmp)/
+
+export const getAvatarId = (avatarURL: string) => {
+ if (!avatarURL) return ''
+ const _url = new URL(avatarURL)
+ const match = _url.pathname.match(FACEBOOK_AVATAR_ID_MATCH)
+ if (!match) return ''
+ return match[1]
+}
diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/Avatar.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/Avatar.tsx
index 8c952e3fb8f0..df94236ace70 100644
--- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/Avatar.tsx
+++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/Avatar.tsx
@@ -5,14 +5,14 @@ import { Flags } from '../../../../../shared'
import { getInjectNodeInfo } from '../../utils/avatar'
import { postAvatarsContentSelector } from '../../utils/selector'
import { NFTAvatarMiniClip } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatarClip'
+import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants'
function getTwitterId(ele: HTMLElement) {
const twitterIdNode = (ele.firstChild?.nextSibling as HTMLElement).querySelector(
'[dir="ltr"] > span',
) as HTMLSpanElement
if (!twitterIdNode) return
- const twitterId = twitterIdNode.innerText.trim().replace('@', '')
- return twitterId
+ return twitterIdNode.innerText.trim().replace('@', '')
}
function _(main: () => LiveSelector, signal: AbortSignal) {
@@ -48,6 +48,7 @@ function _(main: () => LiveSelector, signal: AbortSignal) {
avatarId={info.avatarId}
width={info.width - 4}
height={info.height - 4}
+ snsKey={RSS3_KEY_SNS.TWITTER}
/>
)}
,
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 6274a9451929..5042b59cd486 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,7 +2,7 @@ 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, useMemo, useRef } from 'react'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI'
import { resolveOpenSeaLink, useWallet } from '@masknet/web3-shared-evm'
import type { AvatarMetaDB } from '../../../../plugins/Avatar/types'
@@ -14,6 +14,7 @@ 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'
+import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants'
export function injectNFTAvatarInTwitter(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(searchTwitterAvatarSelector())
@@ -50,7 +51,7 @@ function NFTAvatarInTwitter() {
const borderElement = useRef()
const identity = useCurrentVisitingIdentity()
const wallet = useWallet()
- const { value: _avatar } = useNFTAvatar(identity.identifier.userId)
+ const { value: _avatar } = useNFTAvatar(identity.identifier.userId, RSS3_KEY_SNS.TWITTER)
const [avatar, setAvatar] = useState()
const windowSize = useWindowSize()
const location = useLocation()
@@ -98,6 +99,7 @@ function NFTAvatarInTwitter() {
avatarId: getAvatarId(identity.avatar ?? ''),
} as AvatarMetaDB,
identity.identifier.network,
+ RSS3_KEY_SNS.TWITTER,
).catch((error) => {
setNFTEvent(undefined)
setAvatar(undefined)
@@ -207,6 +209,7 @@ function NFTAvatarInTwitter() {
<>
{showAvatar ? (
LiveSelector, signal: AbortSignal) {
startWatch(
@@ -27,7 +28,7 @@ function _(main: () => LiveSelector, signal: AbortSignal) {
const root = createReactRootShadowed(proxy.afterShadow, { signal })
root.render(
-
+
,
)
remover = root.destory
diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx
index 643107f7359d..d4d9edf6afea 100644
--- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx
+++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { blobToArrayBuffer } from '@dimensiondev/kit'
import { createReactRootShadowed, MaskMessages, NFTAvatarEvent, startWatch } from '../../../../utils'
import {
- searchAvatarOpenFileSelector,
+ searchTwitterAvatarOpenFilesSelector,
searchProfessionalButtonSelector,
searchProfileSaveSelector,
searchProfileSetAvatarSelector,
@@ -58,12 +58,13 @@ function getStyles() {
async function changeImageToActiveElements(image: File | Blob): Promise {
const imageBuffer = await blobToArrayBuffer(image)
hookInputUploadOnce('image/png', 'avatar.png', new Uint8Array(imageBuffer))
- ;(searchAvatarOpenFileSelector().evaluate()[0]?.parentElement?.children[0] as HTMLElement)?.click()
+ ;(searchTwitterAvatarOpenFilesSelector().evaluate()[0]?.parentElement?.children[0] as HTMLElement)?.click()
}
function NFTAvatarInTwitter() {
const { classes } = useStyles(getStyles())
const identity = useCurrentVisitingIdentity()
+
const [avatarEvent, setAvatarEvent] = useState()
const onChange = async (token: ERC721TokenDetailed) => {
diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/TweetNFTAvatar.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/TweetNFTAvatar.tsx
index 1f355a6e56d9..7c07bf1c29da 100644
--- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/TweetNFTAvatar.tsx
+++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/TweetNFTAvatar.tsx
@@ -5,6 +5,7 @@ import { Flags } from '../../../../../shared'
import { getInjectNodeInfo } from '../../utils/avatar'
import { searchRetweetAvatarSelector, searchTweetAvatarSelector } from '../../utils/selector'
import { NFTAvatarMiniClip } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatarClip'
+import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants'
function _(main: () => LiveSelector, signal: AbortSignal) {
startWatch(
@@ -24,7 +25,12 @@ function _(main: () => LiveSelector, signal: AbortSignal) {
{info.isTwitterNFT ? (
) : (
-
+
)}
,
)
diff --git a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts
index 9459c06de3eb..3306e6b4f213 100644
--- a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts
+++ b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts
@@ -197,7 +197,7 @@ export const selfInfoSelectors = () => ({
userAvatar: p(avatar, 1),
})
-// #region nft avatar
+// #region twitter nft avatar
export const searchProfileAvatarSelector = () => {
return querySelectorAll('[data-testid="fileInput"]').at(1).closest(4)
}
@@ -217,7 +217,7 @@ export const searchAvatarSelectorImage = () =>
.closest(2)
.querySelector('div > div > :nth-child(2) > div > img')
-export const searchAvatarOpenFileSelector = () => querySelectorAll('[data-testid="fileInput"]').at(1)
+export const searchTwitterAvatarOpenFilesSelector = () => querySelectorAll('[data-testid="fileInput"]').at(1)
export const searchProfileSaveSelector = () => querySelector('[data-testid="Profile_Save_Button"]')
export const searchProfessionalButtonSelector = () => querySelector('[data-testid*="ProfessionalButton"]')