Skip to content

Commit 6d74669

Browse files
authored
refactor: post modifier dirty detection (#12205)
1 parent 7b6d6ab commit 6d74669

File tree

8 files changed

+129
-28
lines changed

8 files changed

+129
-28
lines changed

packages/mask/content-script/components/InjectedComponents/PostReplacer.tsx

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
import { useEffect, useMemo, useState } from 'react'
2-
import { produce } from 'immer'
1+
import {
2+
usePostInfoAuthor,
3+
usePostInfoPostIVIdentifier,
4+
usePostInfoRawMessage,
5+
usePostInfoURL,
6+
} from '@masknet/plugin-infra/content-script'
7+
import { DirtyDetection, useDirtyDetection } from '@masknet/plugin-infra/dom'
8+
import {
9+
EXIST_EVM_ADDRESS_RE,
10+
EXIST_SOLANA_ADDRESS_RE,
11+
EXIST_TORN_ADDRESS_RE,
12+
MaskMessages,
13+
} from '@masknet/shared-base'
314
import { makeStyles } from '@masknet/theme'
415
import {
516
type TransformationContext,
617
type TypedMessage,
7-
isTypedMessageEqual,
818
emptyTransformationContext,
919
FlattenTypedMessage,
1020
forEachTypedMessageChild,
1121
isTypedMessageAnchor,
12-
makeTypedMessageText,
22+
isTypedMessageEqual,
1323
isTypedMessageText,
24+
makeTypedMessageText,
1425
} from '@masknet/typed-message'
15-
import {
16-
EXIST_EVM_ADDRESS_RE,
17-
EXIST_SOLANA_ADDRESS_RE,
18-
EXIST_TORN_ADDRESS_RE,
19-
MaskMessages,
20-
} from '@masknet/shared-base'
2126
import { TypedMessageRender, useTransformedValue } from '@masknet/typed-message-react'
22-
import {
23-
usePostInfoAuthor,
24-
usePostInfoPostIVIdentifier,
25-
usePostInfoRawMessage,
26-
usePostInfoURL,
27-
} from '@masknet/plugin-infra/content-script'
27+
import { produce } from 'immer'
28+
import { useEffect, useMemo, useState } from 'react'
2829
import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context.js'
29-
import { useCurrentIdentity } from '../DataSource/useActivatedUI.js'
3030
import { activatedSiteAdaptorUI } from '../../site-adaptor-infra/ui.js'
31+
import { useCurrentIdentity } from '../DataSource/useActivatedUI.js'
3132

3233
const useStyles = makeStyles()({
3334
root: {
@@ -75,7 +76,9 @@ export function PostReplacer(props: PostReplacerProps) {
7576
textResizer={activatedSiteAdaptorUI!.networkIdentifier !== 'twitter.com'}
7677
renderFragments={activatedSiteAdaptorUI?.customization.componentOverwrite?.RenderFragments}
7778
context={initialTransformationContext}>
78-
<Transformer {...props} message={postMessage} />
79+
<DirtyDetection>
80+
<Transformer {...props} message={postMessage} />
81+
</DirtyDetection>
7982
</TypedMessageRenderContext>
8083
</span>
8184
)
@@ -90,7 +93,7 @@ function Transformer({
9093
} & PostReplacerProps) {
9194
const after = useTransformedValue(message)
9295

93-
const shouldReplace = useMemo(() => {
96+
const staticGuess = useMemo(() => {
9497
const flatten = FlattenTypedMessage(message, emptyTransformationContext)
9598
if (!isTypedMessageEqual(flatten, after)) return true
9699
if (hasCashOrHashTag(after)) return true
@@ -99,14 +102,22 @@ function Transformer({
99102
return false
100103
}, [message, after])
101104

105+
const { isDirty, isPending } = useDirtyDetection()
106+
107+
const shouldReplace = staticGuess ? isPending || isDirty : false
102108
useEffect(() => {
109+
if (isPending) return
103110
if (shouldReplace) zip?.()
104111
else unzip?.()
105112

106113
return () => unzip?.()
107-
}, [])
114+
}, [shouldReplace, isPending])
108115

109-
if (shouldReplace) return <TypedMessageRender message={after} />
116+
if (shouldReplace || isDirty) {
117+
const rendered = <TypedMessageRender message={after} />
118+
if (isPending) return <div style={{ display: 'none' }}>{rendered}</div>
119+
return rendered
120+
}
110121
return null
111122
}
112123
function hasCashOrHashTag(message: TypedMessage): boolean {

packages/mask/content-script/site-adaptors/twitter.com/injection/PostReplacer.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,21 @@ export async function injectPostReplacerAtTwitter(signal: AbortSignal, current:
4141
[
4242
'a[role="link"][href*="cashtag_click"]',
4343
'a[role="link"][href*="hashtag_click"]',
44-
'a[role="link"][href*="/hashtag/"][href*="/i/communities/"]', // tag in community
44+
// in communities post <a href="/i/communities/1722516678070972815/hashtag/BTC" />
45+
'a[role="link"][href*="/i/communities/"][href*="/hashtag/"]',
4546
].join(','),
4647
) ?? [],
4748
)
48-
if (!tags.length) return
49+
const mentions = Array.from(
50+
rootNode.querySelectorAll<HTMLAnchorElement>(
51+
[
52+
'a[href^="/"]:not([role="link"][href*="mention_click"])',
53+
'a[href^="/"]:not([role="link"][href*="/i/communities/"][href*="/mention/"])',
54+
'a[href^="/"]:not([role="link"][href*="/i/communities/"][href*="/hashtag/"])',
55+
].join(','),
56+
) ?? [],
57+
).filter((x) => x.textContent?.startsWith('@'))
58+
if (!tags.length && !mentions.length) return
4959
}
5060

5161
return injectPostReplacer({
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { isEqual, noop } from 'lodash-es'
2+
import {
3+
createContext,
4+
memo,
5+
useContext,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
type Dispatch,
10+
type PropsWithChildren,
11+
type SetStateAction,
12+
} from 'react'
13+
14+
interface Dependency {
15+
id: string
16+
status: boolean
17+
pending: boolean
18+
}
19+
20+
interface Options {
21+
setDependencies: Dispatch<SetStateAction<Dependency[]>>
22+
isDirty: boolean
23+
isPending: boolean
24+
}
25+
26+
export const DirtyDetectionContext = createContext<Options>({
27+
setDependencies: noop,
28+
isDirty: false,
29+
isPending: false,
30+
})
31+
DirtyDetectionContext.displayName = 'DirtyDetectionContext'
32+
33+
export const DirtyDetection = memo<PropsWithChildren>(function DirtyDetection({ children }) {
34+
const [deps, setDeps] = useState<Dependency[]>([])
35+
36+
const contextValue = useMemo(() => {
37+
const isDirty = deps.some((dep) => dep.status)
38+
const isPending = !deps.length || deps.some((dep) => dep.pending)
39+
return {
40+
setDependencies: setDeps,
41+
isDirty,
42+
isPending,
43+
}
44+
}, [deps])
45+
46+
return <DirtyDetectionContext value={contextValue}>{children}</DirtyDetectionContext>
47+
})
48+
49+
export function useDirtyDetection() {
50+
const context = useContext(DirtyDetectionContext)
51+
return context
52+
}
53+
54+
export function useDirtyDetectionDependency(status: boolean, pending: boolean, dependencyId: string) {
55+
const { setDependencies } = useDirtyDetection()
56+
57+
useEffect(() => {
58+
setDependencies((deps) => {
59+
if (!deps.some((dep) => dep.id === dependencyId)) {
60+
return [...deps, { id: dependencyId, status, pending }]
61+
}
62+
const dep = deps.find((dep) => dep.id === dependencyId)
63+
if (isEqual(dep, { id: dependencyId, status, pending })) return deps
64+
return deps.map((dep) => (dep.id === dependencyId ? { ...dep, status, pending } : dep))
65+
})
66+
}, [status, dependencyId, pending])
67+
}

packages/plugin-infra/src/dom/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { PluginTransFieldRender, type PluginTransFieldRenderProps, usePluginTran
33

44
export { type PluginWrapperMethods, type PluginWrapperComponentProps, usePluginWrapper } from './usePluginWrapper.js'
55
export { __setUIContext__, type __UIContext__ } from './context.js'
6+
export * from './DirtyDetectionContext.js'

packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { extractAddresses } from '../../utils.js'
66
import { usePopoverControl } from './usePopoverControl.js'
77
import { WarningCard } from './WarningCard.js'
88
import { useDetectAddress } from '../hooks/useDetectAddress.js'
9+
import { useDirtyDetectionDependency } from '@masknet/plugin-infra/dom'
910

1011
const useStyles = makeStyles()((theme) => ({
1112
text: {
@@ -97,6 +98,8 @@ export const TextModifier = memo<TextModifierProps>(function TextModifier({ fall
9798
segments.push({ type: 'text', value: fullText.slice(offset) })
9899
return segments
99100
}, [addresses, fullText])
101+
const isDirty = !!addresses.length && !segments.length
102+
useDirtyDetectionDependency(isDirty, false, fullText)
100103

101104
if (addresses.length === 0 || segments.length === 0) return fallback
102105

packages/plugins/Trader/src/SiteAdaptor/components/MentionModifier.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Plugin } from '@masknet/plugin-infra'
2+
import { useDirtyDetectionDependency } from '@masknet/plugin-infra/dom'
23
import { PluginTraderMessages } from '@masknet/plugin-trader'
34
import { makeStyles } from '@masknet/theme'
45
import type { Web3Helper } from '@masknet/web3-helpers'
@@ -34,7 +35,7 @@ type TagSearchResult =
3435
export const MentionModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['MentionModifier']>>(
3536
function MentionModifier({ children, href }) {
3637
const { classes } = useStyles()
37-
const { data } = useQuery({
38+
const { data, isLoading } = useQuery({
3839
queryKey: ['mention', children],
3940
queryFn: async () => {
4041
return DSearch.search<TagSearchResult>(
@@ -44,6 +45,9 @@ export const MentionModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['Menti
4445
},
4546
})
4647
const timerRef = useRef<NodeJS.Timeout>(undefined)
48+
49+
const isDirty = !!data?.length && !isLoading
50+
useDirtyDetectionDependency(isDirty, isLoading, children)
4751
if (data?.length) {
4852
return (
4953
<span

packages/plugins/Trader/src/SiteAdaptor/components/TagModifier.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Plugin } from '@masknet/plugin-infra'
2+
import { useDirtyDetectionDependency } from '@masknet/plugin-infra/dom'
23
import { PluginTraderMessages } from '@masknet/plugin-trader'
34
import { Image } from '@masknet/shared'
45
import { makeStyles } from '@masknet/theme'
@@ -26,7 +27,8 @@ const useStyles = makeStyles()(() => ({
2627
overflow: 'hidden',
2728
objectFit: 'cover',
2829
},
29-
failedImage: {
30+
imageContainer: {
31+
display: 'contents',
3032
borderRadius: 16,
3133
overflow: 'hidden',
3234
},
@@ -41,12 +43,15 @@ export const TagModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['TagModifi
4143
href,
4244
}) {
4345
const { classes } = useStyles()
44-
const { data } = useQuery({
46+
const { data, isLoading } = useQuery({
4547
queryKey: ['tag', children],
4648
queryFn: async () => {
4749
return DSearch.search<TagSearchResult>(children)
4850
},
4951
})
52+
const isDirty = !!data?.length && !isLoading
53+
useDirtyDetectionDependency(isDirty, isLoading, children)
54+
5055
const timerRef = useRef<NodeJS.Timeout>(undefined)
5156
if (data?.length) {
5257
return (
@@ -68,7 +73,7 @@ export const TagModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['TagModifi
6873
}}>
6974
<Image
7075
size={16}
71-
classes={{ failed: classes.failedImage }}
76+
classes={{ container: classes.imageContainer, image: classes.icon }}
7277
className={classes.icon}
7378
src={data[0].logoURL}
7479
fallback={

packages/shared/src/UI/components/Image/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const MASK_LIGHT_FALLBACK = new URL('./mask-light.png', import.meta.url).href
4343

4444
export interface ImageProps
4545
extends ImgHTMLAttributes<HTMLImageElement>,
46-
withClasses<'container' | 'fallbackImage' | 'imageLoading' | 'failed'> {
46+
withClasses<'container' | 'fallbackImage' | 'imageLoading' | 'failed' | 'image'> {
4747
size?: number | string
4848
rounded?: boolean
4949
fallback?: string | JSX.Element | null

0 commit comments

Comments
 (0)