diff --git a/package.json b/package.json index 6af4357bd5a..eeb6c191903 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@servie/events": "^1.0.0", "@types/bip39": "^3.0.0", "@types/classnames": "^2.2.10", + "@types/d3": "^5.7.2", "@types/elliptic": "^6.4.12", "@types/gun": "^0.9.2", "@types/json-stable-stringify": "^1.0.32", @@ -140,7 +141,6 @@ "@storybook/addons": "^5.3.17", "@storybook/react": "^5.3.19", "@testing-library/react-hooks": "^3.4.1", - "@types/d3": "^5.7.2", "@types/elliptic": "^6.4.12", "@types/enzyme": "^3.10.5", "@types/enzyme-adapter-react-16": "^1.0.6", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 13e23bccdb4..18fccc33c75 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -123,6 +123,9 @@ "import_your_persona": "Import Your Persona", "internal_id": "Internal ID", "keywords": "Keywords", + "language_en": "English", + "language_zh": "中文", + "language_ja": "日本語", "mnemonic_words": "Mnemonic Words", "my_personas": "My Personas", "my_wallets": "My Wallets", @@ -241,6 +244,11 @@ "settings_appearance_light": "Light", "settings_language": "Language", "settings_choose_eth_network": "Choose Ethereum Network", + "settings_post_replace_all_posts": "All Posts", + "settings_post_replace_enhanced_posts": "Enhanced Posts", + "settings_post_replace_encrypted_posts": "Encrypted Posts", + "settings_post_replacement_scope": "Post Replacement Scope", + "settings_post_replacement_scope_desc": "Maskbook will render a new post to replace the original one to provide some enhancement.", "skip": "Skip", "share": "Share", "share_to": "Share to…", @@ -284,5 +292,13 @@ "plugin_file_service_drop_here": "Drop a file here to upload", "plugin_file_service_error_101": "The input is not a single file.", "plugin_file_service_error_102": "The file is too large; limit is {{limit}}.", - "plugin_file_service_on_change_file": "Change File" + "plugin_file_service_on_change_file": "Change File", + "plugin_trader_no_data": "No Data", + "plugin_trader_tab_price": "Price", + "plugin_trader_tab_exchange": "Exchange", + "plugin_trader_tab_switch_data_source": "Switch Data Source: ", + "plugin_trader_table_exchange": "Exchange", + "plugin_trader_table_pair": "Pair", + "plugin_trader_table_price": "Price", + "plugin_trader_table_volume": "Volume (24h)" } diff --git a/src/_locales/ja/messages.json b/src/_locales/ja/messages.json index d9057f4c5e9..c15576bcf94 100644 --- a/src/_locales/ja/messages.json +++ b/src/_locales/ja/messages.json @@ -123,6 +123,9 @@ "import_your_persona": "人格をインポート", "internal_id": "内部 ID", "keywords": "キーワード", + "language_en": "English", + "language_zh": "中文", + "language_ja": "日本語", "mnemonic_words": "パスフレーズ", "my_personas": "私の人格アカウント", "my_wallets": "私のウォレット", @@ -241,6 +244,11 @@ "settings_appearance_light": "ライトモード", "settings_language": "言語を設定", "settings_choose_eth_network": "Ethereum ネットワークを選択してください", + "settings_post_replace_all_posts": "すべての投稿", + "settings_post_replace_enhanced_posts": "強化された投稿", + "settings_post_replace_encrypted_posts": "暗号化された投稿", + "settings_post_replacement_scope": "ポスト交換範囲", + "settings_post_replacement_scope_desc": "Maskbook は、新しい投稿をレンダリングして元の投稿を置き換え、いくつかの拡張機能を提供します。", "skip": "スキップ", "share": "共有", "share_to": "共有先", @@ -284,5 +292,13 @@ "plugin_file_service_drop_here": "ファイルをここにドラッグ&ドロップ", "plugin_file_service_error_101": "一つのファイルだけにしてください!", "plugin_file_service_error_102": "ファイルが大きすぎます!最大容量は{{limit}}です。", - "plugin_file_service_on_change_file": "ファイルの変更" + "plugin_file_service_on_change_file": "ファイルの変更", + "plugin_trader_no_data": "データなし", + "plugin_trader_tab_price": "価格", + "plugin_trader_tab_exchange": "取引所", + "plugin_trader_tab_switch_data_source": "データソースの切り替え: ", + "plugin_trader_table_exchange": "取引所", + "plugin_trader_table_pair": "通貨ペア", + "plugin_trader_table_price": "価格", + "plugin_trader_table_volume": "取引高" } diff --git a/src/_locales/zh/messages.json b/src/_locales/zh/messages.json index 628081cb994..06b39f537e4 100644 --- a/src/_locales/zh/messages.json +++ b/src/_locales/zh/messages.json @@ -123,6 +123,9 @@ "import_your_persona": "導入角色", "internal_id": "內部ID", "keywords": "關鍵字", + "language_en": "English", + "language_zh": "中文", + "language_ja": "日本語", "mnemonic_words": "助記詞", "my_personas": "我的角色", "my_wallets": "我的錢包", @@ -241,6 +244,11 @@ "settings_appearance_light": "淺色", "settings_language": "語言", "settings_choose_eth_network": "選擇以太坊網絡", + "settings_post_replace_all_posts": "所有帖子", + "settings_post_replace_enhanced_posts": "僅增強的帖子", + "settings_post_replace_encrypted_posts": "僅加密的帖子", + "settings_post_replacement_scope": "帖子替換範圍", + "settings_post_replacement_scope_desc": "Maskbook 將呈現一個新帖子以替換原始帖子,以提供一些增強功能。", "skip": "跳過", "share": "分享", "share_to": "分享给…", @@ -284,5 +292,13 @@ "plugin_file_service_drop_here": "拖入文件來上傳", "plugin_file_service_error_101": "只能選擇單個文檔", "plugin_file_service_error_102": "文檔尺寸過大。請不要超過 {{limit}}。", - "plugin_file_service_on_change_file": "選擇其他文檔" + "plugin_file_service_on_change_file": "選擇其他文檔", + "plugin_trader_no_data": "暫無數據", + "plugin_trader_tab_price": "價格", + "plugin_trader_tab_exchange": "交易標的", + "plugin_trader_tab_switch_data_source": "切換數據來源: ", + "plugin_trader_table_exchange": "交易標的", + "plugin_trader_table_pair": "對", + "plugin_trader_table_price": "價格", + "plugin_trader_table_volume": "交易量 (24小時)" } diff --git a/src/components/InjectedComponents/AdditionalPostContent.tsx b/src/components/InjectedComponents/AdditionalPostContent.tsx index e95d8176171..17826b0759a 100644 --- a/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -29,7 +29,7 @@ const useStyles = makeStyles((theme: Theme) => ({ root: { boxSizing: 'border-box', width: '100%', backgroundColor: 'transparent', borderColor: 'transparent' }, title: { display: 'flex', alignItems: 'center' }, icon: { paddingRight: theme.spacing(0.75), display: 'flex', width: 20, height: 20 }, - content: { margin: theme.spacing(1, 0), padding: 0 }, + content: { margin: theme.spacing(1, 0), padding: 0, overflowWrap: 'break-word' }, rightIcon: { paddingLeft: theme.spacing(0.75) }, })) diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx deleted file mode 100644 index 1effd5217c1..00000000000 --- a/src/components/InjectedComponents/PostDummy.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useEffect } from 'react' -import { usePostInfoDetails } from '../DataSource/usePostInfo' -import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' -import { PluginUI } from '../../plugins/plugin' -import { makeTypedMessageCompound, isTypedMessageSuspended, isTypedMessageKnown } from '../../protocols/typed-message' -import { useValueRef } from '../../utils/hooks/useValueRef' -import { currentWholePostVisibilitySettings, WholePostVisibility } from '../../settings/settings' - -export interface PostDummyProps { - zip?: () => void - unzip?: () => void -} - -export function PostDummy(props: PostDummyProps) { - const parsedPostContent = usePostInfoDetails('parsedPostContent') - const postPayload = usePostInfoDetails('postPayload') - const wholePostVisibilitySettings = useValueRef(currentWholePostVisibilitySettings) - - const processedPostMessage = Array.from(PluginUI.values()).reduce( - (x, plugin) => (plugin.postMessageProcessor ? plugin.postMessageProcessor(x) : x), - parsedPostContent, - ) - const postDummyVisible = - // render dummy for all posts - wholePostVisibilitySettings === WholePostVisibility.all || - // render dummy for posts which enhanced by plugins - (wholePostVisibilitySettings === WholePostVisibility.enhancedOnly && - processedPostMessage.items.some((x) => !isTypedMessageKnown(x))) || - // render dummy for posts which encrypted by maskbook - (wholePostVisibilitySettings === WholePostVisibility.encryptedOnly && postPayload.ok) - - // zip original post - useEffect(() => { - if (postDummyVisible) props.zip?.() - else props.unzip?.() - }, [postDummyVisible]) - - return postDummyVisible ? ( - !isTypedMessageSuspended(x)))} - /> - ) : null -} diff --git a/src/components/InjectedComponents/PostReplacer.tsx b/src/components/InjectedComponents/PostReplacer.tsx new file mode 100644 index 00000000000..0699df06c36 --- /dev/null +++ b/src/components/InjectedComponents/PostReplacer.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useMemo } from 'react' +import { usePostInfoDetails } from '../DataSource/usePostInfo' +import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' +import { PluginUI } from '../../plugins/plugin' +import { makeTypedMessageCompound, isTypedMessageSuspended, isTypedMessageKnown } from '../../protocols/typed-message' +import { useValueRef } from '../../utils/hooks/useValueRef' +import { currentPostReplacementScopeSettings, PostReplacementScope } from '../../settings/settings' +import { makeStyles, Theme } from '@material-ui/core' + +const useStlyes = makeStyles((theme: Theme) => ({ + root: { + overflowWrap: 'break-word', + }, +})) + +export interface PostReplacerProps { + zip?: () => void + unzip?: () => void +} + +export function PostReplacer(props: PostReplacerProps) { + const classes = useStlyes() + const postContent = usePostInfoDetails('postContent') + const postMessage = usePostInfoDetails('postMessage') + const postPayload = usePostInfoDetails('postPayload') + const postRepalcementScope = useValueRef(currentPostReplacementScopeSettings) + + const plugins = [...PluginUI.values()] + const processedPostMessage = useMemo( + () => plugins.reduce((x, plugin) => plugin.messageProcessor?.(x) ?? x, postMessage), + [plugins.map((x) => x.identifier).join(), postContent], + ) + const shouldReplacePost = + // replace all posts + postRepalcementScope === PostReplacementScope.all || + // replace posts which enhanced by plugins + (postRepalcementScope === PostReplacementScope.enhancedOnly && + processedPostMessage.items.some((x) => !isTypedMessageKnown(x))) || + // replace posts which encrypted by maskbook + (postRepalcementScope === PostReplacementScope.encryptedOnly && postPayload.ok) + + // zip/unzip original post + useEffect(() => { + if (shouldReplacePost) props.zip?.() + else props.unzip?.() + }, [shouldReplacePost]) + + return shouldReplacePost ? ( + + !isTypedMessageSuspended(x)), + )} + /> + + ) : null +} diff --git a/src/components/InjectedComponents/TypedMessageRenderer.tsx b/src/components/InjectedComponents/TypedMessageRenderer.tsx index 8a9841fb18f..70a44890960 100644 --- a/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ b/src/components/InjectedComponents/TypedMessageRenderer.tsx @@ -82,7 +82,7 @@ export const DefaultTypedMessageImageRenderer = React.memo(function DefaultTyped const { image, width, height } = props.message return renderWithMetadata( props, - + , ) diff --git a/src/extension/options-page/DashboardRouters/Settings.tsx b/src/extension/options-page/DashboardRouters/Settings.tsx index f46a50fe804..54b1f3d671d 100644 --- a/src/extension/options-page/DashboardRouters/Settings.tsx +++ b/src/extension/options-page/DashboardRouters/Settings.tsx @@ -9,11 +9,11 @@ import { languageSettings, Language, renderInShadowRootSettings, - currentWholePostVisibilitySettings, + currentPostReplacementScopeSettings, currentLocalWalletEthereumNetworkSettings, appearanceSettings, Appearance, - WholePostVisibility, + PostReplacementScope, } from '../../../settings/settings' import { useValueRef } from '../../../utils/hooks/useValueRef' @@ -22,7 +22,7 @@ import NoEncryptionIcon from '@material-ui/icons/NoEncryption' import MemoryOutlinedIcon from '@material-ui/icons/MemoryOutlined' import ArchiveOutlinedIcon from '@material-ui/icons/ArchiveOutlined' import UnarchiveOutlinedIcon from '@material-ui/icons/UnarchiveOutlined' -import VisibilityIcon from '@material-ui/icons/Visibility' +import FlipToFrontIcon from '@material-ui/icons/FlipToFront' import TabIcon from '@material-ui/icons/Tab' import PaletteIcon from '@material-ui/icons/Palette' import LanguageIcon from '@material-ui/icons/Language' @@ -118,11 +118,11 @@ export default function DashboardSettingsRouter() { const { t } = useI18N() const currentLang = useValueRef(languageSettings) const currentApperance = useValueRef(appearanceSettings) - const currentWholePostVisibility = useValueRef(currentWholePostVisibilitySettings) + const currentPostReplacementScope = useValueRef(currentPostReplacementScopeSettings) const langMapper = React.useRef((x: Language) => { - if (x === Language.en) return 'English' - if (x === Language.zh) return '中文' - if (x === Language.ja) return '日本語' + if (x === Language.en) return t('language_en') + if (x === Language.zh) return t('language_zh') + if (x === Language.ja) return t('language_ja') return x }).current const apperanceMapper = React.useRef((x: Appearance) => { @@ -130,10 +130,10 @@ export default function DashboardSettingsRouter() { if (x === Appearance.light) return t('settings_appearance_light') return t('settings_appearance_default') }).current - const wholePostVisibilityMapper = React.useRef((x: WholePostVisibility) => { - if (x === WholePostVisibility.all) return 'All Posts' - if (x === WholePostVisibility.encryptedOnly) return 'Encrypted Posts' - return 'Enhanced Posts' + const postReplacerMapper = React.useRef((x: PostReplacementScope) => { + if (x === PostReplacementScope.all) return t('settings_post_replace_all_posts') + if (x === PostReplacementScope.encryptedOnly) return t('settings_post_replace_encrypted_posts') + return t('settings_post_replace_enhanced_posts') }).current const classes = useStyles() const shadowRoot = useValueRef(renderInShadowRootSettings) @@ -184,11 +184,10 @@ export default function DashboardSettingsRouter() { ) : null} } - value={currentWholePostVisibilitySettings} + enumObject={PostReplacementScope} + getText={postReplacerMapper} + icon={} + value={currentPostReplacementScopeSettings} /> diff --git a/src/plugins/Trader/UI/PriceChanged.tsx b/src/plugins/Trader/UI/PriceChanged.tsx index 1b695a0b63a..72eeed6a439 100644 --- a/src/plugins/Trader/UI/PriceChanged.tsx +++ b/src/plugins/Trader/UI/PriceChanged.tsx @@ -2,12 +2,24 @@ import React from 'react' import classNames from 'classnames' import { useColorStyles } from '../../../utils/theme' import { makeStyles, Theme, createStyles } from '@material-ui/core' +import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp' +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown' const useStyles = makeStyles((theme: Theme) => { return createStyles({ root: { fontSize: 'inherit', - marginLeft: theme.spacing(1), + position: 'relative', + }, + icon: { + top: 0, + bottom: 0, + margin: 'auto', + position: 'absolute', + verticalAlign: 'middle', + }, + value: { + marginLeft: theme.spacing(3), }, }) }) @@ -21,8 +33,9 @@ export function PriceChanged(props: PriceChangedProps) { const classes = useStyles() return ( 0 ? color.success : color.error)}> - {props.amount > 0 ? '\u25B2 ' : '\u25BC '} - {props.amount.toFixed(2)}% + {props.amount > 0 ? : null} + {props.amount < 0 ? : null} + {props.amount.toFixed(2)}% ) } diff --git a/src/plugins/Trader/UI/PriceChangedTable.tsx b/src/plugins/Trader/UI/PriceChangedTable.tsx index 4832af8165e..1320e7ed665 100644 --- a/src/plugins/Trader/UI/PriceChangedTable.tsx +++ b/src/plugins/Trader/UI/PriceChangedTable.tsx @@ -15,11 +15,15 @@ import { PriceChanged } from './PriceChanged' const useStyles = makeStyles((theme: Theme) => createStyles({ - container: {}, + container: { + '&::-webkit-scrollbar': { + display: 'none', + }, + }, table: {}, cell: { - paddingLeft: theme.spacing(1.5), - paddingRight: theme.spacing(1), + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1.5), whiteSpace: 'nowrap', }, }), diff --git a/src/plugins/Trader/UI/PriceChart.tsx b/src/plugins/Trader/UI/PriceChart.tsx index 553ee1df0ac..ca5831e174b 100644 --- a/src/plugins/Trader/UI/PriceChart.tsx +++ b/src/plugins/Trader/UI/PriceChart.tsx @@ -1,15 +1,17 @@ -import React, { useRef, useEffect } from 'react' -import * as d3 from 'd3' +import React, { useRef } from 'react' import type { Stat } from '../types' import { makeStyles, Theme, createStyles, CircularProgress, Typography } from '@material-ui/core' +import { useDimension, Dimension } from '../graphs/useDimension' +import { usePriceLineChart } from '../graphs/usePriceLineChart' +import { useI18N } from '../../../utils/i18n-next-ui' -const DEFAULT_WIDTH = 460 -const DEFAULT_HEIGHT = 250 -const DEFAULT_MARGIN = { +const DEFAULT_DIMENSION: Dimension = { top: 32, right: 16, bottom: 32, left: 16, + width: 410, + height: 200, } const useStyles = makeStyles((theme: Theme) => { @@ -24,7 +26,8 @@ const useStyles = makeStyles((theme: Theme) => { position: 'absolute', }, placeholder: { - paddingTop: theme.spacing(14), + paddingTop: theme.spacing(10), + borderStyle: 'none', }, }) }) @@ -38,98 +41,36 @@ export interface PriceChartProps { } export function PriceChart(props: PriceChartProps) { + const { t } = useI18N() const classes = useStyles() const svgRef = useRef(null) - // define dimensions - const { - width = DEFAULT_WIDTH - DEFAULT_MARGIN.left - DEFAULT_MARGIN.right, - height = DEFAULT_HEIGHT - DEFAULT_MARGIN.top - DEFAULT_MARGIN.bottom, - } = props - const canvasWidth = width + DEFAULT_MARGIN.left + DEFAULT_MARGIN.right - const canvasHeight = height + DEFAULT_MARGIN.top + DEFAULT_MARGIN.bottom - - // process data - const data = props.stats.map(([date, price]) => ({ - date: new Date(date), - value: price, - })) - - useEffect(() => { - if (!svgRef.current) return - - // empty the svg - svgRef.current.innerHTML = '' - - // render savg if necessary - if (!props.stats.length) return - - // contine to create the chart - const svg = d3 - .select(svgRef.current) - .attr('width', canvasWidth) - .attr('height', canvasHeight) - .append('g') - .attr('transform', `translate(${DEFAULT_MARGIN.left}, ${DEFAULT_MARGIN.top})`) - - // create X axis - const x = d3 - .scaleTime() - .domain(d3.extent(data, (d) => d.date) as [Date, Date]) - .range([0, width]) - - // create Y axis - const min = d3.min(data, (d) => d.value) as number - const max = d3.max(data, (d) => d.value) as number - const dist = Math.abs(max - min) - const y = d3 - .scaleLinear() - .domain([min - dist * 0.05, max + dist * 0.05]) - .range([height, 0]) - - // add X axis - svg.append('g') - .attr('transform', `translate(0, ${height})`) - .call(d3.axisBottom(x).ticks(width / 100)) - - // add Y axis - svg.append('g') - .attr('transform', `translate(0, 0)`) - .call( - d3 - .axisRight(y) - .ticks(height / 50, '$,.2s') - .tickSize(width), - ) - .call((g) => g.select('.domain').remove()) - .call((g) => g.selectAll('.tick line').attr('stroke-opacity', 0.5).attr('stroke-dasharray', '2,2')) - .call((g) => g.selectAll('.tick text').attr('x', 4).attr('dy', -4)) - - // add line - svg.append('path') - .datum(data) - .attr('fill', 'none') - .attr('stroke', 'steelblue') - .attr('stroke-width', 1.5) - .attr( - 'd', - d3 - .line() - .x((d) => x((d as any).date)) - .y((d) => y((d as any).value)) as any, - ) - }, [svgRef, data.length]) + useDimension(svgRef, DEFAULT_DIMENSION) + usePriceLineChart( + svgRef, + props.stats.map(([date, price]) => ({ + date: new Date(date), + value: price, + })), + DEFAULT_DIMENSION, + 'x-trader-price-line-chart', + ) return ( -
+
{props.loading ? : null} {props.stats.length ? ( <> {props.children} - + ) : ( - No Data + {t('plugin_trader_no_data')} )}
diff --git a/src/plugins/Trader/UI/ScreenModal.tsx b/src/plugins/Trader/UI/ScreenModal.tsx deleted file mode 100644 index a18ffa8f2dd..00000000000 --- a/src/plugins/Trader/UI/ScreenModal.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export interface TraderScreenModalProps {} - -export function TraderScreenModal(props: TraderScreenModalProps) { - return null -} diff --git a/src/plugins/Trader/UI/SettingsDialog.tsx b/src/plugins/Trader/UI/SettingsDialog.tsx deleted file mode 100644 index 033c2448bbb..00000000000 --- a/src/plugins/Trader/UI/SettingsDialog.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import React from 'react' -import { useI18N } from '../../../utils/i18n-next-ui' -import { - makeStyles, - DialogTitle, - IconButton, - Typography, - DialogContent, - Theme, - DialogProps, - FormControl, - Select, - MenuItem, - InputLabel, - createStyles, - DialogActions, - Button, - Divider, - MenuProps, -} from '@material-ui/core' -import ShadowRootDialog from '../../../utils/jss/ShadowRootDialog' -import { DialogDismissIconUI } from '../../../components/InjectedComponents/DialogDismissIcon' -import { Currency, Platform, resolveCurrencyName, resolvePlatformName } from '../types' -import { useStylesExtends } from '../../../components/custom-ui-helper' -import { getActivatedUI } from '../../../social-network/ui' -import { - useTwitterDialog, - useTwitterButton, - useTwitterCloseButton, -} from '../../../social-network-provider/twitter.com/utils/theme' -import { PortalShadowRoot } from '../../../utils/jss/ShadowRootPortal' -import { getEnumAsArray } from '../../../utils/enum' - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - title: { - marginLeft: 6, - }, - form: { - '& > *': { - margin: theme.spacing(1, 0), - }, - }, - menuPaper: { - maxHeight: 300, - }, - }), -) - -interface SettingsDialogUIProps - extends withClasses< - | KeysInferFromUseStyles - | 'root' - | 'dialog' - | 'backdrop' - | 'container' - | 'paper' - | 'header' - | 'content' - | 'actions' - | 'close' - | 'button' - > { - open: boolean - theme?: Theme - currencies: Currency[] - currency: Currency - platform: Platform - onCurrencyChange?: (currency: Currency) => void - 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) - return ( -
- - - - - - - {t('post_dialog__title')} - - - - -
- - Data Source - - - - Currency - - -
-
- - - -
-
- ) -} - -export interface SettingsDialogProps extends SettingsDialogUIProps {} - -export function SettingsDialog(props: SettingsDialogProps) { - const ui = getActivatedUI() - const twitterClasses = { - ...useTwitterDialog(), - ...useTwitterButton(), - ...useTwitterCloseButton(), - } - return ui.internalName === 'twitter' ? ( - - ) : ( - - ) -} diff --git a/src/plugins/Trader/UI/TickersTable.tsx b/src/plugins/Trader/UI/TickersTable.tsx index f8f74ff9830..1a6e1469edb 100644 --- a/src/plugins/Trader/UI/TickersTable.tsx +++ b/src/plugins/Trader/UI/TickersTable.tsx @@ -14,11 +14,15 @@ import { } from '@material-ui/core' import type { Ticker, Platform } from '../types' import { formatCurrency, formatEthAddress } from '../../Wallet/formatter' +import { useI18N } from '../../../utils/i18n-next-ui' const useStyles = makeStyles((theme: Theme) => createStyles({ container: { - height: 316, + height: 266, + '&::-webkit-scrollbar': { + display: 'none', + }, }, table: {}, cell: { @@ -31,7 +35,7 @@ const useStyles = makeStyles((theme: Theme) => height: 20, }, placeholder: { - paddingTop: theme.spacing(16), + paddingTop: theme.spacing(10), borderStyle: 'none', }, }), @@ -43,8 +47,14 @@ export interface TickersTableProps { } export function TickersTable(props: TickersTableProps) { + const { t } = useI18N() const classes = useStyles() - const rows = ['Exchange', 'Pair', 'Price', 'Volumn (24h)'] + const rows = [ + t('plugin_trader_table_exchange'), + t('plugin_trader_table_pair'), + t('plugin_trader_table_price'), + t('plugin_trader_table_volume'), + ] const tickers = props.tickers.map((ticker, index) => ( {ticker.market_name} @@ -60,8 +70,8 @@ export function TickersTable(props: TickersTableProps) { ) })()} - ${formatCurrency(ticker.price)} - ${formatCurrency(ticker.volumn)} + {formatCurrency(ticker.price, '$')} + {formatCurrency(ticker.volume, '$')} )) @@ -84,7 +94,7 @@ export function TickersTable(props: TickersTableProps) { - No Data + {t('plugin_trader_no_data')} diff --git a/src/plugins/Trader/UI/TrendingPopper.tsx b/src/plugins/Trader/UI/TrendingPopper.tsx index eedffcfccb7..f31cdbc63f9 100644 --- a/src/plugins/Trader/UI/TrendingPopper.tsx +++ b/src/plugins/Trader/UI/TrendingPopper.tsx @@ -1,7 +1,8 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react' +import React, { useState, useEffect, useRef } from 'react' import type PopperJs from 'popper.js' -import { Popper, ClickAwayListener, PopperProps } from '@material-ui/core' -import { MessageCenter, ObserveCashTagEvent } from '../messages' +import { Popper, ClickAwayListener, PopperProps, Fade } from '@material-ui/core' +import { MessageCenter } from '../messages' +import { useLocation, useWindowScroll } from 'react-use' export interface TrendingPopperProps { children?: (name: string, reposition?: () => void) => React.ReactNode @@ -13,29 +14,62 @@ export function TrendingPopper(props: TrendingPopperProps) { const [name, setName] = useState('') const [anchorEl, setAnchorEl] = useState(null) + // open popper + useEffect( + () => + MessageCenter.on('cashTagObserved', (ev) => { + const update = () => { + setName(ev.name) + setAnchorEl(ev.element) + } + + // close popper on previous element + if (anchorEl && anchorEl !== ev.element) { + setAnchorEl(null) + setTimeout(update, 400) + return + } + update() + }), + [anchorEl], + ) + + // close popper if location was changed + const location = useLocation() + useEffect(() => { + setAnchorEl(null) + }, [location.state?.key, location.href]) + + // close popper if scroll out of visual screen + const position = useWindowScroll() useEffect(() => { - const off = MessageCenter.on('cashTagObserved', (ev: ObserveCashTagEvent) => { - setName(ev.name) - setAnchorEl(ev.element) - }) - return () => { - off() + if (!anchorEl) return + const rect = anchorEl.getBoundingClientRect() + if ( + rect.top < 0 || // out off top bound + rect.top > document.documentElement.clientHeight // out off bottom bound + ) setAnchorEl(null) - } - }, []) + }, [anchorEl, Math.floor(position.y / 50)]) if (!anchorEl) return null return ( setAnchorEl(null)}> - {props.children?.(name, () => setTimeout(() => popperRef.current?.scheduleUpdate(), 0))} + {({ TransitionProps }) => ( + +
+ {props.children?.(name, () => setTimeout(() => popperRef.current?.scheduleUpdate(), 100))} +
+
+ )}
) diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index 2f875074f20..20d250fbb25 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -31,12 +31,13 @@ import { PriceChartDaysControl } from './PriceChartDaysControl' import { useCurrentPlatform } from '../hooks/useCurrentPlatform' import { useCurrentCurrency } from '../hooks/useCurrentCurrency' import { currentTrendingViewPlatformSettings } from '../settings' +import { useI18N } from '../../../utils/i18n-next-ui' const useStyles = makeStyles((theme: Theme) => { const internalName = getActivatedUI()?.internalName return createStyles({ root: { - width: 500, + width: 450, overflow: 'auto', '&::-webkit-scrollbar': { display: 'none', @@ -63,18 +64,15 @@ const useStyles = makeStyles((theme: Theme) => { justifyContent: 'flex-end', }, tabs: { - width: 468, + width: '100%', + height: 35, + minHeight: 'unset', }, - section: {}, - description: { - overflow: 'auto', - maxHeight: '4.3em', - wordBreak: 'break-word', - marginBottom: theme.spacing(2), - '&::-webkit-scrollbar': { - display: 'none', - }, + tab: { + height: 35, + minHeight: 'unset', }, + section: {}, rank: { color: theme.palette.text.primary, fontWeight: 300, @@ -97,12 +95,38 @@ const useStyles = makeStyles((theme: Theme) => { }) }) +//#region skeleton +interface TrendingViewSkeletonProps {} + +function TrendingViewSkeleton(props: TrendingViewSkeletonProps) { + const classes = useStyles() + return ( + + } + title={} + subheader={} + /> + + + + + + + + + ) +} +//#endregion + +//#region trending view export interface TrendingViewProps extends withClasses> { name: string onUpdate?: () => void } export function TrendingView(props: TrendingViewProps) { + const { t } = useI18N() const classes = useStyles() const [tabIndex, setTabIndex] = useState(0) @@ -129,30 +153,14 @@ export function TrendingView(props: TrendingViewProps) { //#endregion //#region display loading skeleton - if (loadingCurrency || loadingTrending) - return ( - - } - title={} - subheader={} - /> - - - - - - - - - ) + if (loadingCurrency || loadingTrending) return //#endregion //#region error handling // error: fail to load currency if (!currency) return null - // error: unknown coin + // error: unknown coin or api error if (!trending) return null //#endregion @@ -207,8 +215,8 @@ export function TrendingView(props: TrendingViewProps) { display: 'none', }, }}> - - + + {tabIndex === 0 ? ( <> @@ -224,16 +232,14 @@ export function TrendingView(props: TrendingViewProps) { - Switch Data Source: + {t('plugin_trader_tab_switch_data_source')} {getEnumAsArray(Platform).map((x) => ( { - currentTrendingViewPlatformSettings[getActivatedUI().networkIdentifier].value = String( - x.value, - ) + currentTrendingViewPlatformSettings.value = String(x.value) }}> {resolvePlatformName(x.value)} @@ -243,3 +249,4 @@ export function TrendingView(props: TrendingViewProps) { ) } +//#endregion diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 7745c81245e..68df2ddcdd8 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,8 +1,8 @@ -const BASE_URL = 'https://api.coingecko.com/api/v3' +import { COIN_GECKO_BASE_URL } from '../../constants' //#region get currency export async function getAllCurrenies() { - const response = await fetch(`${BASE_URL}/simple/supported_vs_currencies`, { cache: 'force-cache' }) + const response = await fetch(`${COIN_GECKO_BASE_URL}/simple/supported_vs_currencies`, { cache: 'force-cache' }) return response.json() as Promise } //#endregion @@ -15,7 +15,7 @@ export interface Coin { } export async function getAllCoins() { - const response = await fetch(`${BASE_URL}/coins/list`, { cache: 'force-cache' }) + const response = await fetch(`${COIN_GECKO_BASE_URL}/coins/list`, { cache: 'force-cache' }) return response.json() as Promise } //#endregion @@ -82,7 +82,7 @@ export interface CoinInfo { logo: string } last: number - volumn: number + volume: number converted_last: { btc: number eth: number @@ -107,7 +107,9 @@ export interface CoinInfo { } export async function getCoinInfo(coinId: string) { - const response = await fetch(`${BASE_URL}/coins/${coinId}?developer_data=false&community_data=false&tickers=true`) + const response = await fetch( + `${COIN_GECKO_BASE_URL}/coins/${coinId}?developer_data=false&community_data=false&tickers=true`, + ) return response.json() as Promise } //#endregion @@ -120,7 +122,7 @@ export async function getPriceStats(coinId: string, currencyId: string, days: nu params.append('vs_currency', currencyId) params.append('days', String(days)) - const response = await fetch(`${BASE_URL}/coins/${coinId}/market_chart?${params.toString()}`) + const response = await fetch(`${COIN_GECKO_BASE_URL}/coins/${coinId}/market_chart?${params.toString()}`) return response.json() as Promise<{ market_caps: Stat[] prices: Stat[] diff --git a/src/plugins/Trader/apis/coinmarketcap/index.ts b/src/plugins/Trader/apis/coinmarketcap/index.ts index 38b9e3d4bd2..21183bb85a9 100644 --- a/src/plugins/Trader/apis/coinmarketcap/index.ts +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -1,12 +1,5 @@ import CURRENCY_DATA from './currency.json' - -// proxy: https://web-api.coinmarketcap.com/v1 -const BASE_URL_v1 = 'https://coinmarketcap.provide.maskbook.com/v1' - -// proxy: https://web-api.coinmarketcap.com/v1.1 -const BASE_URL_v1_1 = 'https://coinmarketcap.provide.maskbook.com/v1' - -const WIDGET_BASE_URL = 'https://widgets.coinmarketcap.com/v2' +import { CMC_V1_BASE_URL, CMC_V2_BASE_URL } from '../../constants' export interface Status { credit_count: number @@ -42,7 +35,7 @@ export interface Coin { export async function getAllCoins() { const response = await fetch( - `${BASE_URL_v1}/cryptocurrency/map?aux=status,platform&listing_status=active,untracked&sort=cmc_rank`, + `${CMC_V1_BASE_URL}/cryptocurrency/map?aux=status,platform&listing_status=active,untracked&sort=cmc_rank`, { cache: 'force-cache' }, ) return response.json() as Promise<{ @@ -80,7 +73,7 @@ export async function getCoinInfo(id: string, currency: string) { const params = new URLSearchParams('ref=widget') params.append('convert', currency) - const response = await fetch(`${WIDGET_BASE_URL}/ticker/${id}/?${params.toString()}`) + const response = await fetch(`${CMC_V2_BASE_URL}/ticker/${id}/?${params.toString()}`) return response.json() as Promise<{ data: CoinInfo status: Status @@ -106,7 +99,7 @@ export async function getHistorical( params.append('time_end', toUnixTimestamp(endDate)) params.append('time_start', toUnixTimestamp(startDate)) - const response = await fetch(`${BASE_URL_v1_1}/cryptocurrency/quotes/historical?${params.toString()}`) + const response = await fetch(`${CMC_V1_BASE_URL}/cryptocurrency/quotes/historical?${params.toString()}`) return response.json() as Promise<{ data: Record> status: Status @@ -164,7 +157,7 @@ export async function getLatestMarketPairs(id: string, currency: string) { params.append('convert', currency) params.append('id', id) - const response = await fetch(`${BASE_URL_v1}/cryptocurrency/market-pairs/latest?${params.toString()}`) + const response = await fetch(`${CMC_V1_BASE_URL}/cryptocurrency/market-pairs/latest?${params.toString()}`) return response.json() as Promise<{ data: { id: number diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 86b747a498a..5672c83b5b0 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -3,6 +3,7 @@ import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' import { Days } from '../UI/PriceChartDaysControl' import { getEnumAsArray } from '../../../utils/enum' +import { BTC_FIRST_LEGER_DATE, CRYPTOCURRENCY_MAP_EXPIRES_AT } from '../constants' export async function getCurrenies(platform: Platform): Promise { if (platform === Platform.COIN_GECKO) { @@ -60,9 +61,9 @@ export async function checkAvailabilityAtPlatform(platform: Platform, keyword: s if ( // cache never built before !availabilityCache.has(platform) || - // cache expired in 24h + // cache expired new Date().getTime() - (availabilityCache.get(platform)?.lastUpdated.getTime() ?? 0) > - 24 /* hours */ * 60 /* minutes */ * 60 /* seconds */ * 1000 /* milliseconds */ + CRYPTOCURRENCY_MAP_EXPIRES_AT ) { const coins = await getCoins(platform) availabilityCache.set(platform, { @@ -110,7 +111,7 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr base_name: x.base, target_name: x.target, price: x.converted_last.usd, - volumn: x.converted_volume.usd, + volume: x.converted_volume.usd, score: x.trust_score, })), } @@ -147,7 +148,7 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr pair.market_pair_base.currency_id === market.id ? pair.quote[currencyName].price : pair.quote[currencyName].price_quote, - volumn: pair.quote[currencyName].volume_24h, + volume: pair.quote[currencyName].volume_24h, score: String(pair.market_score), })), } @@ -179,8 +180,7 @@ export async function getPriceStats(id: string, platform: Platform, currency: Cu const stats = await coinMarketCapAPI.getHistorical( id, currency.name.toUpperCase(), - // the bitcoin ledger started at 03 Jan 2009 - days === Days.MAX ? new Date(1230940800000) : startDate, + days === Days.MAX ? BTC_FIRST_LEGER_DATE : startDate, endDate, interval, ) diff --git a/src/plugins/Trader/constants.ts b/src/plugins/Trader/constants.ts index c71de33a4aa..ad206a4ded6 100644 --- a/src/plugins/Trader/constants.ts +++ b/src/plugins/Trader/constants.ts @@ -1,2 +1,23 @@ +//#region plugin definitions export const PLUGIN_IDENTIFIER = 'co.maskbook.trader' export const PLUGIN_METADATA_KEY = 'com.maskbook.trader:1' +//#endregion + +//#region apis +export const COIN_GECKO_BASE_URL = 'https://api.coingecko.com/api/v3' + +// proxy: https://web-api.coinmarketcap.com/v1 +export const CMC_V1_BASE_URL = 'https://coinmarketcap.provide.maskbook.com/v1' + +// proxy: https://web-api.coinmarketcap.com/v1.1 +export const CMC_V2_BASE_URL = 'https://widgets.coinmarketcap.com/v2' +//#endregion + +// the bitcoin ledger started at 03 Jan 2009 +export const BTC_FIRST_LEGER_DATE = new Date('2009-01-03T00:00:00.000Z') + +//#region settings +// cache expired in 24h +export const CRYPTOCURRENCY_MAP_EXPIRES_AT = + 24 /* hours */ * 60 /* minutes */ * 60 /* seconds */ * 1000 /* milliseconds */ +//#endregion diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index 142c9714fa1..782bf50aa5d 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -2,7 +2,7 @@ import React from 'react' import type { PluginConfig } from '../plugin' import { TypedMessage, - isTypedMessgaeAnchor, + isTypedMessageAnchor, TypedMessageAnchor, TypedMessageCompound, } from '../../protocols/typed-message' @@ -10,17 +10,15 @@ import { makeTypedMessageCashTrending } from './messages/TypedMessageCashTrendin import { TrendingPopper } from './UI/TrendingPopper' import { TrendingView } from './UI/TrendingView' import Services from '../../extension/service' -import { Platform } from './types' -import { getEnumAsArray } from '../../utils/enum' import { PLUGIN_IDENTIFIER, PLUGIN_METADATA_KEY } from './constants' -const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => isTypedMessgaeAnchor(m) && m.category === 'cash' +const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => isTypedMessageAnchor(m) && m.category === 'cash' export const TraderPluginDefine: PluginConfig = { pluginName: 'Trader', identifier: PLUGIN_IDENTIFIER, postDialogMetadataBadge: new Map([[PLUGIN_METADATA_KEY, (meta) => 'no metadata']]), - postMessageProcessor(message: TypedMessageCompound) { + messageProcessor(message: TypedMessageCompound) { return { ...message, items: message.items.map((m: TypedMessage) => (isCashTagMessage(m) ? makeTypedMessageCashTrending(m) : m)), @@ -28,9 +26,8 @@ export const TraderPluginDefine: PluginConfig = { }, pageInspector() { // build availability cache in the background page - getEnumAsArray(Platform).forEach((p) => - Services.Plugin.invokePlugin('maskbook.trader', 'checkAvailability', 'BTC'), - ) + Services.Plugin.invokePlugin('maskbook.trader', 'checkAvailability', 'BTC') + return ( {(name: string, reposition?: () => void) => } diff --git a/src/plugins/Trader/graphs/useDimension.ts b/src/plugins/Trader/graphs/useDimension.ts new file mode 100644 index 00000000000..80ba177f22f --- /dev/null +++ b/src/plugins/Trader/graphs/useDimension.ts @@ -0,0 +1,18 @@ +import * as d3 from 'd3' +import { useEffect, RefObject } from 'react' + +export interface Dimension { + width: number + height: number + top: number + right: number + bottom: number + left: number +} + +export function useDimension(svgRef: RefObject, { width, height }: Dimension) { + useEffect(() => { + if (!svgRef.current) return + d3.select(svgRef.current).attr('width', width).attr('height', height) + }, [svgRef.current, width, height]) +} diff --git a/src/plugins/Trader/graphs/usePriceLineChart.ts b/src/plugins/Trader/graphs/usePriceLineChart.ts new file mode 100644 index 00000000000..deea0c34c44 --- /dev/null +++ b/src/plugins/Trader/graphs/usePriceLineChart.ts @@ -0,0 +1,84 @@ +import * as d3 from 'd3' +import { useEffect, RefObject } from 'react' +import stringify from 'json-stable-stringify' +import type { Dimension } from './useDimension' + +export function usePriceLineChart( + svgRef: RefObject, + data: { date: Date; value: number }[], + dimension: Dimension, + id: string, + color = 'steelblue', + sign = '$', +) { + const { top, right, bottom, left, width, height } = dimension + const contentWidth = width - left - right + const contentHeight = height - top - bottom + + useEffect(() => { + if (!svgRef.current) return + + // remove old graph + d3.select(svgRef.current).select(`#${id}`).remove() + + // not necessary + if (data.length === 0) return + + // create new graph + const graph = d3 + .select(svgRef.current) + .append('g') + .attr('id', id) + .attr('transform', `translate(${left}, ${top})`) + + // create X axis + const x = d3 + .scaleTime() + .domain(d3.extent(data, (d) => d.date) as [Date, Date]) + .range([0, contentWidth]) + + // create Y axis + const min = d3.min(data, (d) => d.value) as number + const max = d3.max(data, (d) => d.value) as number + const dist = Math.abs(max - min) + const y = d3 + .scaleLinear() + .domain([min - dist * 0.05, max + dist * 0.05]) + .range([contentHeight, 0]) + + // add X axis + graph + .append('g') + .attr('transform', `translate(0, ${contentHeight})`) + .call(d3.axisBottom(x).ticks(contentWidth / 100)) + + // add Y axis + graph + .append('g') + .attr('transform', 'translate(0, 0)') + .call( + d3 + .axisRight(y) + .ticks(contentHeight / 50, `${sign},.2s`) + .tickSize(contentWidth), + ) + .call((g) => g.select('.domain').remove()) + .call((g) => g.selectAll('.tick line').attr('stroke-opacity', 0.5).attr('stroke-dasharray', '2,2')) + .call((g) => g.selectAll('.tick text').attr('x', 4).attr('dy', -4)) + + // add line + graph + .append('path') + .datum(data) + .attr('fill', 'none') + .attr('stroke', color) + .attr('stroke-width', 1.5) + .attr( + 'd', + d3 + .line() + .x((d) => x((d as any).date)) + .y((d) => y((d as any).value)) as any, + ) + }, [svgRef, data.length, stringify(dimension), sign]) +} diff --git a/src/plugins/Trader/hooks/useCurrentCurrency.ts b/src/plugins/Trader/hooks/useCurrentCurrency.ts index 68319433d56..1b8bd16d9f5 100644 --- a/src/plugins/Trader/hooks/useCurrentCurrency.ts +++ b/src/plugins/Trader/hooks/useCurrentCurrency.ts @@ -8,12 +8,10 @@ import { getCurrentTrendingViewPlatformSettings } from '../settings' export function useCurrentCurrency(platform: Platform) { const [currency, setCurrency] = useState(null) - const trendingSettings = useValueRef( - getCurrentTrendingViewPlatformSettings(platform)[getActivatedUI().networkIdentifier], - ) + const trendingSettings = useValueRef(getCurrentTrendingViewPlatformSettings(platform)) // TODO: - // support multiple currencies + // support multiple crcurrencies const { value: currencies = [], loading, error } = useAsync( () => Services.Plugin.invokePlugin('maskbook.trader', 'getLimitedCurrenies', platform), [platform], diff --git a/src/plugins/Trader/hooks/useCurrentPlatform.ts b/src/plugins/Trader/hooks/useCurrentPlatform.ts index 81c08809239..cc13f7239a0 100644 --- a/src/plugins/Trader/hooks/useCurrentPlatform.ts +++ b/src/plugins/Trader/hooks/useCurrentPlatform.ts @@ -1,14 +1,11 @@ import { useState, useEffect } from 'react' import { Platform } from '../types' -import { getActivatedUI } from '../../../social-network/ui' import { useValueRef } from '../../../utils/hooks/useValueRef' import { currentTrendingViewPlatformSettings } from '../settings' export function useCurrentPlatform(defaultPlatform: Platform) { const [platform, setPlatform] = useState(defaultPlatform) - const trendingPlatformSettings = useValueRef( - currentTrendingViewPlatformSettings[getActivatedUI().networkIdentifier], - ) + const trendingPlatformSettings = useValueRef(currentTrendingViewPlatformSettings) // sync platform useEffect(() => { diff --git a/src/plugins/Trader/messages.ts b/src/plugins/Trader/messages.ts index 155fbf83974..c7b9fa71a0b 100644 --- a/src/plugins/Trader/messages.ts +++ b/src/plugins/Trader/messages.ts @@ -1,13 +1,13 @@ import type { Currency, Platform } from './types' import { BatchedMessageCenter } from '../../utils/messages' -export interface SettingsEvent { +interface SettingsEvent { currency: Currency platform: Platform currencies: Currency[] } -export interface ObserveCashTagEvent { +interface CashTagEvent { name: string element: HTMLAnchorElement | null } @@ -16,12 +16,12 @@ interface MaskbookTraderMessages { /** * View a cash tag */ - cashTagObserved: ObserveCashTagEvent + cashTagObserved: CashTagEvent /** * Update settings dialog */ - settingsDialogUpdated: SettingsEvent + settingsUpdated: 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 2faef19a735..f1d1c2da851 100644 --- a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -1,43 +1,57 @@ -import React from 'react' +import React, { useState } from 'react' +import { v4 as uuid } from 'uuid' import { TypedMessageAnchor, registerTypedMessageRenderer } from '../../../protocols/typed-message' import { Link, Typography } from '@material-ui/core' import type { TypedMessageRendererProps } from '../../../components/InjectedComponents/TypedMessageRenderer' import { MessageCenter } from '../messages' import Services from '../../../extension/service' +import { useUnmount, useEffectOnce } from 'react-use' export interface TypedMessageCashTrending extends Omit { - readonly type: 'anchor/cash_trending' + readonly type: 'x-cash-trending' readonly name: string } export function makeTypedMessageCashTrending(message: TypedMessageAnchor) { return { ...message, - type: 'anchor/cash_trending', + type: 'x-cash-trending', name: message.content.substr(1).toLowerCase(), } as TypedMessageCashTrending } -registerTypedMessageRenderer('anchor/cash_trending', { +registerTypedMessageRenderer('x-cash-trending', { component: DefaultTypedMessageCashTrendingRenderer, - id: 'co.maskbook.trader.cash_trending', + id: 'co.maskbook.trader.x-cash-trending', priority: 0, }) function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { - const onHoverCashTag = async (ev: React.MouseEvent) => { - // should cache before async operations + const [openTimer, setOpenTimer] = useState(null) + const onMouseOver = (ev: React.MouseEvent) => { + // cache for async operations const element = ev.currentTarget - if (await Services.Plugin.invokePlugin('maskbook.trader', 'checkAvailability', props.message.name)) { - MessageCenter.emit('cashTagObserved', { - name: props.message.name, - element, - }) - } + if (openTimer !== null) clearTimeout(openTimer) + setOpenTimer( + setTimeout(async () => { + const available = await Services.Plugin.invokePlugin( + 'maskbook.trader', + 'checkAvailability', + props.message.name, + ) + if (available) MessageCenter.emit('cashTagObserved', { name: props.message.name, element }) + }, 500), + ) + } + const onMouseLeave = (ev: React.MouseEvent) => { + if (openTimer !== null) clearTimeout(openTimer) + } + const onClick = (ev: React.MouseEvent) => { + ev.stopPropagation() } return ( - + {props.message.content} diff --git a/src/plugins/Trader/settings.ts b/src/plugins/Trader/settings.ts index b5ab64adca0..623868ce8fd 100644 --- a/src/plugins/Trader/settings.ts +++ b/src/plugins/Trader/settings.ts @@ -1,10 +1,18 @@ -import { createNetworkSettings } from '../../settings/createSettings' +import { createInternalSettings } from '../../settings/createSettings' import { Platform } from './types' +import { PLUGIN_IDENTIFIER } from './constants' -export const currentTrendingViewPlatformSettings = createNetworkSettings('currentTrendingViewPlatformSettings') +function createPluginInternalSettings(key: string, initial: T) { + return createInternalSettings(`${PLUGIN_IDENTIFIER}+${key}`, initial) +} + +export const currentTrendingViewPlatformSettings = createPluginInternalSettings( + 'currentTrendingViewPlatformSettings', + String(Platform.COIN_GECKO), +) -const coinGeckoSettings = createNetworkSettings('currentTrendingViewPlatformCoinGeckoSettings') -const coinMarketCapSettings = createNetworkSettings('currentTrendingViewPlatformCoinMarketCapSettings') +const coinGeckoSettings = createPluginInternalSettings('currentTrendingViewPlatformCoinGeckoSettings', '') +const coinMarketCapSettings = createPluginInternalSettings('currentTrendingViewPlatformCoinMarketCapSettings', '') export function getCurrentTrendingViewPlatformSettings(platform: Platform) { return platform === Platform.COIN_GECKO ? coinGeckoSettings : coinMarketCapSettings diff --git a/src/plugins/Trader/types.ts b/src/plugins/Trader/types.ts index 94bc7975d94..62e035d526c 100644 --- a/src/plugins/Trader/types.ts +++ b/src/plugins/Trader/types.ts @@ -45,7 +45,7 @@ export interface Ticker { base_name: string target_name: string price: number - volumn: number + volume: number score: string } diff --git a/src/plugins/Wallet/formatter.ts b/src/plugins/Wallet/formatter.ts index 322fc5ff651..155604f6887 100644 --- a/src/plugins/Wallet/formatter.ts +++ b/src/plugins/Wallet/formatter.ts @@ -22,8 +22,8 @@ export function formatBalance(balance: BigNumber, decimals: number, precision: n return raw.indexOf('.') > -1 ? raw.replace(/0+$/, '').replace(/\.$/, '') : raw } -export function formatCurrency(balance: number) { - return balance.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,') +export function formatCurrency(balance: number, sign: string = '$') { + return balance.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, `${sign}&,`) } export function formatEthAddress(address: string, size = 2) { diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index aebaa35331e..b0c88a4b5ad 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -1,4 +1,5 @@ import type { TypedMessage, TypedMessageCompound } from '../protocols/typed-message' +import type { PostInfo } from '../social-network/PostInfo' type PluginInjectFunction = | { @@ -13,8 +14,8 @@ export interface PluginConfig { successDecryptionInspector?: PluginInjectFunction<{ message: TypedMessage }> pageInspector?: React.ComponentType<{}> postInspector?: PluginInjectFunction<{}> - postMessageProcessor?: (message: TypedMessageCompound) => TypedMessageCompound postDialogMetadataBadge?: Map string> + messageProcessor?: (message: TypedMessageCompound) => TypedMessageCompound } const plugins = new Set() @@ -22,7 +23,6 @@ export const PluginUI: ReadonlySet = plugins import { GitcoinPluginDefine } from './Gitcoin/define' import { RedPacketPluginDefine } from './RedPacket/define' -import type { PostInfo } from '../social-network/PostInfo' import { StorybookPluginDefine } from './Storybook/define' import { FileServicePluginDefine } from './FileService/define' import { TraderPluginDefine } from './Trader/define' @@ -30,7 +30,5 @@ import { Flags } from '../utils/flags' plugins.add(GitcoinPluginDefine) plugins.add(RedPacketPluginDefine) if (Flags.file_service_enabled) plugins.add(FileServicePluginDefine) -plugins.add(TraderPluginDefine) -if (process.env.STORYBOOK) { - plugins.add(StorybookPluginDefine) -} +if (Flags.trader_enabled) plugins.add(TraderPluginDefine) +if (process.env.STORYBOOK) plugins.add(StorybookPluginDefine) diff --git a/src/protocols/typed-message/helpers.ts b/src/protocols/typed-message/helpers.ts index 0ba01b04a44..237991f926d 100644 --- a/src/protocols/typed-message/helpers.ts +++ b/src/protocols/typed-message/helpers.ts @@ -3,7 +3,7 @@ import { isTypedMessageText, isTypedMessageCompound, TypedMessageCompound, - isTypedMessgaeAnchor, + isTypedMessageAnchor, } from './types' import { Result, Ok, Err } from 'ts-results' import { eq } from 'lodash-es' @@ -14,6 +14,7 @@ import { eq } from 'lodash-es' export function extractTextFromTypedMessage(message: TypedMessage | null): Result { if (message === null) return Err.EMPTY if (isTypedMessageText(message)) return Ok(message.content) + if (isTypedMessageAnchor(message)) return Ok(message.content) if (isTypedMessageCompound(message)) { const str: string[] = [] for (const item of message.items) { @@ -50,13 +51,3 @@ export function isTypedMessageEqual(message1: TypedMessage, message2: TypedMessa return eq(message1, message2) } } - -/** - * Serialize typed message - */ -export function serializeTypedMessage(message: TypedMessage | null) { - if (!message) return '' - if (isTypedMessageText(message)) return message.content - if (isTypedMessgaeAnchor(message)) return message.content - return '' -} diff --git a/src/protocols/typed-message/types.ts b/src/protocols/typed-message/types.ts index 88328b0454c..68dc1032dd5 100644 --- a/src/protocols/typed-message/types.ts +++ b/src/protocols/typed-message/types.ts @@ -50,7 +50,7 @@ export interface TypedMessageSuspended ex export function isTypedMessageText(x: TypedMessage): x is TypedMessageText { return x.type === 'text' } -export function isTypedMessgaeAnchor(x: TypedMessage): x is TypedMessageAnchor { +export function isTypedMessageAnchor(x: TypedMessage): x is TypedMessageAnchor { return x.type === 'anchor' } export function isTypedMessageKnown(x: TypedMessage) { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index aeb1d66f65e..6284a6d5ff7 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -44,18 +44,18 @@ export const appearanceSettings = createGlobalSettings('apperance', primary: () => i18n.t('settings_appearance'), }) -export enum WholePostVisibility { +export enum PostReplacementScope { all = 'all', enhancedOnly = 'enhancedOnly', encryptedOnly = 'encryptedOnly', } -export const currentWholePostVisibilitySettings = createGlobalSettings( - 'whole post visibility', - WholePostVisibility.all, +export const currentPostReplacementScopeSettings = createGlobalSettings( + 'post replacement scope', + PostReplacementScope.enhancedOnly, { - primary: () => 'Whole Post Visibility', - secondary: () => '', + primary: () => i18n.t('settings_post_replacement_scope'), + secondary: () => i18n.t('settings_post_replacement_scope_desc'), }, ) diff --git a/src/social-network-provider/facebook.com/UI/collectPosts.tsx b/src/social-network-provider/facebook.com/UI/collectPosts.tsx index 89eb603b342..9bad783b6ac 100644 --- a/src/social-network-provider/facebook.com/UI/collectPosts.tsx +++ b/src/social-network-provider/facebook.com/UI/collectPosts.tsx @@ -85,7 +85,7 @@ export function collectPostsFacebook(this: SocialNetworkUI) { nextTypedMessage.push(makeTypedMessageImage(url)) } // parse post content - info.parsedPostContent.value = makeTypedMessageCompound(nextTypedMessage) + info.postMessage.value = makeTypedMessageCompound(nextTypedMessage) } collectPostInfo() info.postPayload.value = deconstructPayload(info.postContent.value, this.payloadDecoder) diff --git a/src/social-network-provider/facebook.com/UI/injectPostDummy.tsx b/src/social-network-provider/facebook.com/UI/injectPostDummy.tsx deleted file mode 100644 index a7133158a53..00000000000 --- a/src/social-network-provider/facebook.com/UI/injectPostDummy.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function injectPostDummyFacebook() { - return () => {} -} diff --git a/src/social-network-provider/facebook.com/UI/injectPostReplacer.tsx b/src/social-network-provider/facebook.com/UI/injectPostReplacer.tsx new file mode 100644 index 00000000000..1ad49765fd4 --- /dev/null +++ b/src/social-network-provider/facebook.com/UI/injectPostReplacer.tsx @@ -0,0 +1,3 @@ +export function injectPostReplacerFacebook() { + return () => {} +} diff --git a/src/social-network-provider/facebook.com/ui-provider.ts b/src/social-network-provider/facebook.com/ui-provider.ts index 13d4e0a6533..dd58773996a 100644 --- a/src/social-network-provider/facebook.com/ui-provider.ts +++ b/src/social-network-provider/facebook.com/ui-provider.ts @@ -14,7 +14,7 @@ import { pasteIntoBioFacebook } from './tasks/pasteIntoBio' import { injectPostCommentsDefault } from '../../social-network/defaults/injectComments' import { dispatchCustomEvents, selectElementContents, sleep } from '../../utils/utils' import { collectPostsFacebook } from './UI/collectPosts' -import { injectPostDummyFacebook } from './UI/injectPostDummy' +import { injectPostReplacerFacebook } from './UI/injectPostReplacer' import { injectPostInspectorFacebook } from './UI/injectPostInspector' import { injectPageInspectorFacebook } from './UI/injectPageInspector' import { setStorage } from '../../utils/browser.storage' @@ -94,7 +94,7 @@ export const facebookUISelf = defineSocialNetworkUI({ if (!root.innerText.includes(encryptedComment)) return fail() } }), - injectPostDummy: injectPostDummyFacebook, + injectPostReplacer: injectPostReplacerFacebook, injectPostInspector: injectPostInspectorFacebook, injectPageInspector: injectPageInspectorFacebook, collectPeople: collectPeopleFacebook, diff --git a/src/social-network-provider/twitter.com/ui/fetch.ts b/src/social-network-provider/twitter.com/ui/fetch.ts index fafd247c8ef..6b9889aefc5 100644 --- a/src/social-network-provider/twitter.com/ui/fetch.ts +++ b/src/social-network-provider/twitter.com/ui/fetch.ts @@ -19,8 +19,8 @@ import { makeTypedMessageFromList, makeTypedMessageEmpty, makeTypedMessageSuspended, - serializeTypedMessage, makeTypedMessageCompound, + extractTextFromTypedMessage, } from '../../../protocols/typed-message' import { Flags } from '../../../utils/flags' @@ -182,7 +182,12 @@ function collectPostInfo(tweetNode: HTMLDivElement | null, info: PostInfo, self: if (!pid) return const postBy = new ProfileIdentifier(self.networkIdentifier, handle) info.postID.value = pid - info.postContent.value = messages.map(serializeTypedMessage).join('') + info.postContent.value = messages + .map((x) => { + const extracted = extractTextFromTypedMessage(x) + return extracted.ok ? extracted.val : '' + }) + .join('') if (!info.postBy.value.equals(postBy)) info.postBy.value = postBy info.nickname.value = name info.avatarURL.value = avatar || null @@ -198,5 +203,5 @@ function collectPostInfo(tweetNode: HTMLDivElement | null, info: PostInfo, self: }) .catch(() => makeTypedMessageEmpty()) - info.parsedPostContent.value = makeTypedMessageCompound([...messages, makeTypedMessageSuspended(images)]) + info.postMessage.value = makeTypedMessageCompound([...messages, makeTypedMessageSuspended(images)]) } diff --git a/src/social-network-provider/twitter.com/ui/inject.tsx b/src/social-network-provider/twitter.com/ui/inject.tsx index 16c859aed7e..49e81df16b5 100644 --- a/src/social-network-provider/twitter.com/ui/inject.tsx +++ b/src/social-network-provider/twitter.com/ui/inject.tsx @@ -5,7 +5,7 @@ import { injectPostDialogHintAtTwitter } from './injectPostDialogHint' import { injectPostInspectorAtTwitter } from './injectPostInspector' import { injectPageInspectorAtTwitter } from './injectPageInspector' import { injectPostDialogIconAtTwitter } from './injectPostDialogIcon' -import { injectPostDummyAtTwitter } from './injectPostDummy' +import { injectPostReplacerAtTwitter } from './injectPostReplacer' const injectPostBox = () => { injectPostDialogAtTwitter() @@ -15,7 +15,7 @@ const injectPostBox = () => { export const twitterUIInjections: SocialNetworkUIInjections = { injectPostBox, - injectPostDummy: injectPostDummyAtTwitter, + injectPostReplacer: injectPostReplacerAtTwitter, injectPostInspector: injectPostInspectorAtTwitter, injectPageInspector: injectPageInspectorAtTwitter, injectKnownIdentity: injectKnownIdentityAtTwitter, diff --git a/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx b/src/social-network-provider/twitter.com/ui/injectPostReplacer.tsx similarity index 73% rename from src/social-network-provider/twitter.com/ui/injectPostDummy.tsx rename to src/social-network-provider/twitter.com/ui/injectPostReplacer.tsx index c4360e84037..64d798a1c47 100644 --- a/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx +++ b/src/social-network-provider/twitter.com/ui/injectPostReplacer.tsx @@ -1,12 +1,12 @@ import type { PostInfo } from '../../../social-network/PostInfo' -import { injectPostDummyDefault } from '../../../social-network/defaults/injectPostDummy' +import { injectPostReplacer } from '../../../social-network/defaults/injectPostReplacer' function resolveLangNode(node: HTMLElement) { return node.hasAttribute('lang') ? node : node.querySelector('[lang]') } -export function injectPostDummyAtTwitter(current: PostInfo) { - return injectPostDummyDefault({ +export function injectPostReplacerAtTwitter(current: PostInfo) { + return injectPostReplacer({ zipPost(node) { const langNode = resolveLangNode(node.current) if (langNode) langNode.style.display = 'none' diff --git a/src/social-network-provider/twitter.com/utils/fetch.ts b/src/social-network-provider/twitter.com/utils/fetch.ts index 52103bf4b4d..856c1709c20 100644 --- a/src/social-network-provider/twitter.com/utils/fetch.ts +++ b/src/social-network-provider/twitter.com/utils/fetch.ts @@ -195,14 +195,14 @@ export const postContentMessageParser = (node: HTMLElement) => { if (node.nodeType === Node.TEXT_NODE) { if (!node.nodeValue) return makeTypedMessageEmpty() return makeTypedMessageText(node.nodeValue) - } else if (nodeName === 'a') { - const anchor = node as HTMLAnchorElement + } else if (node instanceof HTMLAnchorElement) { + const anchor = node const href = anchor.getAttribute('href') const content = anchor.textContent if (!content) return makeTypedMessageEmpty() - return makeTypedMessageAnchor(resolve(content), href ?? 'javascript: void(0);', content) - } else if (nodeName === 'img') { - const image = node as HTMLImageElement + return makeTypedMessageAnchor(resolve(content), href ?? 'javascript: void 0;', content) + } else if (node instanceof HTMLImageElement) { + const image = node const src = image.getAttribute('src') const matched = src?.match(/emoji\/v2\/svg\/([\d\w]+)\.svg/) if (matched && matched[1]) @@ -217,8 +217,8 @@ export const postContentMessageParser = (node: HTMLElement) => { } const lang = node.parentElement!.querySelector('[lang]') if (!lang) return [] - const maked = make(lang) - return Array.isArray(maked) ? maked : [maked] + const made = make(lang) + return Array.isArray(made) ? made : [made] } export const postImagesParser = async (node: HTMLElement): Promise => { diff --git a/src/social-network/PostInfo.ts b/src/social-network/PostInfo.ts index 9ac0cce533f..a4f185a8a95 100644 --- a/src/social-network/PostInfo.ts +++ b/src/social-network/PostInfo.ts @@ -33,17 +33,17 @@ export abstract class PostInfo { readonly postID = new ValueRef(null) /** This property is auto computed. */ readonly postIdentifier = new ValueRef>(null, Identifier.equals) - /** @deprecated Use parsedPostContent instead */ + /** The post message in plain text */ readonly postContent = new ValueRef('') - /** @deprecated It should appear in the transformedPostContent */ - readonly postPayload = new ValueRef>(Err(new Error('Empty'))) - abstract readonly commentsSelector?: LiveSelector - abstract readonly commentBoxSelector?: LiveSelector /** * The un-decrypted post content. * It MUST be the original result (but can be updated by the original parser). */ - readonly parsedPostContent = new ValueRef(makeTypedMessageCompound([]), isTypedMessageEqual) + readonly postMessage = new ValueRef(makeTypedMessageCompound([]), isTypedMessageEqual) + /** @deprecated It should appear in the transformedPostContent */ + readonly postPayload = new ValueRef>(Err(new Error('Empty'))) + abstract readonly commentsSelector?: LiveSelector + abstract readonly commentBoxSelector?: LiveSelector /** * The un-decrypted post content after transformation. */ @@ -58,7 +58,7 @@ export abstract class PostInfo { readonly postMentionedLinks = new ObservableSet() /** * The images as attachment of post - * @deprecated it should appear in parsedPostContent + * @deprecated it should appear in postMessage */ readonly postMetadataImages = new ObservableSet() /** diff --git a/src/social-network/defaults/emptyDefinition.ts b/src/social-network/defaults/emptyDefinition.ts index c47567e40a5..e91b6627e35 100644 --- a/src/social-network/defaults/emptyDefinition.ts +++ b/src/social-network/defaults/emptyDefinition.ts @@ -30,7 +30,7 @@ export const emptyDefinition: SocialNetworkUIDefinition = { injectCommentBox: nopWithUnmount, injectPostBox: nop, injectPostComments: nopWithUnmount, - injectPostDummy: nopWithUnmount, + injectPostReplacer: nopWithUnmount, injectPostInspector: nopWithUnmount, injectPageInspector: nopWithUnmount, resolveLastRecognizedIdentity: nop, diff --git a/src/social-network/defaults/injectPostDummy.tsx b/src/social-network/defaults/injectPostReplacer.tsx similarity index 61% rename from src/social-network/defaults/injectPostDummy.tsx rename to src/social-network/defaults/injectPostReplacer.tsx index cbd50a82660..3d24c29d71e 100644 --- a/src/social-network/defaults/injectPostDummy.tsx +++ b/src/social-network/defaults/injectPostReplacer.tsx @@ -1,33 +1,33 @@ import React from 'react' import { renderInShadowRoot } from '../../utils/jss/renderInShadowRoot' import { PostInfoContext } from '../../components/DataSource/usePostInfo' -import { PostDummy, PostDummyProps } from '../../components/InjectedComponents/PostDummy' +import { PostReplacer, PostReplacerProps } from '../../components/InjectedComponents/PostReplacer' import type { PostInfo } from '../PostInfo' import { makeStyles } from '@material-ui/core' import type { DOMProxy } from '@holoflows/kit/es' import { noop } from 'lodash-es' -export function injectPostDummyDefault( - config: InjectPostDummyDefaultConfig = {}, - additionalPropsToPostDummy: (classes: Record) => Partial = () => ({}), +export function injectPostReplacer( + config: injectPostReplacerConfig = {}, + additionalPropsToPostReplacer: (classes: Record) => Partial = () => ({}), useCustomStyles: (props?: any) => Record = makeStyles({}) as any, ) { - const PostDummyDefault = React.memo(function PostDummyDefault(props: { - zipPost: PostDummyProps['zip'] - unZipPost: PostDummyProps['unzip'] + const PostReplacerDefault = React.memo(function PostReplacerDefault(props: { + zipPost: PostReplacerProps['zip'] + unZipPost: PostReplacerProps['unzip'] }) { const classes = useCustomStyles() - const additionalProps = additionalPropsToPostDummy(classes) - return + const additionalProps = additionalPropsToPostReplacer(classes) + return }) const { zipPost, unzipPost } = config const zipPostF = zipPost || noop const unzipPostF = unzipPost || noop - return function injectPostDummy(current: PostInfo) { + return function injectPostReplacer(current: PostInfo) { return renderInShadowRoot( - zipPostF(current.rootNodeProxy)} unZipPost={() => unzipPostF(current.rootNodeProxy)} {...current} @@ -42,7 +42,7 @@ export function injectPostDummyDefault( } } -interface InjectPostDummyDefaultConfig { +interface injectPostReplacerConfig { zipPost?(node: DOMProxy): void unzipPost?(node: DOMProxy): void } diff --git a/src/social-network/ui.ts b/src/social-network/ui.ts index 639bd75dfd0..7ff2c3015cd 100644 --- a/src/social-network/ui.ts +++ b/src/social-network/ui.ts @@ -98,7 +98,7 @@ export interface SocialNetworkUIInjections { */ injectPostBox(): void /** - * This function should inject rthe page inspector + * This function should inject the page inspector */ injectPageInspector(): void /** @@ -129,11 +129,11 @@ export interface SocialNetworkUIInjections { */ injectCommentBox?: ((current: PostInfo) => () => void) | 'disabled' /** - * This function should inject the post dummy + * This function should inject the post replacer * @param current The current post * @returns unmount the injected components */ - injectPostDummy(current: PostInfo): () => void + injectPostReplacer(current: PostInfo): () => void /** * This function should inject the post box * @param current The current post @@ -339,7 +339,7 @@ function hookUIPostMap(ui: SocialNetworkUI) { const unmountFunctions = new WeakMap void>() ui.posts.event.on('set', (key, value) => { const unmountPostInspector = ui.injectPostInspector(value) - const unmountPostDummy = ui.injectPostDummy(value) + const unmountPostReplacer = ui.injectPostReplacer(value) const unmountCommentBox: () => void = ui.injectCommentBox === 'disabled' ? nopWithUnmount : defaultTo(ui.injectCommentBox, nopWithUnmount)(value) const unmountPostComments: () => void = @@ -348,7 +348,7 @@ function hookUIPostMap(ui: SocialNetworkUI) { : defaultTo(ui.injectPostComments, nopWithUnmount)(value) unmountFunctions.set(key, () => { unmountPostInspector() - unmountPostDummy() + unmountPostReplacer() unmountCommentBox() unmountPostComments() }) diff --git a/src/utils/flags.ts b/src/utils/flags.ts index d3bb00759f5..bf0a2b98937 100644 --- a/src/utils/flags.ts +++ b/src/utils/flags.ts @@ -21,6 +21,7 @@ export const Flags = { // TODO: document why it enabled on app support_eth_network_switch: process.env.NODE_ENV === 'development' || process.env.architecture === 'app', //#region Experimental features + trader_enabled: process.env.architecture === 'app' || process.env.NODE_ENV === 'development', file_service_enabled: process.env.architecture === 'app' || process.env.NODE_ENV === 'development', matrix_based_service_enabled: process.env.NODE_ENV === 'development', //#endregion