Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mf-6036 add farcaster widget #11369

Merged
merged 2 commits into from
Feb 7, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/icons/brands/DarkLens.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/icons/icon-generated-as-jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export const DangerOutline = /*#__PURE__*/ __createIcon('DangerOutline', [
s: true,
},
])
export const DarkLens = /*#__PURE__*/ __createIcon('DarkLens', [
{
u: () => new URL('./brands/DarkLens.svg', import.meta.url).href,
},
])
export const Debank = /*#__PURE__*/ __createIcon('Debank', [
{
u: () => new URL('./brands/Debank.svg', import.meta.url).href,
Expand Down
1 change: 1 addition & 0 deletions packages/icons/icon-generated-as-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function cross_sync_url() { return new URL("./brands/CrossSync.svg", impo
export function cyber_connect_url() { return new URL("./brands/CyberConnect.svg", import.meta.url).href }
export function danger_url() { return new URL("./brands/Danger.svg", import.meta.url).href }
export function danger_outline_url() { return new URL("./brands/DangerOutline.svg", import.meta.url).href }
export function dark_lens_url() { return new URL("./brands/DarkLens.svg", import.meta.url).href }
export function debank_url() { return new URL("./brands/Debank.svg", import.meta.url).href }
export function discord_url() { return new URL("./brands/Discord.svg", import.meta.url).href }
export function discord_round_url() { return new URL("./brands/DiscordRound.svg", import.meta.url).href }
Expand Down
1 change: 1 addition & 0 deletions packages/mask/content-script/site-adaptor-infra/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export async function activateSiteAdaptorUIInner(ui_deferred: SiteAdaptorUI.Defe
ui.injection.profileAvatar?.(signal)
ui.injection.tips?.(signal)
ui.injection.nameWidget?.(signal)
ui.injection.farcaster?.(signal)
ui.injection.lens?.(signal)

ui.injection.enhancedProfileNFTAvatar?.(signal)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { injectFarcasterOnConversation } from './injectFarcasterOnConversation.js'
import { injectFarcasterOnPost } from './injectFarcasterOnPost.js'
import { injectFarcasterOnProfile } from './injectFarcasterOnProfile.js'
import { injectFarcasterOnSpaceDock } from './injectFarcasterOnSpaceDock.js'
import { injectFarcasterOnUserCell } from './injectFarcasterOnUserCell.js'

export function injectFarcaster(signal: AbortSignal) {
injectFarcasterOnProfile(signal)
injectFarcasterOnPost(signal)
injectFarcasterOnUserCell(signal)
injectFarcasterOnConversation(signal)
injectFarcasterOnSpaceDock(signal)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { memo, useMemo, useState } from 'react'
import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
import { createInjectHooksRenderer, Plugin, useActivatedPluginsSiteAdaptor } from '@masknet/plugin-infra/content-script'
import { EnhanceableSite, ProfileIdentifier } from '@masknet/shared-base'
import { makeStyles } from '@masknet/theme'
import { attachReactTreeWithContainer } from '../../../../utils/shadow-root/renderInShadowRoot.js'
import { querySelectorAll } from '../../utils/selector.js'
import { startWatch } from '../../../../utils/startWatch.js'

function selector() {
return querySelectorAll<HTMLElement>('[data-testid=conversation] div:not([tabindex]) div[dir] + div[dir]')
}

const useStyles = makeStyles()((theme) => ({
hide: {
display: 'none',
},
slot: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 999,
marginLeft: theme.spacing(0.5),
verticalAlign: 'top',
},
}))

interface Props {
userId: string
}

function createRootElement() {
const span = document.createElement('span')
Object.assign(span.style, {
verticalAlign: 'bottom',
height: '21px',
alignItems: 'center',
justifyContent: 'center',
display: 'inline-flex',
} as CSSStyleDeclaration)
return span
}

const ConversationFarcasterSlot = memo(function ConversationFarcasterSlot({ userId }: Props) {
const [disabled, setDisabled] = useState(true)
const { classes, cx } = useStyles()

const component = useMemo(() => {
const Component = createInjectHooksRenderer(
useActivatedPluginsSiteAdaptor.visibility.useNotMinimalMode,
(plugin) => plugin.Farcaster?.UI?.Content,
undefined,
createRootElement,
)
const identifier = ProfileIdentifier.of(EnhanceableSite.Twitter, userId).unwrapOr(null)
if (!identifier) return null

return (
<Component
identity={identifier}
slot={Plugin.SiteAdaptor.FarcasterSlot.Sidebar}
onStatusUpdate={setDisabled}
/>
)
}, [userId])

if (!component) return null

return <span className={cx(classes.slot, disabled ? classes.hide : null)}>{component}</span>
})

/**
* Inject on conversation, including both DM drawer and message page (/messages/xxx)
*/
export function injectFarcasterOnConversation(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(selector())
startWatch(watcher, signal)
watcher.useForeach((node, _, proxy) => {
const spans = node
.closest('[data-testid=conversation]')
?.querySelectorAll<HTMLElement>('[tabindex] [dir] span:not([data-testid=tweetText])')
if (!spans) return
const userId = [...spans].reduce((id, node) => {
if (id) return id
if (node.textContent?.match(/@\w/)) {
return node.textContent.trim().slice(1)
}
return ''
}, '')
if (!userId) return
attachReactTreeWithContainer(proxy.afterShadow, { signal, untilVisible: true }).render(
<ConversationFarcasterSlot userId={userId} />,
)
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useMemo, useState } from 'react'
import { EnhanceableSite, ProfileIdentifier } from '@masknet/shared-base'
import { makeStyles } from '@masknet/theme'
import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
import { createInjectHooksRenderer, Plugin, useActivatedPluginsSiteAdaptor } from '@masknet/plugin-infra/content-script'
import { startWatch } from '../../../../utils/startWatch.js'
import { attachReactTreeWithContainer } from '../../../../utils/shadow-root/renderInShadowRoot.js'
import { querySelectorAll } from '../../utils/selector.js'

function selector() {
return querySelectorAll<HTMLElement>('[data-testid=User-Name] div').filter((node) => {
return node.firstElementChild?.matches('a[role=link]:not([tabindex])')
})
}

// structure: <user-name> <user-id> <timestamp>
export function injectFarcasterOnPost(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(selector())
startWatch(watcher, signal)
watcher.useForeach((node, _, proxy) => {
const link = node.querySelector('a[href][role=link]')
// To simplify the selector above, we do this checking manually
// <user-id> has tabindex=-1, <timestamp> has a child time element
if (link?.hasAttribute('tabindex') || link?.querySelector('time')) return
const href = link?.getAttribute('href')
const userId = href?.split('/')[1]
if (!userId) return
attachReactTreeWithContainer(proxy.afterShadow, { signal, untilVisible: true }).render(
<PostFarcasterSlot userId={userId} />,
)
})
}

const useStyles = makeStyles()((theme) => ({
hide: {
display: 'none',
},
slot: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 999,
marginLeft: theme.spacing(0.5),
verticalAlign: 'top',
},
}))

interface Props {
userId: string
}

function createRootElement() {
const span = document.createElement('span')
Object.assign(span.style, {
alignItems: 'center',
display: 'flex',
})
return span
}
function PostFarcasterSlot({ userId }: Props) {
const [disabled, setDisabled] = useState(true)
const { classes, cx } = useStyles()

const component = useMemo(() => {
const Component = createInjectHooksRenderer(
useActivatedPluginsSiteAdaptor.visibility.useNotMinimalMode,
(plugin) => plugin.Farcaster?.UI?.Content,
undefined,
createRootElement,
)
const identifier = ProfileIdentifier.of(EnhanceableSite.Twitter, userId).unwrap()
if (!identifier) return null

return (
<Component
identity={identifier}
slot={Plugin.SiteAdaptor.FarcasterSlot.Post}
onStatusUpdate={setDisabled}
/>
)
}, [userId])

if (!component) return null

return <span className={cx(classes.slot, disabled ? classes.hide : null)}>{component}</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useMemo, useState } from 'react'
import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
import { createInjectHooksRenderer, Plugin, useActivatedPluginsSiteAdaptor } from '@masknet/plugin-infra/content-script'
import { makeStyles } from '@masknet/theme'
import { startWatch } from '../../../../utils/startWatch.js'
import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI.js'
import { attachReactTreeWithContainer } from '../../../../utils/shadow-root/renderInShadowRoot.js'
import { querySelector } from '../../utils/selector.js'

function selector() {
return querySelector<HTMLElement>('[data-testid=UserName] div[dir]')
}

export function injectFarcasterOnProfile(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(selector())
startWatch(watcher, signal)
attachReactTreeWithContainer(watcher.firstDOMProxy.afterShadow, { signal }).render(<ProfileFarcasterSlot />)
}

const useStyles = makeStyles()((theme) => ({
hide: {
display: 'none',
},
slot: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 999,
marginLeft: theme.spacing(0.5),
verticalAlign: 'top',
},
}))

function ProfileFarcasterSlot() {
const visitingIdentity = useCurrentVisitingIdentity()
const [disabled, setDisabled] = useState(true)
const { classes, cx } = useStyles()

const component = useMemo(() => {
const Component = createInjectHooksRenderer(
useActivatedPluginsSiteAdaptor.visibility.useNotMinimalMode,
(plugin) => plugin.Farcaster?.UI?.Content,
)

return (
<Component
identity={visitingIdentity.identifier}
slot={Plugin.SiteAdaptor.FarcasterSlot.ProfileName}
onStatusUpdate={setDisabled}
/>
)
}, [visitingIdentity.identifier])

if (!component || !visitingIdentity.identifier) return null

return <span className={cx(classes.slot, disabled ? classes.hide : null)}>{component}</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useMemo, useState } from 'react'
import { makeStyles } from '@masknet/theme'
import { EnhanceableSite, ProfileIdentifier } from '@masknet/shared-base'
import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit'
import { createInjectHooksRenderer, Plugin, useActivatedPluginsSiteAdaptor } from '@masknet/plugin-infra/content-script'
import { startWatch } from '../../../../utils/startWatch.js'
import { attachReactTreeWithContainer } from '../../../../utils/shadow-root/renderInShadowRoot.js'
import { querySelectorAll } from '../../utils/selector.js'

function avatarSelector() {
return querySelectorAll<HTMLElement>(
'[data-testid=SpaceDockExpanded] [data-testid^=UserAvatar-Container-],[data-testid=sheetDialog] [data-testid^=UserAvatar-Container-]',
).map((node) => {
const span = node.parentElement?.parentElement?.nextElementSibling?.querySelector('div > span + span > span')
return span
})
}

/**
* Inject on space dock
*/
export function injectFarcasterOnSpaceDock(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(avatarSelector())
startWatch(watcher, signal)
watcher.useForeach((node, _, proxy) => {
const avatar = node
.closest('div[dir]')
?.previousElementSibling?.querySelector<HTMLElement>('[data-testid^=UserAvatar-Container-]')
if (!avatar) return
const userId = avatar.dataset.testid?.slice('UserAvatar-Container-'.length)
if (!userId) return
attachReactTreeWithContainer(proxy.afterShadow, { signal, untilVisible: true }).render(
<SpaceDockFarcasterSlot userId={userId} />,
)
})
}

const useStyles = makeStyles()((theme) => ({
hide: {
display: 'none',
},
slot: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 999,
marginLeft: theme.spacing(0.5),
verticalAlign: 'top',
},
}))

interface Props {
userId: string
}

function createRootElement() {
const span = document.createElement('span')
Object.assign(span.style, {
verticalAlign: 'bottom',
height: '21px',
alignItems: 'center',
justifyContent: 'center',
display: 'inline-flex',
} as CSSStyleDeclaration)
return span
}

function SpaceDockFarcasterSlot({ userId }: Props) {
const [disabled, setDisabled] = useState(true)
const { classes, cx } = useStyles()

const component = useMemo(() => {
const Component = createInjectHooksRenderer(
useActivatedPluginsSiteAdaptor.visibility.useNotMinimalMode,
(plugin) => plugin.Farcaster?.UI?.Content,
undefined,
createRootElement,
)
const identifier = ProfileIdentifier.of(EnhanceableSite.Twitter, userId).unwrap()
if (!identifier) return null

return (
<Component
identity={identifier}
slot={Plugin.SiteAdaptor.FarcasterSlot.Sidebar}
onStatusUpdate={setDisabled}
/>
)
}, [userId])

if (!component) return null

return <span className={cx(classes.slot, disabled ? classes.hide : null)}>{component}</span>
}