diff --git a/packages/dashboard/src/initialization/PluginHost.ts b/packages/dashboard/src/initialization/PluginHost.ts index b3f0bdd3d8dd..d3a57c2072e9 100644 --- a/packages/dashboard/src/initialization/PluginHost.ts +++ b/packages/dashboard/src/initialization/PluginHost.ts @@ -9,9 +9,15 @@ import { InMemoryStorages, PersistentStorages } from '../utils/kv-storage' const PluginHost: Plugin.__Host.Host = { enabled: { + // Due to MASK-391, we don't have a user configurable "disabled" plugin. + // All plugins are always loaded but it might be displayed in the invisible mode. + isEnabled: () => true, + events: new Emitter(), + }, + minimalMode: { events: new Emitter(), isEnabled: (id) => { - return Services.Settings.getPluginEnabled(id) + return Services.Settings.getPluginMinimalModeEnabled(id) }, }, addI18NResource(plugin, resource) { @@ -27,7 +33,8 @@ const PluginHost: Plugin.__Host.Host = { }, } setTimeout(() => { - Messages.events.pluginEnabled.on((id) => PluginHost.enabled.events.emit('enabled', id)) - Messages.events.pluginDisabled.on((id) => PluginHost.enabled.events.emit('disabled', id)) + Messages.events.pluginMinimalModeChanged.on(([id, status]) => { + PluginHost.minimalMode.events.emit(status ? 'enabled' : 'disabled', id) + }) startPluginDashboard(PluginHost) }) diff --git a/packages/dashboard/src/pages/Labs/components/PluginItem.tsx b/packages/dashboard/src/pages/Labs/components/PluginItem.tsx index acd79bfde7c9..1585a54e15ca 100644 --- a/packages/dashboard/src/pages/Labs/components/PluginItem.tsx +++ b/packages/dashboard/src/pages/Labs/components/PluginItem.tsx @@ -58,6 +58,7 @@ export interface PluginItemProps { desc: string icon?: ReactNode enabled?: boolean + hideSwitch?: boolean onSwitch: (id: string, checked: boolean) => void onTwitter?: (id: string) => void onFacebook?: (id: string) => void @@ -72,7 +73,20 @@ export function PluginItemPlaceholder() { } export default function PluginItem(props: PluginItemProps) { - const { id, title, desc, icon, enabled, onSwitch, onTwitter, onFacebook, onExplore, onSetting, onTutorial } = props + const { + id, + title, + desc, + icon, + enabled = false, + hideSwitch, + onSwitch, + onTwitter, + onFacebook, + onExplore, + onSetting, + onTutorial, + } = props const { classes } = useStyles() return ( @@ -98,7 +112,7 @@ export default function PluginItem(props: PluginItemProps) { {onExplore ? onExplore(id)} /> : null} ) : null} - {id ? ( + {!hideSwitch ? ( + Messages.events.pluginMinimalModeChanged.on(([id, newValue]) => + setPluginStatus({ ...pluginStatus, [id]: newValue }), + ), + [pluginStatus], + ) + const plugins = [ { id: PluginId.RedPacket, @@ -107,6 +115,7 @@ export default function Plugins() { icon: , enabled: pluginStatus[PluginId.Trader], setting: true, + hideSwitch: true, }, { id: PluginId.Transak, @@ -114,6 +123,7 @@ export default function Plugins() { desc: t.labs_transak_desc(), icon: , enabled: pluginStatus[PluginId.Transak], + hideSwitch: true, }, { id: PluginId.Collectible, @@ -189,7 +199,7 @@ export default function Plugins() { const { openDialog: openSwapDialog } = useRemoteControlledDialog(PluginMessages.Swap.swapDialogUpdated) async function onSwitch(id: string, checked: boolean) { - await Services.Settings.setPluginEnabled(id, checked) + await Services.Settings.setPluginMinimalModeEnabled(id, !checked) setPluginStatus({ ...pluginStatus, [id]: checked }) } @@ -215,7 +225,7 @@ export default function Plugins() { useEffect(() => { Object.values(PluginId).forEach(async (id) => { - const enabled = await Services.Settings.getPluginEnabled(id) + const enabled = await Services.Settings.getPluginMinimalModeEnabled(id) setPluginStatus((status) => ({ ...status, [id]: enabled })) }) }, []) @@ -247,6 +257,7 @@ export default function Plugins() { onSwitch={onSwitch} onTutorial={onTutorial} onSetting={p.setting ? onSetting : undefined} + hideSwitch={p.hideSwitch} /> ))} diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 67ff71baf5ee..2cd1d5681ae9 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -91,6 +91,8 @@ "user_guide_tip_2": "Click here to have a quick start.", "create_persona": "Create persona", "connect_persona": "Connect persona", + "please_create_persona": "Please create persona", + "please_connect_persona": "Please connect persona", "mask_network": "Mask Network", "import": "Import", "no_search_result": "No result", @@ -174,6 +176,7 @@ "wallet_search_no_result": "No results.", "wallet_confirm_with_password": "Confirm with password", "wallet_airdrop_nft_unclaimed_title": "NFT Airdrop Unclaimed:", + "plugin_not_enabled": "{{plugin}} (Not Enabled)", "plugin_external_unknown_plugin": "New unknown Mask plugins found. Do you want to load them?", "plugin_external_loader_search_holder": "Search for an external plugin", "plugin_external_loader_search_button": "Search for plugin", diff --git a/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx b/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx index c3d7855e8575..6e3216f8ecb8 100644 --- a/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx +++ b/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx @@ -18,7 +18,7 @@ export interface BadgeRendererProps { } export function BadgeRenderer({ meta, onDeleteMeta, readonly }: BadgeRendererProps) { - const plugins = useActivatedPluginsSNSAdaptor() + const plugins = useActivatedPluginsSNSAdaptor('any') const i18n = usePluginI18NField() const { t } = useI18N() if (!meta) return null diff --git a/packages/mask/src/components/CompositionDialog/PluginEntryRender.tsx b/packages/mask/src/components/CompositionDialog/PluginEntryRender.tsx index ea2f7bf2cbb2..4ec9176689ec 100644 --- a/packages/mask/src/components/CompositionDialog/PluginEntryRender.tsx +++ b/packages/mask/src/components/CompositionDialog/PluginEntryRender.tsx @@ -29,7 +29,7 @@ export const PluginEntryRender = memo( const chainId = useChainId() const pluginID = usePluginIDContext() const operatingSupportedChainMapping = useActivatedPluginSNSAdaptor_Web3Supported(chainId, pluginID) - const result = [...useActivatedPluginsSNSAdaptor()] + const result = [...useActivatedPluginsSNSAdaptor('any')] .sort((plugin) => { // TODO: support priority order if (plugin.ID === RedPacketPluginID || plugin.ID === ITO_PluginID) return -1 diff --git a/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts b/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts new file mode 100644 index 000000000000..82f9f49f67b9 --- /dev/null +++ b/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts @@ -0,0 +1,39 @@ +import { DashboardRoutes, ProfileIdentifier } from '@masknet/shared-base' +import stringify from 'json-stable-stringify' +import { useMemo } from 'react' +import Services from '../../extension/service' +import { currentSetupGuideStatus } from '../../settings/settings' +import { activatedSocialNetworkUI } from '../../social-network' +import { SetupGuideStep } from '../InjectedComponents/SetupGuide' +import { useLastRecognizedIdentity } from './useActivatedUI' +import { useMyPersonas } from './useMyPersonas' + +const createPersona = () => { + Services.Welcome.openOptionsPage(DashboardRoutes.Setup) +} + +const connectPersona = async () => { + const currentPersonaIdentifier = await Services.Settings.getCurrentPersonaIdentifier() + currentSetupGuideStatus[activatedSocialNetworkUI.networkIdentifier].value = stringify({ + status: SetupGuideStep.FindUsername, + persona: currentPersonaIdentifier?.toText(), + }) +} + +export function usePersonaConnectStatus() { + const personas = useMyPersonas() + const lastRecognized = useLastRecognizedIdentity() + + return useMemo(() => { + const id = new ProfileIdentifier(activatedSocialNetworkUI.networkIdentifier, lastRecognized.identifier.userId) + let connected = false + personas.forEach((p) => { + p.identifier + if (p.linkedProfiles.get(id)) { + connected = true + } + }) + const action = !personas.length ? createPersona : !connected ? connectPersona : null + return { connected, action, hasPersona: !!personas.length } + }, [personas, lastRecognized, activatedSocialNetworkUI]) +} diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx index 986db2cb87bc..0c59454c81f9 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptPostFailed.tsx @@ -1,11 +1,12 @@ import { memo } from 'react' import { useI18N } from '../../../utils' import { AdditionalContent, AdditionalContentProps } from '../AdditionalPostContent' -import { NotSetupYetPrompt } from '../../shared/NotSetupYetPrompt' import type { BannerProps } from '../../Welcomes/Banner' import { DecryptFailedReason } from '../../../utils/constants' import type { ProfileIdentifier } from '@masknet/shared-base' import { wrapAuthorDifferentMessage } from './authorDifferentMessage' +import MaskPluginWrapper from '../../../plugins/MaskPluginWrapper' + export interface DecryptPostFailedProps { error: Error AdditionalContentProps?: Partial @@ -16,10 +17,12 @@ export interface DecryptPostFailedProps { postedBy?: ProfileIdentifier } export const DecryptPostFailed = memo(function DecryptPostFailed(props: DecryptPostFailedProps) { - const { AdditionalContentProps, NotSetupYetPromptProps, author, postedBy, error } = props + const { AdditionalContentProps, author, postedBy, error } = props const { t } = useI18N() - if (error?.message === DecryptFailedReason.MyCryptoKeyNotFound) - return + + if (error?.message === DecryptFailedReason.MyCryptoKeyNotFound) { + return + } return ( x.DecryptedInspector) +const PluginRenderer = createInjectHooksRenderer( + useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, + (x) => x.DecryptedInspector, +) +function PluginRendererWithSuggestion(props: MetadataRendererProps) { + const a = useDisabledPluginSuggestionFromMeta(props.metadata || new Map()) + const b = useDisabledPluginSuggestionFromPost(extractTextFromTypedMessage(props.message), []) + + const suggest = Array.from(new Set(a.concat(b))) + return ( + <> + + + + ) +} export interface DecryptPostSuccessProps extends withClasses { data: { content: TypedMessage } requestAppendRecipients?(to: Profile[]): Promise @@ -55,7 +77,7 @@ export const DecryptPostSuccess = memo(function DecryptPostSuccess(props: Decryp <> {shareMenu.ShareMenu} x.ID)) + const minimalMode = new Set(useActivatedPluginsSNSAdaptor(true).map((x) => x.ID)) + const disabledPlugins = [...registeredPlugins].filter((x) => !activated.has(x.ID) || minimalMode.has(x.ID)) + return disabledPlugins +} + +export function useDisabledPluginSuggestionFromPost(postContent: Result, metaLinks: string[]) { + const disabled = useDisabledPlugins().filter((x) => x.contribution?.postContent) + + const { ok, val } = postContent + const matches = disabled.filter((x) => { + for (const pattern of x.contribution!.postContent!) { + if (ok && val.match(pattern)) return true + if (metaLinks.some((link) => link.match(pattern))) return true + } + return false + }) + return matches +} + +export function useDisabledPluginSuggestionFromMeta(meta: ReadonlyMap) { + const disabled = useDisabledPlugins().filter((x) => x.contribution?.metadataKeys) + const keys = [...meta.keys()] + + const matches = disabled.filter((x) => { + const contributes = x.contribution!.metadataKeys! + return keys.some((key) => contributes.has(key)) + }) + return matches +} + +export function PossiblePluginSuggestionPostInspector() { + const message = extractTextFromTypedMessage(usePostInfoDetails.rawMessage()) + const metaLinks = usePostInfoDetails.postMetadataMentionedLinks().concat(usePostInfoDetails.mentionedLinks()) + const matches = useDisabledPluginSuggestionFromPost(message, metaLinks) + return +} +export function PossiblePluginSuggestionUI(props: { plugins: Plugin.DeferredDefinition[] }) { + const { t } = useI18N() + const { plugins } = props + if (!plugins.length) return null + return ( + <> + {plugins.map((x) => ( + Services.Settings.setPluginMinimalModeEnabled(x.ID, false)} + /> + } + /> + ))} + + ) +} diff --git a/packages/mask/src/components/InjectedComponents/PageInspector.tsx b/packages/mask/src/components/InjectedComponents/PageInspector.tsx index 6174af5e1bb7..df1da63ed3c3 100644 --- a/packages/mask/src/components/InjectedComponents/PageInspector.tsx +++ b/packages/mask/src/components/InjectedComponents/PageInspector.tsx @@ -5,7 +5,10 @@ import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@maskn import { useMatchXS, MaskMessages, useI18N } from '../../utils' import { useAutoPasteFailedDialog } from './AutoPasteFailedDialog' -const PluginRender = createInjectHooksRenderer(useActivatedPluginsSNSAdaptor, (x) => x.GlobalInjection) +const PluginRender = createInjectHooksRenderer( + useActivatedPluginsSNSAdaptor.visibility.useAnyMode, + (x) => x.GlobalInjection, +) export interface PageInspectorProps {} diff --git a/packages/mask/src/components/InjectedComponents/PostInspector.tsx b/packages/mask/src/components/InjectedComponents/PostInspector.tsx index 2625f9f6543d..0c12711e0ff1 100644 --- a/packages/mask/src/components/InjectedComponents/PostInspector.tsx +++ b/packages/mask/src/components/InjectedComponents/PostInspector.tsx @@ -17,8 +17,12 @@ import { DebugList } from '../DebugModeUI/DebugList' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { decodePublicKeyUI } from '../../social-network/utils/text-payload-ui' import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' +import { PossiblePluginSuggestionPostInspector } from './DisabledPluginSuggestion' -const PluginHooksRenderer = createInjectHooksRenderer(useActivatedPluginsSNSAdaptor, (plugin) => plugin.PostInspector) +const PluginHooksRenderer = createInjectHooksRenderer( + useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, + (plugin) => plugin.PostInspector, +) export interface PostInspectorProps { onDecrypted(post: TypedMessageTuple): void @@ -119,6 +123,7 @@ export function PostInspector(props: PostInspectorProps) { ) : null} {props.slotPosition !== 'after' && slot} {x} + {debugInfo} {props.slotPosition !== 'before' && slot} diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index fe508e876bb3..2d3145494b9a 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -29,7 +29,7 @@ export function PostReplacer(props: PostReplacerProps) { const postPayload = usePostInfoDetails.containingMaskPayload() const allPostReplacement = useValueRef(allPostReplacementSettings) - const plugins = useActivatedPluginsSNSAdaptor() + const plugins = useActivatedPluginsSNSAdaptor(false) const processedPostMessage = useMemo( () => plugins.reduce((x, plugin) => { diff --git a/packages/mask/src/components/InjectedComponents/ProfileSlider.tsx b/packages/mask/src/components/InjectedComponents/ProfileSlider.tsx index 82988c29e0d4..5d0529635dfd 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileSlider.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileSlider.tsx @@ -1,6 +1,6 @@ import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -const PluginRenderer = createInjectHooksRenderer(useActivatedPluginsSNSAdaptor, (x) => { +const PluginRenderer = createInjectHooksRenderer(useActivatedPluginsSNSAdaptor.visibility.useAnyMode, (x) => { return () => { return
Profile Slider
} diff --git a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx index e41aa86b1f5b..1fef1f56d258 100644 --- a/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx +++ b/packages/mask/src/components/InjectedComponents/ProfileTabContent.tsx @@ -11,7 +11,7 @@ import { MaskMessages, useI18N } from '../../utils' import { useCurrentVisitingIdentity } from '../DataSource/useActivatedUI' function getTabContent(tabId: string) { - return createInjectHooksRenderer(useActivatedPluginsSNSAdaptor, (x) => { + return createInjectHooksRenderer(useActivatedPluginsSNSAdaptor.visibility.useAnyMode, (x) => { const tab = x.ProfileTabs?.find((x) => x.ID === tabId) if (!tab) return return tab.UI?.TabContent @@ -48,7 +48,7 @@ export function ProfileTabContent(props: ProfileTabContentProps) { const identity = useCurrentVisitingIdentity() const { value: addressNames, loading: loadingAddressNames } = useAddressNames(identity) - const tabs = useActivatedPluginsSNSAdaptor() + const tabs = useActivatedPluginsSNSAdaptor('any') .flatMap((x) => x.ProfileTabs?.map((y) => ({ ...y, pluginID: x.ID })) ?? []) .filter((z) => z.Utils?.shouldDisplay?.(identity, addressNames) ?? true) .sort((a, z) => { diff --git a/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx b/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx index 11437515e38b..0ca0f3934593 100644 --- a/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx +++ b/packages/mask/src/components/InjectedComponents/SearchResultBox.tsx @@ -1,6 +1,9 @@ import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -const PluginRenderer = createInjectHooksRenderer(useActivatedPluginsSNSAdaptor, (x) => x.SearchResultBox) +const PluginRenderer = createInjectHooksRenderer( + useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, + (x) => x.SearchResultBox, +) export interface SearchResultBoxProps {} diff --git a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx index 38d2e0645632..399897911df2 100644 --- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx +++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx @@ -24,7 +24,6 @@ import { } from '@masknet/plugin-infra' import { useCallback, useMemo } from 'react' import { useRemoteControlledDialog, WalletIcon } from '@masknet/shared' -import { ProfileIdentifier, DashboardRoutes } from '@masknet/shared-base' import { WalletMessages } from '../../plugins/Wallet/messages' import { hasNativeAPI, nativeAPI, useI18N } from '../../utils' import { useRecentTransactions } from '../../plugins/Wallet/hooks/useRecentTransactions' @@ -32,13 +31,7 @@ import GuideStep from '../GuideStep' import { MaskFilledIcon } from '../../resources/MaskIcon' import { makeStyles } from '@masknet/theme' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' -import { useMyPersonas } from '../DataSource/useMyPersonas' -import { useLastRecognizedIdentity } from '../DataSource/useActivatedUI' -import { activatedSocialNetworkUI } from '../../social-network' -import { Services } from '../../extension/service' -import { currentSetupGuideStatus } from '../../settings/settings' -import { SetupGuideStep } from './SetupGuide' -import stringify from 'json-stable-stringify' +import { usePersonaConnectStatus } from '../DataSource/usePersonaConnectStatus' const useStyles = makeStyles()((theme) => ({ font: { @@ -102,37 +95,18 @@ export function ToolboxHintUnstyled(props: ToolboxHintProps) { const networkDescriptor = useNetworkDescriptor() const providerDescriptor = useProviderDescriptor() - - const personas = useMyPersonas() - const lastRecognized = useLastRecognizedIdentity() - - const personaConnected = useMemo(() => { - const id = new ProfileIdentifier(activatedSocialNetworkUI.networkIdentifier, lastRecognized.identifier.userId) - let connected = false - personas.forEach((p) => { - if (p.linkedProfiles.get(id)) { - connected = true - } - }) - return connected - }, [personas, lastRecognized, activatedSocialNetworkUI]) + const personaConnectStatus = usePersonaConnectStatus() const title = useMemo(() => { - return !personas.length ? t('create_persona') : !personaConnected ? t('connect_persona') : walletTitle - }, [personas, personaConnected, walletTitle, t]) - - const onClick = async () => { - if (!personas.length) { - Services.Welcome.openOptionsPage(DashboardRoutes.Setup) - } else if (!personaConnected) { - const currentPersona = await Services.Settings.getCurrentPersonaIdentifier() - currentSetupGuideStatus[activatedSocialNetworkUI.networkIdentifier].value = stringify({ - status: SetupGuideStep.FindUsername, - persona: currentPersona?.toText(), - }) - } else { - openWallet() - } + return !personaConnectStatus.hasPersona + ? t('create_persona') + : !personaConnectStatus.connected + ? t('connect_persona') + : walletTitle + }, [personaConnectStatus, walletTitle, t]) + + const onClick = () => { + personaConnectStatus.action ? personaConnectStatus.action() : openWallet() } return ( diff --git a/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx b/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx index 8d25cfa9dd27..9980572d87b9 100644 --- a/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ b/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx @@ -21,7 +21,7 @@ import { deconstructPayload } from '../../utils/type-transform/Payload' import { PayloadReplacer } from './PayloadReplacer' import { useI18N } from '../../utils' -interface MetadataRendererProps { +export interface MetadataRendererProps { metadata: TypedMessage['meta'] message: TypedMessage } diff --git a/packages/mask/src/extension/background-script/SettingsService.ts b/packages/mask/src/extension/background-script/SettingsService.ts index 115fd5dfc3fc..b8790de613b1 100644 --- a/packages/mask/src/extension/background-script/SettingsService.ts +++ b/packages/mask/src/extension/background-script/SettingsService.ts @@ -5,7 +5,7 @@ import { appearanceSettings, currentPersonaIdentifier, languageSettings, - currentPluginEnabledStatus, + currentPluginMinimalModeNOTEnabled, pluginIDSettings, } from '../../settings/settings' import { @@ -36,7 +36,7 @@ import { currentMaskWalletNetworkSettings, currentBalancesSettings, } from '../../plugins/Wallet/settings' -import { Flags } from '../../../shared' +import { Flags, MaskMessages } from '../../../shared' import { indexedDB_KVStorageBackend, inMemory_KVStorageBackend } from '../../../background/database/kv-storage' function create(settings: InternalSettings) { @@ -126,11 +126,13 @@ export async function setCurrentPersonaIdentifier(x: PersonaIdentifier) { await currentPersonaIdentifier.readyPromise currentPersonaIdentifier.value = x.toText() } -export async function getPluginEnabled(id: string) { - return currentPluginEnabledStatus['plugin:' + id].value +export async function getPluginMinimalModeEnabled(id: string) { + return !currentPluginMinimalModeNOTEnabled['plugin:' + id].value } -export async function setPluginEnabled(id: string, enabled: boolean) { - currentPluginEnabledStatus['plugin:' + id].value = enabled +export async function setPluginMinimalModeEnabled(id: string, enabled: boolean) { + currentPluginMinimalModeNOTEnabled['plugin:' + id].value = !enabled + + MaskMessages.events.pluginMinimalModeChanged.sendToAll([id, enabled]) } export async function openTab(url: string) { diff --git a/packages/mask/src/plugin-infra/host.ts b/packages/mask/src/plugin-infra/host.ts index 287e271576d8..d4dc1dd8b308 100644 --- a/packages/mask/src/plugin-infra/host.ts +++ b/packages/mask/src/plugin-infra/host.ts @@ -3,41 +3,34 @@ import './register' import type { Plugin } from '@masknet/plugin-infra' import { Emitter } from '@servie/events' -import { currentPluginEnabledStatus } from '../settings/settings' -import { isEnvironment, Environment } from '@dimensiondev/holoflows-kit' // Do not export from '../utils/' to prevent initialization failure import { MaskMessages } from '../utils/messages' import i18nNextInstance from '../../shared-ui/locales_legacy' +import Services from '../extension/service' import { createI18NBundle } from '@masknet/shared-base' export function createPluginHost( signal: AbortSignal | undefined, createContext: (plugin: string, signal: AbortSignal) => Context, ): Plugin.__Host.Host { - const listening = new Set() - const enabled: Plugin.__Host.EnabledStatusReporter = { - isEnabled: (id) => { - const status = currentPluginEnabledStatus['plugin:' + id] - if (!listening.has(id)) { - listening.add(id) - const undo = status.addListener((newVal) => enabled.events.emit(newVal ? 'enabled' : 'disabled', id)) - signal?.addEventListener('abort', undo) - - // TODO: move it elsewhere. - if (isEnvironment(Environment.ManifestBackground)) { - status.addListener((newVal) => { - if (newVal) MaskMessages.events.pluginEnabled.sendToAll(id) - else MaskMessages.events.pluginDisabled.sendToAll(id) - }) - } - } - return status.value - }, + const minimalMode: Plugin.__Host.EnabledStatusReporter = { + isEnabled: Services.Settings.getPluginMinimalModeEnabled, events: new Emitter(), } + const removeListener = MaskMessages.events.pluginMinimalModeChanged.on(([id, val]) => + minimalMode.events.emit(val ? 'enabled' : 'disabled', id), + ) + signal?.addEventListener('abort', removeListener) + return { signal, - enabled, + // Due to MASK-391, we don't have a user configurable "disabled" plugin. + // All plugins are always loaded but it might be displayed in the summary mode. + enabled: { + events: new Emitter(), + isEnabled: () => true, + }, + minimalMode, addI18NResource(plugin, resource) { createI18NBundle(plugin, resource)(i18nNextInstance) }, diff --git a/packages/mask/src/plugins/Collectible/base.ts b/packages/mask/src/plugins/Collectible/base.ts index 1e8c8306cc0f..a63f7ded5c20 100644 --- a/packages/mask/src/plugins/Collectible/base.ts +++ b/packages/mask/src/plugins/Collectible/base.ts @@ -12,4 +12,10 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([ + /opensea.io\/assets\/(0x[\dA-Fa-f]{40})\/(\d+)/, + /rarible.com\/token\/(0x[\dA-Fa-f]{40}):(\d+)/, + ]), + }, } diff --git a/packages/mask/src/plugins/FindTruman/base.ts b/packages/mask/src/plugins/FindTruman/base.ts index ab6da4427748..e18c8bc2cd13 100644 --- a/packages/mask/src/plugins/FindTruman/base.ts +++ b/packages/mask/src/plugins/FindTruman/base.ts @@ -14,4 +14,9 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([ + /https:\/\/findtruman.io\/#\/(findtruman\/stories\/[\dA-Za-z]+(\/|\/(puzzles|polls|puzzle_result|poll_result)\/[\dA-Za-z]+\/?)?|encryption\?payload=.+)/, + ]), + }, } diff --git a/packages/mask/src/plugins/Furucombo/base.tsx b/packages/mask/src/plugins/Furucombo/base.tsx index 6371b0623a7d..f6043e6ee753 100644 --- a/packages/mask/src/plugins/Furucombo/base.tsx +++ b/packages/mask/src/plugins/Furucombo/base.tsx @@ -15,4 +15,7 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([/https:\/\/furucombo.app\/invest\/(pool|farm)\/(137|1)\/(0x\w+)/]), + }, } diff --git a/packages/mask/src/plugins/Gitcoin/base.ts b/packages/mask/src/plugins/Gitcoin/base.ts index bcfcb5ea43db..351f388d12bd 100644 --- a/packages/mask/src/plugins/Gitcoin/base.ts +++ b/packages/mask/src/plugins/Gitcoin/base.ts @@ -18,4 +18,5 @@ export const base: Plugin.Shared.Definition = { }, }, }, + contribution: { postContent: new Set([/https:\/\/gitcoin.co\/grants\/\d+/]) }, } diff --git a/packages/mask/src/plugins/GoodGhosting/base.ts b/packages/mask/src/plugins/GoodGhosting/base.ts index 07d100412494..097f3dc73f56 100644 --- a/packages/mask/src/plugins/GoodGhosting/base.ts +++ b/packages/mask/src/plugins/GoodGhosting/base.ts @@ -12,4 +12,7 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([/https:\/\/goodghosting.com/]), + }, } diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/index.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/index.tsx index 444b7f0d8168..ebb9bd6d081f 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/index.tsx @@ -34,7 +34,7 @@ const sns: Plugin.SNSAdaptor.Definition = { const payload = ITO_MetadataReader(props.message.meta) if (!payload.ok) return null return ( - + diff --git a/packages/mask/src/plugins/ITO/base.ts b/packages/mask/src/plugins/ITO/base.ts index 9f5031e9d21b..819600954a46 100644 --- a/packages/mask/src/plugins/ITO/base.ts +++ b/packages/mask/src/plugins/ITO/base.ts @@ -1,6 +1,6 @@ import { NetworkPluginID, Plugin } from '@masknet/plugin-infra' import { ChainId } from '@masknet/web3-shared-evm' -import { ITO_PluginID } from './constants' +import { ITO_MetaKey_1, ITO_MetaKey_2, ITO_PluginID } from './constants' export const base: Plugin.Shared.Definition = { ID: ITO_PluginID, @@ -27,4 +27,5 @@ export const base: Plugin.Shared.Definition = { }, }, }, + contribution: { metadataKeys: new Set([ITO_MetaKey_1, ITO_MetaKey_2]) }, } diff --git a/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx b/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx index de9872bb3173..aa79198dcb17 100644 --- a/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx @@ -39,7 +39,7 @@ function Renderer(props: React.PropsWithChildren<{ url: string }>) { if (!chainId || !boxId) return null return ( - + }> diff --git a/packages/mask/src/plugins/MaskBox/base.ts b/packages/mask/src/plugins/MaskBox/base.ts index d427d29ce0a1..f57de4254308 100644 --- a/packages/mask/src/plugins/MaskBox/base.ts +++ b/packages/mask/src/plugins/MaskBox/base.ts @@ -15,4 +15,7 @@ export const base: Plugin.Shared.Definition = { }, experimentalMark: true, i18n: languages, + contribution: { + postContent: new Set(['https://box-beta.mask.io', 'https://box.mask.io']), + }, } diff --git a/packages/mask/src/plugins/MaskPluginWrapper.tsx b/packages/mask/src/plugins/MaskPluginWrapper.tsx index 24795da818b0..98b7c47a73d0 100644 --- a/packages/mask/src/plugins/MaskPluginWrapper.tsx +++ b/packages/mask/src/plugins/MaskPluginWrapper.tsx @@ -1,13 +1,19 @@ -import { Typography, SnackbarContent } from '@mui/material' -import { makeStyles } from '@masknet/theme' +import { Typography, SnackbarContent, Button, Link } from '@mui/material' +import { makeStyles, MaskColorVar } from '@masknet/theme' import { activatedSocialNetworkUI } from '../social-network' import { MaskIcon } from '../resources/MaskIcon' -import { Suspense } from 'react' +import { Suspense, ReactNode, useMemo } from 'react' import { isTwitter } from '../social-network-adaptor/twitter.com/base' +import { usePersonaConnectStatus } from '../components/DataSource/usePersonaConnectStatus' +import { useI18N } from '../utils' +import { Box } from '@mui/system' +import type { Plugin } from '@masknet/plugin-infra' interface PluginWrapperProps extends React.PropsWithChildren<{}> { pluginName: string width?: number + action?: ReactNode + publisher?: Plugin.Shared.Publisher } const useStyles = makeStyles()((theme) => { @@ -26,38 +32,90 @@ const useStyles = makeStyles()((theme) => { : null), }, header: { - backgroundColor: theme.palette.background.paper, + backgroundColor: 'transparent', color: theme.palette.text.primary, display: 'flex', alignItems: 'center', - padding: theme.spacing(1, 2), - borderBottom: `1px solid ${theme.palette.divider}`, + padding: theme.spacing(2), }, title: { display: 'flex', flexDirection: 'column', - paddingLeft: theme.spacing(1), + paddingLeft: theme.spacing(1.5), + }, + action: { + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', }, body: { - margin: theme.spacing(2), + borderTop: `1px solid ${theme.palette.divider}`, + padding: theme.spacing(2), + }, + button: { + color: MaskColorVar.twitterButtonText, + '&,&:hover': { + background: MaskColorVar.twitterButton, + }, }, } }) export default function MaskPluginWrapper(props: PluginWrapperProps) { const { classes } = useStyles() - const { pluginName, children } = props + const { pluginName, children, action, publisher } = props + const personaConnectStatus = usePersonaConnectStatus() + const { t } = useI18N() + + const name = !personaConnectStatus.hasPersona + ? t('please_create_persona') + : !personaConnectStatus.connected + ? t('please_connect_persona') + : pluginName + + const actionButton = useMemo(() => { + if (!personaConnectStatus.action) return null + + const button = personaConnectStatus.hasPersona ? t('connect_persona') : t('create_persona') + return ( + + ) + }, [personaConnectStatus, t]) + + const publisherInfo = useMemo(() => { + if (!publisher) return null + return ( + + + Provided by + + + + {publisher.name.fallback} + + + + ) + }, [publisher]) const inner = (
ev.stopPropagation()}>
- +
- Mask Plugin - {pluginName} + + Mask Plugin {!personaConnectStatus.connected && pluginName ? `(${pluginName})` : ''} + + + {name} +
+
{actionButton || action || publisherInfo}
-
{children}
+ {personaConnectStatus.connected && children ?
{children}
: null}
) return } children={inner} /> diff --git a/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx b/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx index 295f78502587..22abc0b7888a 100644 --- a/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx +++ b/packages/mask/src/plugins/Pets/SNSAdaptor/animate.tsx @@ -5,6 +5,8 @@ import Drag from './drag' import AnimatedMessage from './animatedMsg' import Tip from './tooltip' import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' +import { PetsPluginID } from '../constants' +import { useIsMinimalMode } from '@masknet/plugin-infra' const useStyles = makeStyles()(() => ({ root: { @@ -36,13 +38,14 @@ const AnimatePic = () => { const [show, setShow] = useState(false) const [infoShow, setInfoShow] = useState(false) + const disabled = useIsMinimalMode(PetsPluginID) const identity = useCurrentVisitingIdentity() useEffect(() => { const userId = identity.identifier.userId const maskId = 'realMaskNetwork' - setShow(userId === maskId) - }, [identity]) + setShow(!disabled && userId === maskId) + }, [identity, disabled]) const handleClose = () => setShow(false) const handleMouseEnter = () => setInfoShow(true) diff --git a/packages/mask/src/plugins/Polls/base.ts b/packages/mask/src/plugins/Polls/base.ts index b3fca3c7e1b7..4d2d194e3510 100644 --- a/packages/mask/src/plugins/Polls/base.ts +++ b/packages/mask/src/plugins/Polls/base.ts @@ -1,5 +1,5 @@ import type { Plugin } from '@masknet/plugin-infra' -import { PLUGIN_ID, PLUGIN_ICON, PLUGIN_NAME, PLUGIN_DESCRIPTION } from './constants' +import { PLUGIN_ID, PLUGIN_ICON, PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_META_KEY } from './constants' export const base: Plugin.Shared.Definition = { ID: PLUGIN_ID, @@ -13,4 +13,5 @@ export const base: Plugin.Shared.Definition = { target: 'insider', }, experimentalMark: true, + contribution: { metadataKeys: new Set([PLUGIN_META_KEY]) }, } diff --git a/packages/mask/src/plugins/PoolTogether/base.tsx b/packages/mask/src/plugins/PoolTogether/base.tsx index 9c46710e72c4..874152ecbd37 100644 --- a/packages/mask/src/plugins/PoolTogether/base.tsx +++ b/packages/mask/src/plugins/PoolTogether/base.tsx @@ -1,5 +1,5 @@ import type { Plugin } from '@masknet/plugin-infra' -import { POOLTOGETHER_PLUGIN_ID } from './constants' +import { POOLTOGETHER_PLUGIN_ID, URL_PATTERN } from './constants' import { PoolTogetherIcon } from '../../resources/PoolTogetherIcon' export const base: Plugin.Shared.Definition = { @@ -13,4 +13,5 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { postContent: new Set([URL_PATTERN]) }, } diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/index.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/index.tsx index f8b006e42506..e574c2840ffb 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/index.tsx @@ -27,7 +27,7 @@ const sns: Plugin.SNSAdaptor.Definition = { DecryptedInspector(props) { if (RedPacketMetadataReader(props.message.meta).ok) return ( - + {renderWithRedPacketMetadata(props.message.meta, (r) => ( ))} @@ -36,7 +36,7 @@ const sns: Plugin.SNSAdaptor.Definition = { if (RedPacketNftMetadataReader(props.message.meta).ok) return ( - + {renderWithRedPacketNftMetadata(props.message.meta, (r) => ( ))} diff --git a/packages/mask/src/plugins/RedPacket/base.ts b/packages/mask/src/plugins/RedPacket/base.ts index 00b69c14551d..eaa63eeb0f29 100644 --- a/packages/mask/src/plugins/RedPacket/base.ts +++ b/packages/mask/src/plugins/RedPacket/base.ts @@ -1,6 +1,6 @@ import { NetworkPluginID, Plugin } from '@masknet/plugin-infra' import { ChainId } from '@masknet/web3-shared-evm' -import { RedPacketPluginID } from './constants' +import { RedPacketMetaKey, RedPacketNftMetaKey, RedPacketPluginID } from './constants' export const base: Plugin.Shared.Definition = { ID: RedPacketPluginID, @@ -28,4 +28,7 @@ export const base: Plugin.Shared.Definition = { }, }, }, + contribution: { + metadataKeys: new Set([RedPacketMetaKey, RedPacketNftMetaKey]), + }, } diff --git a/packages/mask/src/plugins/Snapshot/base.ts b/packages/mask/src/plugins/Snapshot/base.ts index 483e9f2b6adb..db6dc7546454 100644 --- a/packages/mask/src/plugins/Snapshot/base.ts +++ b/packages/mask/src/plugins/Snapshot/base.ts @@ -14,4 +14,7 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([/https:\/\/(?:www.)?snapshot.(org|page)\/#\/(.*?)\/proposal\/[\dA-Za-z]+/]), + }, } diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx index 87397d08bf2c..e01eed0d588b 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/trending/TrendingViewDeck.tsx @@ -126,7 +126,7 @@ export function TrendingViewDeck(props: TrendingViewDeckProps) { const classes = useStylesExtends(useStyles(), props) //#region buy - const transakPluginEnabled = useActivatedPluginsSNSAdaptor().find((x) => x.ID === PluginId.Transak) + const transakPluginEnabled = useActivatedPluginsSNSAdaptor('any').find((x) => x.ID === PluginId.Transak) const account = useAccount() const isAllowanceCoin = useTransakAllowanceCoin(coin) const { setDialog: setBuyDialog } = useRemoteControlledDialog(PluginTransakMessages.buyTokenDialogUpdated) diff --git a/packages/mask/src/plugins/UnlockProtocol/base.ts b/packages/mask/src/plugins/UnlockProtocol/base.ts index 9fe57f779d9b..47001914e48f 100644 --- a/packages/mask/src/plugins/UnlockProtocol/base.ts +++ b/packages/mask/src/plugins/UnlockProtocol/base.ts @@ -1,6 +1,6 @@ import { NetworkPluginID, Plugin } from '@masknet/plugin-infra' import { ChainId } from '@masknet/web3-shared-evm' -import { pluginDescription, pluginIcon, pluginName, pluginId } from './constants' +import { pluginDescription, pluginIcon, pluginName, pluginId, pluginMetaKey } from './constants' export const base: Plugin.Shared.Definition = { ID: pluginId, @@ -18,4 +18,7 @@ export const base: Plugin.Shared.Definition = { }, }, }, + contribution: { + metadataKeys: new Set([pluginMetaKey]), + }, } diff --git a/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx b/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx index 842dbc23956c..f25f766efebb 100644 --- a/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx @@ -6,12 +6,7 @@ import { base } from '../base' import MaskPluginWrapper from '../../MaskPluginWrapper' import { PoolView } from '../UI/PoolView' import { InvestDialog } from '../UI/InvestDialog' -import { escapeRegExp } from 'lodash-unified' -import { BASE_URL, STAGING_URL } from '../constants' - -function createMatchLink() { - return new RegExp(`(${escapeRegExp(BASE_URL)}|${escapeRegExp(STAGING_URL)})/pool/(\\w+)`) -} +import { createMatchLink } from '../constants' function getPoolFromLink(link: string) { const matchLink = createMatchLink() diff --git a/packages/mask/src/plugins/dHEDGE/base.tsx b/packages/mask/src/plugins/dHEDGE/base.tsx index 1a2c7971fa6b..04c341b8eb50 100644 --- a/packages/mask/src/plugins/dHEDGE/base.tsx +++ b/packages/mask/src/plugins/dHEDGE/base.tsx @@ -1,5 +1,5 @@ import type { Plugin } from '@masknet/plugin-infra' -import { DHEDGE_PLUGIN_ID } from './constants' +import { createMatchLink, DHEDGE_PLUGIN_ID } from './constants' import { DHEDGEIcon } from '../../resources/DHEDGEIcon' export const base: Plugin.Shared.Definition = { @@ -13,4 +13,7 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, + contribution: { + postContent: new Set([createMatchLink()]), + }, } diff --git a/packages/mask/src/plugins/dHEDGE/constants.ts b/packages/mask/src/plugins/dHEDGE/constants.ts index 253a3c6a401c..97dfb0f01059 100644 --- a/packages/mask/src/plugins/dHEDGE/constants.ts +++ b/packages/mask/src/plugins/dHEDGE/constants.ts @@ -1,3 +1,5 @@ +import { escapeRegExp } from 'lodash-unified' + export const DHEDGE_PLUGIN_ID = 'org.dhedge' export const POOL_DESCRIPTION_LIMIT = 210 export const BLOCKIES_OPTIONS = { @@ -9,3 +11,7 @@ export const BLOCKIES_OPTIONS = { export const API_URL = 'https://api-v2.dhedge.org/graphql' export const BASE_URL = 'https://app.dhedge.org' export const STAGING_URL = 'https://dh-pre-prod.web.app' + +export function createMatchLink() { + return new RegExp(`(${escapeRegExp(BASE_URL)}|${escapeRegExp(STAGING_URL)})/pool/(\\w+)`) +} diff --git a/packages/mask/src/settings/settings.ts b/packages/mask/src/settings/settings.ts index cc0501082e52..b26a0119b832 100644 --- a/packages/mask/src/settings/settings.ts +++ b/packages/mask/src/settings/settings.ts @@ -73,7 +73,11 @@ export const userGuideStatus: NetworkSettings = createNetworkSettings('u * use `useActivatedPluginsSNSAdaptor().find((x) => x.ID === PLUGIN_ID)` or * `useActivatedPluginsDashboard().find((x) => x.ID === PLUGIN_ID)` instead */ -export const currentPluginEnabledStatus: NetworkSettings = createNetworkSettings('pluginsEnabled', true) +// This was "currentPluginEnabled" before, but we used it to represent minimal mode now to make the settings be able to migrate. +export const currentPluginMinimalModeNOTEnabled: NetworkSettings = createNetworkSettings( + 'pluginsEnabled', + true, +) //#endregion export const launchPageSettings = createGlobalSettings('launchPage', LaunchPage.dashboard, { diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts index a804e39f006a..ba8533891f7a 100644 --- a/packages/mask/src/utils/native-rpc/Web.ts +++ b/packages/mask/src/utils/native-rpc/Web.ts @@ -101,8 +101,8 @@ export const MaskNetworkAPI: MaskNetworkAPIs = { }) return stringify(connectedPersonas) }, - app_isPluginEnabled: ({ pluginID }) => Services.Settings.getPluginEnabled(pluginID), - app_setPluginStatus: ({ pluginID, enabled }) => Services.Settings.setPluginEnabled(pluginID, enabled), + app_isPluginEnabled: ({ pluginID }) => Services.Settings.getPluginMinimalModeEnabled(pluginID).then((x) => !x), + app_setPluginStatus: ({ pluginID, enabled }) => Services.Settings.setPluginMinimalModeEnabled(pluginID, !enabled), setting_getNetworkTraderProvider: ({ network }) => { switch (network) { case NetworkType.Ethereum: diff --git a/packages/mask/src/web3/UI/EthereumChainBoundary.tsx b/packages/mask/src/web3/UI/EthereumChainBoundary.tsx index 32f0f38d865d..869a9afadbc9 100644 --- a/packages/mask/src/web3/UI/EthereumChainBoundary.tsx +++ b/packages/mask/src/web3/UI/EthereumChainBoundary.tsx @@ -44,7 +44,7 @@ export function EthereumChainBoundary(props: EthereumChainBoundaryProps) { const { t } = useI18N() const pluginID = usePluginIDContext() - const plugin = useActivatedPlugin(pluginID) + const plugin = useActivatedPlugin(pluginID, 'any') const account = useAccount() const chainId = useChainId() diff --git a/packages/plugin-infra/src/hooks/useActivatedPlugin.ts b/packages/plugin-infra/src/hooks/useActivatedPlugin.ts index 057b8d21d13b..a4255c6bee81 100644 --- a/packages/plugin-infra/src/hooks/useActivatedPlugin.ts +++ b/packages/plugin-infra/src/hooks/useActivatedPlugin.ts @@ -1,8 +1,8 @@ import { useActivatedPluginDashboard } from '../manager/dashboard' import { useActivatedPluginSNSAdaptor } from '../manager/sns-adaptor' -export function useActivatedPlugin(pluginID: string) { - const pluginSNSAdaptor = useActivatedPluginSNSAdaptor(pluginID) +export function useActivatedPlugin(pluginID: string, minimalModeEqualsTo: 'any' | boolean) { + const pluginSNSAdaptor = useActivatedPluginSNSAdaptor(pluginID, minimalModeEqualsTo) const pluginDashboard = useActivatedPluginDashboard(pluginID) return pluginSNSAdaptor ?? pluginDashboard ?? null } diff --git a/packages/plugin-infra/src/hooks/useActivatedPluginWeb3State.ts b/packages/plugin-infra/src/hooks/useActivatedPluginWeb3State.ts index 6b265aeb28cc..769c39f1ed9e 100644 --- a/packages/plugin-infra/src/hooks/useActivatedPluginWeb3State.ts +++ b/packages/plugin-infra/src/hooks/useActivatedPluginWeb3State.ts @@ -1,6 +1,6 @@ import { useActivatedPlugin } from './useActivatedPlugin' export function useActivatedPluginWeb3State(pluginID: string) { - const activatedPlugin = useActivatedPlugin(pluginID) + const activatedPlugin = useActivatedPlugin(pluginID, 'any') return activatedPlugin?.Web3State ?? null } diff --git a/packages/plugin-infra/src/hooks/useActivatedPluginWeb3UI.ts b/packages/plugin-infra/src/hooks/useActivatedPluginWeb3UI.ts index bd612859af00..cfbf11f0388c 100644 --- a/packages/plugin-infra/src/hooks/useActivatedPluginWeb3UI.ts +++ b/packages/plugin-infra/src/hooks/useActivatedPluginWeb3UI.ts @@ -1,6 +1,6 @@ import { useActivatedPlugin } from './useActivatedPlugin' export function useActivatedPluginWeb3UI(pluginID: string) { - const activatedPlugin = useActivatedPlugin(pluginID) + const activatedPlugin = useActivatedPlugin(pluginID, 'any') return activatedPlugin?.Web3UI ?? null } diff --git a/packages/plugin-infra/src/hooks/useAllPluginsWeb3State.ts b/packages/plugin-infra/src/hooks/useAllPluginsWeb3State.ts index 81e1a55d6fd1..bd86889f7cef 100644 --- a/packages/plugin-infra/src/hooks/useAllPluginsWeb3State.ts +++ b/packages/plugin-infra/src/hooks/useAllPluginsWeb3State.ts @@ -3,7 +3,7 @@ import { useActivatedPluginsSNSAdaptor } from '../manager/sns-adaptor' import type { Web3Plugin } from '../web3-types' export function useAllPluginsWeb3State() { - const pluginsSNSAdaptor = useActivatedPluginsSNSAdaptor() + const pluginsSNSAdaptor = useActivatedPluginsSNSAdaptor('any') const pluginsDashboard = useActivatedPluginsDashboard() return [...pluginsSNSAdaptor, ...pluginsDashboard].reduce< diff --git a/packages/plugin-infra/src/manager/manage.ts b/packages/plugin-infra/src/manager/manage.ts index b755fca3749d..78ac14bbd1af 100644 --- a/packages/plugin-infra/src/manager/manage.ts +++ b/packages/plugin-infra/src/manager/manage.ts @@ -1,4 +1,5 @@ import { Emitter, ALL_EVENTS } from '@servie/events' +import { noop } from 'lodash-unified' import type { Plugin } from '../types' import { getPluginDefine, registeredPluginIDs, registeredPlugins } from './store' @@ -17,16 +18,18 @@ export function createManager< } const resolved = new Map() const activated = new Map() + const minimalModePluginIDs = new Set() let _host: Plugin.__Host.Host = undefined! const events = new Emitter<{ - activated: [id: string] - stopped: [id: string] + activateChanged: [id: string, enabled: boolean] + minimalModeChanged: [id: string, enabled: boolean] }>() return { configureHostHooks: (host: Plugin.__Host.Host) => (_host = host), activatePlugin, stopPlugin, + isMinimalMode, isActivated, startDaemon, activated: { @@ -37,16 +40,29 @@ export function createManager< }, } as Iterable, }, + minimalMode: { + *[Symbol.iterator]() { + yield* minimalModePluginIDs + }, + }, events, } function startDaemon(host: Plugin.__Host.Host, extraCheck?: (id: string) => boolean) { _host = host - const { enabled, signal, addI18NResource } = _host - const removeListener = enabled.events.on(ALL_EVENTS, checkRequirementAndStartOrStop) + const { enabled, signal, addI18NResource, minimalMode } = _host + const removeListener1 = enabled.events.on(ALL_EVENTS, checkRequirementAndStartOrStop) + const removeListener2 = minimalMode.events.on('enabled', (id) => { + minimalModePluginIDs.add(id) + events.emit('minimalModeChanged', id, true) + }) + const removeListener3 = minimalMode.events.on('disabled', (id) => { + minimalModePluginIDs.delete(id) + events.emit('minimalModeChanged', id, false) + }) signal?.addEventListener('abort', () => [...activated.keys()].forEach(stopPlugin)) - signal?.addEventListener('abort', removeListener) + signal?.addEventListener('abort', () => void [removeListener1(), removeListener2(), removeListener3()]) for (const plugin of registeredPlugins) { plugin.i18n && addI18NResource(plugin.ID, plugin.i18n) @@ -62,9 +78,7 @@ export function createManager< async function meetRequirement(id: string) { const define = getPluginDefine(id) if (!define) return false - if (!define.management?.alwaysOn) { - if (!(await enabled.isEnabled(id))) return false - } + if (!(await enabled.isEnabled(id))) return false if (extraCheck && !extraCheck(id)) return false return true } @@ -82,6 +96,10 @@ export function createManager< const definition = await __getDefinition(id) if (!definition) return + Promise.resolve(_host.minimalMode.isEnabled(id)).then( + (enabled) => (enabled ? minimalModePluginIDs.add(id) : minimalModePluginIDs.delete(id)), + noop, + ) { const icon = definition.icon if (typeof icon === 'string' && (icon.codePointAt(0) || 0) < 256) { @@ -106,7 +124,7 @@ export function createManager< } activated.set(id, activatedPlugin) await definition.init(activatedPlugin.controller.signal, activatedPlugin.context) - events.emit('activated', id) + events.emit('activateChanged', id, true) } function stopPlugin(id: string) { @@ -114,13 +132,17 @@ export function createManager< if (!instance) return instance.controller.abort() activated.delete(id) - events.emit('stopped', id) + events.emit('activateChanged', id, false) } function isActivated(id: string) { return activated.has(id) } + function isMinimalMode(id: string) { + return minimalModePluginIDs.has(id) + } + async function __getDefinition(id: string) { if (resolved.has(id)) return resolved.get(id)! diff --git a/packages/plugin-infra/src/manager/sns-adaptor.ts b/packages/plugin-infra/src/manager/sns-adaptor.ts index 3cd823d35607..2e2e33bfc2d6 100644 --- a/packages/plugin-infra/src/manager/sns-adaptor.ts +++ b/packages/plugin-infra/src/manager/sns-adaptor.ts @@ -1,27 +1,62 @@ -import { ALL_EVENTS } from '@servie/events' import { useSubscription, Subscription } from 'use-subscription' import { createManager } from './manage' import { getPluginDefine } from './store' import type { CurrentSNSNetwork, Plugin } from '../types' import type { NetworkPluginID } from '..' +import { unreachable } from '@dimensiondev/kit' -const { events, activated, startDaemon } = createManager((def) => def.SNSAdaptor) +const { events, activated, startDaemon, minimalMode } = createManager((def) => def.SNSAdaptor) -const subscription: Subscription = { +const activatedSub: Subscription = { getCurrentValue: () => [...activated.plugins], - subscribe: (f) => events.on(ALL_EVENTS, f), + subscribe: (f) => events.on('activateChanged', f), } -export function useActivatedPluginsSNSAdaptor() { - return useSubscription(subscription) +const minimalModeSub: Subscription = { + getCurrentValue: () => [...minimalMode], + subscribe: (f) => events.on('minimalModeChanged', f), +} +export function useActivatedPluginsSNSAdaptor(minimalModeEqualsTo: 'any' | boolean) { + const minimalMode = useSubscription(minimalModeSub) + const result = useSubscription(activatedSub) + if (minimalModeEqualsTo === 'any') return result + else if (minimalModeEqualsTo === true) return result.filter((x) => minimalMode.includes(x.ID)) + else if (minimalModeEqualsTo === false) return result.filter((x) => !minimalMode.includes(x.ID)) + unreachable(minimalModeEqualsTo) +} +useActivatedPluginsSNSAdaptor.visibility = { + useMinimalMode: useActivatedPluginsSNSAdaptor.bind(null, true), + useNotMinimalMode: useActivatedPluginsSNSAdaptor.bind(null, false), + useAnyMode: useActivatedPluginsSNSAdaptor.bind(null, 'any'), +} + +export function useIsMinimalMode(pluginID: string) { + return useSubscription(minimalModeSub).includes(pluginID) } -export function useActivatedPluginSNSAdaptor(pluginID: string) { - const plugins = useActivatedPluginsSNSAdaptor() - return plugins.find((x) => x.ID === pluginID) +/** + * + * @param pluginID Get the plugin ID + * @param visibility Should invisible plugin included? + * @returns + */ +export function useActivatedPluginSNSAdaptor(pluginID: string, minimalModeEqualsTo: 'any' | boolean) { + const plugins = useActivatedPluginsSNSAdaptor(minimalModeEqualsTo) + const minimalMode = useSubscription(minimalModeSub) + const result = plugins.find((x) => x.ID === pluginID) + if (!result) return result + if (minimalModeEqualsTo === 'any') return result + else if (minimalModeEqualsTo === true) { + if (minimalMode.includes(result.ID)) return result + return undefined + } else if (minimalModeEqualsTo === false) { + if (minimalMode.includes(result.ID)) return undefined + return result + } + unreachable(minimalModeEqualsTo) } export function useActivatedPluginSNSAdaptor_Web3Supported(chainId: number, pluginID: string) { - const plugins = useActivatedPluginsSNSAdaptor() + const plugins = useActivatedPluginsSNSAdaptor('any') return plugins.reduce>((acc, cur) => { if (!cur.enableRequirement.web3) { acc[cur.ID] = true diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index 98fefb17694f..21bee580d888 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -48,6 +48,9 @@ export declare namespace Plugin { Dashboard?: Loader /** Load the Worker part of the plugin. */ Worker?: Loader + /** Load the General UI of the plugin. */ + // TODO: not supported yet. + // GeneralUI?: Loader } } /** @@ -98,8 +101,6 @@ export namespace Plugin.Shared { * This does not affect if the plugin enable or not. */ experimentalMark?: boolean - /** Configuration of how this plugin is managed by the Mask Network. */ - management?: ManagementProperty /** i18n resources of this plugin */ i18n?: I18NResource /** Introduce networks information. */ @@ -108,6 +109,14 @@ export namespace Plugin.Shared { declareWeb3Providers?: Web3Plugin.ProviderDescriptor[] /** Introduce application category information. */ declareApplicationCategories?: Web3Plugin.ApplicationCategoryDescriptor[] + /** + * Declare what this plugin provides. + * + * Declare this field properly so Mask Network can suggest your plugin when needed. + */ + contribution?: Contribution + /** Declare ability this plugin supported. */ + ability?: Ability } /** * This part is shared between Dashboard, SNSAdaptor and Worker part @@ -153,18 +162,6 @@ export namespace Plugin.Shared { /** The Web3 Network this plugin supports */ web3?: Web3Plugin.EnableRequirement } - export interface ManagementProperty { - /** This plugin should not displayed in the plugin management page. */ - internal?: boolean - /** - * This plugin should not allow to be "disabled" in the plugin management page. - * - * This property is for the Wallet plugin. It's the core of almost all other plugins. - * - * It should be replaced by "dependency" management in the future (if there are more cases than the Wallet one). - */ - alwaysOn?: boolean - } export interface SupportedNetworksDeclare { /** * opt-in means the listed networks is supported. @@ -177,6 +174,25 @@ export namespace Plugin.Shared { export type I18NKey = string export type I18NValue = string export type I18NResource = Record> + export interface Contribution { + /** This plugin can recognize and react to the following metadata keys. */ + metadataKeys?: ReadonlySet + /** This plugin can recognize and enhance the post that matches the following matchers. */ + postContent?: ReadonlySet + } + export interface Ability { + /** + * Declare that this plugin supports minimal mode. + * In this mode, the automated minimal mode is not applied to this plugin. + * + * The plugin MUST follow the design guide to behave like it is in the automated minimal mode, e.g.: + * + * - Do not display full UI in PostInspector + * - Do not display full UI in DecryptedPostInspector + */ + // TODO: implement this flag when there is use case. + // UX_NEED_APPROVAL_manualMinimalMode?: boolean + } } /** This part runs in the SNSAdaptor */ @@ -383,6 +399,8 @@ export namespace Plugin.Dashboard { Web3UI?: Web3Plugin.UI.UI /** This is the context of the currently chosen network. */ Web3State?: Web3Plugin.ObjectCapabilities.Capabilities + /** Plugin DO NOT need to define this. This will be auto set by the plugin host. */ + __general_ui__?: GeneralUI.DefinitionDeferred } } @@ -493,6 +511,90 @@ export namespace Plugin.Worker { } } +/** This part defines the plugin part that does not context aware. */ +export namespace Plugin.GeneralUI { + export interface DefinitionDeferred { + /** + * Render metadata in many different environments. + * + * 1. Environment + * + * The render component MUST NOT assume they are running in a specific environment (e.g. SNS Adaptor). + * Plugin messages and RPC MAY NOT working. + * + * It MUST NOT assume the environment using the `context` props. + * ALL actions MUST BE DONE with the given props. + * + * Here is some example of *possible* environments. + * - inside SNS Adaptor, given "composition" context, running in the CompositionDialog. + * - inside SNS Adaptor, given "post" context, running in the DecryptedPost. + * - inside Dashboard, given "post" context, running in the PostHistory as the previewer. + * - inside Popups, given "post" context, running in the PostInspector (Isolated mode). + * - on mask.io, given "post" context, allowing preview the message without extension installed. + * + * 2. Contexts + * + * The render component might be used in many different contexts. + * + * - "composition" context, the render should be editable, but not interactive (e.g. allow vote). + * - "post" context, the render should be readonly, but interactive. + * + * 3. Actions + * + * The render component MUST BE a ForwardRefExotic React Component + * that support operations defined in `Plugin.ContextFree.MetadataRender.RenderActions` + */ + metadataRender: MetadataRender.StaticRender | MetadataRender.DynamicRender + } + + export namespace MetadataRender { + export type MetadataReader = (meta: TypedMessage['meta']) => Result + //#region Static render + // new Map([ [reader, react component] ]) + export type StaticRender = ReadonlyMap, StaticRenderComponent> + export type StaticRenderComponent = Omit>, 'propTypes'> + export type StaticRenderProps = Context & React.RefAttributes> & { metadata: T } + //#endregion + //#region DynamicRender + export type DynamicRender = Omit, 'propTypes'> + export type DynamicRenderProps = Context & + React.RefAttributes> & { metadata: TypedMessage['meta'] } + //#endregion + export type RenderActions = { + /** + * This action make the render into the edit state. + * It should report the result via onEditComplete() props. + * + * If this action does not exist, it will be rendered as non-editable. + */ + edit?(): void + /** + * This action make the render quit the edit state. + * If save is true, the render MUST report the new result via onEditComplete. + * + * If this action does not exist, the render should handle the save/cancel by themselves. + */ + quitEdit?(save: boolean): void + } + export type Context = CompositionContext | DecryptedPostContext + /** This metadata render is called in a composition preview context. */ + export interface CompositionContext { + context: 'composition' + /** + * When edit() is called, this component should go into to editable state. + * If the edit completes, the new metadata will be used to replace the old one. + */ + onEditComplete(metaKey: string, replaceMeta: T): void + } + /** + * This metadata render is called in the decrypted post. + */ + export interface DecryptedPostContext { + context: 'post' + } + } +} + // Helper types export namespace Plugin { /** @@ -546,6 +648,7 @@ export enum CurrentSNSNetwork { Facebook = 1, Twitter = 2, Instagram = 3, + Minds = 4, } /** @@ -600,7 +703,22 @@ export interface Pageable { // --------------------------------------------------- export namespace Plugin.__Host { export interface Host { + /** + * Control if the plugin is enabled or not. + * + * Note: This API currently is not in use. + * + * The "enabled/disabled" UI in the dashboard actually reflects to the "minimalMode" below. + */ enabled: EnabledStatusReporter + /** + * Control if the plugin is in the minimal mode. + * + * If it is in the minimal mode, it will be omitted in some cases. + * + * Plugin can use + */ + minimalMode: EnabledStatusReporter addI18NResource(pluginID: string, resources: Plugin.Shared.I18NResource): void createContext(id: string, signal: AbortSignal): Context signal?: AbortSignal diff --git a/packages/plugins/Wallet/src/base.ts b/packages/plugins/Wallet/src/base.ts index 5d3d695b1807..fcb97dfcd068 100644 --- a/packages/plugins/Wallet/src/base.ts +++ b/packages/plugins/Wallet/src/base.ts @@ -13,6 +13,5 @@ export const base: Plugin.Shared.Definition = { networks: { type: 'opt-out', networks: {} }, target: 'stable', }, - management: { alwaysOn: true }, i18n: languages, } diff --git a/packages/plugins/example/package.json b/packages/plugins/example/package.json index 735809cc9a6e..a7ad9c01e469 100644 --- a/packages/plugins/example/package.json +++ b/packages/plugins/example/package.json @@ -4,6 +4,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@masknet/plugin-infra": "workspace:*" + "@masknet/plugin-infra": "workspace:*", + "ts-results": "^3.3.0" } } diff --git a/packages/plugins/example/src/ContextFree/index.tsx b/packages/plugins/example/src/ContextFree/index.tsx new file mode 100644 index 000000000000..9f1a12119734 --- /dev/null +++ b/packages/plugins/example/src/ContextFree/index.tsx @@ -0,0 +1,18 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { forwardRef, useImperativeHandle } from 'react' +import { Ok, Err } from 'ts-results' + +const metadataReader: Plugin.GeneralUI.MetadataRender.MetadataReader = (meta) => { + const raw = meta?.get('io.mask.example/v1') + if (raw) return Ok(raw) + return Err.EMPTY +} +const render: Plugin.GeneralUI.MetadataRender.StaticRenderComponent = forwardRef((props, ref) => { + useImperativeHandle(ref, () => ({}), []) + return <>Metadata render for key "io.mask.example/v1" {JSON.stringify(props.metadata)} +}) + +const contextFree: Plugin.GeneralUI.DefinitionDeferred = { + metadataRender: new Map([[metadataReader, render]]), +} +export default contextFree diff --git a/packages/shared-base/src/Messages/Mask.ts b/packages/shared-base/src/Messages/Mask.ts index 0886d8c45c1f..f3ad28372301 100644 --- a/packages/shared-base/src/Messages/Mask.ts +++ b/packages/shared-base/src/Messages/Mask.ts @@ -70,8 +70,7 @@ export interface MaskEvents extends MaskSettingsEvents, MaskMobileOnlyEvents, Ma restoreSuccess: void profilesChanged: UpdateEvent[] relationsChanged: RelationChangedEvent[] - pluginEnabled: string - pluginDisabled: string + pluginMinimalModeChanged: [id: string, newStatus: boolean] requestExtensionPermission: RequestExtensionPermissionEvent signRequestApproved: PersonaSignApprovedEvent diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71abef900029..ad19d3e016fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -741,8 +741,10 @@ importers: packages/plugins/example: specifiers: '@masknet/plugin-infra': workspace:* + ts-results: ^3.3.0 dependencies: '@masknet/plugin-infra': link:../../plugin-infra + ts-results: 3.3.0 packages/polyfills: specifiers: