diff --git a/packages/dashboard/src/pages/Welcome/index.tsx b/packages/dashboard/src/pages/Welcome/index.tsx index 9e4893222ee7..bd570f27bdc3 100644 --- a/packages/dashboard/src/pages/Welcome/index.tsx +++ b/packages/dashboard/src/pages/Welcome/index.tsx @@ -1,12 +1,11 @@ import { Button, useTheme } from '@mui/material' -import { useNavigate } from 'react-router-dom' -import { DashboardRoutes } from '@masknet/shared-base' import { ColumnLayout } from '../../components/RegisterFrame/ColumnLayout' import { styled } from '@mui/material/styles' import { memo, MutableRefObject, useEffect, useMemo, useRef } from 'react' import { useDashboardI18N } from '../../locales' import links from '../../components/FooterLine/links.json' import { openWindow } from '@masknet/shared-base-ui' +import { Services } from '../../API' const Content = styled('div')(({ theme }) => ({ padding: `${theme.spacing(1)} ${theme.spacing(4)}`, @@ -33,7 +32,6 @@ const IFrame = styled('iframe')(({ theme }) => ({ export default function Welcome() { const iframeRef = useRef(null) const mode = useTheme().palette.mode - const navigate = useNavigate() const agreementContentPageURL = new URL('./en.html', import.meta.url).toString() const privacyPolicyDocument = useMemo(() => () => iframeRef?.current?.contentWindow?.document, [iframeRef]) @@ -79,7 +77,10 @@ export default function Welcome() { iframeRef={iframeRef} privacyPolicyURL={agreementContentPageURL} iframeLoadHandler={handleIFrameLoad} - agreeHandler={() => navigate(DashboardRoutes.Setup)} + agreeHandler={async () => { + const url = await Services.SocialNetwork.setupSocialNetwork('twitter.com', false) + if (url) location.assign(url) + }} cancelHandler={() => window.close()} /> ) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 60953b29f574..572c82424858 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -111,9 +111,10 @@ "setup_guide_pin_tip_step_find_left": "Find Mask Network in the list of extensions and click the ", "setup_guide_pin_tip_step_find_right": " button.", "setup_guide_pin_tip_successfully": "Pinned successfully.", - "user_guide_tip_1": "Connect wallet to explore multi-chain dApps.", - "user_guide_tip_2": "Setup your exclusive NFT Avatar, explore the endless possibilities of Web 3.0.", - "user_guide_tip_3": "Click here to have a quick start.", + "user_guide_tip_1": "Explore multi-chain dApps.", + "user_guide_tip_2": "Connect and switch your wallet.", + "user_guide_tip_3": "Browse the Web 3 footprints of your Twitter friends, enjoy the freedom of Web 3.", + "user_guide_tip_4": "Click here to have a quick start.", "create_persona": "Create persona", "connect_persona": "Connect persona", "please_create_persona": "Please create persona", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index e24719227be6..a1f9e2629639 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -106,8 +106,9 @@ "setup_guide_pin_tip_step_find_right": " 按钮。", "setup_guide_pin_tip_successfully": "固定成功。", "user_guide_tip_1": "点击这里快速访问主要功能。", - "user_guide_tip_2": "点击这里连接您的钱包。您可以在此选择网络或更改您的钱包。", - "user_guide_tip_3": "点击这里快速开始。", + "user_guide_tip_2": "点击这里连接或更改您的钱包。", + "user_guide_tip_3": "浏览 Twitter 好友的 Web 3 足迹,畅享 Web 3 的自由。", + "user_guide_tip_4": "尝试快速开始探索 Web 3 世界。", "create_persona": "创建身份", "connect_persona": "连接身份", "please_create_persona": "请创建身份", diff --git a/packages/mask/src/components/GuideStep/index.tsx b/packages/mask/src/components/GuideStep/index.tsx index e28f0b2c4004..2b326fc1b9c9 100644 --- a/packages/mask/src/components/GuideStep/index.tsx +++ b/packages/mask/src/components/GuideStep/index.tsx @@ -119,6 +119,10 @@ export default function GuideStep({ if (disabled) return const open = +lastStep === step setOpen(open) + + if (!open) return + if (location.pathname === '/home') return + location.assign('/home') }, [lastStep]) useEffect(() => { @@ -169,7 +173,7 @@ export default function GuideStep({ return () => { window.removeEventListener('resize', onResize) } - }, [childrenRef]) + }, [childrenRef, lastStep, open]) return ( <> diff --git a/packages/mask/src/components/InjectedComponents/PostDialogHint.tsx b/packages/mask/src/components/InjectedComponents/PostDialogHint.tsx index ff863261e754..960e9a4522d3 100644 --- a/packages/mask/src/components/InjectedComponents/PostDialogHint.tsx +++ b/packages/mask/src/components/InjectedComponents/PostDialogHint.tsx @@ -73,7 +73,7 @@ const EntryIconButton = memo((props: PostDialogHintUIProps) => { return disableGuideTip ? ( getEntry() ) : ( - + {getEntry()} ) diff --git a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx index c2e6530ba5df..cef8c97882b1 100644 --- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx +++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx @@ -34,6 +34,7 @@ import { AccountBalanceWalletIcon } from '@masknet/icons' import { makeStyles } from '@masknet/theme' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' import { NextIDVerificationStatus, useNextIDConnectStatus } from '../DataSource/useNextID' +import { MaskIcon } from '../../resources/MaskIcon' const useStyles = makeStyles()((theme) => ({ title: { @@ -100,31 +101,27 @@ function ToolboxHintForApplication(props: ToolboxHintProps) { const { t } = useI18N() const { openDialog } = useRemoteControlledDialog(WalletMessages.events.ApplicationDialogUpdated) return ( - - - - {mini ? null : ( - - {t('mask_network')} - - } - /> - )} - - + + + + + {mini ? null : ( + + {t('mask_network')} + + } + /> + )} + + + ) } @@ -157,8 +154,8 @@ function ToolboxHintForWallet(props: ToolboxHintProps) { return ( <> - - + + {isWalletValid ? ( @@ -196,8 +193,8 @@ function ToolboxHintForWallet(props: ToolboxHintProps) { /> )} - - + + ) } diff --git a/packages/mask/src/extension/background-script/SocialNetworkService.ts b/packages/mask/src/extension/background-script/SocialNetworkService.ts index b6ca32eca50c..59931d5b45ed 100644 --- a/packages/mask/src/extension/background-script/SocialNetworkService.ts +++ b/packages/mask/src/extension/background-script/SocialNetworkService.ts @@ -1,12 +1,17 @@ -import { definedSocialNetworkUIs, getNetworkWorker, loadSocialNetworkUI, SocialNetworkUI } from '../../social-network' -import { Flags } from '../../../shared' - -import { currentSetupGuideStatus } from '../../settings/settings' +import { currentSetupGuideStatus, userGuideStatus } from '../../settings/settings' import stringify from 'json-stable-stringify' import { SetupGuideStep } from '../../components/InjectedComponents/SetupGuide/types' import type { PersonaIdentifier, ProfileIdentifier } from '@masknet/shared-base' import { delay } from '@dimensiondev/kit' import { requestExtensionPermission } from '../../../background/services/helper' +import { + definedSocialNetworkUIs, + getNetworkWorker, + loadSocialNetworkUI, + loadSocialNetworkUIs, + SocialNetworkUI, +} from '../../social-network' +import { Flags } from '../../../shared' export async function getDefinedSocialNetworkUIs() { return [...definedSocialNetworkUIs.values()].map(({ networkIdentifier }) => { @@ -22,6 +27,30 @@ function requestSNSAdaptorPermission(ui: SocialNetworkUI.Definition) { return requestExtensionPermission({ origins: [...ui.declarativePermissions.origins] }) } +function requestSNSAdaptorsPermission(uis: SocialNetworkUI.Definition[]) { + return requestExtensionPermission({ + origins: uis.map((x) => x.declarativePermissions.origins).flat(), + }) +} + +export async function setupSocialNetwork(defaultNetwork: string, newTab = true) { + const ui = await loadSocialNetworkUI(defaultNetwork) + const home = ui.utils.getHomePage?.() + + const uis = await loadSocialNetworkUIs() + if (!Flags.no_web_extension_dynamic_permission_request) { + if (!(await requestSNSAdaptorsPermission(uis))) return + } + + userGuideStatus[defaultNetwork].value = '1' + await delay(100) + if (!home) return + if (!newTab) return home + + browser.tabs.create({ active: true, url: home }) + return +} + export async function connectSocialNetwork( identifier: PersonaIdentifier, network: string, diff --git a/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts b/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts index e1eca67fb2c3..52eb21793cf8 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts @@ -8,6 +8,7 @@ import { searchSelfHandleSelector, searchSelfNicknameSelector, searchSelfAvatarSelector, + selfInfoSelectors, } from '../utils/selector' import { creator, SocialNetworkUI as Next } from '../../../social-network' import Services from '../../../extension/service' @@ -15,14 +16,41 @@ import { twitterBase } from '../base' import { getAvatar, getBio, getNickname, getTwitterId, getPersonalHomepage } from '../utils/user' import { isMobileTwitter } from '../utils/isMobile' +function recognizeDesktop() { + const collect = () => { + const handle = selfInfoSelectors().handle.evaluate() + const nickname = selfInfoSelectors().name.evaluate() + const avatar = selfInfoSelectors().userAvatar.evaluate() + + return { handle, nickname, avatar } + } + + const watcher = new MutationObserverWatcher(selfInfoSelectors().handle) + + return { watcher, collect } +} + +function recognizeMobile() { + const collect = () => { + const avatar = searchSelfAvatarSelector().evaluate()?.getAttribute('src') ?? '' + const handle = searchSelfHandleSelector().evaluate()?.textContent?.trim()?.replace(/^@/, '') + const nickname = searchSelfNicknameSelector().evaluate()?.textContent?.trim() ?? '' + + return { handle, nickname, avatar } + } + + const watcher = new MutationObserverWatcher(searchSelfHandleSelector()) + + return { watcher, collect } +} + function resolveLastRecognizedIdentityInner( ref: Next.CollectingCapabilities.IdentityResolveProvider['recognized'], cancel: AbortSignal, ) { + const task = isMobileTwitter ? recognizeMobile() : recognizeDesktop() const assign = () => { - const avatar = searchSelfAvatarSelector().evaluate()?.getAttribute('src') ?? '' - const handle = searchSelfHandleSelector().evaluate()?.textContent?.trim()?.replace(/^@/, '') - const nickname = searchSelfNicknameSelector().evaluate()?.textContent?.trim() ?? '' + const { handle, nickname, avatar } = task.collect() if (handle) { ref.value = { @@ -32,7 +60,7 @@ function resolveLastRecognizedIdentityInner( } } } - const watcher = new MutationObserverWatcher(searchSelfHandleSelector()) + const watcher = task.watcher .addListener('onAdd', () => assign()) .addListener('onChange', () => assign()) .startWatch({ diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint.tsx index ce817fb80aa4..a993225f7f51 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint.tsx @@ -15,5 +15,5 @@ export function injectToolboxHintAtTwitter(signal: AbortSignal, category: 'walle function injectProfile(signal: AbortSignal) { const watcher = new MutationObserverWatcher(sideBarProfileSelector()) startWatch(watcher, signal) - createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() + createReactRootShadowed(watcher.firstDOMProxy.beforeShadow, { signal }).render() } diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint_UI.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint_UI.tsx index c15c32e1485d..ff333eab0d3d 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint_UI.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/ToolboxHint_UI.tsx @@ -52,7 +52,7 @@ export function ToolboxHintAtTwitter(props: { category: 'wallet' | 'application' export function ProfileLinkAtTwitter() { const { t } = useI18N() return ( - + ) 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 a55cdeeff773..9ef0a8486c87 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 @@ -1,4 +1,5 @@ import { LiveSelector } from '@dimensiondev/holoflows-kit' +import { regexMatch } from '../../../utils/utils' import { isMobileTwitter } from './isMobile' import { isCompose } from './postBox' @@ -113,7 +114,7 @@ export const postEditorInPopupSelector: () => LiveSelector = () => export const toolBoxInSideBarSelector: () => LiveSelector = () => querySelector('[role="banner"] [role="navigation"] > div') export const sideBarProfileSelector: () => LiveSelector = () => - querySelector('[role="banner"] [role="navigation"] [aria-label="Lists"] > div') + querySelector('[role="banner"] [role="navigation"] [aria-label="Profile"] > div') export const postEditorInTimelineSelector: () => LiveSelector = () => querySelector('[role="main"] :not(aside) > [role="progressbar"] ~ div [role="button"][aria-label]:nth-child(6)') @@ -216,6 +217,25 @@ export const postsContentSelector = () => export const postAvatarsContentSelector = () => querySelectorAll('[data-testid="tweet"] > div > div > div > :nth-child(2)') +const base = querySelector('#react-root + script') +const handle = /"screen_name":"(.*?)"/ +const name = /"name":"(.*?)"/ +const bio = /"description":"(.*?)"/ +const avatar = /"profile_image_url_https":"(.*?)"/ +/** + * first matched element can be extracted by index zero, followings are all capture groups, if no 'g' specified. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match + */ +const p = (regex: RegExp, index: number) => { + return base.clone().map((x) => regexMatch(x.innerText, regex, index)) +} +export const selfInfoSelectors = () => ({ + handle: p(handle, 1), + name: p(name, 1), + bio: p(bio, 1), + userAvatar: p(avatar, 1), +}) + // #region self info export const searchSelfHandleSelector = () => { return querySelector( diff --git a/packages/mask/src/social-network/ui.ts b/packages/mask/src/social-network/ui.ts index aac67f5a4517..9743c49ee192 100644 --- a/packages/mask/src/social-network/ui.ts +++ b/packages/mask/src/social-network/ui.ts @@ -90,7 +90,6 @@ export async function activateSocialNetworkUIInner(ui_deferred: SocialNetworkUI. ui.injection.toolbox?.(signal, 'wallet') ui.injection.toolbox?.(signal, 'application') } - ui.injection.setupPrompt?.(signal) ui.injection.newPostComposition?.start?.(signal) ui.injection.searchResult?.(signal) ui.injection.userBadge?.(signal) @@ -229,6 +228,19 @@ export async function activateSocialNetworkUIInner(ui_deferred: SocialNetworkUI. } } +export async function loadSocialNetworkUIs(): Promise { + const defines = [...definedSocialNetworkUIs.values()].map(async (x) => x.load()) + const uis = (await Promise.all(defines)).map((x) => x.default) + + if (!defines) throw new Error('SNS adaptor load failed') + + for (const ui of uis) { + definedSocialNetworkUIsResolved.set(ui.networkIdentifier, ui) + } + + return uis +} + export async function loadSocialNetworkUI(identifier: string): Promise { if (definedSocialNetworkUIsResolved.has(identifier)) return definedSocialNetworkUIsResolved.get(identifier)! const define = definedSocialNetworkUIs.get(identifier) diff --git a/packages/shared/src/contexts/components/InjectedDialog.tsx b/packages/shared/src/contexts/components/InjectedDialog.tsx index bdb15c599dfe..649929c9e2ef 100644 --- a/packages/shared/src/contexts/components/InjectedDialog.tsx +++ b/packages/shared/src/contexts/components/InjectedDialog.tsx @@ -10,6 +10,7 @@ import { DialogProps, DialogTitle, IconButton, + Stack, Typography, useMediaQuery, useTheme, @@ -27,6 +28,14 @@ const useStyles = makeStyles()((theme, { clean }) => ({ dialogTitle: { padding: theme.spacing(1, 2), whiteSpace: 'nowrap', + gridTemplateColumns: '50px auto 50px', + }, + dialogTitleEndingContent: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minWidth: 46, + minHeight: 46, }, dialogContent: { overscrollBehavior: 'contain', @@ -52,6 +61,7 @@ const useStyles = makeStyles()((theme, { clean }) => ({ export type InjectedDialogClassKey = | DialogClassKey | 'dialogTitle' + | 'dialogTitleEndingContent' | 'dialogContent' | 'dialogActions' | 'dialogTitleTypography' @@ -78,6 +88,7 @@ export function InjectedDialog(props: InjectedDialogProps) { dialogCloseButton, dialogContent, dialogTitle, + dialogTitleEndingContent, dialogTitleTypography, dialogBackdropRoot, container, @@ -147,7 +158,7 @@ export function InjectedDialog(props: InjectedDialogProps) { {title} - {titleTail} + {titleTail} ) : null}