diff --git a/package.json b/package.json index 22912c4d45f0..f55fe022b617 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,12 @@ }, "dependencies": { "@dimensiondev/kit": "0.0.0-20220228054820-f2378be", - "@emotion/cache": "^11.7.1", - "@emotion/react": "^11.8.2", - "@emotion/serialize": "^1.0.2", - "@emotion/styled": "^11.8.1", - "@emotion/utils": "^1.1.0", + "@emotion/cache": "11.7.1", + "@emotion/react": "11.8.2", + "@emotion/serialize": "1.0.2", + "@emotion/server": "11.4.0", + "@emotion/styled": "11.8.1", + "@emotion/utils": "1.1.0", "@mui/icons-material": "5.5.1", "@mui/lab": "5.0.0-alpha.75", "@mui/material": "5.5.3", diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index 8fe5fbdd2ac9..5366a168b41b 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -279,7 +279,7 @@ export function createConfiguration(rawFlags: BuildFlags): Configuration { const plugins = baseConfig.plugins! const entries: Record = (baseConfig.entry = { dashboard: normalizeEntryDescription(join(__dirname, '../src/extension/dashboard/index.tsx')), - popups: normalizeEntryDescription(join(__dirname, '../src/extension/popups/render.tsx')), + popups: normalizeEntryDescription(join(__dirname, '../src/extension/popups/SSR-client.ts')), contentScript: normalizeEntryDescription(join(__dirname, '../src/content-script.ts')), debug: normalizeEntryDescription(join(__dirname, '../src/extension/debug-page/index.tsx')), }) @@ -353,7 +353,7 @@ function addHTMLEntry( if (options.sourceMap) { templateContent = templateContent.replace( ``, - ``, + ``, ) } return new HTMLPlugin({ diff --git a/packages/mask/.webpack/manifest.ts b/packages/mask/.webpack/manifest.ts index 6e023d2394e6..dc03597ad110 100644 --- a/packages/mask/.webpack/manifest.ts +++ b/packages/mask/.webpack/manifest.ts @@ -41,11 +41,6 @@ function modify(manifest: Manifest, flags: NormalizedFlags) { stableDevelopmentExtensionID(manifest) } - // Mask 2.0 - if (flags.mode === 'development' || flags.channel === 'beta' || flags.channel === 'insider') { - manifest.browser_action = { default_popup: 'popups.html' } - } - if (flags.hmr) { manifest.web_accessible_resources.push('*.json', '*.js') } diff --git a/packages/mask/background/services/identity/index.ts b/packages/mask/background/services/identity/index.ts index 0196cfdcbe81..ce73908fb79c 100644 --- a/packages/mask/background/services/identity/index.ts +++ b/packages/mask/background/services/identity/index.ts @@ -1,3 +1,9 @@ export { createPersonaByPrivateKey } from './persona/create' export { signWithPersona, type SignRequest, type SignRequestResult, generateSignResult } from './persona/sign' export { exportPersonaMnemonicWords, exportPersonaPrivateKey } from './persona/backup' +export { queryOwnedPersonaInformation, queryCurrentPersona, queryCurrentPersona_internal } from './persona/query' +export { + type ProfileInformationWithNextID, + queryOwnedProfileInformationWithNextID, + queryOwnedProfileInformationWithNextID_internal, +} from './profile/query' diff --git a/packages/mask/background/services/identity/persona/query.ts b/packages/mask/background/services/identity/persona/query.ts new file mode 100644 index 000000000000..faeadc5f2d9e --- /dev/null +++ b/packages/mask/background/services/identity/persona/query.ts @@ -0,0 +1,57 @@ +import { ECKeyIdentifier, PersonaInformation, ProfileInformation } from '@masknet/shared-base' +import { queryAvatarDataURL } from '../../../database/avatar-cache/avatar' +import { queryPersonasDB, queryProfileDB } from '../../../database/persona/db' +import { getBrowserStorageUnchecked, InternalStorageKeys } from '../../settings/utils' + +export async function queryOwnedPersonaInformation(): Promise { + const personas = await queryPersonasDB({ hasPrivateKey: true }) + const result: PersonaInformation[] = [] + for (const persona of personas.sort((a, b) => (a.updatedAt > b.updatedAt ? 1 : -1))) { + const map: ProfileInformation[] = [] + result.push({ + nickname: persona.nickname, + identifier: persona.identifier, + linkedProfiles: map, + publicHexKey: persona.publicHexKey, + }) + for (const [profile] of persona.linkedProfiles) { + const linkedProfile = await queryProfileDB(profile) + + map.push({ + identifier: profile, + nickname: linkedProfile?.nickname, + avatar: linkedProfile + ? await queryAvatarDataURL(linkedProfile.identifier).catch(() => undefined) + : undefined, + }) + } + } + return result +} + +export async function queryCurrentPersona(): Promise { + const id = await queryCurrentPersonaIdentifierUnchecked() + const owned = await queryOwnedPersonaInformation() + + if (!owned.length) return + if (!id) return owned[0].identifier + + if (owned.some((x) => x.identifier.equals(id))) return id + return owned[0].identifier +} + +/** @internal */ +export async function queryCurrentPersona_internal(owned: PersonaInformation[]) { + const id = await queryCurrentPersonaIdentifierUnchecked() + + if (!owned.length) return + if (!id) return owned[0].identifier + + if (owned.some((x) => x.identifier.equals(id))) return id + return owned[0].identifier +} + +async function queryCurrentPersonaIdentifierUnchecked(): Promise { + const raw = String(await getBrowserStorageUnchecked(InternalStorageKeys.currentPersona)) + return ECKeyIdentifier.fromString(raw, ECKeyIdentifier).unwrapOr(undefined) +} diff --git a/packages/mask/background/services/identity/profile/query.ts b/packages/mask/background/services/identity/profile/query.ts new file mode 100644 index 000000000000..fdadb81edf83 --- /dev/null +++ b/packages/mask/background/services/identity/profile/query.ts @@ -0,0 +1,42 @@ +import type { PersonaInformation, ProfileInformation, NextIDPlatform, PersonaIdentifier } from '@masknet/shared-base' +import { NextIDProof } from '@masknet/web3-providers' +import { queryCurrentPersona_internal, queryOwnedPersonaInformation } from '../persona/query' + +export interface ProfileInformationWithNextID extends ProfileInformation { + is_valid?: boolean + identity?: string + platform?: NextIDPlatform +} +export async function queryOwnedProfileInformationWithNextID() { + const list = await queryOwnedPersonaInformation() + const id = await queryCurrentPersona_internal(list) + return queryOwnedProfileInformationWithNextID_internal(list, id) +} +/** @internal */ +export async function queryOwnedProfileInformationWithNextID_internal( + ownedPersonas: PersonaInformation[], + currentPersonaID: PersonaIdentifier | undefined, +): Promise { + const currentPersona = ownedPersonas.find((x) => x.identifier.equals(currentPersonaID)) + + if (!currentPersona) return [] + if (!currentPersona.publicHexKey) return currentPersona.linkedProfiles + + const response = await NextIDProof.queryExistedBindingByPersona(currentPersona.publicHexKey) + if (!response) return currentPersona.linkedProfiles + + return currentPersona.linkedProfiles.map((profile) => { + const target = response.proofs.find( + (x) => + profile.identifier.userId.toLowerCase() === x.identity.toLowerCase() && + profile.identifier.network.replace('.com', '') === x.platform, + ) + + return { + ...profile, + platform: target?.platform, + identity: target?.identity, + is_valid: target?.is_valid, + } + }) +} diff --git a/packages/mask/background/services/settings/index.ts b/packages/mask/background/services/settings/index.ts new file mode 100644 index 000000000000..35ee438398fd --- /dev/null +++ b/packages/mask/background/services/settings/index.ts @@ -0,0 +1,9 @@ +import { LanguageOptions } from '@masknet/public-api' +import { getBrowserStorageUnchecked, InternalStorageKeys } from './utils' + +export async function getLanguagePreference(): Promise { + const raw = String(await getBrowserStorageUnchecked(InternalStorageKeys.language)) + + if (Object.values(LanguageOptions).some((x) => x === raw)) return raw as LanguageOptions + return LanguageOptions.__auto__ +} diff --git a/packages/mask/background/services/settings/utils.ts b/packages/mask/background/services/settings/utils.ts new file mode 100644 index 000000000000..06fd8e1b71de --- /dev/null +++ b/packages/mask/background/services/settings/utils.ts @@ -0,0 +1,31 @@ +// TODO: This is a hack. We cannot access "settings" API in this TS project. This should be an temporally workaround. +/** + * @internal + * @deprecated + */ +export enum InternalStorageKeys { + currentPersona = 'settings+currentPersonaIdentifier', + language = 'settings+language', +} +/** + * @internal + * @deprecated + */ +export async function getBrowserStorageUnchecked( + arg: InternalStorageKeys, +): Promise +export async function getBrowserStorageUnchecked( + ...args: T[] +): Promise> +export async function getBrowserStorageUnchecked( + ...args: T[] +): Promise> { + const raw_result = await browser.storage.local.get(args) + const result: Record = {} as any + + if (args.length === 1) return Reflect.get(raw_result, args[0]) + for (const [key, value] of Object.entries(raw_result)) { + Reflect.set(result, Reflect.get(InternalStorageKeys, key), value) + } + return result +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/cache.ts b/packages/mask/background/tasks/Cancellable/PopupSSR/cache.ts new file mode 100644 index 000000000000..fe0ce3543dda --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/cache.ts @@ -0,0 +1,45 @@ +import { throttle } from 'lodash-unified' +import { MaskMessages } from '../../../../shared/messages' +import { InternalStorageKeys } from '../../../services/settings/utils' +import type { PopupSSR_Props } from './type' +import { queryOwnedPersonaInformation, queryCurrentPersona_internal } from '../../../services/identity' +import { queryOwnedProfileInformationWithNextID_internal } from '../../../services/identity/profile/query' +import { getLanguagePreference } from '../../../services/settings' + +export let cache: { html: string; css: string } = { html: '', css: '' } +export function startListen( + render: (props: PopupSSR_Props) => Promise<{ html: string; css: string }>, + signal: AbortSignal, +) { + const task = throttle( + async function task() { + cache = await prepareData().then(render) + }, + 2000, + { leading: true }, + ) + + task()?.then(() => console.log('[Popup SSR] Page ready.')) + MaskMessages.events.ownPersonaChanged.on(task, { signal }) + MaskMessages.events.createInternalSettingsUpdated.on( + (event) => { + if (event.initial) return + if (event.key === InternalStorageKeys.currentPersona || event.key === InternalStorageKeys.language) task() + }, + { signal }, + ) +} + +async function prepareData(): Promise { + const language = getLanguagePreference() + const personas = await queryOwnedPersonaInformation() + const currentPersona = await queryCurrentPersona_internal(personas) + const profilesWithNextID = await queryOwnedProfileInformationWithNextID_internal(personas, currentPersona) + + return { + profilesWithNextID, + currentPersona, + personas, + language: await language, + } +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/index.mv3.ts b/packages/mask/background/tasks/Cancellable/PopupSSR/index.mv3.ts new file mode 100644 index 000000000000..7c9afadbe345 --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/index.mv3.ts @@ -0,0 +1,15 @@ +import { cache, startListen } from './cache' +export default async function PopupSSR(signal: AbortSignal) { + browser.runtime.onMessage.addListener(f) + signal.addEventListener('abort', () => browser.runtime.onMessage.removeListener(f), { once: true }) + + startListen(async (props) => { + const { main } = await import('./worker') + return main(props) + }, signal) +} + +function f(message: any) { + if (!(message.type === 'popups-ssr')) return + return Promise.resolve(cache) +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/index.ts b/packages/mask/background/tasks/Cancellable/PopupSSR/index.ts new file mode 100644 index 000000000000..709c8ea17a15 --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/index.ts @@ -0,0 +1,31 @@ +import { serializer } from '@masknet/shared-base' +import { OnDemandWorker } from '../../../../utils-pure/OnDemandWorker' +import { cache, startListen } from './cache' + +const worker = new OnDemandWorker(new URL('./worker_init.ts', import.meta.url), { name: 'PopupSSR-Worker' }) +export default async function PopupSSR(signal: AbortSignal) { + browser.runtime.onMessage.addListener(f) + + signal.addEventListener( + 'abort', + () => { + browser.runtime.onMessage.removeListener(f) + worker.terminate() + }, + { once: true }, + ) + + startListen((props) => { + return new Promise((resolve) => { + Promise.resolve(serializer.serialization(props)).then((data) => worker.postMessage(data)) + worker.addEventListener('message', (data) => resolve(data.data), { once: true }) + }) + }, signal) +} + +function f(message: any) { + if (!(message.type === 'popups-ssr')) return + return new Promise((resolve) => { + resolve(cache) + }) +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/loader.js b/packages/mask/background/tasks/Cancellable/PopupSSR/loader.js new file mode 100644 index 000000000000..ea2f4d35168a --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/loader.js @@ -0,0 +1,6 @@ +// This is a JS file to make TypeScript happy. +import { render as _ } from '../../../../src/extension/popups/SSR-server.tsx' + +export let render = _ +import.meta.webpackHot && + import.meta.webpackHot.accept('../../../../src/extension/popups/SSR-server.tsx', () => (render = _)) diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/type.ts b/packages/mask/background/tasks/Cancellable/PopupSSR/type.ts new file mode 100644 index 000000000000..0666db52cb4d --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/type.ts @@ -0,0 +1,10 @@ +import type { LanguageOptions } from '@masknet/public-api' +import type { PersonaIdentifier, PersonaInformation } from '@masknet/shared-base' +import type { ProfileInformationWithNextID } from '../../../services/identity/profile/query' + +export interface PopupSSR_Props { + personas: PersonaInformation[] | undefined + currentPersona: PersonaIdentifier | undefined + profilesWithNextID: ProfileInformationWithNextID[] + language: LanguageOptions +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/worker.tsx b/packages/mask/background/tasks/Cancellable/PopupSSR/worker.tsx new file mode 100644 index 000000000000..24febb6aa52b --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/worker.tsx @@ -0,0 +1,10 @@ +// I'm a WebWorker! + +// @ts-ignore +import { render } from './loader' +import type { PopupSSR_Props } from './type' + +const Render: (props: PopupSSR_Props) => Promise<{ html: string; css: string }> = render +export async function main(props: PopupSSR_Props): Promise<{ html: string; css: string }> { + return Render(props) +} diff --git a/packages/mask/background/tasks/Cancellable/PopupSSR/worker_init.ts b/packages/mask/background/tasks/Cancellable/PopupSSR/worker_init.ts new file mode 100644 index 000000000000..76e62e7b30eb --- /dev/null +++ b/packages/mask/background/tasks/Cancellable/PopupSSR/worker_init.ts @@ -0,0 +1,9 @@ +import { main } from './worker' +import { serializer } from '@masknet/shared-base' + +globalThis.addEventListener('message', async (event) => { + const data = serializer.deserialization(event.data) as any + const result = await main(data) + globalThis.postMessage(result) +}) +globalThis.postMessage('alive') diff --git a/packages/mask/background/tasks/setup.hmr.ts b/packages/mask/background/tasks/setup.hmr.ts index fb221264c44e..e2c81d13ce27 100644 --- a/packages/mask/background/tasks/setup.hmr.ts +++ b/packages/mask/background/tasks/setup.hmr.ts @@ -4,15 +4,24 @@ import * as InjectContentScriptMV3 from './Cancellable/InjectContentScripts-mv3' import * as IsolatedDashboardBridge from './Cancellable/IsolatedDashboardBridge' import * as CleanupProfileDatabase from './Cancellable/CleanProfileAndAvatar' import * as NotificationsToMobile from './Cancellable/NotificationsToMobile' +import * as PopupSSR from './Cancellable/PopupSSR' +import * as PopupSSR_MV3 from './Cancellable/PopupSSR/index.mv3' type CancelableJob = { default: (signal: AbortSignal) => void } const CancelableJobs: CancelableJob[] = [ NewInstalled, process.env.manifest === '2' ? InjectContentScript : InjectContentScriptMV3, IsolatedDashboardBridge, - process.env.architecture === 'app' ? null! : CleanupProfileDatabase, NotificationsToMobile, -].filter(Boolean) +] + +if (process.env.architecture === 'web') { + CancelableJobs.push( + // Web only + process.env.manifest === '2' ? PopupSSR : PopupSSR_MV3, + CleanupProfileDatabase, + ) +} const abort = new AbortController() CancelableJobs.forEach((task) => task.default(abort.signal)) diff --git a/packages/mask/background/tsconfig.json b/packages/mask/background/tsconfig.json index b97bc2510f7c..6abcef6274e0 100644 --- a/packages/mask/background/tsconfig.json +++ b/packages/mask/background/tsconfig.json @@ -14,6 +14,7 @@ { "path": "../shared" }, { "path": "../utils-pure" }, { "path": "../../encryption" }, - { "path": "../../gun-utils" } + { "path": "../../gun-utils" }, + { "path": "../../web3-providers" } ] } diff --git a/packages/mask/public/patches.js b/packages/mask/public/patches.js index 4e3f6866390a..15196a4545a8 100644 --- a/packages/mask/public/patches.js +++ b/packages/mask/public/patches.js @@ -83,13 +83,17 @@ globalThis.regeneratorRuntime = undefined } { - if (typeof trustedTypes === 'object') { + if (typeof trustedTypes === 'object' && location.protocol.includes('extension')) { trustedTypes.createPolicy('default', { createHTML: (string) => { console.trace('[Trusted Types](default policy): Possible XSS happened. Please remove it.', string) return string }, }) + + if (location.pathname !== '/popups.html') { + trustedTypes.createPolicy('ssr', {}) + } } } diff --git a/packages/mask/src/UIRoot.tsx b/packages/mask/src/UIRoot.tsx index aa9a5f12c085..9a9dc2b98630 100644 --- a/packages/mask/src/UIRoot.tsx +++ b/packages/mask/src/UIRoot.tsx @@ -1,6 +1,6 @@ import { Suspense } from 'react' import { Web3Provider } from '@masknet/web3-shared-evm' -import { CssBaseline, StyledEngineProvider, Theme } from '@mui/material' +import { StyledEngineProvider, Theme } from '@mui/material' import { NetworkPluginID, PluginsWeb3ContextProvider, useAllPluginsWeb3State } from '@masknet/plugin-infra' import { I18NextProviderHMR, SharedContextProvider } from '@masknet/shared' import { ErrorBoundary, ErrorBoundaryBuildInfoContext, useValueRef } from '@masknet/shared-base-ui' @@ -57,10 +57,9 @@ export function MaskUIRoot({ children, kind, useTheme }: MaskUIRootProps) { useMaskIconPalette={useMaskIconPalette} CustomSnackbarOffsetY={isFacebook(activatedSocialNetworkUI) ? 80 : undefined} useTheme={useTheme} - baseline={kind === 'page'}> - - {jsx} - + baseline={kind === 'page'} + children={jsx} + /> ), (jsx) => {jsx}, ) diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index ba1095534d49..3235fba1b8e1 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -19,8 +19,6 @@ import { ProfileIdentifier, ECKeyIdentifierFromJsonWebKey, EC_JsonWebKey, - PersonaInformation, - ProfileInformation, PostIVIdentifier, RelationFavor, NextIDAction, @@ -160,25 +158,6 @@ export async function queryLastPersonaCreated() { return first(orderBy(all, (x) => x.createdAt, 'desc')) } -export async function queryOwnedPersonaInformation(): Promise { - const personas = await queryPersonas(undefined, true) - const result: PersonaInformation[] = [] - for (const persona of personas.sort((a, b) => (a.updatedAt > b.updatedAt ? 1 : -1))) { - const map: ProfileInformation[] = [] - result.push({ - nickname: persona.nickname, - identifier: persona.identifier, - linkedProfiles: map, - publicHexKey: persona.publicHexKey, - }) - for (const [profile] of persona.linkedProfiles) { - const linkedProfile = await queryProfile(profile) - - map.push({ identifier: profile, nickname: linkedProfile.nickname, avatar: linkedProfile.avatar }) - } - } - return result -} export async function restoreFromObject(object: null | BackupJSONFileLatest): Promise { if (!object) return null await restoreBackup(object) diff --git a/packages/mask/src/extension/options-page/DashboardDialogs/Base.tsx b/packages/mask/src/extension/options-page/DashboardDialogs/Base.tsx index 94633995c73a..c343885b9ce5 100644 --- a/packages/mask/src/extension/options-page/DashboardDialogs/Base.tsx +++ b/packages/mask/src/extension/options-page/DashboardDialogs/Base.tsx @@ -10,9 +10,13 @@ import { IconButtonProps, } from '@mui/material' import { makeStyles } from '@masknet/theme' -import { ThemeProvider } from '@mui/material/styles' +import { Theme, ThemeOptions, ThemeProvider } from '@mui/material/styles' import CloseIcon from '@mui/icons-material/Close' -import { extendsTheme, useClassicMaskFullPageTheme, useMatchXS } from '../../../utils' +import { useMatchXS } from '../../../utils' +import { useValueRef } from '@masknet/shared-base-ui' +import { appearanceSettings, languageSettings } from '../../../settings/settings' +import { cloneDeep, merge } from 'lodash-unified' +import { useClassicMaskFullPageTheme } from '../../../utils/theme/useClassicMaskFullPageTheme' const useStyles = makeStyles()((theme) => ({ root: { @@ -101,7 +105,7 @@ export function useModal @@ -160,6 +164,9 @@ const useDashboardDialogWrapperStyles = makeStyles( }, })) +function extendsTheme(extend: (theme: Theme) => ThemeOptions) { + return (theme: Theme) => merge(cloneDeep(theme), extend(theme)) +} const dialogTheme = extendsTheme((theme) => ({ components: { MuiOutlinedInput: { diff --git a/packages/mask/src/extension/popups/SSR-client.ts b/packages/mask/src/extension/popups/SSR-client.ts new file mode 100644 index 000000000000..0d114a91c3a2 --- /dev/null +++ b/packages/mask/src/extension/popups/SSR-client.ts @@ -0,0 +1,52 @@ +// This file should not import any file!! +/// + +// If the current page is "", navigate to "#/personas", therefore we can avoid a Router skip when hydrating. +if (location.hash === '') location.assign('#/personas') + +declare const trustedTypes: any +let trustedHTML: (x: string) => string +{ + if (typeof trustedTypes === 'object') { + const policy = trustedTypes.createPolicy('ssr', { + createHTML: (x: string) => String(x), + }) + trustedHTML = (x) => policy.createHTML(x) + } else { + trustedHTML = (x) => x + } +} + +if (location.hash === '#/personas') { + console.time('[SSR] Request') + browser.runtime.sendMessage({ type: 'popups-ssr' }).then(({ html, css }) => { + // React go first, but is that possible? + if (document.querySelector('#root')) return + + // Push SSR code + document.head.insertAdjacentHTML('beforeend', trustedHTML(css)) + document.body.innerHTML = trustedHTML('
' + html + '
') + console.timeEnd('[SSR] Request') + + console.time('[SSR] Hydrate') + }) + setTimeout(() => { + import(/* webpackPreload: true */ './normal-client') + }, 15) +} else { + import(/* webpackPreload: true */ './normal-client') +} + +// this function is never called, but it will hint webpack to preload modules we need +function prefetch() { + // Pages + import(/* webpackPreload: true */ './pages/Personas') + import(/* webpackPreload: true */ './pages/Personas/Home') + import(/* webpackPrefetch: true */ './pages/Wallet') +} + +// To make prefecher not tree-shaked +if (''.toLowerCase() === 'hint') { + prefetch() +} +export {} diff --git a/packages/mask/src/extension/popups/SSR-server.tsx b/packages/mask/src/extension/popups/SSR-server.tsx new file mode 100644 index 000000000000..53d18969dcd9 --- /dev/null +++ b/packages/mask/src/extension/popups/SSR-server.tsx @@ -0,0 +1,104 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + +import { Suspense } from 'react' +import { + FacebookColoredIcon, + InstagramColoredIcon, + MindsIcon, + TwitterColoredIcon, + OpenSeaColoredIcon, +} from '@masknet/icons' +import { i18NextInstance, updateLanguage, EnhanceableSite, PopupRoutes } from '@masknet/shared-base' +import { once, noop } from 'lodash-unified' +import { TssCacheProvider, MaskThemeProvider } from '@masknet/theme' +import { CacheProvider } from '@emotion/react' +import { renderToString } from 'react-dom/server' +import createCache from '@emotion/cache' +import createEmotionServer from '@emotion/server/create-instance' +import { initReactI18next } from 'react-i18next' +import { addMaskI18N } from '../../../shared-ui/locales/languages' +import type { PopupSSR_Props } from '../../../background/tasks/Cancellable/PopupSSR/type' +import { StaticRouter } from 'react-router-dom/server' +import { PopupFrame } from './components/PopupFrame' +import { PersonaHomeUI } from './pages/Personas/Home/UI' +import { usePopupFullPageTheme } from '../../utils/theme/useClassicMaskFullPageTheme' + +const init = once(() => + i18NextInstance.init().then(() => { + addMaskI18N(i18NextInstance) + initReactI18next.init(i18NextInstance) + }), +) +export async function render(props: PopupSSR_Props) { + await init() + updateLanguage(props.language) + const muiCache = createCache({ key: 'css' }) + const tssCache = createCache({ key: 'tss' }) + const tssServer = createEmotionServer(tssCache) + const muiServer = createEmotionServer(muiCache) + + const html = renderToString( + + + + + , + ) + .replaceAll('href="/', 'href="#/') + .replaceAll('href="#/dashboard', 'href="/dashboard') + const muiCSS = muiServer.constructStyleTagsFromChunks(muiServer.extractCriticalToChunks(html)) + const tssCSS = tssServer.constructStyleTagsFromChunks(tssServer.extractCriticalToChunks(html)) + return { html, css: muiCSS + tssCSS } +} + +const SOCIAL_MEDIA_ICON_MAPPING: Record = { + [EnhanceableSite.Facebook]: , + [EnhanceableSite.Twitter]: , + [EnhanceableSite.Instagram]: , + [EnhanceableSite.Minds]: , + [EnhanceableSite.OpenSea]: , +} +const DEFINED_SITES = [ + EnhanceableSite.Facebook, + EnhanceableSite.Twitter, + EnhanceableSite.Instagram, + EnhanceableSite.Minds, + EnhanceableSite.OpenSea, +] +function PopupSSR(props: PopupSSR_Props) { + const currentPersona = props.personas?.find((x) => x.identifier.equals(props.currentPersona)) + function useTheme() { + return usePopupFullPageTheme(props.language) + } + return ( + + 'light'}> + + + + + + + + + + ) +} diff --git a/packages/mask/src/extension/popups/UI.tsx b/packages/mask/src/extension/popups/UI.tsx index c50ed7a8e8ef..5fa1f0953fb6 100644 --- a/packages/mask/src/extension/popups/UI.tsx +++ b/packages/mask/src/extension/popups/UI.tsx @@ -1,21 +1,24 @@ -import { lazy } from 'react' +import { lazy, useEffect, useState } from 'react' import { Navigate, Route, Routes, HashRouter } from 'react-router-dom' import { createInjectHooksRenderer, useActivatedPluginsDashboard } from '@masknet/plugin-infra' import { PopupRoutes } from '@masknet/shared-base' -import { useClassicMaskFullPageTheme } from '../../utils' import '../../social-network-adaptor/browser-action' import { Web3Provider } from '@masknet/web3-shared-evm' import { PopupWeb3Context } from '../../web3/context' import { PopupFrame } from './components/PopupFrame' import { Appearance } from '@masknet/theme' import { MaskUIRoot } from '../../UIRoot' +import { useClassicMaskFullPageTheme } from '../../utils/theme/useClassicMaskFullPageTheme' +import { useMyPersonas } from '../../components/DataSource/useMyPersonas' +import { useValueRef } from '@masknet/shared-base-ui' +import { languageSettings } from '../../settings/settings' function useAlwaysLightTheme() { - return useClassicMaskFullPageTheme({ forcePalette: Appearance.light }) + return useClassicMaskFullPageTheme(Appearance.light, useValueRef(languageSettings)) } -const Wallet = lazy(() => import('./pages/Wallet')) -const Personas = lazy(() => import('./pages/Personas')) -const SwapPage = lazy(() => import('./pages/Swap')) +const Wallet = lazy(() => import(/* webpackPrefetch: true */ './pages/Wallet')) +const Personas = lazy(() => import(/* webpackPrefetch: true */ './pages/Personas')) +const SwapPage = lazy(() => import(/* webpackPrefetch: true */ './pages/Swap')) const RequestPermissionPage = lazy(() => import('./RequestPermission')) const PermissionAwareRedirect = lazy(() => import('./PermissionAwareRedirect')) const ThirdPartyRequestPermission = lazy(() => import('./ThirdPartyRequestPermission')) @@ -23,13 +26,17 @@ const ThirdPartyRequestPermission = lazy(() => import('./ThirdPartyRequestPermis const PluginRender = createInjectHooksRenderer(useActivatedPluginsDashboard, (x) => x.GlobalInjection) export default function Popups() { + const personaLength = useMyPersonas().length + const [client, setClient] = useState(false) + useEffect(() => setClient(true), []) + return ( - )} /> - )} /> + )} /> + )} /> } /> } /> } /> @@ -40,13 +47,13 @@ export default function Popups() { } /> {/* TODO: Should only load plugins when the page is plugin-aware. */} - + {client ? : null} ) } -function frame(x: React.ReactNode) { - return +function frame(personaLength: number, x: React.ReactNode) { + return } diff --git a/packages/mask/src/extension/popups/components/EnterDashboard/index.tsx b/packages/mask/src/extension/popups/components/EnterDashboard/index.tsx index f391883dbba1..0deb253dd484 100644 --- a/packages/mask/src/extension/popups/components/EnterDashboard/index.tsx +++ b/packages/mask/src/extension/popups/components/EnterDashboard/index.tsx @@ -1,27 +1,27 @@ // ! This file is used during SSR. DO NOT import new files that does not work in SSR import { memo } from 'react' -import { Box, Typography } from '@mui/material' +import { styled, Typography } from '@mui/material' import { useEnterDashboard } from '../../hook/useEnterDashboard' import { useI18N } from '../../../../utils/i18n-next-ui' +const Button = styled('a')({ + padding: '12px 16px', + cursor: 'pointer', + backgroundColor: '#ffffff', + width: '100%', + position: 'fixed', + bottom: 0, + textDecoration: 'none', +}) export const EnterDashboard = memo(() => { const { t } = useI18N() const onEnter = useEnterDashboard() return ( - + ) }) diff --git a/packages/mask/src/extension/popups/components/InitialPlaceholder/index.tsx b/packages/mask/src/extension/popups/components/InitialPlaceholder/index.tsx index 487429ed5222..ecdd85f9e420 100644 --- a/packages/mask/src/extension/popups/components/InitialPlaceholder/index.tsx +++ b/packages/mask/src/extension/popups/components/InitialPlaceholder/index.tsx @@ -1,3 +1,5 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + import { memo } from 'react' import { Box, Button, Typography } from '@mui/material' import { makeStyles } from '@masknet/theme' @@ -5,7 +7,7 @@ import { useEnterDashboard } from '../../hook/useEnterDashboard' import { useMatch } from 'react-router-dom' import { PopupRoutes } from '@masknet/shared-base' import { MasksIcon, MaskWalletIcon } from '@masknet/icons' -import { useI18N } from '../../../../utils' +import { useI18N } from '../../../../utils/i18n-next-ui' const useStyles = makeStyles()({ container: { @@ -58,7 +60,13 @@ export const InitialPlaceholder = memo(() => { })} - diff --git a/packages/mask/src/extension/popups/components/PopupFrame/index.tsx b/packages/mask/src/extension/popups/components/PopupFrame/index.tsx index 21881ee4d917..063cf0f52a4e 100644 --- a/packages/mask/src/extension/popups/components/PopupFrame/index.tsx +++ b/packages/mask/src/extension/popups/components/PopupFrame/index.tsx @@ -1,12 +1,13 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + import { memo } from 'react' import { NavLink, useNavigate, useLocation, useMatch } from 'react-router-dom' import { Box, GlobalStyles, Paper } from '@mui/material' import { makeStyles } from '@masknet/theme' import { ArrowBackIcon, MiniMaskIcon } from '@masknet/icons' import { PopupRoutes } from '@masknet/shared-base' -import { useMyPersonas } from '../../../../components/DataSource/useMyPersonas' import { InitialPlaceholder } from '../InitialPlaceholder' -import { useI18N } from '../../../../utils' +import { useI18N } from '../../../../utils/i18n-next-ui' function GlobalCss() { return ( @@ -17,7 +18,7 @@ function GlobalCss() { overflowX: 'hidden', margin: '0 auto !important', maxWidth: '100%', - '-webkit-font-smoothing': 'subpixel-antialiased', + WebkitFontSmoothing: 'subpixel-antialiased', '&::-webkit-scrollbar': { display: 'none', }, @@ -68,14 +69,15 @@ const useStyles = makeStyles()((theme) => ({ }, })) -export interface PopupFrameProps extends React.PropsWithChildren<{}> {} +export interface PopupFrameProps extends React.PropsWithChildren<{}> { + personaLength: number +} export const PopupFrame = memo((props) => { const { t } = useI18N() const navigate = useNavigate() const { classes, cx } = useStyles() const location = useLocation() - const personas = useMyPersonas() const excludePath = [ useMatch(PopupRoutes.Wallet), @@ -129,7 +131,7 @@ export const PopupFrame = memo((props) => { - {personas.length === 0 && !matchRecovery ? : props.children} + {props.personaLength === 0 && !matchRecovery ? : props.children} diff --git a/packages/mask/src/extension/popups/hook/useEnterDashboard.ts b/packages/mask/src/extension/popups/hook/useEnterDashboard.ts index f283318f6aab..e3c7866c7b37 100644 --- a/packages/mask/src/extension/popups/hook/useEnterDashboard.ts +++ b/packages/mask/src/extension/popups/hook/useEnterDashboard.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react' export const useEnterDashboard = () => { return useCallback((event: React.MouseEvent) => { + event.preventDefault() if (event.shiftKey) { browser.tabs.create({ active: true, diff --git a/packages/mask/src/extension/popups/normal-client.tsx b/packages/mask/src/extension/popups/normal-client.tsx new file mode 100644 index 000000000000..aa27c1814c2f --- /dev/null +++ b/packages/mask/src/extension/popups/normal-client.tsx @@ -0,0 +1,54 @@ +import { startPluginDashboard } from '@masknet/plugin-infra' +import { createNormalReactRoot, hydrateNormalReactRoot } from '../../utils' +import { createPluginHost } from '../../plugin-infra/host' +import { Services } from '../service' +import { status } from '../../setup.ui' +import Popups from './UI' +import { InMemoryStorages, PersistentStorages } from '../../../shared/kv-storage' +import createCache from '@emotion/cache' +import { CacheProvider } from '@emotion/react' +import { TssCacheProvider } from '@masknet/theme' +import { initData } from './pages/Personas/hooks/usePersonaContext' + +if (location.hash === '#/personas') { + async function hydrate() { + await Promise.all([ + Services.Identity.queryCurrentPersona().then((x) => (initData.currentIdentifier = x?.toText())), + Services.Identity.queryOwnedPersonaInformation().then((x) => (initData.personas = x)), + Services.Identity.queryOwnedProfileInformationWithNextID().then((x) => (initData.profiles = x)), + status, + ]) + + const muiCache = createCache({ key: 'css' }) + const tssCache = createCache({ key: 'tss' }) + hydrateNormalReactRoot( + + + + + , + ) + startPluginHost() + console.timeEnd('[SSR] Hydrate') + } + hydrate() +} else { + status.then(() => createNormalReactRoot()).then(startPluginHost) +} + +function startPluginHost() { + // TODO: Should only load plugins when the page is plugin-aware. + startPluginDashboard( + createPluginHost(undefined, (pluginID, signal) => { + return { + createKVStorage(type, defaultValues) { + if (type === 'memory') + return InMemoryStorages.Plugin.createSubScope(pluginID, defaultValues, signal) + else return PersistentStorages.Plugin.createSubScope(pluginID, defaultValues, signal) + }, + personaSign: Services.Identity.signWithPersona, + walletSign: Services.Ethereum.personalSign, + } + }), + ) +} diff --git a/packages/mask/src/extension/popups/pages/Personas/Home/UI.tsx b/packages/mask/src/extension/popups/pages/Personas/Home/UI.tsx index ff6008a700fe..2da2c91e0728 100644 --- a/packages/mask/src/extension/popups/pages/Personas/Home/UI.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/Home/UI.tsx @@ -17,7 +17,7 @@ import { EnterDashboard } from '../../../components/EnterDashboard' import { PersonaListUI } from '../components/PersonaList' import { useI18N } from '../../../../../utils/i18n-next-ui' import urlcat from 'urlcat' -import type { NavigateFunction } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' const useStyles = makeStyles()({ content: { @@ -96,14 +96,14 @@ const useStyles = makeStyles()({ }) export interface PersonaHomeUIProps extends ProfileListProps { - navigate: NavigateFunction currentPersona: PersonaInformation | undefined personas: PersonaInformation[] | undefined onChangeCurrentPersona: (identifier: ECKeyIdentifier) => void onDeletePersona: (persona: PersonaInformation | undefined) => void } export const PersonaHomeUI = memo((props: PersonaHomeUIProps) => { - const { navigate, currentPersona, personas, onDeletePersona, onChangeCurrentPersona } = props + const navigate = useNavigate() + const { currentPersona, personas, onDeletePersona, onChangeCurrentPersona } = props const { t } = useI18N() const { classes, cx } = useStyles() @@ -157,7 +157,7 @@ export const PersonaHomeUI = memo((props: PersonaHomeUIProps) => { onConfirmDisconnect={props.onConfirmDisconnect} onDisconnectProfile={props.onDisconnectProfile} openProfilePage={props.openProfilePage} - mergedProfiles={props.mergedProfiles} + profilesWithNextID={props.profilesWithNextID} definedSocialNetworks={props.definedSocialNetworks} SOCIAL_MEDIA_ICON_MAPPING={props.SOCIAL_MEDIA_ICON_MAPPING} /> diff --git a/packages/mask/src/extension/popups/pages/Personas/Home/index.tsx b/packages/mask/src/extension/popups/pages/Personas/Home/index.tsx index ab1a295cca27..599b1e99f82a 100644 --- a/packages/mask/src/extension/popups/pages/Personas/Home/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/Home/index.tsx @@ -1,5 +1,5 @@ import { memo, useCallback } from 'react' -import { PersonaContext } from '../hooks/usePersonaContext' +import { initData, PersonaContext } from '../hooks/usePersonaContext' import { useNavigate } from 'react-router-dom' import { PersonaHomeUI } from './UI' import { @@ -57,27 +57,10 @@ const PersonaHome = memo(() => { [currentPersona], ) - const { value: mergedProfiles, retry: refreshProfileList } = useAsyncRetry(async () => { - if (!currentPersona) return [] - if (!currentPersona.publicHexKey) return currentPersona.linkedProfiles - const response = await NextIDProof.queryExistedBindingByPersona(currentPersona.publicHexKey) - if (!response) return currentPersona.linkedProfiles - - return currentPersona.linkedProfiles.map((profile) => { - const target = response.proofs.find( - (x) => - profile.identifier.userId.toLowerCase() === x.identity.toLowerCase() && - profile.identifier.network.replace('.com', '') === x.platform, - ) - - return { - ...profile, - platform: target?.platform, - identity: target?.identity, - is_valid: target?.is_valid, - } - }) - }, [currentPersona]) + const { value: profilesWithNextID = initData.profiles, retry: refreshProfileList } = useAsyncRetry( + Services.Identity.queryOwnedProfileInformationWithNextID, + [currentPersona], + ) const [{ loading: confirmLoading }, onConfirmDisconnect] = useAsyncFn( async (unbind: UnbindStatus) => { @@ -121,12 +104,11 @@ const PersonaHome = memo(() => { onConnectNextID={onConnectNextID} onConnectProfile={onConnectProfile} onDisconnectProfile={Services.Identity.detachProfile} - mergedProfiles={mergedProfiles ?? EMPTY_LIST} + profilesWithNextID={profilesWithNextID ?? EMPTY_LIST} openProfilePage={Services.SocialNetwork.openProfilePage} SOCIAL_MEDIA_ICON_MAPPING={SOCIAL_MEDIA_ICON_MAPPING} definedSocialNetworks={definedSocialNetworks} currentPersona={currentPersona} - navigate={navigate} personas={personas} onDeletePersona={setDeletingPersona} onChangeCurrentPersona={onChangeCurrentPersona} diff --git a/packages/mask/src/extension/popups/pages/Personas/components/DisconnectDialog/index.tsx b/packages/mask/src/extension/popups/pages/Personas/components/DisconnectDialog/index.tsx index 55a1efcb25c1..4b59bc01606e 100644 --- a/packages/mask/src/extension/popups/pages/Personas/components/DisconnectDialog/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/components/DisconnectDialog/index.tsx @@ -5,7 +5,7 @@ import { Button, Dialog, DialogActions, DialogContent, Typography, DialogProps } import { makeStyles } from '@masknet/theme' import { formatPersonaFingerprint, PersonaInformation, type ProfileIdentifier } from '@masknet/shared-base' import { LoadingButton } from '@mui/lab' -import { useI18N } from '../../../../../../utils' +import { useI18N } from '../../../../../../utils/i18n-next-ui' const useStyles = makeStyles()(() => ({ title: { diff --git a/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx b/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx index dd93dbe5de73..5fa25b0858f8 100644 --- a/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx @@ -2,16 +2,16 @@ import React, { memo, Suspense, useCallback, useState } from 'react' import { Avatar, Link, List, ListItem, ListItemText, Typography } from '@mui/material' -import type { ProfileIdentifier, ProfileInformation, NextIDPlatform, PersonaInformation } from '@masknet/shared-base' +import type { ProfileIdentifier, NextIDPlatform, PersonaInformation } from '@masknet/shared-base' import { makeStyles } from '@masknet/theme' import { useI18N } from '../../../../../../utils/i18n-next-ui' import { GrayMasks } from '@masknet/icons' import { DisconnectDialog } from '../DisconnectDialog' +import type { ProfileInformationWithNextID } from '../../../../../background-script/IdentityService' const useStyles = makeStyles()({ list: { padding: '0 0 70px 0', - height: 487, overflow: 'auto', }, item: { @@ -113,7 +113,7 @@ export const ProfileList = memo((props: ProfileListProps) => { <> { ) }) -interface MergedProfileInformation extends ProfileInformation { - is_valid?: boolean - identity?: string - platform?: NextIDPlatform -} - interface ProfileListUIProps { onConnectProfile(network: string): void onConnectNextID(profile: ProfileIdentifier): void onDisconnect(identifier: ProfileIdentifier, is_valid?: boolean, platform?: NextIDPlatform, identity?: string): void openProfilePage(profile: ProfileIdentifier): void - mergedProfiles: MergedProfileInformation[] + profilesWithNextID: ProfileInformationWithNextID[] definedSocialNetworks: string[] SOCIAL_MEDIA_ICON_MAPPING: Record } @@ -155,7 +149,7 @@ interface ProfileListUIProps { const ProfileListUI = memo((props: ProfileListUIProps) => { const { definedSocialNetworks, - mergedProfiles: profiles, + profilesWithNextID: profiles, onConnectProfile, onConnectNextID, onDisconnect, diff --git a/packages/mask/src/extension/popups/pages/Personas/hooks/usePersonaContext.ts b/packages/mask/src/extension/popups/pages/Personas/hooks/usePersonaContext.ts index bb3ffe6c81f5..fb140af383b4 100644 --- a/packages/mask/src/extension/popups/pages/Personas/hooks/usePersonaContext.ts +++ b/packages/mask/src/extension/popups/pages/Personas/hooks/usePersonaContext.ts @@ -2,20 +2,29 @@ import { createContainer } from 'unstated-next' import { useValueRef } from '@masknet/shared-base-ui' import { ECKeyIdentifier, Identifier, PersonaInformation } from '@masknet/shared-base' import { currentPersonaIdentifier } from '../../../../../settings/settings' -import { useAsyncRetry } from 'react-use' import Services from '../../../../service' import { head } from 'lodash-unified' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { MaskMessages } from '../../../../../utils' +import type { ProfileInformationWithNextID } from '../../../../background-script/IdentityService' +export const initData: { + currentIdentifier?: string + personas?: PersonaInformation[] + profiles?: ProfileInformationWithNextID[] +} = {} function usePersonaContext() { const [deletingPersona, setDeletingPersona] = useState() - const currentIdentifier = useValueRef(currentPersonaIdentifier) - const { value: personas, retry } = useAsyncRetry(async () => Services.Identity.queryOwnedPersonaInformation()) + let currentIdentifier = useValueRef(currentPersonaIdentifier) + if (!currentPersonaIdentifier.ready && initData.currentIdentifier) currentIdentifier = initData.currentIdentifier + + const [personas, setPersonas] = useState(initData.personas) useEffect(() => { - return MaskMessages.events.ownPersonaChanged.on(retry) - }, [retry]) + const f = Services.Identity.queryOwnedPersonaInformation + if (!initData.personas) f().then(setPersonas) + return MaskMessages.events.ownPersonaChanged.on(() => f().then(setPersonas)) + }, [initData.personas]) const currentPersona = personas?.find((x) => x.identifier.equals( @@ -23,14 +32,20 @@ function usePersonaContext() { ), ) - const otherPersonas = personas?.filter((x) => !x.identifier.equals(currentPersona?.identifier)) + const otherPersonas = useMemo( + () => personas?.filter((x) => !x.identifier.equals(currentPersona?.identifier)), + [personas, currentPersona?.identifier], + ) - return { - deletingPersona, - setDeletingPersona, - personas: otherPersonas, - currentPersona, - } + return useMemo( + () => ({ + deletingPersona, + setDeletingPersona, + personas: otherPersonas, + currentPersona, + }), + [deletingPersona, otherPersonas, currentPersona], + ) } export const PersonaContext = createContainer(usePersonaContext) diff --git a/packages/mask/src/extension/popups/pages/Personas/index.tsx b/packages/mask/src/extension/popups/pages/Personas/index.tsx index 8e1a5ead5ca1..788b2e7d2126 100644 --- a/packages/mask/src/extension/popups/pages/Personas/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/index.tsx @@ -5,10 +5,10 @@ import { PersonaContext } from './hooks/usePersonaContext' import { PopupRoutes, relativeRouteOf } from '@masknet/shared-base' import { Route, Routes } from 'react-router-dom' -const Home = lazy(() => import('./Home')) -const Logout = lazy(() => import('./Logout')) -const PersonaRename = lazy(() => import('./Rename')) -const PersonaSignRequest = lazy(() => import('./PersonaSignRequest')) +const Home = lazy(() => import(/* webpackPrefetch: true */ './Home')) +const Logout = lazy(() => import(/* webpackPrefetch: true */ './Logout')) +const PersonaRename = lazy(() => import(/* webpackPrefetch: true */ './Rename')) +const PersonaSignRequest = lazy(() => import(/* webpackPrefetch: true */ './PersonaSignRequest')) const r = relativeRouteOf(PopupRoutes.Personas) const Persona = memo(() => { diff --git a/packages/mask/src/extension/popups/pages/Swap/index.tsx b/packages/mask/src/extension/popups/pages/Swap/index.tsx index 4adb25540b66..8bd2cb7bcaec 100644 --- a/packages/mask/src/extension/popups/pages/Swap/index.tsx +++ b/packages/mask/src/extension/popups/pages/Swap/index.tsx @@ -9,10 +9,11 @@ import { WalletStateBarUI } from '../../components/WalletStateBar' import { SwapBox } from './SwapBox' import { SwapWeb3Context } from '../../../../web3/context' import { PopupRoutes } from '@masknet/shared-base' -import { useI18N, usePopupsMaskFullPageTheme } from '../../../../utils' +import { useI18N } from '../../../../utils' import { NetworkPluginID, useReverseAddress } from '@masknet/plugin-infra' import { TargetChainIdContext } from '../../../../plugins/Trader/trader/useTargetChainIdContext' import { AllProviderTradeContext } from '../../../../plugins/Trader/trader/useAllProviderTradeContext' +import { useSwapPageTheme } from '../../../../utils/theme/usePopupsMaskFullPageTheme' const useStyles = makeStyles()((theme) => { return { @@ -68,7 +69,7 @@ export default function SwapPage() { const { t } = useI18N() const { classes } = useStyles() const chainId = useChainId() - const theme = usePopupsMaskFullPageTheme() + const theme = useSwapPageTheme() const { value: pendingTransactions = [] } = useRecentTransactions({ status: TransactionStatusType.NOT_DEPEND, }) diff --git a/packages/mask/src/extension/popups/pages/Wallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/index.tsx index 5da93e45c4dd..bba50776c50d 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/index.tsx @@ -13,25 +13,25 @@ import SelectWallet from './SelectWallet' import { useWalletLockStatus } from './hooks/useWalletLockStatus' import urlcat from 'urlcat' -const ImportWallet = lazy(() => import('./ImportWallet')) -const AddDeriveWallet = lazy(() => import('./AddDeriveWallet')) -const WalletSettings = lazy(() => import('./WalletSettings')) -const WalletRename = lazy(() => import('./WalletRename')) -const DeleteWallet = lazy(() => import('./DeleteWallet')) -const CreateWallet = lazy(() => import('./CreateWallet')) -const SwitchWallet = lazy(() => import('./SwitchWallet')) -const BackupWallet = lazy(() => import('./BackupWallet')) -const AddToken = lazy(() => import('./AddToken')) -const TokenDetail = lazy(() => import('./TokenDetail')) -const SignRequest = lazy(() => import('./SignRequest')) -const GasSetting = lazy(() => import('./GasSetting')) -const Transfer = lazy(() => import('./Transfer')) -const ContractInteraction = lazy(() => import('./ContractInteraction')) -const Unlock = lazy(() => import('./Unlock')) -const SetPaymentPassword = lazy(() => import('./SetPaymentPassword')) -const WalletRecovery = lazy(() => import('./WalletRecovery')) -const LegacyWalletRecovery = lazy(() => import('./LegacyWalletRecovery')) -const ReplaceTransaction = lazy(() => import('./ReplaceTransaction')) +const ImportWallet = lazy(() => import(/* webpackPrefetch: true */ './ImportWallet')) +const AddDeriveWallet = lazy(() => import(/* webpackPrefetch: true */ './AddDeriveWallet')) +const WalletSettings = lazy(() => import(/* webpackPrefetch: true */ './WalletSettings')) +const WalletRename = lazy(() => import(/* webpackPrefetch: true */ './WalletRename')) +const DeleteWallet = lazy(() => import(/* webpackPrefetch: true */ './DeleteWallet')) +const CreateWallet = lazy(() => import(/* webpackPrefetch: true */ './CreateWallet')) +const SwitchWallet = lazy(() => import(/* webpackPrefetch: true */ './SwitchWallet')) +const BackupWallet = lazy(() => import(/* webpackPrefetch: true */ './BackupWallet')) +const AddToken = lazy(() => import(/* webpackPrefetch: true */ './AddToken')) +const TokenDetail = lazy(() => import(/* webpackPrefetch: true */ './TokenDetail')) +const SignRequest = lazy(() => import(/* webpackPrefetch: true */ './SignRequest')) +const GasSetting = lazy(() => import(/* webpackPrefetch: true */ './GasSetting')) +const Transfer = lazy(() => import(/* webpackPrefetch: true */ './Transfer')) +const ContractInteraction = lazy(() => import(/* webpackPrefetch: true */ './ContractInteraction')) +const Unlock = lazy(() => import(/* webpackPrefetch: true */ './Unlock')) +const SetPaymentPassword = lazy(() => import(/* webpackPrefetch: true */ './SetPaymentPassword')) +const WalletRecovery = lazy(() => import(/* webpackPrefetch: true */ './WalletRecovery')) +const LegacyWalletRecovery = lazy(() => import(/* webpackPrefetch: true */ './LegacyWalletRecovery')) +const ReplaceTransaction = lazy(() => import(/* webpackPrefetch: true */ './ReplaceTransaction')) const r = relativeRouteOf(PopupRoutes.Wallet) export default function Wallet() { diff --git a/packages/mask/src/extension/popups/render.tsx b/packages/mask/src/extension/popups/render.tsx deleted file mode 100644 index 496c8f5138ba..000000000000 --- a/packages/mask/src/extension/popups/render.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { startPluginDashboard } from '@masknet/plugin-infra' -import { createNormalReactRoot } from '../../utils' -import { createPluginHost } from '../../plugin-infra/host' -import { Services } from '../service' -import { status } from '../../setup.ui' -import Popups from './UI' -import { InMemoryStorages, PersistentStorages } from '../../../shared/kv-storage' - -status.then(() => createNormalReactRoot()) - -// TODO: Should only load plugins when the page is plugin-aware. -startPluginDashboard( - createPluginHost(undefined, (pluginID, signal) => { - return { - createKVStorage(type, defaultValues) { - if (type === 'memory') return InMemoryStorages.Plugin.createSubScope(pluginID, defaultValues, signal) - else return PersistentStorages.Plugin.createSubScope(pluginID, defaultValues, signal) - }, - personaSign: Services.Identity.signWithPersona, - walletSign: Services.Ethereum.personalSign, - } - }), -) diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx index d58a66ae9710..6beab8740f0e 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx @@ -95,7 +95,7 @@ const useStyles = makeStyles()((theme, props) => { width: '100%', alignItems: 'center', justifyContent: 'space-between', - '-webkit-font-smoothing': 'antialiased', + WebkitFontSmoothing: 'antialiased', fontSize: 14, }, cardHeaderLocked: { diff --git a/packages/mask/src/plugins/VCent/SNSAdaptor/TweetDialog.tsx b/packages/mask/src/plugins/VCent/SNSAdaptor/TweetDialog.tsx index e67ab12d39da..e5d41cb662e7 100644 --- a/packages/mask/src/plugins/VCent/SNSAdaptor/TweetDialog.tsx +++ b/packages/mask/src/plugins/VCent/SNSAdaptor/TweetDialog.tsx @@ -1,7 +1,7 @@ import { first } from 'lodash-unified' import { Button } from '@mui/material' import { makeStyles } from '@masknet/theme' -import { isDarkTheme } from '../../../utils/theme-tools' +import { isDarkTheme } from '../../../utils/theme' import { ETHIcon } from '../icons/ETH' import { VCentIconLight, VCentIconDark } from '../icons/VCent' import { VALUABLES_VCENT_URL } from '../constants' diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/RiskWarningDialog/index.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/RiskWarningDialog/index.tsx index 0c01a034f884..19b6b54280de 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/RiskWarningDialog/index.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/RiskWarningDialog/index.tsx @@ -50,9 +50,9 @@ const useStyles = makeStyles()((theme) => ({ marginTop: theme.spacing(2), padding: theme.spacing(2), borderRadius: theme.spacing(1), - '&> :first-child': { - paddingBottom: theme.spacing(1), - }, + }, + texts: { + paddingBottom: theme.spacing(1), }, })) @@ -104,10 +104,10 @@ export function WalletRiskWarningDialog() { children={{t('wallet_risk_warning_content')}} /> - + {t('nft_wallet_label')} - + {isMobile ? formatEthereumAddress(account, 5) : account} diff --git a/packages/mask/src/social-network-adaptor/facebook.com/customization/custom.ts b/packages/mask/src/social-network-adaptor/facebook.com/customization/custom.ts index 2d163de13460..2042d879c223 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/customization/custom.ts +++ b/packages/mask/src/social-network-adaptor/facebook.com/customization/custom.ts @@ -5,7 +5,7 @@ import { useMemo } from 'react' import { useValueRef } from '@masknet/shared-base-ui' import { SubscriptionFromValueRef } from '@masknet/shared-base' import type { SocialNetworkUI } from '../../../social-network' -import { fromRGB, isDark, shade, toRGB } from '../../../utils/theme-tools' +import { fromRGB, isDark, shade, toRGB } from '../../../utils/theme' const primaryColorRef = new ValueRef(toRGB([29, 161, 242])) const primaryColorContrastColorRef = new ValueRef(toRGB([255, 255, 255])) diff --git a/packages/mask/src/social-network-adaptor/minds.com/customization/custom.ts b/packages/mask/src/social-network-adaptor/minds.com/customization/custom.ts index 71f388a0f0d7..18b2086f0e34 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/customization/custom.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/customization/custom.ts @@ -5,7 +5,7 @@ import { useMemo } from 'react' import { useValueRef } from '@masknet/shared-base-ui' import { SubscriptionFromValueRef } from '@masknet/shared-base' import type { SocialNetworkUI } from '../../../social-network' -import { fromRGB, getBackgroundColor, getForegroundColor, shade, toRGB } from '../../../utils/theme-tools' +import { fromRGB, getBackgroundColor, getForegroundColor, shade, toRGB } from '../../../utils/theme' import { themeListItemSelector } from '../utils/selector' // TODO: get this from DOM. But currently Minds has a single primary color diff --git a/packages/mask/src/social-network-adaptor/twitter.com/customization/custom.ts b/packages/mask/src/social-network-adaptor/twitter.com/customization/custom.ts index 9377d76923ad..e6cff6cc609a 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/customization/custom.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/customization/custom.ts @@ -5,7 +5,7 @@ import { PaletteMode, Theme, unstable_createMuiStrictModeTheme } from '@mui/mate import produce, { setAutoFreeze } from 'immer' import { useMemo } from 'react' import type { SocialNetworkUI } from '../../../social-network' -import { fromRGB, getBackgroundColor, getForegroundColor, isDark, shade, toRGB } from '../../../utils/theme-tools' +import { fromRGB, getBackgroundColor, getForegroundColor, isDark, shade, toRGB } from '../../../utils/theme' import { isMobileTwitter } from '../utils/isMobile' import { composeAnchorSelector, composeAnchorTextSelector, headingTextSelector } from '../utils/selector' import twitterColorSchema from './twitter-color-schema.json' diff --git a/packages/mask/src/utils/createNormalReactRoot.tsx b/packages/mask/src/utils/createNormalReactRoot.tsx index da1deea5d468..ba9e2b113d33 100644 --- a/packages/mask/src/utils/createNormalReactRoot.tsx +++ b/packages/mask/src/utils/createNormalReactRoot.tsx @@ -1,25 +1,39 @@ import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import { createRoot, hydrateRoot } from 'react-dom/client' import { ErrorBoundary } from '@masknet/shared-base-ui' import { DisableShadowRootContext } from '@masknet/theme' -export function createNormalReactRoot(jsx: JSX.Element, container?: HTMLElement) { +function cleanup() { + if (process.env.NODE_ENV === 'development') { + // Make the document cleaner + setTimeout(() => [...document.querySelectorAll('script')].forEach((x) => x.remove()), 200) + } +} +function getContainer(container?: HTMLElement) { if (!container) container = document.getElementById('root') ?? void 0 if (!container) { container = document.createElement('div') document.body.appendChild(container) } - - if (process.env.NODE_ENV === 'development') { - // Make the document cleaner - setTimeout(() => [...document.querySelectorAll('script')].forEach((x) => x.remove()), 200) - } - - return createRoot(container).render( + return container +} +function Root(jsx: JSX.Element) { + return ( {jsx} - , + ) } +export function createNormalReactRoot(jsx: JSX.Element, dom?: HTMLElement) { + cleanup() + const container = getContainer(dom) + return createRoot(container).render(Root(jsx)) +} + +export function hydrateNormalReactRoot(jsx: JSX.Element, dom?: HTMLElement) { + cleanup() + const container = getContainer(dom) + return hydrateRoot(container, Root(jsx)) +} diff --git a/packages/mask/src/utils/index.ts b/packages/mask/src/utils/index.ts index c6a009552f39..eba2a666feb7 100644 --- a/packages/mask/src/utils/index.ts +++ b/packages/mask/src/utils/index.ts @@ -11,7 +11,6 @@ export * from './i18n-next-ui' export * from './messages' export * from './permissions' export * from './createNormalReactRoot' -export * from './theme-tools' export * from './theme' export * from './utils' export * from './watcher' diff --git a/packages/mask/src/utils/theme/MaskTheme.ts b/packages/mask/src/utils/theme/MaskTheme.ts new file mode 100644 index 000000000000..3aad54159e25 --- /dev/null +++ b/packages/mask/src/utils/theme/MaskTheme.ts @@ -0,0 +1,96 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + +import { unstable_createMuiStrictModeTheme, type ThemeOptions } from '@mui/material' +import { grey, orange } from '@mui/material/colors' +import { cloneDeep, merge } from 'lodash-unified' + +function getFontFamily(monospace?: boolean) { + // We want to look native. + // Windows has no CJK sans monospace. Accommodate that. + // We only use it for fingerprints anyway so CJK coverage ain't a problem... yet. + const monofont = navigator.platform.startsWith('Win') ? 'Consolas, monospace' : 'monospace' + // https://caniuse.com/font-family-system-ui + // Firefox does NOT support yet it in any form on Windows, but tests indicate that it agrees with Edge in using the UI font for sans-serif: + // Microsoft YaHei on zh-Hans-CN. + return !monospace ? '-apple-system, system-ui, sans-serif' : monofont +} +const base: ThemeOptions = { + palette: { + primary: { main: '#1c68f3' }, + secondary: orange, + text: { hint: 'rgba(0, 0, 0, 0.38)' }, + }, + typography: { + fontFamily: getFontFamily(), + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 1112, + lg: 1280, + xl: 1920, + }, + }, + components: { + MuiLink: { defaultProps: { underline: 'hover' } }, + MuiButton: { + styleOverrides: { + root: { + textTransform: 'unset', + minWidth: '100px', + }, + }, + defaultProps: { + size: 'small', + disableElevation: true, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: 'unset', + padding: '0', + // up-sm + '@media screen and (min-width: 600px)': { + minWidth: 160, + }, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + borderRadius: '12px', + }, + }, + }, + }, +} +const lightThemePatch: Partial = { + palette: { + mode: 'light', + }, +} +const darkThemePatch: Partial = { + palette: { + mode: 'dark', + background: { + paper: grey[900], + }, + }, + components: { + MuiPaper: { + // https://github.com/mui-org/material-ui/pull/25522 + styleOverrides: { root: { backgroundImage: 'unset' } }, + }, + }, +} +const baseTheme = (theme: 'dark' | 'light') => { + if (theme === 'light') return merge(cloneDeep(base), lightThemePatch) + return merge(cloneDeep(base), darkThemePatch) +} +/** @deprecated Only use it from useClassicMaskSNSTheme */ +export const MaskLightTheme = unstable_createMuiStrictModeTheme(baseTheme('light')) +/** @deprecated Only use it from useClassicMaskSNSTheme */ +export const MaskDarkTheme = unstable_createMuiStrictModeTheme(baseTheme('dark')) diff --git a/packages/mask/src/utils/theme-tools.ts b/packages/mask/src/utils/theme/color-tools.ts similarity index 94% rename from packages/mask/src/utils/theme-tools.ts rename to packages/mask/src/utils/theme/color-tools.ts index 197f11140ba4..68c7c239196c 100644 --- a/packages/mask/src/utils/theme-tools.ts +++ b/packages/mask/src/utils/theme/color-tools.ts @@ -1,3 +1,5 @@ +import { clamp } from 'lodash-unified' + type RGB = [number, number, number] type RGBA = [number, number, number, number] @@ -19,12 +21,6 @@ export function fromRGB(rgb: string): RGB | undefined { return } -export function clamp(num: number, min: number, max: number) { - if (num < min) return min - if (num > max) return max - return num -} - export function shade(channels: RGB, percentage: number): RGB { return channels.map((c) => clamp(Math.floor((c * (100 + percentage)) / 100), 0, 255)) as RGB } diff --git a/packages/mask/src/utils/theme/index.ts b/packages/mask/src/utils/theme/index.ts new file mode 100644 index 000000000000..76a6527c393c --- /dev/null +++ b/packages/mask/src/utils/theme/index.ts @@ -0,0 +1,9 @@ +import './theme-global.d' + +// Do not export more files. Those files are deprecated. +export * from './color-tools' +export * from './MaskTheme' +export * from './useColorStyles' +export * from './useErrorStyles' +export * from './useThemeLanguage' +export * from './useClassicMaskSNSTheme' diff --git a/packages/mask/src/utils/theme-global.d.ts b/packages/mask/src/utils/theme/theme-global.d.ts similarity index 100% rename from packages/mask/src/utils/theme-global.d.ts rename to packages/mask/src/utils/theme/theme-global.d.ts diff --git a/packages/mask/src/utils/theme/useClassicMaskFullPageTheme.ts b/packages/mask/src/utils/theme/useClassicMaskFullPageTheme.ts new file mode 100644 index 000000000000..8a0da1724299 --- /dev/null +++ b/packages/mask/src/utils/theme/useClassicMaskFullPageTheme.ts @@ -0,0 +1,26 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + +import type { LanguageOptions } from '@masknet/public-api' +import { Appearance } from '@masknet/theme' +import { PaletteMode, unstable_createMuiStrictModeTheme } from '@mui/material' +import { MaskDarkTheme, MaskLightTheme } from './MaskTheme' +import { useThemeLanguage } from './useThemeLanguage' + +/** + * @deprecated Should migrate to \@masknet/theme + */ +export function useClassicMaskFullPageTheme(userPreference: Appearance, language: LanguageOptions) { + const systemPreference: PaletteMode = + 'matchMedia' in globalThis ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : 'light' + const finalPalette: PaletteMode = userPreference === Appearance.default ? systemPreference : userPreference + + const baseTheme = finalPalette === 'dark' ? MaskDarkTheme : MaskLightTheme + return unstable_createMuiStrictModeTheme(baseTheme, useThemeLanguage(language)) +} + +/** + * @deprecated Should migrate to \@masknet/theme + */ +export function usePopupFullPageTheme(language: LanguageOptions) { + return useClassicMaskFullPageTheme(Appearance.light, language) +} diff --git a/packages/mask/src/utils/theme/useClassicMaskSNSTheme.ts b/packages/mask/src/utils/theme/useClassicMaskSNSTheme.ts new file mode 100644 index 000000000000..e8f23320ff84 --- /dev/null +++ b/packages/mask/src/utils/theme/useClassicMaskSNSTheme.ts @@ -0,0 +1,28 @@ +import { unstable_createMuiStrictModeTheme } from '@mui/material' +import type { Theme } from '@mui/material/styles/createTheme' +import { useRef } from 'react' +import { activatedSocialNetworkUI } from '../../social-network' +import { useSubscription } from 'use-subscription' +import { MaskDarkTheme, MaskLightTheme } from './MaskTheme' +import { useThemeLanguage } from './useThemeLanguage' +import { SubscriptionFromValueRef } from '@masknet/shared-base' +import { ValueRef } from '@dimensiondev/holoflows-kit' +import { useValueRef } from '@masknet/shared-base-ui' +import { languageSettings } from '../../settings/settings' + +const staticRef = SubscriptionFromValueRef(new ValueRef('light')) +const defaultUseTheme = (t: Theme) => t +/** + * @deprecated Should migrate to \@masknet/theme + */ +export function useClassicMaskSNSTheme() { + const provider = useRef(activatedSocialNetworkUI.customization.paletteMode?.current || staticRef).current + const usePostTheme = useRef(activatedSocialNetworkUI.customization.useTheme || defaultUseTheme).current + const palette = useSubscription(provider) + const baseTheme = palette === 'dark' ? MaskDarkTheme : MaskLightTheme + + // TODO: support RTL? + const [localization, isRTL] = useThemeLanguage(useValueRef(languageSettings)) + const theme = unstable_createMuiStrictModeTheme(baseTheme, localization) + return usePostTheme(theme) +} diff --git a/packages/mask/src/utils/theme/useColorStyles.ts b/packages/mask/src/utils/theme/useColorStyles.ts new file mode 100644 index 000000000000..1d0d1e1ea458 --- /dev/null +++ b/packages/mask/src/utils/theme/useColorStyles.ts @@ -0,0 +1,18 @@ +import { makeStyles } from '@masknet/theme' +import { blue, green, red } from '@mui/material/colors' +import type { MaskDarkTheme } from './MaskTheme' + +export const useColorStyles = makeStyles()((theme: typeof MaskDarkTheme) => { + const dark = theme.palette.mode === 'dark' + return { + error: { + color: dark ? red[500] : red[900], + }, + success: { + color: dark ? green[500] : green[800], + }, + info: { + color: dark ? blue[500] : blue[800], + }, + } +}) diff --git a/packages/mask/src/utils/theme/useErrorStyles.ts b/packages/mask/src/utils/theme/useErrorStyles.ts new file mode 100644 index 000000000000..23734f2110af --- /dev/null +++ b/packages/mask/src/utils/theme/useErrorStyles.ts @@ -0,0 +1,21 @@ +import { makeStyles } from '@masknet/theme' +import { red } from '@mui/material/colors' + +export const useErrorStyles = makeStyles()((theme) => { + const dark = theme.palette.mode === 'dark' + return { + containedPrimary: { + backgroundColor: dark ? red[500] : red[900], + '&:hover': { + backgroundColor: dark ? red[900] : red[700], + }, + }, + outlinedPrimary: { + borderColor: dark ? red[500] : red[900], + color: dark ? red[500] : red[900], + '&:hover': { + borderColor: dark ? red[900] : red[700], + }, + }, + } +}) diff --git a/packages/mask/src/utils/theme.ts b/packages/mask/src/utils/theme/usePopupsMaskFullPageTheme.ts similarity index 52% rename from packages/mask/src/utils/theme.ts rename to packages/mask/src/utils/theme/usePopupsMaskFullPageTheme.ts index 3baa14bdc2ab..f03e10a0f54a 100644 --- a/packages/mask/src/utils/theme.ts +++ b/packages/mask/src/utils/theme/usePopupsMaskFullPageTheme.ts @@ -1,149 +1,13 @@ -import { ValueRef } from '@dimensiondev/holoflows-kit' -import { useValueRef } from '@masknet/shared-base-ui' -import { SubscriptionFromValueRef } from '@masknet/shared-base' -import { Appearance, or, makeStyles, parseColor } from '@masknet/theme' -import { LanguageOptions, SupportedLanguages } from '@masknet/public-api' -import { PaletteMode, unstable_createMuiStrictModeTheme } from '@mui/material' -import { blue, green, grey, orange, red } from '@mui/material/colors' -import { jaJP, koKR, zhTW, zhCN, enUS, Localization } from '@mui/material/locale/index' -import type { Theme, ThemeOptions } from '@mui/material/styles/createTheme' -import { cloneDeep, merge } from 'lodash-unified' -import { useRef } from 'react' -import { appearanceSettings, languageSettings } from '../settings/settings' -import { activatedSocialNetworkUI } from '../social-network' -import './theme-global.d' -import { Subscription, useSubscription } from 'use-subscription' +import { parseColor } from '@masknet/theme' +import { unstable_createMuiStrictModeTheme } from '@mui/material' import produce, { setAutoFreeze } from 'immer' -import twitterColorSchema from '../social-network-adaptor/twitter.com/customization/twitter-color-schema.json' - -function getFontFamily(monospace?: boolean) { - // We want to look native. - - // Windows has no CJK sans monospace. Accommodate that. - // We only use it for fingerprints anyway so CJK coverage ain't a problem... yet. - const monofont = navigator.platform.startsWith('Win') ? 'Consolas, monospace' : 'monospace' - // https://caniuse.com/font-family-system-ui - // Firefox does NOT support yet it in any form on Windows, but tests indicate that it agrees with Edge in using the UI font for sans-serif: - // Microsoft YaHei on zh-Hans-CN. - return !monospace ? '-apple-system, system-ui, sans-serif' : monofont -} - -const base: ThemeOptions = { - palette: { - primary: { main: '#1c68f3' }, // blue, - secondary: orange, - text: { hint: 'rgba(0, 0, 0, 0.38)' }, - }, - typography: { - fontFamily: getFontFamily(), - }, - breakpoints: { - values: { - xs: 0, - sm: 600, - md: 1112, - lg: 1280, - xl: 1920, - }, - }, - components: { - MuiLink: { defaultProps: { underline: 'hover' } }, - MuiButton: { - styleOverrides: { - root: { - textTransform: 'unset', - minWidth: '100px', - }, - }, - defaultProps: { - size: 'small', - disableElevation: true, - }, - }, - MuiTab: { - styleOverrides: { - root: { - textTransform: 'unset', - padding: '0', - // up-sm - '@media screen and (min-width: 600px)': { - minWidth: 160, - }, - }, - }, - }, - MuiDialog: { - styleOverrides: { - paper: { - borderRadius: '12px', - }, - }, - }, - }, -} - -const lightThemePatch: Partial = { - palette: { - mode: 'light', - }, -} - -const darkThemePatch: Partial = { - palette: { - mode: 'dark', - background: { - paper: grey[900], - }, - }, - components: { - MuiPaper: { - // https://github.com/mui-org/material-ui/pull/25522 - styleOverrides: { root: { backgroundImage: 'unset' } }, - }, - }, -} - -const baseTheme = (theme: 'dark' | 'light') => { - if (theme === 'light') return merge(cloneDeep(base), lightThemePatch) - return merge(cloneDeep(base), darkThemePatch) -} - -// Theme -const MaskLightTheme = unstable_createMuiStrictModeTheme(baseTheme('light')) -const MaskDarkTheme = unstable_createMuiStrictModeTheme(baseTheme('dark')) -const staticSubscription: Subscription = SubscriptionFromValueRef(new ValueRef('light')) -export function useClassicMaskSNSTheme() { - const { current: provider } = useRef( - activatedSocialNetworkUI.customization.paletteMode?.current || staticSubscription, - ) - const { current: usePostTheme = (t: Theme) => t } = useRef(activatedSocialNetworkUI.customization.useTheme) - const palette = useSubscription(provider) - const baseTheme = palette === 'dark' ? MaskDarkTheme : MaskLightTheme - - // TODO: support RTL? - const [localization, isRTL] = useThemeLanguage() - const theme = unstable_createMuiStrictModeTheme(baseTheme, localization) - return usePostTheme(theme) -} -/** - * @deprecated - * - Popups: migrate to \@masknet/theme package - */ -export function useClassicMaskFullPageTheme(overwrite?: ClassicMaskFullPageThemeOptions) { - const userPreference = or(overwrite?.forcePalette, useValueRef(appearanceSettings)) - const systemPreference: PaletteMode = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' - const finalPalette: PaletteMode = userPreference === Appearance.default ? systemPreference : userPreference - - const baseTheme = finalPalette === 'dark' ? MaskDarkTheme : MaskLightTheme - const [localization, isRTL] = useThemeLanguage() - // TODO: support RTL - return unstable_createMuiStrictModeTheme(baseTheme, localization) -} +import twitterColorSchema from '../../social-network-adaptor/twitter.com/customization/twitter-color-schema.json' +import { MaskLightTheme } from './MaskTheme' /** - * Only used in swap pages under popups, will replace it in the future + * @deprecated Should migrate to \@masknet/theme */ -export function usePopupsMaskFullPageTheme() { +export function useSwapPageTheme() { const baseTheme = MaskLightTheme setAutoFreeze(false) @@ -348,60 +212,3 @@ export function usePopupsMaskFullPageTheme() { }) return unstable_createMuiStrictModeTheme(PopupTheme) } - -function useThemeLanguage(): [loc: Localization, RTL: boolean] { - let language = useValueRef(languageSettings) - // TODO: support auto language - if (language === LanguageOptions.__auto__) language = LanguageOptions.enUS - - const displayLanguage = language as any as SupportedLanguages - - const langs: Record = { - [SupportedLanguages.enUS]: enUS, - [SupportedLanguages.jaJP]: jaJP, - [SupportedLanguages.koKR]: koKR, - [SupportedLanguages.zhTW]: zhTW, - [SupportedLanguages.zhCN]: zhCN, - } - return [langs[displayLanguage] || enUS, false] -} - -export interface ClassicMaskFullPageThemeOptions { - forcePalette?: Appearance -} - -export const useColorStyles = makeStyles()((theme: typeof MaskDarkTheme) => { - const dark = theme.palette.mode === 'dark' - return { - error: { - color: dark ? red[500] : red[900], - }, - success: { - color: dark ? green[500] : green[800], - }, - info: { - color: dark ? blue[500] : blue[800], - }, - } -}) -export const useErrorStyles = makeStyles()((theme) => { - const dark = theme.palette.mode === 'dark' - return { - containedPrimary: { - backgroundColor: dark ? red[500] : red[900], - '&:hover': { - backgroundColor: dark ? red[900] : red[700], - }, - }, - outlinedPrimary: { - borderColor: dark ? red[500] : red[900], - color: dark ? red[500] : red[900], - '&:hover': { - borderColor: dark ? red[900] : red[700], - }, - }, - } -}) -export function extendsTheme(extend: (theme: Theme) => ThemeOptions) { - return (theme: Theme) => merge(cloneDeep(theme), extend(theme)) -} diff --git a/packages/mask/src/utils/theme/useThemeLanguage.ts b/packages/mask/src/utils/theme/useThemeLanguage.ts new file mode 100644 index 000000000000..841c44fbcf6e --- /dev/null +++ b/packages/mask/src/utils/theme/useThemeLanguage.ts @@ -0,0 +1,28 @@ +// ! This file is used during SSR. DO NOT import new files that does not work in SSR + +import { LanguageOptions, SupportedLanguages } from '@masknet/public-api' +import { jaJP, koKR, zhTW, zhCN, enUS, Localization } from '@mui/material/locale/index' +import { updateLanguage } from '@masknet/shared-base' +import { startTransition, useEffect } from 'react' + +const langs: Record = { + [SupportedLanguages.enUS]: enUS, + [SupportedLanguages.jaJP]: jaJP, + [SupportedLanguages.koKR]: koKR, + [SupportedLanguages.zhTW]: zhTW, + [SupportedLanguages.zhCN]: zhCN, +} +export function useThemeLanguage(language: LanguageOptions): [loc: Localization, RTL: boolean] { + useEffect(() => { + if (language !== LanguageOptions.__auto__) return + startTransition(() => updateLanguage(language)) + }, [language]) + + if (language === LanguageOptions.__auto__) { + // we've scheduled an update above. + language = LanguageOptions.enUS + } + + const displayLanguage = language as any as SupportedLanguages + return [langs[displayLanguage] || enUS, false] +} diff --git a/packages/plugins/GoPlusSecurity/src/constants.ts b/packages/plugins/GoPlusSecurity/src/constants.ts index 313d7a66f428..4c8011695027 100644 --- a/packages/plugins/GoPlusSecurity/src/constants.ts +++ b/packages/plugins/GoPlusSecurity/src/constants.ts @@ -1,7 +1,6 @@ import { PluginId } from '@masknet/plugin-infra' export const PLUGIN_ID = PluginId.GoPlusSecurity -export const PLUGIN_META_KEY = `${PluginId.GoPlusSecurity}:1` export const PLUGIN_DESCRIPTION = 'Go+ Security Engine' export const PLUGIN_NAME = 'GoPlusSecurity' export const PLUGIN_OFFICIAL_WEBSITE = 'https://gopluslabs.io' diff --git a/packages/shared/src/constants.tsx b/packages/shared/src/constants.tsx index 03e0e05d3d99..37e1655d5a44 100644 --- a/packages/shared/src/constants.tsx +++ b/packages/shared/src/constants.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react' import { FacebookColoredIcon, InstagramColoredIcon, @@ -13,7 +12,8 @@ export const TWITTER_ID = 'twitter.com' export const INSTAGRAM_ID = 'instagram.com' export const OPENSEA_ID = 'opensea.io' -export const SOCIAL_MEDIA_ICON_MAPPING: Record = { +// When you add a new icon, don't forget to add it in packages/mask/src/extension/popups/SSR-server.tsx +export const SOCIAL_MEDIA_ICON_MAPPING: Record = { [TWITTER_ID]: , [FACEBOOK_ID]: , [MINDS_ID]: , diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 3af80710f206..2764905d6304 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -47,6 +47,7 @@ export * from './UIHelper/custom-ui-helper' export * from './CSSVariableInjector' export { getMaskColor, useMaskColor, MaskColorVar, applyMaskColorVars } from './constants' export type { MaskCSSVariableColor } from './constants' +export { TssCacheProvider } from 'tss-react' const query = '(prefers-color-scheme: dark)' export function useSystemPreferencePalette(): PaletteMode { diff --git a/patches/@emotion+cache+11.7.1.patch b/patches/@emotion+cache+11.7.1.patch new file mode 100644 index 000000000000..cf1d0874d8b7 --- /dev/null +++ b/patches/@emotion+cache+11.7.1.patch @@ -0,0 +1,23 @@ +# generated by patch-package 6.5.0 on 2022-03-31 20:04:11 +# +# command: +# npx patch-package @emotion/cache --exclude nothing +# +# declared package: +# @emotion/cache: 11.7.1 +# +diff --git a/node_modules/@emotion/cache/package.json b/node_modules/@emotion/cache/package.json +index 437f0bf..c74881a 100644 +--- a/node_modules/@emotion/cache/package.json ++++ b/node_modules/@emotion/cache/package.json +@@ -4,10 +4,6 @@ + "description": "emotion's cache", + "main": "dist/emotion-cache.cjs.js", + "module": "dist/emotion-cache.esm.js", +- "browser": { +- "./dist/emotion-cache.cjs.js": "./dist/emotion-cache.browser.cjs.js", +- "./dist/emotion-cache.esm.js": "./dist/emotion-cache.browser.esm.js" +- }, + "types": "types/index.d.ts", + "license": "MIT", + "repository": "https://github.com/emotion-js/emotion/tree/main/packages/cache", diff --git a/patches/@emotion+react+11.8.2.patch b/patches/@emotion+react+11.8.2.patch new file mode 100644 index 000000000000..15bbdbd43a85 --- /dev/null +++ b/patches/@emotion+react+11.8.2.patch @@ -0,0 +1,23 @@ +# generated by patch-package 6.5.0 on 2022-03-31 20:04:21 +# +# command: +# npx patch-package @emotion/react --exclude nothing +# +# declared package: +# @emotion/react: 11.8.2 +# +diff --git a/node_modules/@emotion/react/package.json b/node_modules/@emotion/react/package.json +index 22fba2d..fafe561 100644 +--- a/node_modules/@emotion/react/package.json ++++ b/node_modules/@emotion/react/package.json +@@ -3,10 +3,6 @@ + "version": "11.8.2", + "main": "dist/emotion-react.cjs.js", + "module": "dist/emotion-react.esm.js", +- "browser": { +- "./dist/emotion-react.cjs.js": "./dist/emotion-react.browser.cjs.js", +- "./dist/emotion-react.esm.js": "./dist/emotion-react.browser.esm.js" +- }, + "types": "types/index.d.ts", + "files": [ + "src", diff --git a/patches/@emotion+serialize+1.0.2.patch b/patches/@emotion+serialize+1.0.2.patch new file mode 100644 index 000000000000..593b0577dd1c --- /dev/null +++ b/patches/@emotion+serialize+1.0.2.patch @@ -0,0 +1,23 @@ +# generated by patch-package 6.5.0 on 2022-03-31 20:04:26 +# +# command: +# npx patch-package @emotion/serialize --exclude nothing +# +# declared package: +# @emotion/serialize: 1.0.2 +# +diff --git a/node_modules/@emotion/serialize/package.json b/node_modules/@emotion/serialize/package.json +index ef39314..15044d3 100644 +--- a/node_modules/@emotion/serialize/package.json ++++ b/node_modules/@emotion/serialize/package.json +@@ -27,9 +27,5 @@ + "src", + "dist", + "types/*.d.ts" +- ], +- "browser": { +- "./dist/emotion-serialize.cjs.js": "./dist/emotion-serialize.browser.cjs.js", +- "./dist/emotion-serialize.esm.js": "./dist/emotion-serialize.browser.esm.js" +- } ++ ] + } diff --git a/patches/@emotion+server+11.4.0.patch b/patches/@emotion+server+11.4.0.patch new file mode 100644 index 000000000000..c6165591f537 --- /dev/null +++ b/patches/@emotion+server+11.4.0.patch @@ -0,0 +1,22 @@ +# generated by patch-package 6.5.0 on 2022-03-31 20:04:34 +# +# command: +# npx patch-package @emotion/server --exclude nothing +# +# declared package: +# @emotion/server: 11.4.0 +# +diff --git a/node_modules/@emotion/server/package.json b/node_modules/@emotion/server/package.json +index fdb6ec6..26c61fd 100644 +--- a/node_modules/@emotion/server/package.json ++++ b/node_modules/@emotion/server/package.json +@@ -50,9 +50,6 @@ + "bugs": { + "url": "https://github.com/emotion-js/emotion/issues" + }, +- "browser": { +- "./dist/emotion-server.cjs.js": "./dist/emotion-server.browser.cjs.js" +- }, + "preconstruct": { + "entrypoints": [ + "./index.js", diff --git a/patches/@emotion+styled+11.8.1.patch b/patches/@emotion+styled+11.8.1.patch new file mode 100644 index 000000000000..ccbd4e348950 --- /dev/null +++ b/patches/@emotion+styled+11.8.1.patch @@ -0,0 +1,38 @@ +# generated by patch-package 6.5.0 on 2022-04-01 16:22:25 +# +# command: +# npx patch-package @emotion/styled --exclude nothing +# +# declared package: +# @emotion/styled: 11.8.1 +# +diff --git a/node_modules/@emotion/styled/base/package.json b/node_modules/@emotion/styled/base/package.json +index 96f433f..bd1ec82 100644 +--- a/node_modules/@emotion/styled/base/package.json ++++ b/node_modules/@emotion/styled/base/package.json +@@ -2,10 +2,6 @@ + "main": "dist/emotion-styled-base.cjs.js", + "module": "dist/emotion-styled-base.esm.js", + "umd:main": "dist/emotion-styled-base.umd.min.js", +- "browser": { +- "./dist/emotion-styled-base.cjs.js": "./dist/emotion-styled-base.browser.cjs.js", +- "./dist/emotion-styled-base.esm.js": "./dist/emotion-styled-base.browser.esm.js" +- }, + "types": "../types/base", + "preconstruct": { + "umdName": "emotionStyledBase" +diff --git a/node_modules/@emotion/styled/package.json b/node_modules/@emotion/styled/package.json +index fec82fb..a18ff0e 100644 +--- a/node_modules/@emotion/styled/package.json ++++ b/node_modules/@emotion/styled/package.json +@@ -49,10 +49,6 @@ + "macro.js.flow" + ], + "umd:main": "dist/emotion-styled.umd.min.js", +- "browser": { +- "./dist/emotion-styled.cjs.js": "./dist/emotion-styled.browser.cjs.js", +- "./dist/emotion-styled.esm.js": "./dist/emotion-styled.browser.esm.js" +- }, + "preconstruct": { + "umdName": "emotionStyled", + "entrypoints": [ diff --git a/patches/@emotion+utils+1.1.0.patch b/patches/@emotion+utils+1.1.0.patch new file mode 100644 index 000000000000..aa458cbe8864 --- /dev/null +++ b/patches/@emotion+utils+1.1.0.patch @@ -0,0 +1,23 @@ +# generated by patch-package 6.5.0 on 2022-03-31 20:04:44 +# +# command: +# npx patch-package @emotion/utils --exclude nothing +# +# declared package: +# @emotion/utils: 1.1.0 +# +diff --git a/node_modules/@emotion/utils/package.json b/node_modules/@emotion/utils/package.json +index d661529..e5738e8 100644 +--- a/node_modules/@emotion/utils/package.json ++++ b/node_modules/@emotion/utils/package.json +@@ -4,10 +4,6 @@ + "description": "internal utils for emotion", + "main": "dist/emotion-utils.cjs.js", + "module": "dist/emotion-utils.esm.js", +- "browser": { +- "./dist/emotion-utils.cjs.js": "./dist/emotion-utils.browser.cjs.js", +- "./dist/emotion-utils.esm.js": "./dist/emotion-utils.browser.esm.js" +- }, + "types": "types/index.d.ts", + "license": "MIT", + "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ce135339edc..1108efc7dc44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,11 +14,12 @@ importers: '@dimensiondev/eslint-plugin': ^0.0.1-20220117062517-fd3cb01 '@dimensiondev/kit': 0.0.0-20220228054820-f2378be '@dimensiondev/patch-package': ^6.5.0 - '@emotion/cache': ^11.7.1 - '@emotion/react': ^11.8.2 - '@emotion/serialize': ^1.0.2 - '@emotion/styled': ^11.8.1 - '@emotion/utils': ^1.1.0 + '@emotion/cache': 11.7.1 + '@emotion/react': 11.8.2 + '@emotion/serialize': 1.0.2 + '@emotion/server': 11.4.0 + '@emotion/styled': 11.8.1 + '@emotion/utils': 1.1.0 '@jest/globals': ^28.0.0-alpha.3 '@magic-works/i18n-codegen': ^0.1.0 '@masknet/cli': workspace:* @@ -64,6 +65,7 @@ importers: '@emotion/cache': 11.7.1 '@emotion/react': 11.8.2_00a11a054056e3746b551a83b9229751 '@emotion/serialize': 1.0.2 + '@emotion/server': 11.4.0 '@emotion/styled': 11.8.1_99e6c630d8f9e4aaf14c79d0138a2fa8 '@emotion/utils': 1.1.0 '@mui/icons-material': 5.5.1_25c2be9db5642b081e6658d3ddfb5f23 @@ -5270,7 +5272,7 @@ packages: dependencies: '@babel/helper-module-imports': 7.16.7 '@babel/plugin-syntax-jsx': 7.16.7 - '@babel/runtime': 7.17.2 + '@babel/runtime': 7.17.8 '@emotion/hash': 0.8.0 '@emotion/memoize': 0.7.5 '@emotion/serialize': 1.0.2 @@ -5358,7 +5360,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.17.2 + '@babel/runtime': 7.17.8 '@emotion/babel-plugin': 11.7.2 '@emotion/cache': 11.7.1 '@emotion/serialize': 1.0.2 @@ -5397,7 +5399,7 @@ packages: '@emotion/css': optional: true dependencies: - '@emotion/utils': 1.0.0 + '@emotion/utils': 1.1.0 html-tokenize: 2.0.1 multipipe: 1.0.2 through: 2.3.8 @@ -5450,7 +5452,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.17.2 + '@babel/runtime': 7.17.8 '@emotion/babel-plugin': 11.7.2 '@emotion/is-prop-valid': 1.1.2 '@emotion/react': 11.8.2_00a11a054056e3746b551a83b9229751 @@ -5471,10 +5473,6 @@ packages: resolution: {integrity: sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==} dev: true - /@emotion/utils/1.0.0: - resolution: {integrity: sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==} - dev: false - /@emotion/utils/1.1.0: resolution: {integrity: sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==} dev: false @@ -11760,7 +11758,7 @@ packages: /babel-plugin-macros/2.8.0: resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} dependencies: - '@babel/runtime': 7.17.2 + '@babel/runtime': 7.17.8 cosmiconfig: 6.0.0 resolve: 1.22.0 @@ -17813,7 +17811,7 @@ packages: dependencies: buffer-from: 0.1.2 inherits: 2.0.4 - minimist: 1.2.5 + minimist: 1.2.6 readable-stream: 1.0.34 through2: 0.4.2 dev: false @@ -19789,7 +19787,7 @@ packages: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true dependencies: - minimist: 1.2.5 + minimist: 1.2.6 /json5/2.2.0: resolution: {integrity: sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==} @@ -20812,7 +20810,6 @@ packages: /minimist/1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} - dev: false /minipass-collect/1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} @@ -25644,7 +25641,7 @@ packages: dependencies: '@types/json5': 0.0.29 json5: 1.0.1 - minimist: 1.2.5 + minimist: 1.2.6 strip-bom: 3.0.0 dev: true