diff --git a/src/components/InjectedComponents/PageInspector.tsx b/src/components/InjectedComponents/PageInspector.tsx new file mode 100644 index 00000000000..010427218ef --- /dev/null +++ b/src/components/InjectedComponents/PageInspector.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { PluginUI, PluginConfig } from '../../plugins/plugin' + +export interface PageInspectorProps {} + +export function PageInspector(props: PageInspectorProps) { + return ( + <> + {[...PluginUI.values()].map((x) => ( + + ))} + + ) +} + +function PluginPageInspectorForEach({ config }: { config: PluginConfig }) { + const F = config.pageInspector + if (typeof F === 'function') return + return null +} diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx index b71ef9413f2..120fa5d7022 100644 --- a/src/components/InjectedComponents/PostDummy.tsx +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -2,7 +2,6 @@ import React from 'react' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' import { PluginUI } from '../../plugins/plugin' -import { remove } from 'lodash-es' import { makeTypedMessageCompound, isTypedMessageSuspended } from '../../protocols/typed-message' export interface PostDummyProps {} diff --git a/src/plugins/Trader/UI/SettingsDialog.tsx b/src/plugins/Trader/UI/SettingsDialog.tsx index ec494a6fe16..b83c470ab54 100644 --- a/src/plugins/Trader/UI/SettingsDialog.tsx +++ b/src/plugins/Trader/UI/SettingsDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { useI18N } from '../../../utils/i18n-next-ui' import { makeStyles, @@ -16,6 +16,7 @@ import { DialogActions, Button, Divider, + MenuProps, } from '@material-ui/core' import ShadowRootDialog from '../../../utils/jss/ShadowRootDialog' import { DialogDismissIconUI } from '../../../components/InjectedComponents/DialogDismissIcon' @@ -69,16 +70,13 @@ interface SettingsDialogUIProps onPlatformChange?: (platform: Platform) => void onClose?: () => void DialogProps?: Partial + MenuProps?: Partial } function SettingsDialogUI(props: SettingsDialogUIProps) { const { t } = useI18N() const { currency, platform, currencies } = props const classes = useStylesExtends(useStyles(), props) - - console.log('DEBUG: SettingsDialogUI') - console.log(props) - return (
props.onPlatformChange?.(e.target.value as Platform)} - MenuProps={{ container: props.DialogProps?.container ?? PortalShadowRoot }}> + MenuProps={{ + classes: { paper: classes.menuPaper }, + container: props.DialogProps?.container ?? PortalShadowRoot, + ...props.MenuProps, + }}> {getEnumAsArray(Platform).map(({ key, value }) => ( {resolvePlatformName(value)} @@ -134,8 +136,9 @@ function SettingsDialogUI(props: SettingsDialogUIProps) { if (target) props.onCurrencyChange?.(target) }} MenuProps={{ - container: props.DialogProps?.container ?? PortalShadowRoot, classes: { paper: classes.menuPaper }, + container: props.DialogProps?.container ?? PortalShadowRoot, + ...props.MenuProps, }}> {currencies.map((x) => ( diff --git a/src/plugins/Trader/UI/TradeDialog.tsx b/src/plugins/Trader/UI/TradeDialog.tsx new file mode 100644 index 00000000000..07bfe0d0ab5 --- /dev/null +++ b/src/plugins/Trader/UI/TradeDialog.tsx @@ -0,0 +1,5 @@ +export interface TradeDialogProps {} + +export function TradeDialog(props: TradeDialogProps) { + return null +} diff --git a/src/plugins/Trader/UI/TrendingPopper.tsx b/src/plugins/Trader/UI/TrendingPopper.tsx new file mode 100644 index 00000000000..46ee79030dc --- /dev/null +++ b/src/plugins/Trader/UI/TrendingPopper.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react' +import { Popper, ClickAwayListener, PopperProps } from '@material-ui/core' +import { MessageCenter, ObserveCashTagEvent } from '../messages' + +export interface TrendingPopperProps { + children?: (name: string) => React.ReactNode + PopperProps?: Partial +} + +export function TrendingPopper(props: TrendingPopperProps) { + const [name, setName] = useState('') + const [anchorEl, setAnchorEl] = useState(null) + + useEffect(() => { + const off = MessageCenter.on('cashTagObserved', (ev: ObserveCashTagEvent) => { + setName(ev.name) + setAnchorEl(ev.element) + }) + return () => { + off() + setAnchorEl(null) + } + }, []) + + if (!anchorEl) return null + return ( + setAnchorEl(null)}> + + {props.children?.(name)} + + + ) +} diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index a3b0b5e10a3..7dba0dff4bb 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -1,29 +1,11 @@ -import React, { useState, useEffect } from 'react' -import { - makeStyles, - Avatar, - Typography, - Card, - CardHeader, - IconButton, - CardActions, - Theme, - createStyles, - CircularProgress, - CardContent, -} from '@material-ui/core' -import { useAsync } from 'react-use' -import SettingsIcon from '@material-ui/icons/Settings' +import React from 'react' +import { makeStyles, Avatar, Typography, Card, CardHeader, CardActions, Theme, createStyles } from '@material-ui/core' import classNames from 'classnames' -import { SettingsDialog, SettingsDialogProps } from './SettingsDialog' -import { Platform, Currency, resolvePlatformName, Settings } from '../type' +import { resolvePlatformName } from '../type' import { getActivatedUI } from '../../../social-network/ui' -import stringify from 'json-stable-stringify' -import { currentTrendingViewSettings, currentTrendingViewPlatformSettings } from '../settings' -import { useValueRef } from '../../../utils/hooks/useValueRef' -import Services from '../../../extension/service' import { formatCurrency } from '../../Wallet/formatter' import { useColorStyles } from '../../../utils/theme' +import { useTrending } from '../hooks/useTrending' const network = getActivatedUI().networkIdentifier @@ -53,134 +35,46 @@ const useStyles = makeStyles((theme: Theme) => { }) export interface TrendingViewProps extends withClasses> { - keyword: string - SettingsDialogProps?: Partial + name: string } export function TrendingView(props: TrendingViewProps) { const classes = useStyles() const color = useColorStyles() - const [settingsDialogOpen, setSettingsDialogOpen] = useState(false) - - const [platform, setPlatform] = useState(Platform.COIN_GECKO) - const [currency, setCurrency] = useState(null) - - const networkKey = `${network}-${platform}` - const trendingSettings = useValueRef(currentTrendingViewSettings[networkKey]) - const trendingPlatformSettings = useValueRef(currentTrendingViewPlatformSettings[network]) - - //#region currency & platform - const { value: currencies = [], loading: loadingCurrencies } = useAsync( - () => Services.Plugin.invokePlugin('maskbook.trader', 'getCurrenies', platform), - [platform], - ) - - // sync platform - useEffect(() => { - if (String(platform) !== trendingPlatformSettings) { - if (trendingPlatformSettings === String(Platform.COIN_GECKO)) setPlatform(Platform.COIN_GECKO) - if (trendingPlatformSettings === String(Platform.COIN_MARKET_CAP)) setPlatform(Platform.COIN_MARKET_CAP) - } - }, [platform, trendingPlatformSettings]) - - // sync currency - useEffect(() => { - if (!currencies.length) return - try { - const parsed = JSON.parse(trendingSettings || '{}') as Settings - if (parsed.currency && currencies.some((x) => x.id === parsed.currency.id)) setCurrency(parsed.currency) - else setCurrency(currencies[0]) - } catch (e) { - setCurrency(null) - } - }, [trendingSettings, currencies.length]) - //#endregion - - //#region coins info - const { value: coinInfo, loading: loadingCoinInfo, error } = useAsync(async () => { - if (!currency) return null - return Services.Plugin.invokePlugin( - 'maskbook.trader', - 'getCoinTrendingByKeyword', - props.keyword, - platform, - currency, - ) - }, [platform, currency, props.keyword]) - //#endregion - - if (loadingCurrencies || loadingCoinInfo) - return ( - - - - - - ) - if (!currency) return null - if (!coinInfo) return null - + const { value: trending } = useTrending(props.name) + if (!trending) return null + const { currency, platform } = trending return ( - <> - - } - action={ - { - console.log('DEUBG: click') - setSettingsDialogOpen(true) - }}> - - - } - title={ - {`${coinInfo.coin.symbol.toUpperCase()} / ${ - currency.name - }`} - } - subheader={ - - {`${currency.symbol ?? `${currency.name} `}${formatCurrency( - coinInfo.market.current_price, - )}`} - {coinInfo.market.price_change_24h ? ( - 0 ? color.success : color.error, - )}> - {coinInfo.market.price_change_24h > 0 ? '\u25B2 ' : '\u25BC '} - {coinInfo.market.price_change_24h.toFixed(2)}% - - ) : null} - - } - /> - - - Powered by {resolvePlatformName(platform)} + + } + title={ + {`${trending.coin.symbol.toUpperCase()} / ${currency.name}`} + } + subheader={ + + {`${currency.symbol ?? `${currency.name} `}${formatCurrency( + trending.market.current_price, + )}`} + {trending.market.price_change_24h ? ( + 0 ? color.success : color.error, + )}> + {trending.market.price_change_24h > 0 ? '\u25B2 ' : '\u25BC '} + {trending.market.price_change_24h.toFixed(2)}% + + ) : null} - - - { - currentTrendingViewSettings[networkKey].value = stringify({ - currency, - }) - }} - onPlatformChange={(platform) => { - currentTrendingViewPlatformSettings[network].value = String(platform) - }} - onClose={() => setSettingsDialogOpen(false)} - {...props.SettingsDialogProps} + } /> - + + + Powered by {resolvePlatformName(platform)} + + + ) } diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index a32cfb9c3d1..15495d4bf69 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from 'react' +import React from 'react' import type { PluginConfig } from '../plugin' import { TypedMessage, @@ -7,6 +7,8 @@ import { TypedMessageCompound, } from '../../protocols/typed-message' import { makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' +import { TrendingPopper } from './UI/TrendingPopper' +import { TrendingView } from './UI/TrendingView' const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => isTypedMessgaeAnchor(m) && m.category === 'cash' @@ -20,4 +22,13 @@ export const TraderPluginDefine: PluginConfig = { items: message.items.map((m: TypedMessage) => (isCashTagMessage(m) ? makeTypedMessageCashTrending(m) : m)), } }, + pageInspector() { + return ( + + {(name: string) => { + return + }} + + ) + }, } diff --git a/src/plugins/Trader/hooks/useTrending.ts b/src/plugins/Trader/hooks/useTrending.ts new file mode 100644 index 00000000000..10551bc4395 --- /dev/null +++ b/src/plugins/Trader/hooks/useTrending.ts @@ -0,0 +1,56 @@ +import { useState, useEffect } from 'react' +import { Platform, Currency, Settings } from '../type' +import { useValueRef } from '../../../utils/hooks/useValueRef' +import { currentTrendingViewSettings, currentTrendingViewPlatformSettings } from '../settings' +import { getActivatedUI } from '../../../social-network/ui' +import { useAsync } from 'react-use' +import Services from '../../../extension/service' + +export function useTrending(keyword: string) { + const [platform, setPlatform] = useState(Platform.COIN_GECKO) + const [currency, setCurrency] = useState(null) + + const networkKey = `${getActivatedUI().networkIdentifier}-${platform}` + const trendingSettings = useValueRef(currentTrendingViewSettings[networkKey]) + const trendingPlatformSettings = useValueRef(currentTrendingViewPlatformSettings[networkKey]) + + //#region currency & platform + const { value: currencies = [], loading: loadingCurrencies, error: errorCurrencies } = useAsync( + () => Services.Plugin.invokePlugin('maskbook.trader', 'getCurrenies', platform), + [platform], + ) + + // sync platform + useEffect(() => { + if (String(platform) !== trendingPlatformSettings) { + if (trendingPlatformSettings === String(Platform.COIN_GECKO)) setPlatform(Platform.COIN_GECKO) + if (trendingPlatformSettings === String(Platform.COIN_MARKET_CAP)) setPlatform(Platform.COIN_MARKET_CAP) + } + }, [platform, trendingPlatformSettings]) + + // sync currency + useEffect(() => { + if (!currencies.length) return + try { + const parsed = JSON.parse(trendingSettings || '{}') as Settings + if (parsed.currency && currencies.some((x) => x.id === parsed.currency.id)) setCurrency(parsed.currency) + else setCurrency(currencies[0]) + } catch (e) { + setCurrency(null) + } + }, [trendingSettings, currencies.length]) + //#endregion + + //#region trending + const { value: trending, loading: loadingTrending, error: errorTrending } = useAsync(async () => { + if (!currency) return null + return Services.Plugin.invokePlugin('maskbook.trader', 'getCoinTrendingByKeyword', keyword, platform, currency) + }, [platform, currency, keyword]) + //#endregion + + return { + value: trending, + loading: loadingCurrencies || loadingTrending, + error: errorCurrencies || errorTrending, + } +} diff --git a/src/plugins/Trader/messages.ts b/src/plugins/Trader/messages.ts new file mode 100644 index 00000000000..5f36af87797 --- /dev/null +++ b/src/plugins/Trader/messages.ts @@ -0,0 +1,27 @@ +import type { Currency, Platform } from './type' +import { BatchedMessageCenter } from '../../utils/messages' + +export interface SettingsEvent { + currency: Currency + platform: Platform + currencies: Currency[] +} + +export interface ObserveCashTagEvent { + name: string + element: HTMLAnchorElement | null +} + +interface MaskbookTraderMessages { + /** + * View a cash tag + */ + cashTagObserved: ObserveCashTagEvent + + /** + * Update settings dialog + */ + settingsDialogUpdated: SettingsEvent +} + +export const MessageCenter = new BatchedMessageCenter(true, 'maskbook-trader-events') diff --git a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx index d5dacc9bf44..a278fad70f5 100644 --- a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -1,8 +1,8 @@ -import React, { useState, useRef } from 'react' +import React from 'react' import { TypedMessageAnchor, registerTypedMessageRenderer } from '../../../protocols/typed-message' -import { Link, Typography, Popper } from '@material-ui/core' +import { Link, Typography } from '@material-ui/core' import type { TypedMessageRendererProps } from '../../../components/InjectedComponents/TypedMessageRenderer' -import { TrendingView } from '../UI/TrendingView' +import { MessageCenter } from '../messages' export interface TypedMessageCashTrending extends Omit { readonly type: 'anchor/cash_trending' @@ -24,27 +24,18 @@ registerTypedMessageRenderer('anchor/cash_trending', { }) function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { - const rootEl = useRef(null) - const [anchorEl, setAnchorEl] = useState(null) + const onHoverCashTag = (ev: React.MouseEvent) => { + MessageCenter.emit('cashTagObserved', { + name: props.message.name, + element: ev.currentTarget, + }) + } return ( -
- - ) => setAnchorEl(e.currentTarget)}> - {props.message.content} - - - rootEl.current} - transition - style={{ zIndex: 1 }}> - - -
+ + + {props.message.content} + + ) } diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/type.ts index c233d64cbfd..e3883f1d82c 100644 --- a/src/plugins/Trader/type.ts +++ b/src/plugins/Trader/type.ts @@ -28,9 +28,9 @@ export interface Market { } export interface Trending { + currency: Currency platform: Platform coin: Coin - currency: Currency market: Market } diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 1baa52f6c8f..c46b7b17e33 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -11,8 +11,9 @@ export interface PluginConfig { pluginName: string identifier: string successDecryptionInspector?: PluginInjectFunction<{ message: TypedMessage }> - postMessageProcessor?: (message: TypedMessageCompound) => TypedMessageCompound + pageInspector?: React.ComponentType<{}> postInspector?: PluginInjectFunction<{}> + postMessageProcessor?: (message: TypedMessageCompound) => TypedMessageCompound postDialogMetadataBadge?: Map string> } diff --git a/src/social-network-provider/facebook.com/UI/injectPageInspector.tsx b/src/social-network-provider/facebook.com/UI/injectPageInspector.tsx new file mode 100644 index 00000000000..7a260a5b264 --- /dev/null +++ b/src/social-network-provider/facebook.com/UI/injectPageInspector.tsx @@ -0,0 +1,3 @@ +export function injectPageInspectorFacebook() { + return () => {} +} diff --git a/src/social-network-provider/facebook.com/ui-provider.ts b/src/social-network-provider/facebook.com/ui-provider.ts index 123f98daea0..be976bb80ed 100644 --- a/src/social-network-provider/facebook.com/ui-provider.ts +++ b/src/social-network-provider/facebook.com/ui-provider.ts @@ -16,6 +16,7 @@ import { dispatchCustomEvents, selectElementContents, sleep } from '../../utils/ import { collectPostsFacebook } from './UI/collectPosts' import { injectPostDummyFacebook } from './UI/injectPostDummy' import { injectPostInspectorFacebook } from './UI/injectPostInspector' +import { injectPageInspectorFacebook } from './UI/injectPageInspector' import { setStorage } from '../../utils/browser.storage' import { isMobileFacebook } from './isMobile' import { i18n } from '../../utils/i18n-next' @@ -94,6 +95,7 @@ export const facebookUISelf = defineSocialNetworkUI({ }), injectPostDummy: injectPostDummyFacebook, injectPostInspector: injectPostInspectorFacebook, + injectPageInspector: injectPageInspectorFacebook, collectPeople: collectPeopleFacebook, collectPosts: collectPostsFacebook, taskPasteIntoBio: pasteIntoBioFacebook, diff --git a/src/social-network-provider/twitter.com/ui/inject.tsx b/src/social-network-provider/twitter.com/ui/inject.tsx index d8e70ab16a4..16c859aed7e 100644 --- a/src/social-network-provider/twitter.com/ui/inject.tsx +++ b/src/social-network-provider/twitter.com/ui/inject.tsx @@ -3,6 +3,7 @@ import { injectKnownIdentityAtTwitter } from './injectKnownIdentity' import { injectPostDialogAtTwitter } from './injectPostDialog' import { injectPostDialogHintAtTwitter } from './injectPostDialogHint' import { injectPostInspectorAtTwitter } from './injectPostInspector' +import { injectPageInspectorAtTwitter } from './injectPageInspector' import { injectPostDialogIconAtTwitter } from './injectPostDialogIcon' import { injectPostDummyAtTwitter } from './injectPostDummy' @@ -16,5 +17,6 @@ export const twitterUIInjections: SocialNetworkUIInjections = { injectPostBox, injectPostDummy: injectPostDummyAtTwitter, injectPostInspector: injectPostInspectorAtTwitter, + injectPageInspector: injectPageInspectorAtTwitter, injectKnownIdentity: injectKnownIdentityAtTwitter, } diff --git a/src/social-network-provider/twitter.com/ui/injectPageInspector.tsx b/src/social-network-provider/twitter.com/ui/injectPageInspector.tsx new file mode 100644 index 00000000000..a6a359eee8c --- /dev/null +++ b/src/social-network-provider/twitter.com/ui/injectPageInspector.tsx @@ -0,0 +1,5 @@ +import { injectPageInspectorDefault } from '../../../social-network/defaults/injectPageInspector' + +export function injectPageInspectorAtTwitter() { + return injectPageInspectorDefault()() +} diff --git a/src/social-network/defaults/emptyDefinition.ts b/src/social-network/defaults/emptyDefinition.ts index 36ecc733eff..c47567e40a5 100644 --- a/src/social-network/defaults/emptyDefinition.ts +++ b/src/social-network/defaults/emptyDefinition.ts @@ -32,6 +32,7 @@ export const emptyDefinition: SocialNetworkUIDefinition = { injectPostComments: nopWithUnmount, injectPostDummy: nopWithUnmount, injectPostInspector: nopWithUnmount, + injectPageInspector: nopWithUnmount, resolveLastRecognizedIdentity: nop, posts: new ObservableWeakMap(), friendsRef: new ValueRef([], ProfileArrayComparer), diff --git a/src/social-network/defaults/injectPageInspector.tsx b/src/social-network/defaults/injectPageInspector.tsx new file mode 100644 index 00000000000..b92841f97c8 --- /dev/null +++ b/src/social-network/defaults/injectPageInspector.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core' +import { PageInspector, PageInspectorProps } from '../../components/InjectedComponents/PageInspector' +import { renderInShadowRoot } from '../../utils/jss/renderInShadowRoot' +import { MutationObserverWatcher, LiveSelector } from '@holoflows/kit/es' + +export function injectPageInspectorDefault( + config: InjectPageInspectorDefaultConfig = {}, + additionalPropsToPageInspector: (classes: Record) => Partial = () => ({}), + useCustomStyles: (props?: any) => Record = makeStyles({}) as any, +) { + const PageInspectorDefault = React.memo(function PageInspectorDefault() { + const classes = useCustomStyles() + const additionalProps = additionalPropsToPageInspector(classes) + return + }) + + return function injectPageInspector() { + const watcher = new MutationObserverWatcher(new LiveSelector().querySelector('body')) + .setDOMProxyOption({ + afterShadowRootInit: { mode: webpackEnv.shadowRootMode }, + }) + .startWatch({ + childList: true, + subtree: true, + }) + + return renderInShadowRoot(, { + shadow: () => watcher.firstDOMProxy.afterShadow, + normal: () => watcher.firstDOMProxy.after, + }) + } +} + +interface InjectPageInspectorDefaultConfig {} diff --git a/src/social-network/ui.ts b/src/social-network/ui.ts index 58823f4b4cd..bef45c6e8b3 100644 --- a/src/social-network/ui.ts +++ b/src/social-network/ui.ts @@ -97,6 +97,10 @@ export interface SocialNetworkUIInjections { * This function should inject UI into the Post box */ injectPostBox(): void + /** + * This function should inject rthe page inspector + */ + injectPageInspector(): void /** * This is an optional function. * @@ -300,6 +304,7 @@ export function activateSocialNetworkUI(): void { ui.init(env, {}) ui.resolveLastRecognizedIdentity() ui.injectPostBox() + ui.injectPageInspector() ui.collectPeople() ui.collectPosts() ui.myIdentitiesRef.addListener((val) => {