feat(efp): add Ethereum Follow Protocol plugin#12377
Open
Quantumlyy wants to merge 22 commits into
Open
Conversation
Replace the body-wide GlobalInjection MutationObserver with a per-post hook that uses usePostInfoDetails.rootNode() (NextID pattern) and queries [data-testid=card.wrapper] within the post (Mask Twitter PostInspector pattern). The previous broad scan plus EFP-specific metadata heuristics didn't reliably catch Twitter's lazy-rendered card, leaving a duplicate native preview below the EFP card.
The post's rootNode (per twitter selector at packages/mask/content-script/site-adaptors/twitter.com/utils/selector.ts:186) is the tweetText/tweetPhoto/div[lang] — the card.wrapper is its sibling inside [data-testid=tweet], not a descendant. Climb up to the tweet element before querying so the native EFP card is actually found.
The native EFP detection was failing because Twitter wraps the link in t.co (no href match) and the card.wrapper's textContent only holds 'brantly.eth' — the 'efp.app' reference lives in aria-label on the inner anchor and in the 'From efp.app' footer that is a sibling of card.wrapper. Detect via aria-label so isEFPCard returns true, and hide the parent that's aria-labelledby the card so the footer is hidden along with the wrapper.
[data-testid="tweet"] is sometimes on a nested div (not the article) in this version of Twitter, so closest() can land on an element that doesn't contain card.wrapper. Search from article.parentElement (the timeline section / detail view container) instead — that covers both the timeline layout (card inside article) and the detail layout (card in a sibling subtree). Falls back to document.body when no article ancestor is found.
Twitter's postsContentSelector matches [data-testid="card.wrapper"] directly for link-only tweets, so rootNode can BE the card.wrapper. The strict equality check was correct for that case but missed the defensive case where rootNode might end up nested inside the wrapper. contains() covers both.
article.parentElement is the entire timeline container, so the observer's textContent fallback in isEFPCard could hide a sibling tweet whose Twitter preview happens to mention efp.app/ethfollow.xyz (news article, embed of an EFP-related quote, etc.) even though no EFP plugin is rendering for that post. Use isFocusing to detect detail view, where the card can live in a sibling subtree of the article (per twitter's postsContentSelector at packages/mask/content-script/site-adaptors/twitter.com/utils/selector.ts:195), and only widen the search root there. Timeline view stays scoped to the article.
- Read rootNode/isFocusing via useContext(PostInfoContext) instead of the usePostInfoDetails proxy. The proxy returns plain values for fields like rootNode (no real hook is invoked under the hood) and react-compiler flags the property-access call as 'hook referenced as a normal value'. Reading from the context directly sidesteps the rule and is also one fewer indirection. - Add the 'u' flag to /\\s+/ (require-unicode-regexp). - Use optional chaining on labelledBy.split(...) per @typescript-eslint/prefer-optional-chain. Confirmed clean with 'pnpm exec eslint packages/plugins/EFP --no-cache'.
For tweets that are just an EFP link, Twitter's postsContentSelector matches data-testid=card.wrapper directly as the post's rootNode, and the plugin UI mounts in rootElement.afterShadow — a sibling of the card.wrapper, not a descendant. The previous guard skipped hiding any card that contained rootNode, leaving the native preview rendered alongside the EFP card. Replace the skip with a target choice: hide the card itself when the container would also contain rootNode (so we don't take an ancestor — which holds our afterShadow sibling — down with it), and keep hiding the full container otherwise (so the 'From efp.app' footer goes away with the wrapper).
- Wrap user-visible strings in <Trans> per repo convention (ProfileCard eyebrow/metrics/footer/link, ApplicationEntries name + description) - Dedup EFP host & reserved-path lists between constants.ts and helpers/url.ts - Dedup host-keyword literals in isEFPCard via EFP_HOST_KEYWORDS - Pass parsed EFPProfileLink from inspectors to Renderer (was parsed twice) - Drop completed TODO list from README
Replace the generic Icons.Web3Profile placeholder with the EFP brand logo (gold rounded square + arrow + plus mark) at all three call sites: the App entry tile, the post wrapper, and the og-image fallback inside ProfileCard.
Move fetchEFPProfile (and the EFPProfileResponse type) to a Worker module and expose it via PluginEFPRPC, mirroring the CyberConnect pattern. Network requests now run in the background context instead of the content script, sidestepping CORS preflight on the data.ethfollow.xyz origin and aligning with repo convention for external API calls.
X often renders link text without a scheme (efp.app/vitalik.eth). mentionedLinks() requires URL.canParse (i.e. a protocol), so those get dropped before parseEFPProfileLink can see them. Switch to rawMessage() + parseURLs(text, false), matching the DecryptedInspector in the same file and the rawMessage pattern used by NextID and ScamSniffer.
cspell tokenises PluginEFPRPC as Plugin / EFPRPC (consecutive caps stay in one block), and EFPRPC isn't in any default dictionary. Add it to ignoreWords in alphabetical order.
Author
|
Hello, any updates? |
There was a problem hiding this comment.
Pull request overview
Adds a new @masknet/plugin-efp plugin to embed Ethereum Follow Protocol (EFP) profile/list links in supported posts, including a Worker/RPC bridge for API calls and an EFP icon/CSP updates to support required network/image access.
Changes:
- Introduces the EFP plugin package (SiteAdaptor UI, Worker APIs, URL parsing, and Vitest URL tests).
- Registers the plugin in the Mask app and workspace, and assigns a new
PluginID. - Adds an EFP icon to
@masknet/iconsand extends CSP to allow EFP API fetches and preview images.
Reviewed changes
Copilot reviewed 22 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| security/content-security-policy.json | Allows EFP API origin in connect-src and EFP app origin in img-src. |
| pnpm-lock.yaml | Adds the new workspace plugin entry for @masknet/plugin-efp. |
| packages/shared-base/src/types/PluginID.ts | Introduces PluginID.EFP. |
| packages/plugins/tsconfig.json | Adds project reference for the new EFP plugin. |
| packages/plugins/EFP/tsconfig.json | New TS config for building the EFP plugin package. |
| packages/plugins/EFP/src/Worker/index.ts | Worker entry that starts the service API module. |
| packages/plugins/EFP/src/Worker/apis/index.ts | Worker-side fetch API for EFP profile/list details. |
| packages/plugins/EFP/src/tests/url.ts | Vitest coverage for URL parsing and contribution URL regex behavior. |
| packages/plugins/EFP/src/SiteAdaptor/ProfileCard.tsx | Renders the inline EFP card UI and loads profile data via RPC. |
| packages/plugins/EFP/src/SiteAdaptor/index.tsx | Detects EFP links in posts and hides native Twitter cards to avoid duplicate embeds. |
| packages/plugins/EFP/src/register.ts | Registers SiteAdaptor and Worker loaders (with HMR hooks). |
| packages/plugins/EFP/src/messages.ts | Defines PluginEFPRPC RPC bridge to the Worker APIs. |
| packages/plugins/EFP/src/index.ts | Exports plugin constants and URL helpers. |
| packages/plugins/EFP/src/helpers/url.ts | Parses/validates EFP profile/list links and derives profile/image/API URLs. |
| packages/plugins/EFP/src/env.d.ts | Adds webpack HMR global types reference. |
| packages/plugins/EFP/src/constants.ts | Defines plugin metadata, EFP hosts, and URL matching regex. |
| packages/plugins/EFP/src/base.ts | Declares plugin base definition + contribution matcher. |
| packages/plugins/EFP/README.md | Documents referenced resources and parsing constraints/caveats. |
| packages/plugins/EFP/package.json | Declares the new plugin package exports and workspace deps. |
| packages/mask/shared/plugin-infra/register.js | Registers the EFP plugin in the app’s plugin registry. |
| packages/mask/package.json | Adds @masknet/plugin-efp as a workspace dependency. |
| packages/icons/plugins/EFP.svg | Adds the EFP icon asset. |
| packages/icons/icon-generated-as-url.js | Exposes the EFP icon as a URL export. |
| packages/icons/icon-generated-as-jsx.js | Exposes the EFP icon as a JSX icon export. |
| cspell.json | Adds “efprpc” / “ethfollow” to the spelling dictionary. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Jack-Works
reviewed
May 11, 2026
useHideNativeTwitterCard sets display:none and aria-hidden=true on the native Twitter card but the cleanup only disconnected the MutationObserver, so on unmount (navigation, plugin disabled, post leaving the viewport) the card stayed hidden with no way back. Track each modified element with its previous display/aria-hidden values and revert them on cleanup. Skip elements we've already hidden so re-firings of the observer don't overwrite the stored previous state.
isEFPCard used a[href*="efp.app"] and lowercase substring scans of aria-label and textContent. That would hide any Twitter card whose title or description happens to mention efp.app, or whose host contains the substring. Walk each anchor inside the card and run its href, visible text, and aria-label through parseEFPProfileLink. Only valid EFP profile/list URLs match, which also handles t.co-wrapped hrefs (the real URL surfaces as the anchor's display text). Drops the now-unused EFP_HOST_KEYWORDS export.
Replace the hand-rolled useReducer + useEffect + cancellation flag with @tanstack/react-query (already a workspace dep, used by CyberConnect the same way). The select callback narrows the EFPProfileResponse via isProfileResponse so the rest of ProfileCard keeps its existing data?.foo access shape. Drops the EFPProfileState / EFPProfileAction types, reduceEFPProfileState and the useEFPProfile wrapper. getDisplayName now also accepts undefined since useQuery's data is undefined during the initial load.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
New plugin (
@masknet/plugin-efp) for Ethereum Follow Protocol. Detectsefp.appandethfollow.xyzprofile/list links in Twitter posts (with optional?topEight=true) and renders an inline card with ENS name, description, follower/following counts, and a "View on EFP" link. Data comes fromdata.ethfollow.xyz/api/v1, with a fallback when the API is unreachable.Also hides the native Twitter card for tweets that link to EFP so we don't end up with two embeds for the same URL. Hide is scoped to the article in the timeline and widened by one level in the detail view; there's a guard for link-only tweets where the rootNode is the card itself.
External API calls go through a Worker +
PluginEFPRPC, mirroring the CyberConnect plugin. Addeddata.ethfollow.xyztoconnect-srcin the CSP. Dedicated EFP icon registered in@masknet/icons.No new dependencies.
Type of change
Previews
Checklist
If this PR depends on external APIs: