diff --git a/packages/icons/general/Collectible.tsx b/packages/icons/general/Collectible.tsx new file mode 100644 index 000000000000..d292e5b06161 --- /dev/null +++ b/packages/icons/general/Collectible.tsx @@ -0,0 +1,13 @@ +import { createIcon } from '../utils' + +export const CollectibleIcon = createIcon( + 'CollectibleIcon', + + + , + '0 0 13 12', +) diff --git a/packages/icons/general/index.ts b/packages/icons/general/index.ts index 36e95494a86d..cfc815a0194c 100644 --- a/packages/icons/general/index.ts +++ b/packages/icons/general/index.ts @@ -87,3 +87,4 @@ export * from './Pin' export * from './Selected' export * from './Cram' export * from './TriangleWarning' +export * from './Collectible' diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 196a6eaf93a8..700ebc952933 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -1005,6 +1005,7 @@ "nft_wallet_change": "Change", "nft_button_add_collectible": "Add Collectible", "nft_avatar": "NFT Avatar", + "web3_tab_hint": "No address found.", "plugin_artblocks_not_active": "This project is no more active", "plugin_artblocks_completed": "Completed", "plugin_artblocks_paused": "Paused", diff --git a/packages/mask/src/components/InjectedComponents/PageTab.tsx b/packages/mask/src/components/InjectedComponents/PageTab.tsx index fbfdb58ad268..99314220ce0c 100644 --- a/packages/mask/src/components/InjectedComponents/PageTab.tsx +++ b/packages/mask/src/components/InjectedComponents/PageTab.tsx @@ -1,6 +1,8 @@ import { makeStyles } from '@masknet/theme' import type { Plugin } from '@masknet/plugin-infra' import { PageTabItem } from './PageTabItem' +import { Typography } from '@mui/material' +import { useI18N } from '../../utils' const useStyles = makeStyles()((theme) => ({ root: { @@ -19,7 +21,15 @@ export interface PageTabProps { export function PageTab(props: PageTabProps) { const { tabs, selectedTab, onChange } = props const { classes } = useStyles() + const { t } = useI18N() + if (!tabs.length) { + return ( + + {t('web3_tab_hint')} + + ) + } return (
{tabs.map((x) => ( diff --git a/packages/mask/src/components/InjectedComponents/PageTabItem.tsx b/packages/mask/src/components/InjectedComponents/PageTabItem.tsx index 40222da8338f..e2f835a90a2f 100644 --- a/packages/mask/src/components/InjectedComponents/PageTabItem.tsx +++ b/packages/mask/src/components/InjectedComponents/PageTabItem.tsx @@ -26,6 +26,7 @@ const useStyles = makeStyles()((theme) => ({ selected: { border: `1px solid ${theme.palette.primary.main} !important`, color: `${theme.palette.primary.main} !important`, + borderRadius: 9999, '&:hover': { boxShadow: borderShadows?.[theme.palette.mode], backgroundColor: 'transparent', diff --git a/packages/mask/src/components/InjectedComponents/ProfileTab.tsx b/packages/mask/src/components/InjectedComponents/ProfileTab.tsx index cd68ad891b04..0aa37cb88602 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileTab.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileTab.tsx @@ -9,8 +9,9 @@ export interface ProfileTabProps extends withClasses<'tab' | 'button' | 'selecte reset(): void children?: ReactElement // Required! This component don't have it own style. - classes: Record<'tab' | 'button' | 'selected', string> + classes: Record<'root' | 'button' | 'selected', string> title: string + icon?: React.ReactNode } export function ProfileTab(props: ProfileTabProps) { @@ -30,11 +31,12 @@ export function ProfileTab(props: ProfileTabProps) { }) return ( -
+
+ {props.icon} {title} {active && children ? children : null} diff --git a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx index db0b21e9a617..2b3d0a261a20 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx @@ -36,7 +36,7 @@ const useStyles = makeStyles()((theme) => ({ }, })) -export interface ProfileTabContentProps extends withClasses<'text' | 'button'> {} +export interface ProfileTabContentProps extends withClasses<'text' | 'button' | 'root'> {} export function ProfileTabContent(props: ProfileTabContentProps) { const { t } = useI18N() diff --git a/packages/mask/src/plugins/NextID/base.ts b/packages/mask/src/plugins/NextID/base.ts index 67b7ef3db4a5..1daef79df319 100644 --- a/packages/mask/src/plugins/NextID/base.ts +++ b/packages/mask/src/plugins/NextID/base.ts @@ -1,4 +1,4 @@ -import type { Plugin } from '@masknet/plugin-infra' +import { CurrentSNSNetwork, Plugin } from '@masknet/plugin-infra' import { PLUGIN_DESCRIPTION, PLUGIN_ID, PLUGIN_NAME } from './constants' import { languages } from './locales/languages' @@ -9,7 +9,13 @@ export const base: Plugin.Shared.Definition = { publisher: { name: { fallback: 'Mask Network' }, link: 'https://mask.io/' }, enableRequirement: { architecture: { app: false, web: true }, - networks: { type: 'opt-out', networks: {} }, + networks: { + type: 'opt-in', + networks: { + [CurrentSNSNetwork.Twitter]: true, + [CurrentSNSNetwork.Facebook]: true, + }, + }, target: 'stable', }, experimentalMark: true, diff --git a/packages/mask/src/social-network-adaptor/instagram.com/collecting/identity.ts b/packages/mask/src/social-network-adaptor/instagram.com/collecting/identity.ts new file mode 100644 index 000000000000..707d5242f814 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/instagram.com/collecting/identity.ts @@ -0,0 +1,58 @@ +import { LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-kit' +import { ProfileIdentifier, delay } from '@masknet/shared-base' +import { creator, SocialNetworkUI as Next } from '../../../social-network' +import { instagramBase } from '../base' +import { searchAvatarSelector } from '../utils/selector' +import { getAvatar, getBioDescription, getNickname, getPersonalHomepage, getUserId } from '../utils/user' + +function resolveCurrentVisitingIdentityInner( + ref: Next.CollectingCapabilities.IdentityResolveProvider['recognized'], + cancel: AbortSignal, +) { + const avatarSelector = searchAvatarSelector() + const assign = async () => { + await delay(500) + const bio = getBioDescription() + const homepage = getPersonalHomepage() + const nickname = getNickname() + const handle = getUserId() + const avatar = getAvatar() + + ref.value = { + identifier: new ProfileIdentifier(instagramBase.networkIdentifier, handle), + nickname, + avatar, + bio, + homepage, + } + } + const createWatcher = (selector: LiveSelector) => { + const watcher = new MutationObserverWatcher(selector) + .addListener('onAdd', () => assign()) + .addListener('onChange', () => assign()) + .startWatch({ + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['src', 'content'], + }) + + window.addEventListener('locationchange', assign) + cancel.addEventListener('abort', () => { + window.removeEventListener('locationchange', assign) + watcher.stopWatch() + }) + } + + assign() + + createWatcher(avatarSelector) +} + +export const CurrentVisitingIdentityProviderInstagram: Next.CollectingCapabilities.IdentityResolveProvider = { + hasDeprecatedPlaceholderName: false, + recognized: creator.EmptyIdentityResolveProviderState(), + start(cancel) { + resolveCurrentVisitingIdentityInner(this.recognized, cancel) + }, +} diff --git a/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTab.tsx b/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTab.tsx new file mode 100644 index 000000000000..1ad518cb2b0d --- /dev/null +++ b/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTab.tsx @@ -0,0 +1,119 @@ +import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' +import { CollectibleIcon } from '@masknet/icons' +import { makeStyles } from '@masknet/theme' +import { ProfileTab } from '../../../components/InjectedComponents/ProfileTab' +import { createReactRootShadowed, startWatch, untilElementAvailable } from '../../../utils' +import { + searchProfileActiveTabSelector, + searchProfileTabListLastChildSelector, + searchProfileTabPageSelector, + searchProfileTabSelector, + searchUserIdSelector, +} from '../utils/selector' + +export function injectProfileTabAtInstagram(signal: AbortSignal) { + let tabInjected = false + const contentWatcher = new MutationObserverWatcher(searchProfileTabPageSelector()).useForeach(() => { + const elePage = searchProfileTabPageSelector().evaluate() + if (elePage && !tabInjected) { + const watcher = new MutationObserverWatcher(searchProfileTabListLastChildSelector()) + startWatch(watcher, signal) + createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() + tabInjected = true + } + }) + + startWatch(contentWatcher, signal) +} + +function getStyleProps() { + const EMPTY_STYLE = {} as CSSStyleDeclaration + const eleTab = searchProfileTabSelector().evaluate() + const style = eleTab ? window.getComputedStyle(eleTab) : EMPTY_STYLE + const activeTab = searchUserIdSelector().evaluate() + const activeStyle = activeTab ? window.getComputedStyle(activeTab) : EMPTY_STYLE + return { + color: style.color, + font: style.font, + fontSize: style.fontSize, + padding: style.paddingBottom, + height: style.height, + hover: activeStyle.color, + line: activeStyle.color, + } +} + +const useStyles = makeStyles()(() => { + const props = getStyleProps() + return { + root: { + '&:hover': { + cursor: 'pointer', + }, + display: '-webkit-box', + alignItems: 'center', + justifyContent: 'center', + marginRight: 60, + }, + button: { + color: props.color, + font: props.font, + fontSize: props.fontSize, + height: props.height, + justifyContent: 'center', + alignItems: 'center', + display: 'flex', + borderTop: '1px solid transparent', + }, + selected: { + borderTop: `1px solid ${props.hover}`, + color: props.hover, + }, + line: {}, + icon: { + fontSize: props.fontSize, + paddingRight: 4, + }, + } +}) + +export function ProfileTabAtInstagram() { + const { classes } = useStyles() + + const reset = () => { + const activeTab = searchProfileActiveTabSelector().evaluate() + if (activeTab?.style) { + activeTab.style.borderTop = '' + activeTab.style.color = '' + } + + Array.from(searchProfileTabPageSelector().evaluate()?.childNodes ?? []).forEach((v) => { + const ele = v as HTMLDivElement + if (ele.tagName !== 'SPAN') ele.style.display = '' + }) + } + const clear = async () => { + const style = getStyleProps() + const activeTab = searchProfileActiveTabSelector().evaluate() + if (activeTab?.style) { + activeTab.style.borderTop = 'none' + activeTab.style.color = style.color + } + // hide the content page + await untilElementAvailable(searchProfileTabPageSelector()) + + Array.from(searchProfileTabPageSelector().evaluate()?.childNodes ?? []).forEach((v) => { + const ele = v as HTMLDivElement + if (ele.tagName !== 'SPAN') ele.style.display = 'none' + }) + } + return ( + } + classes={classes} + reset={reset} + clear={clear} + /> + ) +} diff --git a/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTabContent.tsx b/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTabContent.tsx new file mode 100644 index 000000000000..4240830d2ce6 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/instagram.com/injection/ProfileTabContent.tsx @@ -0,0 +1,68 @@ +import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' +import { getMaskColor, makeStyles } from '@masknet/theme' +import { ProfileTabContent } from '../../../components/InjectedComponents/ProfileTabContent' +import { createReactRootShadowed, startWatch } from '../../../utils' +import { + searchProfileActiveTabSelector, + searchProfileTabArticlePageSelector, + searchProfileTabOtherArticlePageSelector, +} from '../utils/selector' + +export function injectProfileTabContentAtInstagram(signal: AbortSignal) { + injectProfileTabContentHaveArticle(signal) + + injectProfileTabContentEmptyArticle(signal) +} + +function injectProfileTabContentHaveArticle(signal: AbortSignal) { + const watcher = new MutationObserverWatcher(searchProfileTabArticlePageSelector()) + startWatch(watcher, signal) + createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() +} + +function injectProfileTabContentEmptyArticle(signal: AbortSignal) { + const watcher = new MutationObserverWatcher(searchProfileTabOtherArticlePageSelector()) + startWatch(watcher, signal) + createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() +} + +function getStyleProps() { + const activeTab = searchProfileActiveTabSelector().evaluate() as HTMLDivElement + return { + backgroundColor: activeTab ? window.getComputedStyle(activeTab).backgroundColor : undefined, + fontFamily: activeTab ? window.getComputedStyle(activeTab as HTMLElement).fontFamily : undefined, + } +} + +const useStyles = makeStyles()((theme) => { + const props = getStyleProps() + + return { + root: { + position: 'relative', + }, + text: { + paddingTop: 29, + paddingBottom: 29, + '& > p': { + fontSize: 28, + fontFamily: props.fontFamily, + fontWeight: 700, + color: getMaskColor(theme).textPrimary, + }, + }, + button: { + backgroundColor: props.backgroundColor, + color: 'white', + marginTop: 18, + '&:hover': { + backgroundColor: props.backgroundColor, + }, + }, + } +}) + +export function ProfileTabContentAtInstagram() { + const { classes } = useStyles() + return +} diff --git a/packages/mask/src/social-network-adaptor/instagram.com/ui-provider.ts b/packages/mask/src/social-network-adaptor/instagram.com/ui-provider.ts index 74a97c9c04e0..23e6c5e3412a 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/ui-provider.ts +++ b/packages/mask/src/social-network-adaptor/instagram.com/ui-provider.ts @@ -5,9 +5,11 @@ import { IdentityProviderInstagram } from './collecting/identity-provider' import { PostProviderInstagram } from './collecting/posts' import { createTaskStartSetupGuideDefault } from '../../social-network/defaults' import { pasteInstagram } from '@masknet/injected-script' -import { injectPostInspectorInstagram } from './injection/post-inspector' -import { newPostCompositionInstagram } from './injection/newPostComposition' import { InitAutonomousStateProfiles } from '../../social-network/defaults/state/InitProfiles' +import { CurrentVisitingIdentityProviderInstagram } from './collecting/identity' +import { injectPostInspectorInstagram } from './injection/post-inspector' +import { injectProfileTabAtInstagram } from './injection/ProfileTab' +import { injectProfileTabContentAtInstagram } from './injection/ProfileTabContent' const define: SocialNetworkUI.Definition = { ...instagramShared, @@ -22,6 +24,7 @@ const define: SocialNetworkUI.Definition = { }, collecting: { identityProvider: IdentityProviderInstagram, + currentVisitingIdentityProvider: CurrentVisitingIdentityProviderInstagram, postsProvider: PostProviderInstagram, }, configuration: { @@ -40,11 +43,13 @@ const define: SocialNetworkUI.Definition = { injection: { setupWizard: createTaskStartSetupGuideDefault(), postInspector: injectPostInspectorInstagram, - newPostComposition: { + profileTab: injectProfileTabAtInstagram, + profileTabContent: injectProfileTabContentAtInstagram, + /* newPostComposition: { start: newPostCompositionInstagram, supportedInputTypes: { text: true, image: true }, supportedOutputTypes: { text: false, image: true }, - }, + },*/ }, } export default define diff --git a/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts new file mode 100644 index 000000000000..1a5ed38db3cf --- /dev/null +++ b/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts @@ -0,0 +1,44 @@ +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 searchProfileTabListLastChildSelector = () => + querySelector('[id="react-root"] section main div[role="tablist"] > :last-child') + +export const searchProfileTabPageSelector = () => + querySelector('[id="react-root"] section main > div > div:last-child') + +export const searchProfileTabSelector = () => + querySelector('[id="react-root"] section main div[role="tablist"] a[aria-selected="false"]') + +export const searchProfileActiveTabSelector = () => + querySelector('[id="react-root"] section main div[role="tablist"] a[aria-selected="true"]') + +export const searchAvatarSelector = () => + querySelector('[id="react-root"] section main header button img') + +export const bioDescriptionSelector = () => + querySelector('[id="react-root"] section main header section > div:last-child div') + +export const personalHomepageSelector = () => + querySelector('[id="react-root"] section main header section > div:last-child a') + +export const searchNickNameSelector = () => + querySelector('[id="react-root"] section main header section > div:last-child > span') + +export const searchUserIdSelector = () => + querySelector('[id="react-root"] section main header section > div:first-child > :first-child') + +export const searchProfileTabArticlePageSelector = () => + querySelector('[id="react-root"] section main > div > div:last-child > article') + +export const searchProfileTabOtherArticlePageSelector = () => + querySelector('[id="react-root"] section main > div > div:last-child > div') diff --git a/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts b/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts new file mode 100644 index 000000000000..81ebe30fcec9 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts @@ -0,0 +1,36 @@ +import { collectNodeText } from '../../../utils' +import { + bioDescriptionSelector, + personalHomepageSelector, + searchAvatarSelector, + searchNickNameSelector, + searchUserIdSelector, +} from './selector' + +export function getBioDescription() { + const bio = bioDescriptionSelector().evaluate() + return bio ? collectNodeText(bio) : '' +} + +export const getPersonalHomepage = () => { + const node = personalHomepageSelector().evaluate() + return node?.textContent || '' +} + +export const getNickname = () => { + const node = searchNickNameSelector().evaluate() + return node ? collectNodeText(node) : '' +} + +export const getUserId = () => { + const node = searchUserIdSelector().evaluate() + return node ? collectNodeText(node) : '' +} + +export const getAvatar = () => { + const node = searchAvatarSelector().evaluate() + if (!node) return '' + + const imageURL = node.getAttribute('src') ?? '' + return imageURL.trim() +} diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx index a2a9f82deae3..3ebfb90446a1 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx @@ -37,7 +37,7 @@ const useStyles = makeStyles()((theme) => { const props = getStyleProps() return { - tab: { + root: { '&:hover': { backgroundColor: new Color(props.hover).alpha(0.1).toString(), cursor: 'pointer', diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTabContent.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTabContent.tsx index a40a63a7ef19..69b29eb6b811 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTabContent.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTabContent.tsx @@ -39,6 +39,13 @@ const useStyles = makeStyles()((theme) => { const props = getStyleProps() return { + root: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 1, + }, text: { paddingTop: 29, paddingBottom: 29,