From b682ec9434b912339abde40aa564e5481afa3192 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Thu, 20 Aug 2020 21:16:33 +0800 Subject: [PATCH 01/16] chore: define the trader plugin --- .../background-script/PluginService.ts | 2 + src/plugins/Trader/UI/ScreenModal.tsx | 5 + src/plugins/Trader/apis/coingecko/index.ts | 71 ++ .../Trader/apis/coinmarketcap/currency.json | 653 ++++++++++++++++++ .../Trader/apis/coinmarketcap/index.ts | 80 +++ src/plugins/Trader/define.tsx | 30 + src/plugins/Trader/service.ts | 1 + src/plugins/plugin.ts | 2 + 8 files changed, 844 insertions(+) create mode 100644 src/plugins/Trader/UI/ScreenModal.tsx create mode 100644 src/plugins/Trader/apis/coingecko/index.ts create mode 100644 src/plugins/Trader/apis/coinmarketcap/currency.json create mode 100644 src/plugins/Trader/apis/coinmarketcap/index.ts create mode 100644 src/plugins/Trader/define.tsx create mode 100644 src/plugins/Trader/service.ts diff --git a/src/extension/background-script/PluginService.ts b/src/extension/background-script/PluginService.ts index 2d493ba8c58..4e403e15058 100644 --- a/src/extension/background-script/PluginService.ts +++ b/src/extension/background-script/PluginService.ts @@ -2,6 +2,7 @@ import * as RedPacket from '../../plugins/RedPacket/state-machine' import * as Wallet from '../../plugins/Wallet/wallet' import * as Gitcoin from '../../plugins/Gitcoin/service' import * as FileService from '../../plugins/FileService/service' +import * as Trader from '../../plugins/Trader/service' import type { ERC20TokenRecord, ManagedWalletRecord, ExoticWalletRecord } from '../../plugins/Wallet/database/types' import { EthereumNetwork } from '../../plugins/Wallet/database/types' import { getWalletProvider, web3 } from '../../plugins/Wallet/web3' @@ -11,6 +12,7 @@ const Plugins = { 'maskbook.red_packet': RedPacket, 'maskbook.wallet': Wallet, 'maskbook.fileservice': FileService, + 'maskbook.trader': Trader, 'co.gitcoin': Gitcoin, } as const type Plugins = typeof Plugins diff --git a/src/plugins/Trader/UI/ScreenModal.tsx b/src/plugins/Trader/UI/ScreenModal.tsx new file mode 100644 index 00000000000..a18ffa8f2dd --- /dev/null +++ b/src/plugins/Trader/UI/ScreenModal.tsx @@ -0,0 +1,5 @@ +export interface TraderScreenModalProps {} + +export function TraderScreenModal(props: TraderScreenModalProps) { + return null +} diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts new file mode 100644 index 00000000000..2edff8ade7e --- /dev/null +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -0,0 +1,71 @@ +const BASE_URL = 'https://coingecko.com/api/documentations/v3' + +//#region get currency +export async function getAllCurrenies() { + const response = await fetch(`${BASE_URL}/simple/supported_vs_currencies`) + return response.json() as Promise +} +//#endregion + +//#region get coins list +export interface Coin { + id: string + name: string + symbol: string +} + +export async function getAllCoins() { + const response = await fetch(`${BASE_URL}/coins/list`) + return response.json() as Promise +} +//#endregion + +//#region get coin info +export interface CoinInfo { + asset_platform_id: string + block_time_in_minutes: number + categories: string[] + contract_address: string + description: Record + developer_score: number + id: string + image: { + large: string + small: string + thumb: string + } + last_updated: string + links: { + announcement_url: string[] + bitcointalk_thread_identifier: null + blockchain_site: string[] + chat_url: string[] + facebook_username: string + homepage: string[] + official_forum_url: string[] + repos_url: { github: string[]; bitbucket: string[] } + subreddit_url: string + telegram_channel_identifier: string + twitter_screen_name: string + } + liquidity_score: string + localization: Record + market_cap_rank: number + market_data: { + high_24h: Record + low_24h: Record + market_cap: Record + market_cap_rank: number + price_change_24h: number + total_supply: number + total_volume: Record + } + name: string + symbol: string +} + +export async function getCoinInfo(id: string) { + const response = await fetch(`${BASE_URL}/coins/${id}?developer_data=false&community_data=false&tickers=false`) + return response.json() as Promise +} +//#endregion diff --git a/src/plugins/Trader/apis/coinmarketcap/currency.json b/src/plugins/Trader/apis/coinmarketcap/currency.json new file mode 100644 index 00000000000..e14e37839ae --- /dev/null +++ b/src/plugins/Trader/apis/coinmarketcap/currency.json @@ -0,0 +1,653 @@ +{ + "usd": { + "id": 2781, + "name": "United States Dollar", + "symbol": "usd", + "token": "$", + "space": "" + }, + "all": { + "id": 3526, + "name": "Albanian Lek", + "symbol": "all", + "token": "L", + "space": "" + }, + "dzd": { + "id": 3537, + "name": "Algerian Dinar", + "symbol": "dzd", + "token": "\u062f.\u062c", + "space": "" + }, + "ars": { + "id": 2821, + "name": "Argentine Peso", + "symbol": "ars", + "token": "$", + "space": "" + }, + "amd": { + "id": 3527, + "name": "Armenian Dram", + "symbol": "amd", + "token": "\u058f", + "space": "" + }, + "aud": { + "id": 2782, + "name": "Australian Dollar", + "symbol": "aud", + "token": "$", + "space": "" + }, + "azn": { + "id": 3528, + "name": "Azerbaijani Manat", + "symbol": "azn", + "token": "\u20bc", + "space": "" + }, + "bhd": { + "id": 3531, + "name": "Bahraini Dinar", + "symbol": "bhd", + "token": ".\u062f.\u0628", + "space": "" + }, + "bdt": { + "id": 3530, + "name": "Bangladeshi Taka", + "symbol": "bdt", + "token": "\u09f3", + "space": "" + }, + "byn": { + "id": 3533, + "name": "Belarusian Ruble", + "symbol": "byn", + "token": "Br", + "space": "" + }, + "bmd": { + "id": 3532, + "name": "Bermudan Dollar", + "symbol": "bmd", + "token": "$", + "space": "" + }, + "bob": { + "id": 2832, + "name": "Bolivian Boliviano", + "symbol": "bob", + "token": "Bs.", + "space": "" + }, + "bam": { + "id": 3529, + "name": "Bosnia-Herzegovina Convertible Mark", + "symbol": "bam", + "token": "KM", + "space": "" + }, + "brl": { + "id": 2783, + "name": "Brazilian Real", + "symbol": "brl", + "token": "R$", + "space": "" + }, + "bgn": { + "id": 2814, + "name": "Bulgarian Lev", + "symbol": "bgn", + "token": "\u043b\u0432", + "space": "" + }, + "khr": { + "id": 3549, + "name": "Cambodian Riel", + "symbol": "khr", + "token": "\u17db", + "space": "" + }, + "cad": { + "id": 2784, + "name": "Canadian Dollar", + "symbol": "cad", + "token": "$", + "space": "" + }, + "clp": { + "id": 2786, + "name": "Chilean Peso", + "symbol": "clp", + "token": "$", + "space": "" + }, + "cny": { + "id": 2787, + "name": "Chinese Yuan", + "symbol": "cny", + "token": "¥", + "space": "" + }, + "cop": { + "id": 2820, + "name": "Colombian Peso", + "symbol": "cop", + "token": "$", + "space": "" + }, + "crc": { + "id": 3534, + "name": "Costa Rican Col\\xf3n", + "symbol": "crc", + "token": "\u20a1", + "space": "" + }, + "hrk": { + "id": 2815, + "name": "Croatian Kuna", + "symbol": "hrk", + "token": "kn", + "space": "" + }, + "cup": { + "id": 3535, + "name": "Cuban Peso", + "symbol": "cup", + "token": "$", + "space": "" + }, + "czk": { + "id": 2788, + "name": "Czech Koruna", + "symbol": "czk", + "token": "K\u010d", + "space": "" + }, + "dkk": { + "id": 2789, + "name": "Danish Krone", + "symbol": "dkk", + "token": "kr", + "space": ". " + }, + "dop": { + "id": 3536, + "name": "Dominican Peso", + "symbol": "dop", + "token": "$", + "space": "" + }, + "egp": { + "id": 3538, + "name": "Egyptian Pound", + "symbol": "egp", + "token": "£", + "space": "" + }, + "eur": { + "id": 2790, + "name": "Euro", + "symbol": "eur", + "token": "\u20ac", + "space": "" + }, + "gel": { + "id": 3539, + "name": "Georgian Lari", + "symbol": "gel", + "token": "\u20be", + "space": "" + }, + "ghs": { + "id": 3540, + "name": "Ghanaian Cedi", + "symbol": "ghs", + "token": "\u20b5", + "space": "" + }, + "gtq": { + "id": 3541, + "name": "Guatemalan Quetzal", + "symbol": "gtq", + "token": "Q", + "space": "" + }, + "hnl": { + "id": 3542, + "name": "Honduran Lempira", + "symbol": "hnl", + "token": "L", + "space": "" + }, + "hkd": { + "id": 2792, + "name": "Hong Kong Dollar", + "symbol": "hkd", + "token": "$", + "space": "" + }, + "huf": { + "id": 2793, + "name": "Hungarian Forint", + "symbol": "huf", + "token": "Ft", + "space": " " + }, + "isk": { + "id": 2818, + "name": "Icelandic Króna", + "symbol": "isk", + "token": "kr", + "space": "" + }, + "inr": { + "id": 2796, + "name": "Indian Rupee", + "symbol": "inr", + "token": "\u20b9", + "space": "" + }, + "idr": { + "id": 2794, + "name": "Indonesian Rupiah", + "symbol": "idr", + "token": "Rp", + "space": " " + }, + "irr": { + "id": 3544, + "name": "Iranian Rial", + "symbol": "irr", + "token": "\ufdfc", + "space": "" + }, + "iqd": { + "id": 3543, + "name": "Iraqi Dinar", + "symbol": "iqd", + "token": "\u0639.\u062f", + "space": "" + }, + "ils": { + "id": 2795, + "name": "Israeli New Shekel", + "symbol": "ils", + "token": "\u20aa", + "space": "" + }, + "jmd": { + "id": 3545, + "name": "Jamaican Dollar", + "symbol": "jmd", + "token": "$", + "space": "" + }, + "jpy": { + "id": 2797, + "name": "Japanese Yen", + "symbol": "jpy", + "token": "¥", + "space": "" + }, + "jod": { + "id": 3546, + "name": "Jordanian Dinar", + "symbol": "jod", + "token": "\u062f.\u0627", + "space": "" + }, + "kzt": { + "id": 3551, + "name": "Kazakhstani Tenge", + "symbol": "kzt", + "token": "\u20b8", + "space": "" + }, + "kes": { + "id": 3547, + "name": "Kenyan Shilling", + "symbol": "kes", + "token": "Sh", + "space": "" + }, + "kwd": { + "id": 3550, + "name": "Kuwaiti Dinar", + "symbol": "kwd", + "token": "\u062f.\u0643", + "space": "" + }, + "kgs": { + "id": 3548, + "name": "Kyrgystani Som", + "symbol": "kgs", + "token": "\u0441", + "space": "" + }, + "lbp": { + "id": 3552, + "name": "Lebanese Pound", + "symbol": "lbp", + "token": "\u0644.\u0644", + "space": "" + }, + "mkd": { + "id": 3556, + "name": "Macedonian Denar", + "symbol": "mkd", + "token": "\u0434\u0435\u043d", + "space": "" + }, + "myr": { + "id": 2800, + "name": "Malaysian Ringgit", + "symbol": "myr", + "token": "RM", + "space": "" + }, + "mur": { + "id": 2816, + "name": "Mauritian Rupee", + "symbol": "mur", + "token": "\u20a8", + "space": "" + }, + "mxn": { + "id": 2799, + "name": "Mexican Peso", + "symbol": "mxn", + "token": "$", + "space": "" + }, + "mdl": { + "id": 3555, + "name": "Moldovan Leu", + "symbol": "mdl", + "token": "L", + "space": "" + }, + "mnt": { + "id": 3558, + "name": "Mongolian Tugrik", + "symbol": "mnt", + "token": "\u20ae", + "space": "" + }, + "mad": { + "id": 3554, + "name": "Moroccan Dirham", + "symbol": "mad", + "token": "\u062f.\u0645.", + "space": "" + }, + "mmk": { + "id": 3557, + "name": "Myanma Kyat", + "symbol": "mmk", + "token": "Ks", + "space": "" + }, + "nad": { + "id": 3559, + "name": "Namibian Dollar", + "symbol": "nad", + "token": "$", + "space": "" + }, + "npr": { + "id": 3561, + "name": "Nepalese Rupee", + "symbol": "npr", + "token": "\u20a8", + "space": "" + }, + "twd": { + "id": 2811, + "name": "New Taiwan Dollar", + "symbol": "twd", + "token": "NT$", + "space": "" + }, + "nzd": { + "id": 2802, + "name": "New Zealand Dollar", + "symbol": "nzd", + "token": "$", + "space": "" + }, + "nio": { + "id": 3560, + "name": "Nicaraguan Córdoba", + "symbol": "nio", + "token": "C$", + "space": "" + }, + "ngn": { + "id": 2819, + "name": "Nigerian Naira", + "symbol": "ngn", + "token": "\u20a6", + "space": "" + }, + "nok": { + "id": 2801, + "name": "Norwegian Krone", + "symbol": "nok", + "token": "kr", + "space": " " + }, + "omr": { + "id": 3562, + "name": "Omani Rial", + "symbol": "omr", + "token": "\u0631.\u0639.", + "space": "" + }, + "pkr": { + "id": 2804, + "name": "Pakistani Rupee", + "symbol": "pkr", + "token": "\u20a8", + "space": " " + }, + "pab": { + "id": 3563, + "name": "Panamanian Balboa", + "symbol": "pab", + "token": "B/.", + "space": "" + }, + "pen": { + "id": 2822, + "name": "Peruvian Sol", + "symbol": "pen", + "token": "S/.", + "space": "" + }, + "php": { + "id": 2803, + "name": "Philippine Peso", + "symbol": "php", + "token": "\u20b1", + "space": "" + }, + "pln": { + "id": 2805, + "name": "Polish Z\u0142oty", + "symbol": "pln", + "token": "z\u0142", + "space": "" + }, + "gbp": { + "id": 2791, + "name": "Pound Sterling", + "symbol": "gbp", + "token": "£", + "space": "" + }, + "qar": { + "id": 3564, + "name": "Qatari Rial", + "symbol": "qar", + "token": "\u0631.\u0642", + "space": "" + }, + "ron": { + "id": 2817, + "name": "Romanian Leu", + "symbol": "ron", + "token": "lei", + "space": "" + }, + "rub": { + "id": 2806, + "name": "Russian Ruble", + "symbol": "rub", + "token": "\u20bd", + "space": "" + }, + "sar": { + "id": 3566, + "name": "Saudi Riyal", + "symbol": "sar", + "token": "\u0631.\u0633", + "space": "" + }, + "rsd": { + "id": 3565, + "name": "Serbian Dinar", + "symbol": "rsd", + "token": "\u0434\u0438\u043d.", + "space": "" + }, + "sgd": { + "id": 2808, + "name": "Singapore Dollar", + "symbol": "sgd", + "token": "S$", + "space": "" + }, + "zar": { + "id": 2812, + "name": "South African Rand", + "symbol": "zar", + "token": "R", + "space": " " + }, + "krw": { + "id": 2798, + "name": "South Korean Won", + "symbol": "krw", + "token": "\u20a9", + "space": "" + }, + "ssp": { + "id": 3567, + "name": "South Sudanese Pound", + "symbol": "ssp", + "token": "£", + "space": "" + }, + "ves": { + "id": 3573, + "name": "Sovereign Bolivar", + "symbol": "ves", + "token": "Bs.", + "space": "" + }, + "lkr": { + "id": 3553, + "name": "Sri Lankan Rupee", + "symbol": "lkr", + "token": "Rs", + "space": "" + }, + "sek": { + "id": 2807, + "name": "Swedish Krona", + "symbol": "sek", + "token": "kr", + "space": " " + }, + "chf": { + "id": 2785, + "name": "Swiss Franc", + "symbol": "chf", + "token": "Fr", + "space": ". " + }, + "thb": { + "id": 2809, + "name": "Thai Baht", + "symbol": "thb", + "token": "\u0e3f", + "space": "" + }, + "ttd": { + "id": 3569, + "name": "Trinidad and Tobago Dollar", + "symbol": "ttd", + "token": "$", + "space": "" + }, + "tnd": { + "id": 3568, + "name": "Tunisian Dinar", + "symbol": "tnd", + "token": "\u062f.\u062a", + "space": "" + }, + "try": { + "id": 2810, + "name": "Turkish Lira", + "symbol": "try", + "token": "\u20ba", + "space": "" + }, + "ugx": { + "id": 3570, + "name": "Ugandan Shilling", + "symbol": "ugx", + "token": "Sh", + "space": "" + }, + "uah": { + "id": 2824, + "name": "Ukrainian Hryvnia", + "symbol": "uah", + "token": "\u20b4", + "space": "" + }, + "aed": { + "id": 2813, + "name": "United Arab Emirates Dirham", + "symbol": "aed", + "token": "\u062f.\u0625", + "space": "" + }, + "uyu": { + "id": 3571, + "name": "Uruguayan Peso", + "symbol": "uyu", + "token": "$", + "space": "" + }, + "uzs": { + "id": 3572, + "name": "Uzbekistan Som", + "symbol": "uzs", + "token": "so'm", + "space": "" + }, + "vnd": { + "id": 2823, + "name": "Vietnamese Dong", + "symbol": "vnd", + "token": "\u20ab", + "space": "" + } +} diff --git a/src/plugins/Trader/apis/coinmarketcap/index.ts b/src/plugins/Trader/apis/coinmarketcap/index.ts new file mode 100644 index 00000000000..e01e6e7601e --- /dev/null +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -0,0 +1,80 @@ +import CURRENCY_DATA from './currency.json' + +// proxy: https://web-api.coinmarketcap.com/v1 +const BASE_URL = 'https://coinmarketcap.provide.maskbook.com/v1' + +// porxy: https://widgets.coinmarketcap.com/v2 +const WIDGET_BASE_URL = 'https://coinmarketcap.provide.maskbook.com/v2' + +export interface Status { + credit_count: number + elapsed: number + error_code: number + error_message: null | string + notice: null | string + timestamp: string +} + +//#region get all currency +export function getAllCurrenies() { + return CURRENCY_DATA +} +//#endregion + +//#region get all coins +export interface Coin { + id: number + name: string + platform?: { + id: string + name: string + slug: string + symbol: string + token_address: string + } + rank: number + slug: string + status: 'active' + symbol: string +} + +export async function getAllCoins() { + const response = await fetch( + `${BASE_URL}/cryptocurrency/map?aux=status,platform&listing_status=active,untracked&sort=cmc_rank`, + ) + return response.json() as Promise<{ + data: Coin[] + status: Status + }> +} +//#endregion + +//#regin get coin info +export interface CoinInfo { + id: number + name: string + symbol: string + website_slug: string + rank: number + circulating_supply: number + total_supply: number + max_supply: null | number + quotes: Record< + string, + { + price: number + volume_24h?: number + market_cap?: number + percent_change_1h?: number + percent_change_24h?: number + percent_change_7d?: number + } + > + last_updated: number +} + +export async function getCoinInfo(id: number, currency: string) { + const response = await fetch(`${WIDGET_BASE_URL}/ticker/${id}/?ref=widget&convert=${currency}`) + return response.json() as Promise +} +//#endregion diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx new file mode 100644 index 00000000000..b9226ef945d --- /dev/null +++ b/src/plugins/Trader/define.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import type { PluginConfig } from '../plugin' +import { usePostInfoDetails } from '../../components/DataSource/usePostInfo' +import MaskbookPluginWrapper from '../MaskbookPluginWrapper' +import { Suspense } from 'react' +import { SnackbarContent } from '@material-ui/core' + +export const TraderPluginDefine: PluginConfig = { + pluginName: 'Trader', + identifier: 'co.maskbook.trader', + postDialogMetadataBadge: new Map([['com.maskbook.trader:1', (meta) => 'no metadata']]), + + postInspector: function Component(): JSX.Element | null { + const tokenName = usePostInfoDetails('postMetadataMentionedLinks') + if (!tokenName) return null + return ( + + }> + + + + ) + }, +} + +interface TranderProps {} + +function Trader(props: TranderProps) { + return null +} diff --git a/src/plugins/Trader/service.ts b/src/plugins/Trader/service.ts new file mode 100644 index 00000000000..177804c7aba --- /dev/null +++ b/src/plugins/Trader/service.ts @@ -0,0 +1 @@ +export function noop() {} diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 49bdecf694b..6d95383b9e8 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -23,10 +23,12 @@ 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' 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) } From 818620f51a569dd6677dce291a6eccbfcfa6e999 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 21 Aug 2020 13:01:21 +0800 Subject: [PATCH 02/16] chore: TypedMessage & TypedMessageCashTrending + chore: add TypedMessageAnchor + chore: parse tweet into TypedMessage + chore: add TypedMessageCashTrending + chore: add PostDummy + chore: render TypedMessageCashTrending + fix: renderCompoundMessage --- .../InjectedComponents/PostDummy.tsx | 15 ++++++ .../TypedMessageRenderer.tsx | 52 ++++++++++++++++-- src/plugins/Trader/UI/TreadingView.tsx | 0 src/plugins/Trader/define.tsx | 35 +++++------- .../messages/TypedMessageCashTrending.tsx | 25 +++++++++ src/plugins/plugin.ts | 3 +- src/protocols/typed-message/factory.ts | 17 ++++++ src/protocols/typed-message/helpers.ts | 19 ++++++- src/protocols/typed-message/types.ts | 11 ++++ .../facebook.com/UI/collectPosts.tsx | 5 +- .../facebook.com/UI/injectPostDummy.tsx | 3 ++ .../facebook.com/ui-provider.ts | 2 + .../twitter.com/ui/fetch.ts | 19 +++---- .../twitter.com/ui/inject.tsx | 2 + .../twitter.com/ui/injectPostDummy.tsx | 6 +++ .../twitter.com/utils/fetch.ts | 54 +++++++++++++++++-- src/social-network/PostInfo.ts | 12 +++-- .../defaults/emptyDefinition.ts | 1 + .../defaults/injectPostDummy.tsx | 33 ++++++++++++ src/social-network/ui.ts | 8 +++ 20 files changed, 272 insertions(+), 50 deletions(-) create mode 100644 src/components/InjectedComponents/PostDummy.tsx create mode 100644 src/plugins/Trader/UI/TreadingView.tsx create mode 100644 src/plugins/Trader/messages/TypedMessageCashTrending.tsx create mode 100644 src/social-network-provider/facebook.com/UI/injectPostDummy.tsx create mode 100644 src/social-network-provider/twitter.com/ui/injectPostDummy.tsx create mode 100644 src/social-network/defaults/injectPostDummy.tsx diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx new file mode 100644 index 00000000000..474dd7fa8b0 --- /dev/null +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { usePostInfoDetails } from '../DataSource/usePostInfo' +import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' +import { PluginUI } from '../../plugins/plugin' + +export interface PostDummyProps {} + +export function PostDummy(props: PostDummyProps) { + const postMessage = usePostInfoDetails('parsedPostContent') + const processedPostMessage = Array.from(PluginUI.values()).reduce( + (x, plugin) => (plugin.postMessageProcessor ? plugin.postMessageProcessor(x) : x), + postMessage, + ) + return +} diff --git a/src/components/InjectedComponents/TypedMessageRenderer.tsx b/src/components/InjectedComponents/TypedMessageRenderer.tsx index 6473c41c6ac..e140878b37e 100644 --- a/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ b/src/components/InjectedComponents/TypedMessageRenderer.tsx @@ -4,11 +4,14 @@ import anchorme from 'anchorme' import { TypedMessage, TypedMessageText, + TypedMessageAnchor, TypedMessageImage, TypedMessageCompound, TypedMessageUnknown, TypedMessageSuspended, registerTypedMessageRenderer, + TypedMessageEmpty, + makeTypedMessageText, } from '../../protocols/typed-message' import { Image } from '../shared/Image' import { useAsync } from 'react-use' @@ -43,7 +46,11 @@ export const DefaultTypedMessageTextRenderer = React.memo(function DefaultTypedM ) { return renderWithMetadata( props, - + , ) @@ -54,13 +61,32 @@ registerTypedMessageRenderer('text', { priority: 0, }) +export const DefaultTypedMessageAnchorRenderer = React.memo(function DefaultTypedMessageAnchorRenderer( + props: TypedMessageRendererProps, +) { + const { content, href } = props.message + return renderWithMetadata( + props, + + + {content} + + , + ) +}) +registerTypedMessageRenderer('anchor', { + component: DefaultTypedMessageAnchorRenderer, + id: 'maskbook.anchor', + priority: 0, +}) + export const DefaultTypedMessageImageRenderer = React.memo(function DefaultTypedMessageImageRenderer( props: TypedMessageRendererProps, ) { const { image, width, height } = props.message return renderWithMetadata( props, - + , ) @@ -99,10 +125,21 @@ registerTypedMessageRenderer('compound', { priority: 0, }) +export const DefaultTypedMessageEmptyRenderer = React.memo(function DefaultTypedMessageEmptyRenderer( + props: TypedMessageRendererProps, +) { + return renderWithMetadata(props, null) +}) +registerTypedMessageRenderer('empty', { + component: DefaultTypedMessageEmptyRenderer, + id: 'maskbook.empty', + priority: 0, +}) + export const DefaultTypedMessageUnknownRenderer = React.memo(function DefaultTypedMessageUnknownRenderer( props: TypedMessageRendererProps, ) { - return renderWithMetadata(props, Unknown message) + return renderWithMetadata(props, Unknown message) }) registerTypedMessageRenderer('unknown', { component: DefaultTypedMessageUnknownRenderer, @@ -115,9 +152,16 @@ export const DefaultTypedMessageSuspendedRenderer = React.memo(function DefaultT ) { const { promise } = props.message const { loading, error, value } = useAsync(() => promise, [promise]) + return renderWithMetadata( props, - loading ? 'Loading...' : error ? 'Error' : , + loading ? ( + + ) : error ? ( + + ) : ( + + ), ) }) registerTypedMessageRenderer('suspended', { diff --git a/src/plugins/Trader/UI/TreadingView.tsx b/src/plugins/Trader/UI/TreadingView.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index b9226ef945d..d0275470982 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -1,30 +1,23 @@ import React from 'react' import type { PluginConfig } from '../plugin' -import { usePostInfoDetails } from '../../components/DataSource/usePostInfo' -import MaskbookPluginWrapper from '../MaskbookPluginWrapper' -import { Suspense } from 'react' -import { SnackbarContent } from '@material-ui/core' +import { + TypedMessage, + isTypedMessgaeAnchor, + TypedMessageAnchor, + TypedMessageCompound, +} from '../../protocols/typed-message' +import { makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' + +const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => isTypedMessgaeAnchor(m) && m.category === 'cash' export const TraderPluginDefine: PluginConfig = { pluginName: 'Trader', identifier: 'co.maskbook.trader', postDialogMetadataBadge: new Map([['com.maskbook.trader:1', (meta) => 'no metadata']]), - - postInspector: function Component(): JSX.Element | null { - const tokenName = usePostInfoDetails('postMetadataMentionedLinks') - if (!tokenName) return null - return ( - - }> - - - - ) + postMessageProcessor(message: TypedMessageCompound) { + return { + ...message, + items: message.items.map((m: TypedMessage) => (isCashTagMessage(m) ? makeTypedMessageCashTrending(m) : m)), + } }, } - -interface TranderProps {} - -function Trader(props: TranderProps) { - return null -} diff --git a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx new file mode 100644 index 00000000000..529d622b6f1 --- /dev/null +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { TypedMessage, TypedMessageAnchor, registerTypedMessageRenderer } from '../../../protocols/typed-message' + +export interface TypedMessageCashTrending extends TypedMessage { + readonly type: 'anchor/cash_trending' + readonly name: string +} + +export function makeTypedMessageCashTrending(message: TypedMessageAnchor) { + return { + ...message, + type: 'anchor/cash_trending', + name: message.content.substr(1).toLowerCase(), + } as TypedMessageCashTrending +} + +registerTypedMessageRenderer('anchor/cash_trending', { + component: DefaultTypedMessageCashTrendingRenderer, + id: 'co.maskbook.trader.cash_trending', + priority: 0, +}) + +function DefaultTypedMessageCashTrendingRenderer() { + return MASKBOOK! +} diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 6d95383b9e8..145e2a9e4bd 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -1,4 +1,4 @@ -import type { TypedMessage } from '../protocols/typed-message' +import type { TypedMessage, TypedMessageCompound } from '../protocols/typed-message' type PluginInjectFunction = | { @@ -11,6 +11,7 @@ export interface PluginConfig { pluginName: string identifier: string successDecryptionInspector?: PluginInjectFunction<{ message: TypedMessage }> + postMessageProcessor?: (message: TypedMessageCompound) => TypedMessageCompound postInspector?: PluginInjectFunction<{}> postDialogMetadataBadge?: Map string> } diff --git a/src/protocols/typed-message/factory.ts b/src/protocols/typed-message/factory.ts index 672e0ba17cd..efa6fadda14 100644 --- a/src/protocols/typed-message/factory.ts +++ b/src/protocols/typed-message/factory.ts @@ -1,5 +1,6 @@ import type { TypedMessageText, + TypedMessageAnchor, TypedMessageImage, TypedMessageUnknown, TypedMessageCompound, @@ -17,6 +18,22 @@ type Meta = TypedMessage['meta'] export function makeTypedMessageText(content: string, meta?: Meta): TypedMessageText { return { type: 'text', version: 1, content, meta } } + +/** + * Create a TypedAnchorText from a html link + * @param content Text + * @param href the hypter link + * @param meta + */ +export function makeTypedMessageAnchor( + category: 'normal' | 'user' | 'cash' | 'hash', + href: string, + content: string, + meta?: Meta, +): TypedMessageAnchor { + return { type: 'anchor', version: 1, category, href, content, meta } +} + /** * Create a TypedMessageCompound from a list of TypedMessage * @param items A ordered list of TypedMessage diff --git a/src/protocols/typed-message/helpers.ts b/src/protocols/typed-message/helpers.ts index cf170d6f749..0ba01b04a44 100644 --- a/src/protocols/typed-message/helpers.ts +++ b/src/protocols/typed-message/helpers.ts @@ -1,4 +1,10 @@ -import { TypedMessage, isTypedMessageText, isTypedMessageCompound, TypedMessageCompound } from './types' +import { + TypedMessage, + isTypedMessageText, + isTypedMessageCompound, + TypedMessageCompound, + isTypedMessgaeAnchor, +} from './types' import { Result, Ok, Err } from 'ts-results' import { eq } from 'lodash-es' /** @@ -36,6 +42,7 @@ export function isTypedMessageEqual(message1: TypedMessage, message2: TypedMessa } case 'image': case 'text': + case 'anchor': case 'unknown': case 'empty': case 'suspended': @@ -43,3 +50,13 @@ 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 e3ea338f09d..cb6051bb529 100644 --- a/src/protocols/typed-message/types.ts +++ b/src/protocols/typed-message/types.ts @@ -11,6 +11,13 @@ export interface TypedMessageText extends TypedMessage { readonly type: 'text' readonly content: string } +/** It represents a signle link */ +export interface TypedMessageAnchor extends TypedMessage { + readonly type: 'anchor' + readonly category: 'normal' | 'user' | 'cash' | 'hash' + readonly href: string + readonly content: string +} /** It represents a single image */ export interface TypedMessageImage extends TypedMessage { readonly type: 'image' @@ -39,9 +46,13 @@ export interface TypedMessageSuspended ex readonly value: T | null readonly tag?: string } + export function isTypedMessageText(x: TypedMessage): x is TypedMessageText { return x.type === 'text' } +export function isTypedMessgaeAnchor(x: TypedMessage): x is TypedMessageAnchor { + return x.type === 'anchor' +} export function isTypedMessageUnknown(x: TypedMessage): x is TypedMessageUnknown { return x.type === 'unknown' } diff --git a/src/social-network-provider/facebook.com/UI/collectPosts.tsx b/src/social-network-provider/facebook.com/UI/collectPosts.tsx index 03a8f160147..4bd32d71636 100644 --- a/src/social-network-provider/facebook.com/UI/collectPosts.tsx +++ b/src/social-network-provider/facebook.com/UI/collectPosts.tsx @@ -5,10 +5,11 @@ import { PostInfo } from '../../../social-network/PostInfo' import { isMobileFacebook } from '../isMobile' import { getProfileIdentifierAtFacebook } from '../getPersonIdentifierAtFacebook' import { + TypedMessage, makeTypedMessageText, makeTypedMessageImage, - TypedMessage, makeTypedMessageFromList, + makeTypedMessageCompound, } from '../../../protocols/typed-message' import { Flags } from '../../../utils/flags' @@ -83,7 +84,7 @@ export function collectPostsFacebook(this: SocialNetworkUI) { info.postMetadataImages.add(url) nextTypedMessage.push(makeTypedMessageImage(url)) } - info.parsedPostContent.value = makeTypedMessageFromList(...nextTypedMessage) + info.parsedPostContent.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 new file mode 100644 index 00000000000..a7133158a53 --- /dev/null +++ b/src/social-network-provider/facebook.com/UI/injectPostDummy.tsx @@ -0,0 +1,3 @@ +export function injectPostDummyFacebook() { + return () => {} +} diff --git a/src/social-network-provider/facebook.com/ui-provider.ts b/src/social-network-provider/facebook.com/ui-provider.ts index 41cb5e3c37d..15988dd8ba3 100644 --- a/src/social-network-provider/facebook.com/ui-provider.ts +++ b/src/social-network-provider/facebook.com/ui-provider.ts @@ -14,6 +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 { injectPostInspectorFacebook } from './UI/injectPostInspector' import { setStorage } from '../../utils/browser.storage' import { isMobileFacebook } from './isMobile' @@ -92,6 +93,7 @@ export const facebookUISelf = defineSocialNetworkUI({ if (!root.innerText.includes(encryptedComment)) return fail() } }), + injectPostDummy: injectPostDummyFacebook, injectPostInspector: injectPostInspectorFacebook, collectPeople: collectPeopleFacebook, collectPosts: collectPostsFacebook, diff --git a/src/social-network-provider/twitter.com/ui/fetch.ts b/src/social-network-provider/twitter.com/ui/fetch.ts index f65eecb1d94..fafd247c8ef 100644 --- a/src/social-network-provider/twitter.com/ui/fetch.ts +++ b/src/social-network-provider/twitter.com/ui/fetch.ts @@ -15,11 +15,12 @@ import Services from '../../../extension/service' import { untilElementAvailable } from '../../../utils/dom' import { injectMaskbookIconToPost } from './injectMaskbookIcon' import { - makeTypedMessageText, makeTypedMessageImage, makeTypedMessageFromList, makeTypedMessageEmpty, makeTypedMessageSuspended, + serializeTypedMessage, + makeTypedMessageCompound, } from '../../../protocols/typed-message' import { Flags } from '../../../utils/flags' @@ -176,12 +177,12 @@ function collectLinks(tweetNode: HTMLDivElement | null, info: PostInfo) { } function collectPostInfo(tweetNode: HTMLDivElement | null, info: PostInfo, self: Required) { if (!tweetNode) return - const { pid, content, handle, name, avatar } = postParser(tweetNode) - if (!pid) return + const { pid, messages, handle, name, avatar } = postParser(tweetNode) + if (!pid) return const postBy = new ProfileIdentifier(self.networkIdentifier, handle) info.postID.value = pid - info.postContent.value = content + info.postContent.value = messages.map(serializeTypedMessage).join('') if (!info.postBy.value.equals(postBy)) info.postBy.value = postBy info.nickname.value = name info.avatarURL.value = avatar || null @@ -191,15 +192,11 @@ function collectPostInfo(tweetNode: HTMLDivElement | null, info: PostInfo, self: const images = untilElementAvailable(postsImageSelector(tweetNode), 10000) .then(() => postImagesParser(tweetNode)) .then((urls) => { - for (const url of urls) { - info.postMetadataImages.add(url) - } + for (const url of urls) info.postMetadataImages.add(url) if (urls.length) return makeTypedMessageFromList(...urls.map((x) => makeTypedMessageImage(x))) return makeTypedMessageEmpty() }) .catch(() => makeTypedMessageEmpty()) - info.parsedPostContent.value = makeTypedMessageFromList( - makeTypedMessageText(content), - makeTypedMessageSuspended(images), - ) + + info.parsedPostContent.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 e6ef3c5b705..d8e70ab16a4 100644 --- a/src/social-network-provider/twitter.com/ui/inject.tsx +++ b/src/social-network-provider/twitter.com/ui/inject.tsx @@ -4,6 +4,7 @@ import { injectPostDialogAtTwitter } from './injectPostDialog' import { injectPostDialogHintAtTwitter } from './injectPostDialogHint' import { injectPostInspectorAtTwitter } from './injectPostInspector' import { injectPostDialogIconAtTwitter } from './injectPostDialogIcon' +import { injectPostDummyAtTwitter } from './injectPostDummy' const injectPostBox = () => { injectPostDialogAtTwitter() @@ -13,6 +14,7 @@ const injectPostBox = () => { export const twitterUIInjections: SocialNetworkUIInjections = { injectPostBox, + injectPostDummy: injectPostDummyAtTwitter, injectPostInspector: injectPostInspectorAtTwitter, injectKnownIdentity: injectKnownIdentityAtTwitter, } diff --git a/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx b/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx new file mode 100644 index 00000000000..c8a64072317 --- /dev/null +++ b/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx @@ -0,0 +1,6 @@ +import type { PostInfo } from '../../../social-network/PostInfo' +import { injectPostDummyDefault } from '../../../social-network/defaults/injectPostDummy' + +export function injectPostDummyAtTwitter(current: PostInfo) { + return injectPostDummyDefault()(current) +} diff --git a/src/social-network-provider/twitter.com/utils/fetch.ts b/src/social-network-provider/twitter.com/utils/fetch.ts index ad3709bd8fe..52103bf4b4d 100644 --- a/src/social-network-provider/twitter.com/utils/fetch.ts +++ b/src/social-network-provider/twitter.com/utils/fetch.ts @@ -4,6 +4,13 @@ import { defaultTo } from 'lodash-es' import { nthChild } from '../../../utils/dom' import { ProfileIdentifier } from '../../../database/type' import { twitterUrl, canonifyImgUrl } from './url' +import { + makeTypedMessageText, + makeTypedMessageAnchor, + makeTypedMessageEmpty, + TypedMessage, + isTypedMessageEmpty, +} from '../../../protocols/typed-message' /** * @example @@ -155,9 +162,7 @@ export const postAvatarParser = (node: HTMLElement) => { export const postContentParser = (node: HTMLElement) => { if (isMobilePost(node)) { const containerNode = node.querySelector('.tweet-text > div') - if (!containerNode) { - return '' - } + if (!containerNode) return '' return Array.from(containerNode.childNodes) .map((node) => { if (node.nodeType === Node.TEXT_NODE) return node.nodeValue @@ -178,6 +183,44 @@ export const postContentParser = (node: HTMLElement) => { } } +export const postContentMessageParser = (node: HTMLElement) => { + function resolve(content: string) { + if (content.startsWith('@')) return 'user' + if (content.startsWith('#')) return 'hash' + if (content.startsWith('$')) return 'cash' + return 'normal' + } + function make(node: Node): TypedMessage | TypedMessage[] { + const nodeName = node.nodeName.toLowerCase() + if (node.nodeType === Node.TEXT_NODE) { + if (!node.nodeValue) return makeTypedMessageEmpty() + return makeTypedMessageText(node.nodeValue) + } else if (nodeName === 'a') { + const anchor = node as HTMLAnchorElement + 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 + const src = image.getAttribute('src') + const matched = src?.match(/emoji\/v2\/svg\/([\d\w]+)\.svg/) + if (matched && matched[1]) + return makeTypedMessageText(String.fromCodePoint(Number.parseInt(`0x${matched[1]}`))) + return makeTypedMessageEmpty() + } else if (node.childNodes) + return Array.from(node.childNodes).flatMap((x) => { + const x_ = make(x) + return Array.isArray(x_) ? x_ : [x_] + }) + else return makeTypedMessageEmpty() + } + const lang = node.parentElement!.querySelector('[lang]') + if (!lang) return [] + const maked = make(lang) + return Array.isArray(maked) ? maked : [maked] +} + export const postImagesParser = async (node: HTMLElement): Promise => { // TODO: Support steganography in legacy twitter if (isMobilePost(node)) return [] @@ -197,9 +240,10 @@ export const postParser = (node: HTMLElement) => { ...postNameParser(node), avatar: postAvatarParser(node), - // TODO: + // FIXME: // we get wrong pid for nested tweet pid: postIdParser(node), - content: postContentParser(node), + + messages: postContentMessageParser(node).filter((x) => !isTypedMessageEmpty(x)), } } diff --git a/src/social-network/PostInfo.ts b/src/social-network/PostInfo.ts index 541165c8542..9ac0cce533f 100644 --- a/src/social-network/PostInfo.ts +++ b/src/social-network/PostInfo.ts @@ -1,7 +1,12 @@ import { DOMProxy, LiveSelector, ValueRef } from '@holoflows/kit/es' import { ProfileIdentifier, PostIdentifier, Identifier } from '../database/type' import type { Payload } from '../utils/type-transform/Payload' -import { TypedMessage, makeTypedMessageCompound, isTypedMessageEqual } from '../protocols/typed-message' +import { + TypedMessage, + makeTypedMessageCompound, + isTypedMessageEqual, + TypedMessageCompound, +} from '../protocols/typed-message' import { Result, Err } from 'ts-results' import { ObservableSet, ObservableMap } from '../utils/ObservableMapSet' import { parseURL } from '../utils/utils' @@ -13,9 +18,6 @@ export abstract class PostInfo { if (by.isUnknown || id === null) this.postIdentifier.value = null else this.postIdentifier.value = new PostIdentifier(by, id) } - if (process.env.NODE_ENV === 'development') { - this.parsedPostContent.addListener((x) => console.log(x)) - } this.postID.addListener(calc) this.postBy.addListener(calc) @@ -41,7 +43,7 @@ export abstract class PostInfo { * 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 parsedPostContent = new ValueRef(makeTypedMessageCompound([]), isTypedMessageEqual) /** * The un-decrypted post content after transformation. */ diff --git a/src/social-network/defaults/emptyDefinition.ts b/src/social-network/defaults/emptyDefinition.ts index 370ed109e3b..36ecc733eff 100644 --- a/src/social-network/defaults/emptyDefinition.ts +++ b/src/social-network/defaults/emptyDefinition.ts @@ -30,6 +30,7 @@ export const emptyDefinition: SocialNetworkUIDefinition = { injectCommentBox: nopWithUnmount, injectPostBox: nop, injectPostComments: nopWithUnmount, + injectPostDummy: nopWithUnmount, injectPostInspector: nopWithUnmount, resolveLastRecognizedIdentity: nop, posts: new ObservableWeakMap(), diff --git a/src/social-network/defaults/injectPostDummy.tsx b/src/social-network/defaults/injectPostDummy.tsx new file mode 100644 index 00000000000..9cb361b3e63 --- /dev/null +++ b/src/social-network/defaults/injectPostDummy.tsx @@ -0,0 +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 type { PostInfo } from '../PostInfo' +import { makeStyles } from '@material-ui/core' + +export function injectPostDummyDefault( + config: InjectPostDummyDefaultConfig = {}, + additionalPropsToPostDummy: (classes: Record) => Partial = () => ({}), + useCustomStyles: (props?: any) => Record = makeStyles({}) as any, +) { + const PostDummyDefault = React.memo(function PostDummyDefault() { + const classes = useCustomStyles() + const additionalProps = additionalPropsToPostDummy(classes) + return + }) + + return function injectPostDummy(current: PostInfo) { + return renderInShadowRoot( + + + , + { + shadow: () => current.rootNodeProxy.afterShadow, + normal: () => current.rootNodeProxy.after, + concurrent: true, + }, + ) + } +} + +interface InjectPostDummyDefaultConfig {} diff --git a/src/social-network/ui.ts b/src/social-network/ui.ts index 82362398835..bbbe7be1534 100644 --- a/src/social-network/ui.ts +++ b/src/social-network/ui.ts @@ -124,6 +124,12 @@ export interface SocialNetworkUIInjections { * @returns unmount the injected components */ injectCommentBox?: ((current: PostInfo) => () => void) | 'disabled' + /** + * This function should inject the post dummy + * @param current The current post + * @returns unmount the injected components + */ + injectPostDummy(current: PostInfo): () => void /** * This function should inject the post box * @param current The current post @@ -328,6 +334,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 unmountCommentBox: () => void = ui.injectCommentBox === 'disabled' ? nopWithUnmount : defaultTo(ui.injectCommentBox, nopWithUnmount)(value) const unmountPostComments: () => void = @@ -336,6 +343,7 @@ function hookUIPostMap(ui: SocialNetworkUI) { : defaultTo(ui.injectPostComments, nopWithUnmount)(value) unmountFunctions.set(key, () => { unmountPostInspector() + unmountPostDummy() unmountCommentBox() unmountPostComments() }) From f0571e6b1bd05b8054551d2b20c594681fb48d50 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Mon, 24 Aug 2020 15:37:39 +0800 Subject: [PATCH 03/16] chore(ui): add TrendingView --- .../shared-settings/useSettingsUI.tsx | 7 +- .../background-script/PluginService.ts | 2 +- src/plugins/Gitcoin/DonateDialog.tsx | 6 +- src/plugins/Trader/UI/SettingsDialog.tsx | 174 ++++++++++++++++++ src/plugins/Trader/UI/TreadingView.tsx | 0 src/plugins/Trader/UI/TrendingView.tsx | 113 ++++++++++++ src/plugins/Trader/apis/coingecko/index.ts | 4 +- src/plugins/Trader/apis/index.ts | 19 ++ src/plugins/Trader/define.tsx | 13 +- .../Trader/{service.ts => services.ts} | 0 src/plugins/Trader/settings.ts | 4 + src/plugins/Trader/type.ts | 34 ++++ .../twitter.com/utils/theme.ts | 1 + src/utils/enum.ts | 8 + 14 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 src/plugins/Trader/UI/SettingsDialog.tsx delete mode 100644 src/plugins/Trader/UI/TreadingView.tsx create mode 100644 src/plugins/Trader/UI/TrendingView.tsx create mode 100644 src/plugins/Trader/apis/index.ts rename src/plugins/Trader/{service.ts => services.ts} (100%) create mode 100644 src/plugins/Trader/settings.ts create mode 100644 src/plugins/Trader/type.ts create mode 100644 src/utils/enum.ts diff --git a/src/components/shared-settings/useSettingsUI.tsx b/src/components/shared-settings/useSettingsUI.tsx index 7f9969a0b45..7c7329a62f0 100644 --- a/src/components/shared-settings/useSettingsUI.tsx +++ b/src/components/shared-settings/useSettingsUI.tsx @@ -16,6 +16,7 @@ import { } from '@material-ui/core' import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos' import { useStylesExtends } from '../custom-ui-helper' +import { getEnumAsArray } from '../../utils/enum' const useStyles = makeStyles((theme) => createStyles({ @@ -153,16 +154,12 @@ export function useSettingsUI(ref: ValueRef) { function useEnumSettings( ...[ref, enumObject, getText, selectProps]: useEnumSettingsParams ): HookedUI { - const enum_ = Object.keys(enumObject) - // Leave only key of enum - .filter((x) => Number.isNaN(parseInt(x))) - .map((key) => ({ key, value: enumObject[key as keyof Q] })) + const enum_ = getEnumAsArray(enumObject) const change = (value: any) => { if (!Number.isNaN(parseInt(value))) { value = parseInt(value) } if (!enum_.some((x) => x.value === value)) { - console.log(value) throw new Error('Invalid state') } ref.value = value diff --git a/src/extension/background-script/PluginService.ts b/src/extension/background-script/PluginService.ts index 4e403e15058..f3ceb6a5c60 100644 --- a/src/extension/background-script/PluginService.ts +++ b/src/extension/background-script/PluginService.ts @@ -2,7 +2,7 @@ import * as RedPacket from '../../plugins/RedPacket/state-machine' import * as Wallet from '../../plugins/Wallet/wallet' import * as Gitcoin from '../../plugins/Gitcoin/service' import * as FileService from '../../plugins/FileService/service' -import * as Trader from '../../plugins/Trader/service' +import * as Trader from '../../plugins/Trader/services' import type { ERC20TokenRecord, ManagedWalletRecord, ExoticWalletRecord } from '../../plugins/Wallet/database/types' import { EthereumNetwork } from '../../plugins/Wallet/database/types' import { getWalletProvider, web3 } from '../../plugins/Wallet/web3' diff --git a/src/plugins/Gitcoin/DonateDialog.tsx b/src/plugins/Gitcoin/DonateDialog.tsx index 44d4922f6f9..9d9c113fb7e 100644 --- a/src/plugins/Gitcoin/DonateDialog.tsx +++ b/src/plugins/Gitcoin/DonateDialog.tsx @@ -35,7 +35,11 @@ import type { ERC20TokenDetails, WalletDetails } from '../../extension/backgroun const useStyles = makeStyles((theme: Theme) => createStyles({ - form: { '& > *': { margin: theme.spacing(1, 0) } }, + form: { + '& > *': { + margin: theme.spacing(1, 0), + }, + }, title: { marginLeft: 6, }, diff --git a/src/plugins/Trader/UI/SettingsDialog.tsx b/src/plugins/Trader/UI/SettingsDialog.tsx new file mode 100644 index 00000000000..ea255c013bb --- /dev/null +++ b/src/plugins/Trader/UI/SettingsDialog.tsx @@ -0,0 +1,174 @@ +import React, { useState } 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, +} from '@material-ui/core' +import ShadowRootDialog from '../../../utils/jss/ShadowRootDialog' +import { DialogDismissIconUI } from '../../../components/InjectedComponents/DialogDismissIcon' +import { Currency, Platform, resolveCurrencyName, resolvePlatformName } from '../type' +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 +} + +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/TreadingView.tsx b/src/plugins/Trader/UI/TreadingView.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx new file mode 100644 index 00000000000..d943419b8e5 --- /dev/null +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect } from 'react' +import { makeStyles, Avatar, Typography, Card, CardHeader, IconButton, CardActions } from '@material-ui/core' +import { useAsync } from 'react-use' +import SettingsIcon from '@material-ui/icons/Settings' +import { SettingsDialog } from './SettingsDialog' +import { Platform, Currency, resolvePlatformName, Settings } from '../type' +import { getCurrenies } from '../apis' +import { PortalShadowRoot, portalShadowRoot } from '../../../utils/jss/ShadowRootPortal' +import { setStorage, getStorage } from '../../../utils/browser.storage' +import { getActivatedUI } from '../../../social-network/ui' +import stringify from 'json-stable-stringify' +import { currentTrendingViewSettings, currentTrendingViewPlatformSettings } from '../settings' +import { useValueRef } from '../../../utils/hooks/useValueRef' + +const network = getActivatedUI().networkIdentifier + +const useStyles = makeStyles({ + root: {}, + header: { + display: 'flex', + }, + body: {}, + avatar: {}, + amount: {}, +}) + +export interface TrendingViewProps extends withClasses> { + keyword: string +} + +export function TrendingView(props: TrendingViewProps) { + const classes = useStyles() + 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, error } = useAsync(() => 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 + + if (loadingCurrencies || !currency) return null + return ( + <> + + + } + action={ + setSettingsDialogOpen(true)}> + + + } + title={{`BTC / ${currency.name}`}} + subheader={ + + {`${currency.name} 12,223`} + (1.2%) + + } + /> + + + Powered by {resolvePlatformName(platform)} + + + + { + currentTrendingViewSettings[networkKey].value = stringify({ + currency, + }) + }} + onPlatformChange={(platform) => { + currentTrendingViewPlatformSettings[network].value = String(platform) + }} + onClose={() => setSettingsDialogOpen(false)} + /> + + ) +} diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 2edff8ade7e..cf335db12fc 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,4 +1,6 @@ -const BASE_URL = 'https://coingecko.com/api/documentations/v3' +import type { Currency } from '../../type' + +const BASE_URL = 'https://api.coingecko.com/api/v3' //#region get currency export async function getAllCurrenies() { diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts new file mode 100644 index 00000000000..a68f9cda46d --- /dev/null +++ b/src/plugins/Trader/apis/index.ts @@ -0,0 +1,19 @@ +import { Platform, Currency } from '../type' +import * as coinGeckoAPI from './coingecko' +import * as coinMarketCapAPI from './coinmarketcap' + +export async function getCurrenies(platform: Platform): Promise { + if (platform === Platform.COIN_GECKO) { + const currencies = await coinGeckoAPI.getAllCurrenies() + return currencies.map((x) => ({ + id: x, + name: x.toUpperCase(), + })) + } + return Object.values(coinMarketCapAPI.getAllCurrenies()).map((x) => ({ + id: String(x.id), + name: x.symbol.toUpperCase(), + symbol: x.token, + description: x.name, + })) +} diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index d0275470982..8c105f82bef 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Suspense } 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 { TrendingView } from './UI/TrendingView' +import MaskbookPluginWrapper from '../MaskbookPluginWrapper' 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)), } }, + postInspector() { + return ( + + + + + + ) + }, } diff --git a/src/plugins/Trader/service.ts b/src/plugins/Trader/services.ts similarity index 100% rename from src/plugins/Trader/service.ts rename to src/plugins/Trader/services.ts diff --git a/src/plugins/Trader/settings.ts b/src/plugins/Trader/settings.ts new file mode 100644 index 00000000000..0442af59263 --- /dev/null +++ b/src/plugins/Trader/settings.ts @@ -0,0 +1,4 @@ +import { createNetworkSettings } from '../../settings/createSettings' + +export const currentTrendingViewSettings = createNetworkSettings('currentTrendingViewSettings') +export const currentTrendingViewPlatformSettings = createNetworkSettings('currentTrendingViewPlatformSettings') diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/type.ts new file mode 100644 index 00000000000..276084d2d1d --- /dev/null +++ b/src/plugins/Trader/type.ts @@ -0,0 +1,34 @@ +export interface Settings { + currency: Currency +} + +export enum Platform { + COIN_GECKO, + COIN_MARKET_CAP, +} + +export interface Currency { + id: string + name: string + symbol?: string + description?: string +} + +export function resolveCurrencyName(currency: Currency) { + return [ + currency.name, + currency.symbol ? `"${currency.symbol}"` : '', + currency.description ? `(${currency.description})` : '', + ].join(' ') +} + +export function resolvePlatformName(platform: Platform) { + switch (platform) { + case Platform.COIN_GECKO: + return 'Coin Gecko' + case Platform.COIN_MARKET_CAP: + return 'Coin Market Cap' + default: + return '' + } +} diff --git a/src/social-network-provider/twitter.com/utils/theme.ts b/src/social-network-provider/twitter.com/utils/theme.ts index 860730f997d..afabb2d74e8 100644 --- a/src/social-network-provider/twitter.com/utils/theme.ts +++ b/src/social-network-provider/twitter.com/utils/theme.ts @@ -99,6 +99,7 @@ export const useTwitterDialog = makeStyles((theme: Theme) => { }, }, actions: { + padding: '10px 15px', [`@media (max-width: ${theme.breakpoints.width('sm')}px)`]: { '&': { display: 'flex', diff --git a/src/utils/enum.ts b/src/utils/enum.ts new file mode 100644 index 00000000000..74cc569db60 --- /dev/null +++ b/src/utils/enum.ts @@ -0,0 +1,8 @@ +export function getEnumAsArray(enumObject: T) { + return ( + Object.keys(enumObject) + // Leave only key of enum + .filter((x) => Number.isNaN(parseInt(x))) + .map((key) => ({ key, value: enumObject[key as keyof T] })) + ) +} From 477aed8e671731d154b4078d0396f80c9fe151b7 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 25 Aug 2020 11:11:21 +0800 Subject: [PATCH 04/16] chore: integrate with APIs --- src/plugins/Trader/UI/TrendingView.tsx | 88 ++++++++++++++----- src/plugins/Trader/apis/coingecko/index.ts | 5 +- .../Trader/apis/coinmarketcap/index.ts | 10 ++- src/plugins/Trader/apis/index.ts | 55 +++++++++++- src/plugins/Trader/services.ts | 2 +- src/plugins/Trader/type.ts | 20 +++++ src/plugins/Wallet/formatter.ts | 4 + 7 files changed, 155 insertions(+), 29 deletions(-) diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index d943419b8e5..0c765dce160 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -1,28 +1,47 @@ import React, { useState, useEffect } from 'react' -import { makeStyles, Avatar, Typography, Card, CardHeader, IconButton, CardActions } from '@material-ui/core' +import { + makeStyles, + Avatar, + Typography, + Card, + CardHeader, + IconButton, + CardActions, + Theme, + createStyles, +} from '@material-ui/core' import { useAsync } from 'react-use' import SettingsIcon from '@material-ui/icons/Settings' +import classNames from 'classnames' +import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp' +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown' import { SettingsDialog } from './SettingsDialog' import { Platform, Currency, resolvePlatformName, Settings } from '../type' -import { getCurrenies } from '../apis' import { PortalShadowRoot, portalShadowRoot } from '../../../utils/jss/ShadowRootPortal' import { setStorage, getStorage } from '../../../utils/browser.storage' 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' const network = getActivatedUI().networkIdentifier -const useStyles = makeStyles({ - root: {}, - header: { - display: 'flex', - }, - body: {}, - avatar: {}, - amount: {}, -}) +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: {}, + header: { + display: 'flex', + }, + body: {}, + avatar: {}, + percentage: { + marginLeft: theme.spacing(1), + }, + }), +) export interface TrendingViewProps extends withClasses> { keyword: string @@ -30,6 +49,7 @@ export interface TrendingViewProps extends withClasses(currentTrendingViewPlatformSettings[network]) //#region currency & platform - const { value: currencies = [], loading: loadingCurrencies, error } = useAsync(() => getCurrenies(platform), [ - platform, - ]) + const { value: currencies = [], loading: loadingCurrencies } = useAsync( + () => Services.Plugin.invokePlugin('maskbook.trader', 'getCurrenies', platform), + [platform], + ) // sync platform useEffect(() => { @@ -65,25 +86,52 @@ export function TrendingView(props: TrendingViewProps) { }, [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 || !currency) return null + if (loadingCoinInfo || !coinInfo) return null + return ( <> - } + avatar={} action={ setSettingsDialogOpen(true)}> } - title={{`BTC / ${currency.name}`}} + title={ + {`${coinInfo.coin.symbol.toUpperCase()} / ${ + currency.name + }`} + } subheader={ - {`${currency.name} 12,223`} - (1.2%) + {`${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} } /> diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index cf335db12fc..7c2e74c7240 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,5 +1,3 @@ -import type { Currency } from '../../type' - const BASE_URL = 'https://api.coingecko.com/api/v3' //#region get currency @@ -54,11 +52,12 @@ export interface CoinInfo { localization: Record market_cap_rank: number market_data: { + current_price: Record high_24h: Record low_24h: Record market_cap: Record market_cap_rank: number - price_change_24h: number + price_change_percentage_24h: number total_supply: number total_volume: Record } diff --git a/src/plugins/Trader/apis/coinmarketcap/index.ts b/src/plugins/Trader/apis/coinmarketcap/index.ts index e01e6e7601e..8dcc4cfe9a5 100644 --- a/src/plugins/Trader/apis/coinmarketcap/index.ts +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -3,8 +3,7 @@ import CURRENCY_DATA from './currency.json' // proxy: https://web-api.coinmarketcap.com/v1 const BASE_URL = 'https://coinmarketcap.provide.maskbook.com/v1' -// porxy: https://widgets.coinmarketcap.com/v2 -const WIDGET_BASE_URL = 'https://coinmarketcap.provide.maskbook.com/v2' +const WIDGET_BASE_URL = 'https://widgets.coinmarketcap.com/v2' export interface Status { credit_count: number @@ -73,8 +72,11 @@ export interface CoinInfo { last_updated: number } -export async function getCoinInfo(id: number, currency: string) { +export async function getCoinInfo(id: string, currency: string) { const response = await fetch(`${WIDGET_BASE_URL}/ticker/${id}/?ref=widget&convert=${currency}`) - return response.json() as Promise + return response.json() as Promise<{ + data: CoinInfo + status: Status + }> } //#endregion diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index a68f9cda46d..327649f45e1 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -1,4 +1,4 @@ -import { Platform, Currency } from '../type' +import { Platform, Currency, Coin, Trending } from '../type' import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' @@ -17,3 +17,56 @@ export async function getCurrenies(platform: Platform): Promise { description: x.name, })) } + +export async function getCoins(platform: Platform): Promise { + if (platform === Platform.COIN_GECKO) return coinGeckoAPI.getAllCoins() + return (await coinMarketCapAPI.getAllCoins()).data.map((x) => ({ + id: String(x.id), + name: x.name, + symbol: x.symbol, + })) +} + +export async function getCoinInfo(id: string, platform: Platform, currency: Currency): Promise { + if (platform === Platform.COIN_GECKO) { + const info = await coinGeckoAPI.getCoinInfo(id) + return { + coin: { + id, + name: info.name, + symbol: info.symbol, + image_url: info.image.thumb, + }, + currency, + platform, + market: { + current_price: info.market_data.current_price[currency.id], + total_volume: info.market_data.total_volume[currency.id], + price_change_24h: info.market_data.price_change_percentage_24h, + }, + } + } + const info = await coinMarketCapAPI.getCoinInfo(id, currency.name.toUpperCase()) + return { + coin: { + id, + name: info.data.name, + symbol: info.data.symbol, + image_url: `https://s2.coinmarketcap.com/static/img/coins/64x64/${id}.png`, + }, + currency, + platform, + market: { + current_price: info.data.quotes[currency.name.toUpperCase()].price, + total_volume: info.data.quotes[currency.name.toUpperCase()].volume_24h, + price_change_24h: info.data.quotes[currency.name.toUpperCase()].percent_change_24h, + }, + } +} + +export async function getCoinTrendingByKeyword(keyword: string, platform: Platform, currency: Currency) { + const coins = await getCoins(platform) + const coin = coins.find((x) => x.symbol.toLowerCase() === keyword.toLowerCase()) + if (!coin) return null + return getCoinInfo(coin.id, platform, currency) +} diff --git a/src/plugins/Trader/services.ts b/src/plugins/Trader/services.ts index 177804c7aba..5914b704d67 100644 --- a/src/plugins/Trader/services.ts +++ b/src/plugins/Trader/services.ts @@ -1 +1 @@ -export function noop() {} +export * from './apis' diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/type.ts index 276084d2d1d..c233d64cbfd 100644 --- a/src/plugins/Trader/type.ts +++ b/src/plugins/Trader/type.ts @@ -14,6 +14,26 @@ export interface Currency { description?: string } +export interface Coin { + id: string + name: string + symbol: string + image_url?: string +} + +export interface Market { + current_price: number + total_volume?: number + price_change_24h?: number +} + +export interface Trending { + platform: Platform + coin: Coin + currency: Currency + market: Market +} + export function resolveCurrencyName(currency: Currency) { return [ currency.name, diff --git a/src/plugins/Wallet/formatter.ts b/src/plugins/Wallet/formatter.ts index 62550546cf8..95e6aea26fd 100644 --- a/src/plugins/Wallet/formatter.ts +++ b/src/plugins/Wallet/formatter.ts @@ -21,3 +21,7 @@ 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, '$&,') +} From d91be233b5e171e79c311be85b1c6fded7c8d7f6 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 25 Aug 2020 16:22:35 +0800 Subject: [PATCH 05/16] chore: support WholePostVisibility --- src/_locales/en/messages.json | 2 +- .../InjectedComponents/PostDummy.tsx | 8 ++- .../TypedMessageRenderer.tsx | 2 +- .../DashboardRouters/Settings.tsx | 17 ++++++ src/plugins/Trader/UI/SettingsDialog.tsx | 4 ++ src/plugins/Trader/UI/TrendingView.tsx | 59 +++++++++++++------ src/plugins/Trader/apis/index.ts | 2 +- src/plugins/Trader/define.tsx | 11 ---- .../messages/TypedMessageCashTrending.tsx | 35 +++++++++-- src/settings/settings.ts | 15 +++++ 10 files changed, 118 insertions(+), 37 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 0e1af11ba97..13e23bccdb4 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -240,7 +240,7 @@ "settings_appearance_dark": "Dark", "settings_appearance_light": "Light", "settings_language": "Language", - "settings_choose_eth_network": "Choose Ethereum network", + "settings_choose_eth_network": "Choose Ethereum Network", "skip": "Skip", "share": "Share", "share_to": "Share to…", diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx index 474dd7fa8b0..b71ef9413f2 100644 --- a/src/components/InjectedComponents/PostDummy.tsx +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -2,6 +2,8 @@ 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 {} @@ -11,5 +13,9 @@ export function PostDummy(props: PostDummyProps) { (x, plugin) => (plugin.postMessageProcessor ? plugin.postMessageProcessor(x) : x), postMessage, ) - return + return ( + !isTypedMessageSuspended(x)))} + /> + ) } diff --git a/src/components/InjectedComponents/TypedMessageRenderer.tsx b/src/components/InjectedComponents/TypedMessageRenderer.tsx index e140878b37e..0739c7b1f35 100644 --- a/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ b/src/components/InjectedComponents/TypedMessageRenderer.tsx @@ -68,7 +68,7 @@ export const DefaultTypedMessageAnchorRenderer = React.memo(function DefaultType return renderWithMetadata( props, - + {content} , diff --git a/src/extension/options-page/DashboardRouters/Settings.tsx b/src/extension/options-page/DashboardRouters/Settings.tsx index d18fb16a475..f46a50fe804 100644 --- a/src/extension/options-page/DashboardRouters/Settings.tsx +++ b/src/extension/options-page/DashboardRouters/Settings.tsx @@ -9,9 +9,11 @@ import { languageSettings, Language, renderInShadowRootSettings, + currentWholePostVisibilitySettings, currentLocalWalletEthereumNetworkSettings, appearanceSettings, Appearance, + WholePostVisibility, } from '../../../settings/settings' import { useValueRef } from '../../../utils/hooks/useValueRef' @@ -20,6 +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 TabIcon from '@material-ui/icons/Tab' import PaletteIcon from '@material-ui/icons/Palette' import LanguageIcon from '@material-ui/icons/Language' @@ -115,6 +118,7 @@ export default function DashboardSettingsRouter() { const { t } = useI18N() const currentLang = useValueRef(languageSettings) const currentApperance = useValueRef(appearanceSettings) + const currentWholePostVisibility = useValueRef(currentWholePostVisibilitySettings) const langMapper = React.useRef((x: Language) => { if (x === Language.en) return 'English' if (x === Language.zh) return '中文' @@ -126,6 +130,11 @@ 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' + }).current const classes = useStyles() const shadowRoot = useValueRef(renderInShadowRootSettings) const theme = useTheme() @@ -173,6 +182,14 @@ export default function DashboardSettingsRouter() { value={currentLocalWalletEthereumNetworkSettings} /> ) : null} + } + value={currentWholePostVisibilitySettings} + /> diff --git a/src/plugins/Trader/UI/SettingsDialog.tsx b/src/plugins/Trader/UI/SettingsDialog.tsx index ea255c013bb..ec494a6fe16 100644 --- a/src/plugins/Trader/UI/SettingsDialog.tsx +++ b/src/plugins/Trader/UI/SettingsDialog.tsx @@ -75,6 +75,10 @@ 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 (
- createStyles({ - root: {}, +const useStyles = makeStyles((theme: Theme) => { + const internalName = getActivatedUI()?.internalName + return createStyles({ + root: { + ...(internalName === 'twitter' + ? { border: `1px solid ${theme.palette.type === 'dark' ? '#2f3336' : '#ccd6dd'}` } + : null), + }, header: { display: 'flex', }, body: {}, + footer: { + justifyContent: 'flex-end', + }, + footnote: { + fontSize: 10, + }, avatar: {}, percentage: { marginLeft: theme.spacing(1), }, - }), -) + }) +}) export interface TrendingViewProps extends withClasses> { keyword: string + SettingsDialogProps?: Partial } export function TrendingView(props: TrendingViewProps) { @@ -98,17 +108,31 @@ export function TrendingView(props: TrendingViewProps) { ) }, [platform, currency, props.keyword]) //#endregion - if (loadingCurrencies || !currency) return null - if (loadingCoinInfo || !coinInfo) return null + + if (loadingCurrencies || loadingCoinInfo) + return ( + + + + + + ) + if (!currency) return null + if (!coinInfo) return null return ( <> - + } action={ - setSettingsDialogOpen(true)}> + { + console.log('DEUBG: click') + setSettingsDialogOpen(true) + }}> } @@ -119,7 +143,7 @@ export function TrendingView(props: TrendingViewProps) { } subheader={ - {`${currency.symbol ?? currency.name} ${formatCurrency( + {`${currency.symbol ?? `${currency.name} `}${formatCurrency( coinInfo.market.current_price, )}`} {coinInfo.market.price_change_24h ? ( @@ -135,8 +159,8 @@ export function TrendingView(props: TrendingViewProps) { } /> - - + + Powered by {resolvePlatformName(platform)} @@ -155,6 +179,7 @@ export function TrendingView(props: TrendingViewProps) { currentTrendingViewPlatformSettings[network].value = String(platform) }} onClose={() => setSettingsDialogOpen(false)} + {...props.SettingsDialogProps} /> ) diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 327649f45e1..edf92b8ad96 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -35,7 +35,7 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr id, name: info.name, symbol: info.symbol, - image_url: info.image.thumb, + image_url: info.image.small, }, currency, platform, diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index 8c105f82bef..a32cfb9c3d1 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -7,8 +7,6 @@ import { TypedMessageCompound, } from '../../protocols/typed-message' import { makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' -import { TrendingView } from './UI/TrendingView' -import MaskbookPluginWrapper from '../MaskbookPluginWrapper' const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => isTypedMessgaeAnchor(m) && m.category === 'cash' @@ -22,13 +20,4 @@ export const TraderPluginDefine: PluginConfig = { items: message.items.map((m: TypedMessage) => (isCashTagMessage(m) ? makeTypedMessageCashTrending(m) : m)), } }, - postInspector() { - return ( - - - - - - ) - }, } diff --git a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx index 529d622b6f1..d5dacc9bf44 100644 --- a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -1,7 +1,10 @@ -import React from 'react' -import { TypedMessage, TypedMessageAnchor, registerTypedMessageRenderer } from '../../../protocols/typed-message' +import React, { useState, useRef } from 'react' +import { TypedMessageAnchor, registerTypedMessageRenderer } from '../../../protocols/typed-message' +import { Link, Typography, Popper } from '@material-ui/core' +import type { TypedMessageRendererProps } from '../../../components/InjectedComponents/TypedMessageRenderer' +import { TrendingView } from '../UI/TrendingView' -export interface TypedMessageCashTrending extends TypedMessage { +export interface TypedMessageCashTrending extends Omit { readonly type: 'anchor/cash_trending' readonly name: string } @@ -20,6 +23,28 @@ registerTypedMessageRenderer('anchor/cash_trending', { priority: 0, }) -function DefaultTypedMessageCashTrendingRenderer() { - return MASKBOOK! +function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { + const rootEl = useRef(null) + const [anchorEl, setAnchorEl] = useState(null) + + return ( +
+ + ) => setAnchorEl(e.currentTarget)}> + {props.message.content} + + + rootEl.current} + transition + style={{ zIndex: 1 }}> + + +
+ ) } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 2847491f6df..aeb1d66f65e 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -44,6 +44,21 @@ export const appearanceSettings = createGlobalSettings('apperance', primary: () => i18n.t('settings_appearance'), }) +export enum WholePostVisibility { + all = 'all', + enhancedOnly = 'enhancedOnly', + encryptedOnly = 'encryptedOnly', +} + +export const currentWholePostVisibilitySettings = createGlobalSettings( + 'whole post visibility', + WholePostVisibility.all, + { + primary: () => 'Whole Post Visibility', + secondary: () => '', + }, +) + export const currentLocalWalletEthereumNetworkSettings = createGlobalSettings( 'eth network', EthereumNetwork.Mainnet, From 4f9f52071da95f16c59fe3cb86dc3b52f435297e Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Wed, 26 Aug 2020 11:31:03 +0800 Subject: [PATCH 06/16] chore: inject page inspector --- .../InjectedComponents/PageInspector.tsx | 20 +++ .../InjectedComponents/PostDummy.tsx | 1 - src/plugins/Trader/UI/SettingsDialog.tsx | 17 +- src/plugins/Trader/UI/TradeDialog.tsx | 5 + src/plugins/Trader/UI/TrendingPopper.tsx | 39 ++++ src/plugins/Trader/UI/TrendingView.tsx | 167 ++++-------------- src/plugins/Trader/apis/index.ts | 18 ++ src/plugins/Trader/define.tsx | 13 +- src/plugins/Trader/hooks/useTrending.ts | 56 ++++++ src/plugins/Trader/messages.ts | 27 +++ .../messages/TypedMessageCashTrending.tsx | 37 ++-- src/plugins/Trader/type.ts | 2 +- src/plugins/plugin.ts | 3 +- .../facebook.com/UI/injectPageInspector.tsx | 3 + .../facebook.com/ui-provider.ts | 2 + .../twitter.com/ui/inject.tsx | 2 + .../twitter.com/ui/injectPageInspector.tsx | 5 + .../defaults/emptyDefinition.ts | 1 + .../defaults/injectPageInspector.tsx | 35 ++++ src/social-network/ui.ts | 5 + 20 files changed, 296 insertions(+), 162 deletions(-) create mode 100644 src/components/InjectedComponents/PageInspector.tsx create mode 100644 src/plugins/Trader/UI/TradeDialog.tsx create mode 100644 src/plugins/Trader/UI/TrendingPopper.tsx create mode 100644 src/plugins/Trader/hooks/useTrending.ts create mode 100644 src/plugins/Trader/messages.ts create mode 100644 src/social-network-provider/facebook.com/UI/injectPageInspector.tsx create mode 100644 src/social-network-provider/twitter.com/ui/injectPageInspector.tsx create mode 100644 src/social-network/defaults/injectPageInspector.tsx 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..238414c9525 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -1,31 +1,22 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { makeStyles, Avatar, Typography, Card, CardHeader, - IconButton, + CardContent, CardActions, Theme, createStyles, CircularProgress, - CardContent, } from '@material-ui/core' -import { useAsync } from 'react-use' -import SettingsIcon from '@material-ui/icons/Settings' 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' - -const network = getActivatedUI().networkIdentifier +import { useTrending } from '../hooks/useTrending' const useStyles = makeStyles((theme: Theme) => { const internalName = getActivatedUI()?.internalName @@ -53,134 +44,54 @@ 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) + const { value: trending, loading } = useTrending(props.name) + if (loading) return ( - + ) - if (!currency) return null - if (!coinInfo) return null - + 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/apis/index.ts b/src/plugins/Trader/apis/index.ts index edf92b8ad96..8950acf4272 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -18,6 +18,24 @@ export async function getCurrenies(platform: Platform): Promise { })) } +export async function getLimitedCurrenies(platform: Platform): Promise { + const usd = + platform === Platform.COIN_GECKO + ? { + id: 'usd', + name: 'USD', + symbol: '$', + description: 'Unite State Dollar', + } + : { + id: '2781', + name: 'USD', + symbol: '$', + description: 'Unite State Dollar', + } + return Promise.resolve([usd]) +} + export async function getCoins(platform: Platform): Promise { if (platform === Platform.COIN_GECKO) return coinGeckoAPI.getAllCoins() return (await coinMarketCapAPI.getAllCoins()).data.map((x) => ({ 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..5c171112921 --- /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', 'getLimitedCurrenies', 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 145e2a9e4bd..aebaa35331e 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 15988dd8ba3..13d4e0a6533 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' @@ -95,6 +96,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 bbbe7be1534..639bd75dfd0 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. * @@ -304,6 +308,7 @@ export function activateSocialNetworkUI(): void { ui.init(env, {}) ui.resolveLastRecognizedIdentity() ui.injectPostBox() + ui.injectPageInspector() ui.collectPeople() ui.collectPosts() ui.injectDashboardEntryInMobile() From 9bcd24c83cb10b5a27951e1425897b73a175f68e Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Wed, 26 Aug 2020 14:15:19 +0800 Subject: [PATCH 07/16] chore: add UI components + chore: add PriceChangedTable + chore: add TickersTable + chore: add MarketCapRank + chore: add PriceChart + chore: add Skeleton & PriceChartDaysControl --- package.json | 2 + src/plugins/Trader/UI/Linking.tsx | 15 + src/plugins/Trader/UI/PriceChanged.tsx | 28 + src/plugins/Trader/UI/PriceChangedTable.tsx | 93 ++++ src/plugins/Trader/UI/PriceChart.tsx | 124 +++++ .../Trader/UI/PriceChartDaysControl.tsx | 48 ++ src/plugins/Trader/UI/TickersTable.tsx | 82 +++ src/plugins/Trader/UI/TrendingPopper.tsx | 2 +- src/plugins/Trader/UI/TrendingView.tsx | 169 ++++-- src/plugins/Trader/apis/coingecko/index.ts | 62 ++- src/plugins/Trader/apis/index.ts | 36 +- src/plugins/Trader/hooks/usePriceStats.ts | 19 + src/plugins/Trader/type.ts | 34 +- src/plugins/Wallet/formatter.ts | 4 + .../defaults/injectPageInspector.tsx | 3 +- yarn.lock | 491 +++++++++++++++++- 16 files changed, 1164 insertions(+), 48 deletions(-) create mode 100644 src/plugins/Trader/UI/Linking.tsx create mode 100644 src/plugins/Trader/UI/PriceChanged.tsx create mode 100644 src/plugins/Trader/UI/PriceChangedTable.tsx create mode 100644 src/plugins/Trader/UI/PriceChart.tsx create mode 100644 src/plugins/Trader/UI/PriceChartDaysControl.tsx create mode 100644 src/plugins/Trader/UI/TickersTable.tsx create mode 100644 src/plugins/Trader/hooks/usePriceStats.ts diff --git a/package.json b/package.json index f9bbd8e992c..6af4357bd5a 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "bip39": "^3.0.2", "classnames": "^2.2.6", "clipboard-polyfill": "^3.0.1", + "d3": "^5.16.0", "elliptic": "^6.5.3", "eth-contract-metadata": "^1.15.0", "fuse.js": "^6.4.1", @@ -139,6 +140,7 @@ "@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/plugins/Trader/UI/Linking.tsx b/src/plugins/Trader/UI/Linking.tsx new file mode 100644 index 00000000000..d49f8db5444 --- /dev/null +++ b/src/plugins/Trader/UI/Linking.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Link } from '@material-ui/core' + +export interface LinkingProps { + href?: string + children?: React.ReactNode +} + +export function Linking(props: LinkingProps) { + return props.href ? ( + + {props.children} + + ) : null +} diff --git a/src/plugins/Trader/UI/PriceChanged.tsx b/src/plugins/Trader/UI/PriceChanged.tsx new file mode 100644 index 00000000000..1b695a0b63a --- /dev/null +++ b/src/plugins/Trader/UI/PriceChanged.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import classNames from 'classnames' +import { useColorStyles } from '../../../utils/theme' +import { makeStyles, Theme, createStyles } from '@material-ui/core' + +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + root: { + fontSize: 'inherit', + marginLeft: theme.spacing(1), + }, + }) +}) + +export interface PriceChangedProps { + amount: number +} + +export function PriceChanged(props: PriceChangedProps) { + const color = useColorStyles() + const classes = useStyles() + return ( + 0 ? color.success : color.error)}> + {props.amount > 0 ? '\u25B2 ' : '\u25BC '} + {props.amount.toFixed(2)}% + + ) +} diff --git a/src/plugins/Trader/UI/PriceChangedTable.tsx b/src/plugins/Trader/UI/PriceChangedTable.tsx new file mode 100644 index 00000000000..990ed7fe392 --- /dev/null +++ b/src/plugins/Trader/UI/PriceChangedTable.tsx @@ -0,0 +1,93 @@ +import React from 'react' +import { + TableContainer, + Table, + makeStyles, + Theme, + createStyles, + TableHead, + TableRow, + TableCell, + TableBody, + Paper, +} from '@material-ui/core' +import type { Market } from '../type' +import { PriceChanged } from './PriceChanged' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + container: {}, + table: {}, + cell: { + paddingLeft: theme.spacing(1.5), + paddingRight: theme.spacing(1), + whiteSpace: 'nowrap', + }, + }), +) + +export interface PriceChangedTableProps { + market: Market +} + +export function PriceChangedTable({ market }: PriceChangedTableProps) { + const classes = useStyles() + const records: { + name: string + percentage?: number + }[] = [ + { + name: '1h', + percentage: market.price_change_percentage_1h_in_currency, + }, + { + name: '24h', + percentage: market.price_change_percentage_24h_in_currency, + }, + { + name: '7d', + percentage: market.price_change_percentage_7d_in_currency, + }, + { + name: '14d', + percentage: market.price_change_percentage_14d_in_currency, + }, + { + name: '30d', + percentage: market.price_change_percentage_30d_in_currency, + }, + { + name: '1y', + percentage: market.price_change_percentage_1y_in_currency, + }, + ] + + return ( + + + + + {records.map((x) => + typeof x.percentage === 'number' ? ( + + {x.name} + + ) : null, + )} + + + + + {records.map((x) => + typeof x.percentage === 'number' ? ( + + + + ) : null, + )} + + +
+
+ ) +} diff --git a/src/plugins/Trader/UI/PriceChart.tsx b/src/plugins/Trader/UI/PriceChart.tsx new file mode 100644 index 00000000000..cbdf9bd5421 --- /dev/null +++ b/src/plugins/Trader/UI/PriceChart.tsx @@ -0,0 +1,124 @@ +import React, { useRef, useEffect } from 'react' +import * as d3 from 'd3' +import type { Stat } from '../type' +import { makeStyles, Theme, createStyles, CircularProgress } from '@material-ui/core' + +const DEFAULT_WIDTH = 460 +const DEFAULT_HEIGHT = 250 +const DEFAULT_MARGIN = { + top: 32, + right: 16, + bottom: 32, + left: 16, +} + +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + root: { + position: 'relative', + }, + svg: {}, + progress: { + bottom: theme.spacing(1), + right: theme.spacing(1), + position: 'absolute', + }, + }) +}) + +export interface PriceChartProps { + stats: Stat[] + loading?: boolean + width?: number + height?: number + children?: React.ReactNode +} + +export function PriceChart(props: PriceChartProps) { + 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 + if (!props.stats.length) return + + // empty the svg + svgRef.current.innerHTML = '' + + // 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]) + return ( +
+ {props.children} + + {props.loading ? : null} +
+ ) +} diff --git a/src/plugins/Trader/UI/PriceChartDaysControl.tsx b/src/plugins/Trader/UI/PriceChartDaysControl.tsx new file mode 100644 index 00000000000..9ee9536fcdb --- /dev/null +++ b/src/plugins/Trader/UI/PriceChartDaysControl.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { makeStyles, Theme, createStyles, Link, Typography } from '@material-ui/core' +import { resolveDaysName } from '../type' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + top: 0, + right: 0, + padding: theme.spacing(1, 2), + position: 'absolute', + }, + link: { + cursor: 'pointer', + marginRight: theme.spacing(1), + '&:last-child': { + marginRight: 0, + }, + }, + text: { + fontSize: 10, + fontWeight: 300, + }, + }), +) + +export interface PriceChartDaysControlProps { + days: number + onDaysChange?: (days: number) => void +} + +export function PriceChartDaysControl(props: PriceChartDaysControlProps) { + const classes = useStyles() + return ( +
+ {[1, 7, 14, 30, 365].map((days) => ( + props.onDaysChange?.(days)}> + + {resolveDaysName(days)} + + + ))} +
+ ) +} diff --git a/src/plugins/Trader/UI/TickersTable.tsx b/src/plugins/Trader/UI/TickersTable.tsx new file mode 100644 index 00000000000..5b4a2cbeb5c --- /dev/null +++ b/src/plugins/Trader/UI/TickersTable.tsx @@ -0,0 +1,82 @@ +import React from 'react' +import { + TableContainer, + Table, + makeStyles, + Theme, + createStyles, + TableHead, + TableRow, + TableCell, + TableBody, + Link, +} from '@material-ui/core' +import type { Ticker } from '../type' +import { formatCurrency, formatEthAddress } from '../../Wallet/formatter' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + container: { + height: 316, + }, + table: {}, + cell: { + paddingLeft: theme.spacing(1.5), + paddingRight: theme.spacing(1), + whiteSpace: 'nowrap', + }, + avatar: { + width: 20, + height: 20, + }, + }), +) + +export interface TickersTableProps { + tickers: Ticker[] +} + +export function TickersTable(props: TickersTableProps) { + const classes = useStyles() + const rows = ['Exchange', 'Pair', 'Price', 'Volumn'] + const tickers = props.tickers.map((ticker) => ( + + + + {ticker.market_name} + + + + {(() => { + const formated = formatEthAddress(ticker.base_name) + return ( + <> + {formated} + / + {ticker.target_name} + + ) + })()} + + ${formatCurrency(ticker.price)} + ${formatCurrency(ticker.volumn)} + + )) + + return ( + + + + + {rows.map((x) => ( + + {x} + + ))} + + + {tickers} +
+
+ ) +} diff --git a/src/plugins/Trader/UI/TrendingPopper.tsx b/src/plugins/Trader/UI/TrendingPopper.tsx index 46ee79030dc..45d7fe6e25d 100644 --- a/src/plugins/Trader/UI/TrendingPopper.tsx +++ b/src/plugins/Trader/UI/TrendingPopper.tsx @@ -30,7 +30,7 @@ export function TrendingPopper(props: TrendingPopperProps) { anchorEl={anchorEl} disablePortal transition - style={{ zIndex: 1 }} + style={{ zIndex: 1, marginTop: 8 }} {...props.PopperProps}> {props.children?.(name)} diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index 238414c9525..387ae880caf 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { makeStyles, Avatar, @@ -9,33 +9,78 @@ import { CardActions, Theme, createStyles, - CircularProgress, + Link, + Box, + Paper, + Tab, + Tabs, } from '@material-ui/core' -import classNames from 'classnames' -import { resolvePlatformName } from '../type' +import { resolvePlatformName, Platform } from '../type' import { getActivatedUI } from '../../../social-network/ui' import { formatCurrency } from '../../Wallet/formatter' -import { useColorStyles } from '../../../utils/theme' import { useTrending } from '../hooks/useTrending' +import { TickersTable } from './TickersTable' +import { PriceChangedTable } from './PriceChangedTable' +import { PriceChanged } from './PriceChanged' +import { PriceChart } from './PriceChart' +import { getEnumAsArray } from '../../../utils/enum' +import { Linking } from './Linking' +import { usePriceStats } from '../hooks/usePriceStats' +import { Skeleton } from '@material-ui/lab' +import { PriceChartDaysControl } from './PriceChartDaysControl' const useStyles = makeStyles((theme: Theme) => { const internalName = getActivatedUI()?.internalName return createStyles({ root: { + width: 500, + overflow: 'auto', + '&::-webkit-scrollbar': { + display: 'none', + }, ...(internalName === 'twitter' ? { border: `1px solid ${theme.palette.type === 'dark' ? '#2f3336' : '#ccd6dd'}` } : null), }, header: { display: 'flex', + position: 'relative', + }, + content: { + paddingTop: 0, + paddingBottom: 0, }, - body: {}, footer: { justifyContent: 'flex-end', }, + tabs: { + width: 468, + }, + section: {}, + description: { + overflow: 'auto', + maxHeight: '4.3em', + wordBreak: 'break-word', + marginBottom: theme.spacing(2), + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + rank: { + color: theme.palette.text.primary, + fontWeight: 300, + marginRight: theme.spacing(1), + }, footnote: { fontSize: 10, }, + platform: { + cursor: 'pointer', + marginRight: theme.spacing(0.5), + '&:last-child': { + marginRight: 0, + }, + }, avatar: {}, percentage: { marginLeft: theme.spacing(1), @@ -49,47 +94,113 @@ export interface TrendingViewProps extends withClasses - - + } + title={} + subheader={} + /> + + + + + ) if (!trending) return null - const { currency, platform } = trending + + const { coin, currency, platform, market, tickers } = trending + return ( - + } + avatar={ + + + + } title={ - {`${trending.coin.symbol.toUpperCase()} / ${currency.name}`} + + + {typeof coin.market_cap_rank === 'number' ? ( + + #{coin.market_cap_rank} + + ) : null} + {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} - + <> + + {`${currency.symbol ?? `${currency.name} `}${formatCurrency( + market.current_price, + )}`} + {typeof market.price_change_percentage_24h === 'number' ? ( + + ) : null} + + } + disableTypography /> + + + , newValue: number) => setTabIndex(newValue)} + TabIndicatorProps={{ + style: { + display: 'none', + }, + }}> + + + + {tabIndex === 0 ? ( + <> + + + + + + ) : ( + + )} + + - Powered by {resolvePlatformName(platform)} + Switch Data Source: + {getEnumAsArray(Platform).map((x) => ( + + {resolvePlatformName(x.value)} + + ))} diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 7c2e74c7240..0beb112c831 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,5 +1,7 @@ const BASE_URL = 'https://api.coingecko.com/api/v3' +const CHART_BASE_URL = 'https://www' + //#region get currency export async function getAllCurrenies() { const response = await fetch(`${BASE_URL}/simple/supported_vs_currencies`) @@ -57,16 +59,70 @@ export interface CoinInfo { low_24h: Record market_cap: Record market_cap_rank: number - price_change_percentage_24h: number + + price_change_percentage_1h_in_currency: number + price_change_percentage_1y_in_currency: number + price_change_percentage_7d_in_currency: number + price_change_percentage_14d_in_currency: number + price_change_percentage_24h_in_currency: number + price_change_percentage_30d_in_currency: number + price_change_percentage_60d_in_currency: number + price_change_percentage_200d_in_currency: number + total_supply: number total_volume: Record } name: string symbol: string + tickers: { + base: string + target: string + market: { + name: 'string' + identifier: string + has_trading_incentive: boolean + logo: string + } + last: number + volumn: number + converted_last: { + btc: number + eth: number + usd: number + } + converted_volume: { + btc: number + eth: number + usd: number + } + trust_score: 'green' + bid_ask_spread_percentage: number + timestamp: string + last_traded_at: string + last_fetch_at: string + is_anomaly: boolean + is_stale: boolean + trade_url: string + coin_id: string + target_coin_id?: string + }[] } -export async function getCoinInfo(id: string) { - const response = await fetch(`${BASE_URL}/coins/${id}?developer_data=false&community_data=false&tickers=false`) +export async function getCoinInfo(coinId: string) { + const response = await fetch(`${BASE_URL}/coins/${coinId}?developer_data=false&community_data=false&tickers=true`) return response.json() as Promise } //#endregion + +//#region get price chart +export type Stat = [number, number] + +export async function getPriceStats(coinId: string, currencyId: string, days: number) { + const response = await fetch(`${BASE_URL}/coins/${coinId}/market_chart?vs_currency=${currencyId}&days=${days}`) + return response.json() as Promise<{ + market_caps: Stat[] + prices: Stat[] + total_volumes: Stat[] + }> +} +//#endregion diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 8950acf4272..9c77ce4a6df 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -1,4 +1,4 @@ -import { Platform, Currency, Coin, Trending } from '../type' +import { Platform, Currency, Coin, Trending, Market, Stat } from '../type' import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' @@ -53,15 +53,31 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr id, name: info.name, symbol: info.symbol, + + // TODO: + // use current language setting + description: info.description.en, + market_cap_rank: info.market_cap_rank, image_url: info.image.small, + home_url: info.links.homepage.filter(Boolean)[0], }, currency, platform, - market: { - current_price: info.market_data.current_price[currency.id], - total_volume: info.market_data.total_volume[currency.id], - price_change_24h: info.market_data.price_change_percentage_24h, - }, + market: Object.entries(info.market_data).reduce((accumulated, [key, value]) => { + if (value && typeof value === 'object') accumulated[key] = value[currency.id] + else accumulated[key] = value + return accumulated + }, {} as any), + tickers: info.tickers.slice(0, 30).map((x) => ({ + logo_url: x.market.logo, + trade_url: x.trade_url, + market_name: x.market.name, + base_name: x.base, + target_name: x.target, + price: x.converted_last.usd, + volumn: x.converted_volume.usd, + score: x.trust_score, + })), } } const info = await coinMarketCapAPI.getCoinInfo(id, currency.name.toUpperCase()) @@ -71,14 +87,16 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr name: info.data.name, symbol: info.data.symbol, image_url: `https://s2.coinmarketcap.com/static/img/coins/64x64/${id}.png`, + market_cap_rank: info.data.rank, }, currency, platform, market: { current_price: info.data.quotes[currency.name.toUpperCase()].price, total_volume: info.data.quotes[currency.name.toUpperCase()].volume_24h, - price_change_24h: info.data.quotes[currency.name.toUpperCase()].percent_change_24h, + price_change_percentage_24h: info.data.quotes[currency.name.toUpperCase()].percent_change_24h, }, + tickers: [], } } @@ -88,3 +106,7 @@ export async function getCoinTrendingByKeyword(keyword: string, platform: Platfo if (!coin) return null return getCoinInfo(coin.id, platform, currency) } + +export async function getPriceStats(id: string, platform: Platform, currency: Currency, days: number): Promise { + return platform === Platform.COIN_GECKO ? (await coinGeckoAPI.getPriceStats(id, currency.id, days)).prices : [] +} diff --git a/src/plugins/Trader/hooks/usePriceStats.ts b/src/plugins/Trader/hooks/usePriceStats.ts new file mode 100644 index 00000000000..ce364ba6a70 --- /dev/null +++ b/src/plugins/Trader/hooks/usePriceStats.ts @@ -0,0 +1,19 @@ +import { useAsync } from 'react-use' +import Services from '../../../extension/service' +import type { Currency, Platform } from '../type' +import { isUndefined } from 'lodash-es' + +interface Options { + coinId?: string + currency?: Currency + days?: number + platform?: Platform +} + +export function usePriceStats({ coinId, currency, days = 30, platform }: Options) { + return useAsync(async () => { + if (days <= 0) return [] + if (isUndefined(coinId) || isUndefined(platform) || isUndefined(currency)) return [] + return Services.Plugin.invokePlugin('maskbook.trader', 'getPriceStats', coinId, platform, currency, days) + }, [coinId, platform, currency?.id, days]) +} diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/type.ts index e3883f1d82c..dea672987ef 100644 --- a/src/plugins/Trader/type.ts +++ b/src/plugins/Trader/type.ts @@ -18,13 +18,35 @@ export interface Coin { id: string name: string symbol: string + home_url?: string image_url?: string + description?: string + market_cap_rank?: number } export interface Market { current_price: number total_volume?: number - price_change_24h?: number + price_change_percentage_24h?: number + price_change_percentage_1h_in_currency?: number + price_change_percentage_1y_in_currency?: number + price_change_percentage_7d_in_currency?: number + price_change_percentage_14d_in_currency?: number + price_change_percentage_24h_in_currency?: number + price_change_percentage_30d_in_currency?: number + price_change_percentage_60d_in_currency?: number + price_change_percentage_200d_in_currency?: number +} + +export interface Ticker { + logo_url: string + trade_url: string + market_name: string + base_name: string + target_name: string + price: number + volumn: number + score: string } export interface Trending { @@ -32,8 +54,11 @@ export interface Trending { platform: Platform coin: Coin market: Market + tickers: Ticker[] } +export type Stat = [number, number] + export function resolveCurrencyName(currency: Currency) { return [ currency.name, @@ -52,3 +77,10 @@ export function resolvePlatformName(platform: Platform) { return '' } } + +export function resolveDaysName(days: number) { + if (days >= 365) return `${Math.floor(days / 365)}y` + if (days >= 30) return `${Math.floor(days / 30)}m` + if (days >= 7) return `${Math.floor(days / 7)}w` + return `${days}d` +} diff --git a/src/plugins/Wallet/formatter.ts b/src/plugins/Wallet/formatter.ts index 95e6aea26fd..322fc5ff651 100644 --- a/src/plugins/Wallet/formatter.ts +++ b/src/plugins/Wallet/formatter.ts @@ -25,3 +25,7 @@ export function formatBalance(balance: BigNumber, decimals: number, precision: n export function formatCurrency(balance: number) { return balance.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,') } + +export function formatEthAddress(address: string, size = 2) { + return /0x[\w\d]{40}/i.test(address) ? `${address.substr(0, 2 + size)}...${address.substr(-size)}` : address +} diff --git a/src/social-network/defaults/injectPageInspector.tsx b/src/social-network/defaults/injectPageInspector.tsx index b92841f97c8..0a6ff3f50dd 100644 --- a/src/social-network/defaults/injectPageInspector.tsx +++ b/src/social-network/defaults/injectPageInspector.tsx @@ -3,6 +3,7 @@ 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' +import { Flags } from '../../utils/flags' export function injectPageInspectorDefault( config: InjectPageInspectorDefaultConfig = {}, @@ -18,7 +19,7 @@ export function injectPageInspectorDefault( return function injectPageInspector() { const watcher = new MutationObserverWatcher(new LiveSelector().querySelector('body')) .setDOMProxyOption({ - afterShadowRootInit: { mode: webpackEnv.shadowRootMode }, + afterShadowRootInit: { mode: Flags.using_ShadowDOM_attach_mode }, }) .startWatch({ childList: true, diff --git a/yarn.lock b/yarn.lock index a37b5eeb003..36db5f27705 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3780,6 +3780,227 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/d3-array@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96" + integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA== + +"@types/d3-array@^1": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.7.tgz#34dc654d34fc058c41c31dbca1ed68071a8fcc17" + integrity sha512-51vHWuUyDOi+8XuwPrTw3cFqyh2Slg9y8COYkRfjCPG9TfYqY0hoNPzv/8BrcAy0FeQBzqEo/D/8Nk2caOQJnA== + +"@types/d3-axis@*": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.12.tgz#8c124edfcc02f3b3a9cdaa2a28b8a20341401799" + integrity sha512-BZISgSD5M8TgURyNtcPAmUB9sk490CO1Thb6/gIn0WZTt3Y50IssX+2Z0vTccoqZksUDTep0b+o4ofXslvNbqg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.1.1.tgz#906875ce42db22fc9cde6d1fb2808f17ecd2ea93" + integrity sha512-Exx14trm/q2cskHyMjCrdDllOQ35r1/pmZXaOIt8bBHwYNk722vWY3VxHvN0jdFFX7p2iL3+gD+cGny/aEmhlw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.9.tgz#ccc5de03ff079025491b7aa6b750670a140b45ae" + integrity sha512-UA6lI9CVW5cT5Ku/RV4hxoFn4mKySHm7HEgodtfRthAj1lt9rKZEPon58vyYfk+HIAm33DtJJgZwMXy2QgyPXw== + +"@types/d3-collection@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.8.tgz#aa9552c570a96e33c132e0fd20e331f64baa9dd5" + integrity sha512-y5lGlazdc0HNO0F3UUX2DPE7OmYvd9Kcym4hXwrJcNUkDaypR5pX+apuMikl9LfTxKItJsY9KYvzBulpCKyvuQ== + +"@types/d3-color@*": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf" + integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw== + +"@types/d3-contour@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-1.3.0.tgz#1a408b121fa5e341f715e3055303ef3079fc7eb0" + integrity sha512-AUCUIjEnC5lCGBM9hS+MryRaFLIrPls4Rbv6ktqbd+TK/RXZPwOy9rtBWmGpbeXcSOYCJTUDwNJuEnmYPJRxHQ== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-dispatch@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.8.tgz#eaeb2ad089d6a0d2685dfa2f2cbbfb7509aae014" + integrity sha512-lCDtqoYez0TgFN3FljBXrz2icqeSzD0gufGook6DPBia+NOh2TBfogjHIsmNa/a+ZOewlHtq4cgLY80O1uLymw== + +"@types/d3-drag@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.3.tgz#d8ddccca28e939e9c689bea6f40a937e48c39051" + integrity sha512-rWB5SPvkYVxW3sqUxHOJUZwifD0KqvKwvt1bhNqcLpW6Azsd0BJgRNcyVW8GAferaAk5r8dzeZnf9zKlg9+xMQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "1.0.36" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.36.tgz#e91129d7c02b1b814838d001e921e8b9a67153d0" + integrity sha512-jbIWQ27QJcBNMZbQv0NSQMHnBDCmxghAxePxgyiPH1XPCRkOsTBei7jcdi3fDrUCGpCV3lKrSZFSlOkhUQVClA== + +"@types/d3-ease@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.9.tgz#1dd849bd7edef6426e915e220ed9970db5ea4e04" + integrity sha512-U5ADevQ+W6fy32FVZZC9EXallcV/Mi12A5Tkd0My5MrC7T8soMQEhlDAg88XUWm0zoCQlB4XV0en/24LvuDB4Q== + +"@types/d3-fetch@*": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-1.1.5.tgz#51601f79dd4653b5d84e6a3176d78145e065db5e" + integrity sha512-o9c0ItT5/Gl3wbNuVpzRnYX1t3RghzeWAjHUVLuyZJudiTxC4f/fC0ZPFWLQ2lVY8pAMmxpV8TJ6ETYCgPeI3A== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.1.tgz#c28803ea36fe29788db69efa0ad6c2dc09544e83" + integrity sha512-jqK+I36uz4kTBjyk39meed5y31Ab+tXYN/x1dn3nZEus9yOHCLc+VrcIYLc/aSQ0Y7tMPRlIhLetulME76EiiA== + +"@types/d3-format@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.1.tgz#35bf88264bd6bcda39251165bb827f67879c4384" + integrity sha512-KAWvReOKMDreaAwOjdfQMm0HjcUMlQG47GwqdVKgmm20vTd2pucj0a70c3gUSHrnsmo6H2AMrkBsZU2UhJLq8A== + +"@types/d3-geo@*": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.1.tgz#e96ec91f16221d87507fec66b2cc889f52d2493e" + integrity sha512-Ox8WWOG3igDRoep/dNsGbOiSJYdUG3ew/6z0ETvHyAtXZVBjOE0S96zSSmzgl0gqQ3RdZjn2eeJOj9oRcMZPkQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#4c017521900813ea524c9ecb8d7985ec26a9ad9a" + integrity sha512-vvSaIDf/Ov0o3KwMT+1M8+WbnnlRiGjlGD5uvk83a1mPCTd/E5x12bUJ/oP55+wUY/4Kb5kc67rVpVGJ2KUHxg== + +"@types/d3-interpolate@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8" + integrity sha512-z8Zmi08XVwe8e62vP6wcA+CNuRhpuUU5XPEfqpG0hRypDE5BWNthQHB1UNWWDB7ojCbGaN4qBdsWp5kWxhT1IQ== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== + +"@types/d3-polygon@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.7.tgz#7b3947aa2d48287ff535230d3d396668ab17bfdf" + integrity sha512-Xuw0eSjQQKs8jTiNbntWH0S+Xp+JyhqxmQ0YAQ3rDu6c3kKMFfgsaGN7Jv5u3zG6yVX/AsLP/Xs/QRjmi9g43Q== + +"@types/d3-quadtree@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.7.tgz#8e29464ff5b326f6612c1428d9362b4b35de2b70" + integrity sha512-0ajFawWicfjsaCLh6NzxOyVDYhQAmMFbsiI3MPGLInorauHFEh9/Cl6UHNf+kt/J1jfoxKY/ZJaKAoDpbvde5Q== + +"@types/d3-random@*": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.2.tgz#6f77e8b7bb64ac393f92d33fe8f71038bc4f3cde" + integrity sha512-Jui+Zn28pQw/3EayPKaN4c/PqTvqNbIPjHkgIIFnxne1FdwNjfHtAIsZIBMKlquQNrrMjFzCrlF2gPs3xckqaA== + +"@types/d3-scale-chromatic@*": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#315367557d51b823bec848614fac095325613fc3" + integrity sha512-9/D7cOBKdZdTCPc6re0HeSUFBM0aFzdNdmYggUWT9SRRiYSOa6Ys2xdTwHKgc1WS3gGfwTMatBOdWCS863REsg== + +"@types/d3-scale@*": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.2.0.tgz#e5987a2857365823eb26ed5eb21bc566c4dcf1c0" + integrity sha512-oQFanN0/PiR2oySHfj+zAAkK1/p4LD32Nt1TMVmzk+bYHk7vgIg/iTXQWitp1cIkDw4LMdcgvO63wL+mNs47YA== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.2.tgz#72dcd61a3aeb9ce3e8d443e3bef7685ffea3413f" + integrity sha512-ksY8UxvTXpzD91Dy3D9zZg98yF2ZEPMKJd8ZQJlZt1QH3Xxr08s6fESEdC2l0Kbe6Xd9VhaoJX06cRaMR1lEnA== + +"@types/d3-shape@*": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.2.tgz#a41d9d6b10d02e221696b240caf0b5d0f5a588ec" + integrity sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.1.tgz#dd2c79ec4575f1355484ab6b10407824668eba42" + integrity sha512-tJSyXta8ZyJ52wDDHA96JEsvkbL6jl7wowGmuf45+fAkj5Y+SQOnz0N7/H68OWmPshPsAaWMQh+GAws44IzH3g== + +"@types/d3-time@*": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.10.tgz#d338c7feac93a98a32aac875d1100f92c7b61f4f" + integrity sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw== + +"@types/d3-timer@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.9.tgz#aed1bde0cf18920d33f5d44839d73de393633fd3" + integrity sha512-WvfJ3LFxBbWjqRGz9n7GJt08RrTHPJDVsIwwoCMROlqF+iDacYiAFjf9oqnq0mXpb2juA2N/qjKP+MKdal3YNQ== + +"@types/d3-transition@*": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.6.tgz#7e52da29749d874866cc803fad13925713a372da" + integrity sha512-/F+O2r4oz4G9ATIH3cuSCMGphAnl7VDx7SbENEK0NlI/FE8Jx2oiIrv0uTrpg7yF/AmuWbqp7AGdEHAPIh24Gg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-voronoi@*": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5" + integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ== + +"@types/d3-zoom@*": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.4.tgz#9226ffd2bd3846ec0e4a4e2bff211612d3aafad5" + integrity sha512-5jnFo/itYhJeB2khO/lKe730kW/h2EbKMOvY0uNp3+7NdPm4w63DwPEMxifQZ7n902xGYK5DdU67FmToSoy4VA== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.7.2.tgz#52235eb71a1d3ca171d6dca52a58f5ccbe0254cc" + integrity sha512-7/wClB8ycneWGy3jdvLfXKTd5SoTg9hji7IdJ0RuO9xTY54YpJ8zlcFADcXhY1J3kCBwxp+/1jeN6a5OMwgYOw== + dependencies: + "@types/d3-array" "^1" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-collection" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-voronoi" "*" + "@types/d3-zoom" "*" + "@types/elliptic@^6.4.12": version "6.4.12" resolved "https://registry.yarnpkg.com/@types/elliptic/-/elliptic-6.4.12.tgz#e8add831f9cc9a88d9d84b3733ff669b68eaa124" @@ -3820,6 +4041,11 @@ "@types/jest" "*" "@types/puppeteer" "*" +"@types/geojson@*": + version "7946.0.7" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" + integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -7540,6 +7766,11 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" +commander@2, commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.3.0, commander@^2.6.0, commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -7547,11 +7778,6 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" -commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.3.0, commander@^2.6.0, commander@^2.9.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" @@ -8213,6 +8439,254 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-axis@1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" + integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== + +d3-brush@1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b" + integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" + integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== + +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + +d3-dispatch@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" + integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== + +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" + integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" + integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== + +d3-fetch@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7" + integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + +d3-format@1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" + integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== + +d3-geo@1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f" + integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg== + dependencies: + d3-array "1" + +d3-hierarchy@1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" + integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== + +d3-interpolate@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-polygon@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + +d3-quadtree@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" + integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== + +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + +d3-scale-chromatic@1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" + integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-selection@1, d3-selection@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" + integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== + +d3-shape@1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" + integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +d3-timer@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-transition@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" + integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +d3-zoom@1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^5.16.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -11590,7 +12064,7 @@ i18next@^19.7.0: dependencies: "@babel/runtime" "^7.10.1" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -18336,6 +18810,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= + rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" From 76b0a07ef9e15d4af07232e2bc99706cdc199068 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Thu, 27 Aug 2020 12:48:41 +0800 Subject: [PATCH 08/16] chore: integrate with CMC apis --- src/plugins/Trader/UI/Linking.tsx | 16 ++-- src/plugins/Trader/UI/PriceChangedTable.tsx | 37 ++++---- src/plugins/Trader/UI/PriceChart.tsx | 21 ++++- .../Trader/UI/PriceChartDaysControl.tsx | 2 +- src/plugins/Trader/UI/TickersTable.tsx | 28 ++++-- src/plugins/Trader/UI/TrendingView.tsx | 47 ++++++++-- .../Trader/apis/coinmarketcap/index.ts | 90 ++++++++++++++++++- src/plugins/Trader/apis/index.ts | 49 ++++++++-- src/plugins/Trader/define.tsx | 2 +- .../Trader/hooks/useCurrentCurrency.ts | 38 ++++++++ .../Trader/hooks/useCurrentPlatform.ts | 21 +++++ src/plugins/Trader/hooks/useTrending.ts | 50 ++--------- src/plugins/Trader/settings.ts | 9 +- src/plugins/Trader/type.ts | 13 ++- 14 files changed, 323 insertions(+), 100 deletions(-) create mode 100644 src/plugins/Trader/hooks/useCurrentCurrency.ts create mode 100644 src/plugins/Trader/hooks/useCurrentPlatform.ts diff --git a/src/plugins/Trader/UI/Linking.tsx b/src/plugins/Trader/UI/Linking.tsx index d49f8db5444..7ace097e4c6 100644 --- a/src/plugins/Trader/UI/Linking.tsx +++ b/src/plugins/Trader/UI/Linking.tsx @@ -7,9 +7,15 @@ export interface LinkingProps { } export function Linking(props: LinkingProps) { - return props.href ? ( - - {props.children} - - ) : null + return ( + <> + {props.href ? ( + + {props.children} + + ) : ( + props.children + )} + + ) } diff --git a/src/plugins/Trader/UI/PriceChangedTable.tsx b/src/plugins/Trader/UI/PriceChangedTable.tsx index 990ed7fe392..a93f8916c97 100644 --- a/src/plugins/Trader/UI/PriceChangedTable.tsx +++ b/src/plugins/Trader/UI/PriceChangedTable.tsx @@ -9,7 +9,6 @@ import { TableRow, TableCell, TableBody, - Paper, } from '@material-ui/core' import type { Market } from '../type' import { PriceChanged } from './PriceChanged' @@ -26,16 +25,18 @@ const useStyles = makeStyles((theme: Theme) => }), ) +type Record = { + name: string + percentage?: number +} + export interface PriceChangedTableProps { market: Market } export function PriceChangedTable({ market }: PriceChangedTableProps) { const classes = useStyles() - const records: { - name: string - percentage?: number - }[] = [ + const records: Record[] = [ { name: '1h', percentage: market.price_change_percentage_1h_in_currency, @@ -62,29 +63,27 @@ export function PriceChangedTable({ market }: PriceChangedTableProps) { }, ] + const filteredRecords = records.filter((record) => typeof record.percentage === 'number') as Required[] + return ( - {records.map((x) => - typeof x.percentage === 'number' ? ( - - {x.name} - - ) : null, - )} + {filteredRecords.map((x) => ( + + {x.name} + + ))} - {records.map((x) => - typeof x.percentage === 'number' ? ( - - - - ) : null, - )} + {filteredRecords.map((x) => ( + + + + ))}
diff --git a/src/plugins/Trader/UI/PriceChart.tsx b/src/plugins/Trader/UI/PriceChart.tsx index cbdf9bd5421..3836e536376 100644 --- a/src/plugins/Trader/UI/PriceChart.tsx +++ b/src/plugins/Trader/UI/PriceChart.tsx @@ -1,7 +1,7 @@ import React, { useRef, useEffect } from 'react' import * as d3 from 'd3' import type { Stat } from '../type' -import { makeStyles, Theme, createStyles, CircularProgress } from '@material-ui/core' +import { makeStyles, Theme, createStyles, CircularProgress, Typography } from '@material-ui/core' const DEFAULT_WIDTH = 460 const DEFAULT_HEIGHT = 250 @@ -23,6 +23,9 @@ const useStyles = makeStyles((theme: Theme) => { right: theme.spacing(1), position: 'absolute', }, + placeholder: { + paddingTop: theme.spacing(14), + }, }) }) @@ -54,11 +57,13 @@ export function PriceChart(props: PriceChartProps) { useEffect(() => { if (!svgRef.current) return - if (!props.stats.length) 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) @@ -116,9 +121,17 @@ export function PriceChart(props: PriceChartProps) { }, [svgRef, data.length]) return (
- {props.children} - {props.loading ? : null} + {props.stats.length ? ( + <> + {props.children} + + + ) : ( + + No Data + + )}
) } diff --git a/src/plugins/Trader/UI/PriceChartDaysControl.tsx b/src/plugins/Trader/UI/PriceChartDaysControl.tsx index 9ee9536fcdb..a3b44a24149 100644 --- a/src/plugins/Trader/UI/PriceChartDaysControl.tsx +++ b/src/plugins/Trader/UI/PriceChartDaysControl.tsx @@ -33,7 +33,7 @@ export function PriceChartDaysControl(props: PriceChartDaysControlProps) { const classes = useStyles() return (
- {[1, 7, 14, 30, 365].map((days) => ( + {[1, 7, 14, 30, 90, 365].map((days) => ( props.onDaysChange?.(days)}> @@ -29,18 +30,23 @@ const useStyles = makeStyles((theme: Theme) => width: 20, height: 20, }, + placeholder: { + paddingTop: theme.spacing(16), + borderStyle: 'none', + }, }), ) export interface TickersTableProps { + platform: Platform tickers: Ticker[] } export function TickersTable(props: TickersTableProps) { const classes = useStyles() - const rows = ['Exchange', 'Pair', 'Price', 'Volumn'] - const tickers = props.tickers.map((ticker) => ( - + const rows = ['Exchange', 'Pair', 'Price', 'Volumn (24h)'] + const tickers = props.tickers.map((ticker, index) => ( + {ticker.market_name} @@ -75,7 +81,19 @@ export function TickersTable(props: TickersTableProps) { ))} - {tickers} + {tickers.length ? ( + {tickers} + ) : ( + + + + + No Data + + + + + )} ) diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index 387ae880caf..3cf68337191 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -28,6 +28,9 @@ import { Linking } from './Linking' import { usePriceStats } from '../hooks/usePriceStats' import { Skeleton } from '@material-ui/lab' import { PriceChartDaysControl } from './PriceChartDaysControl' +import { useCurrentPlatform } from '../hooks/useCurrentPlatform' +import { useCurrentCurrency } from '../hooks/useCurrentCurrency' +import { currentTrendingViewPlatformSettings } from '../settings' const useStyles = makeStyles((theme: Theme) => { const internalName = getActivatedUI()?.internalName @@ -94,19 +97,33 @@ export interface TrendingViewProps extends withClasses } /> - + + ) + //#endregion + + //#region error handling + // error: fail to load currency + if (!currency) return null + + // error: unknown coin if (!trending) return null + //#endregion - const { coin, currency, platform, market, tickers } = trending + const { coin, market, tickers } = trending return ( @@ -186,7 +212,7 @@ export function TrendingView(props: TrendingViewProps) { ) : ( - + )} @@ -197,7 +223,12 @@ export function TrendingView(props: TrendingViewProps) { + color={platform === x.value ? 'primary' : 'textSecondary'} + onClick={() => { + currentTrendingViewPlatformSettings[getActivatedUI().networkIdentifier].value = String( + x.value, + ) + }}> {resolvePlatformName(x.value)} ))} diff --git a/src/plugins/Trader/apis/coinmarketcap/index.ts b/src/plugins/Trader/apis/coinmarketcap/index.ts index 8dcc4cfe9a5..7e5dc53f7b7 100644 --- a/src/plugins/Trader/apis/coinmarketcap/index.ts +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -1,7 +1,10 @@ import CURRENCY_DATA from './currency.json' // proxy: https://web-api.coinmarketcap.com/v1 -const BASE_URL = 'https://coinmarketcap.provide.maskbook.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' @@ -39,7 +42,7 @@ export interface Coin { export async function getAllCoins() { const response = await fetch( - `${BASE_URL}/cryptocurrency/map?aux=status,platform&listing_status=active,untracked&sort=cmc_rank`, + `${BASE_URL_v1}/cryptocurrency/map?aux=status,platform&listing_status=active,untracked&sort=cmc_rank`, ) return response.json() as Promise<{ data: Coin[] @@ -80,3 +83,86 @@ export async function getCoinInfo(id: string, currency: string) { }> } //#endregion + +//#region historical +export type Stat = [number, number, number] + +export async function getHistorical( + id: string, + currency: string, + startDate: Date, + endDate: Date, + interval: string = '1d', +) { + const toUnixTimestamp = (d: Date) => Math.floor(d.getTime() / 1000) + const response = await fetch( + `${BASE_URL_v1_1}/cryptocurrency/quotes/historical?convert=${currency}&format=chart_crypto_details&id=${id}&interval=${interval}&time_end=${toUnixTimestamp( + endDate, + )}&time_start=${toUnixTimestamp(startDate)}`, + ) + return response.json() as Promise<{ + data: Record> + status: Status + }> +} +//#endregion + +//#region latest market pairs +export interface Pair { + exchange: { + id: number + name: string + slug: string + } + market_id: number + market_pair: string + market_pair_base: { + currency_id: number + currency_symbol: string + currency_type: string + exchange_symbol: string + } + market_pair_quote: { + currency_id: number + currency_symbol: string + currency_type: string + exchange_symbol: string + } + market_reputation: number + market_score: number + market_url: string + outlier_detected: 0 | 1 + quote: Record< + string, + { + effective_liquidity: 0 | 1 + last_updated: string + price: number + price_quote: number + volume_24h: number + } + > & { + exchange_reported: { + last_updated: string + price: number + volume_24h_base: number + volume_24h_quote: number + } + } +} +export async function getLatestMarketPairs(id: string, currency: string) { + const response = await fetch( + `${BASE_URL_v1}/cryptocurrency/market-pairs/latest?aux=num_market_pairs,market_url,price_quote,effective_liquidity,market_score,market_reputation&convert=${currency}&id=${id}&limit=40&sort=cmc_rank&start=1`, + ) + return response.json() as Promise<{ + data: { + id: number + market_pairs: Pair[] + name: string + num_market_pairs: number + symbol: string + } + status: Status + }> +} +//#endregion diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 9c77ce4a6df..3bdfd981b0c 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -80,23 +80,39 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr })), } } - const info = await coinMarketCapAPI.getCoinInfo(id, currency.name.toUpperCase()) + + const currencyName = currency.name.toUpperCase() + const { data: info } = await coinMarketCapAPI.getCoinInfo(id, currencyName) + const { data: market } = await coinMarketCapAPI.getLatestMarketPairs(id, currencyName) + return { coin: { id, - name: info.data.name, - symbol: info.data.symbol, + name: info.name, + symbol: info.symbol, image_url: `https://s2.coinmarketcap.com/static/img/coins/64x64/${id}.png`, - market_cap_rank: info.data.rank, + market_cap_rank: info.rank, }, currency, platform, market: { - current_price: info.data.quotes[currency.name.toUpperCase()].price, - total_volume: info.data.quotes[currency.name.toUpperCase()].volume_24h, - price_change_percentage_24h: info.data.quotes[currency.name.toUpperCase()].percent_change_24h, + current_price: info.quotes[currencyName].price, + total_volume: info.quotes[currencyName].volume_24h, + price_change_percentage_24h: info.quotes[currencyName].percent_change_24h, }, - tickers: [], + tickers: market.market_pairs.map((pair) => ({ + logo_url: '', + trade_url: pair.market_url, + market_name: pair.exchange.name, + base_name: pair.market_pair_base.exchange_symbol, + target_name: pair.market_pair_quote.exchange_symbol, + price: + pair.market_pair_base.currency_id === market.id + ? pair.quote[currencyName].price + : pair.quote[currencyName].price_quote, + volumn: pair.quote[currencyName].volume_24h, + score: String(pair.market_score), + })), } } @@ -108,5 +124,20 @@ export async function getCoinTrendingByKeyword(keyword: string, platform: Platfo } export async function getPriceStats(id: string, platform: Platform, currency: Currency, days: number): Promise { - return platform === Platform.COIN_GECKO ? (await coinGeckoAPI.getPriceStats(id, currency.id, days)).prices : [] + if (platform === Platform.COIN_GECKO) { + const stats = await coinGeckoAPI.getPriceStats(id, currency.id, days) + return stats.prices + } + const interval = (() => { + if (days > 365) return '1d' // 1y + if (days > 90) return '2h' // 3m + if (days > 30) return '1h' // 1m + if (days > 7) return '15m' // 1w + return '5m' + })() + const endDate = new Date() + const startDate = new Date() + startDate.setDate(startDate.getDate() - days) + const stats = await coinMarketCapAPI.getHistorical(id, currency.name.toUpperCase(), startDate, endDate, interval) + return Object.entries(stats.data).map(([date, x]) => [date, x[currency.name.toUpperCase()][0]]) } diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index 15495d4bf69..222cbb0d12d 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -26,7 +26,7 @@ export const TraderPluginDefine: PluginConfig = { return ( {(name: string) => { - return + return }} ) diff --git a/src/plugins/Trader/hooks/useCurrentCurrency.ts b/src/plugins/Trader/hooks/useCurrentCurrency.ts new file mode 100644 index 00000000000..b1ae80e0260 --- /dev/null +++ b/src/plugins/Trader/hooks/useCurrentCurrency.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' +import { useAsync } from 'react-use' +import type { Platform, Currency, Settings } from '../type' +import Services from '../../../extension/service' +import { useValueRef } from '../../../utils/hooks/useValueRef' +import { getActivatedUI } from '../../../social-network/ui' +import { getCurrentTrendingViewPlatformSettings } from '../settings' + +export function useCurrentCurrency(platform: Platform) { + const [currency, setCurrency] = useState(null) + const trendingSettings = useValueRef( + getCurrentTrendingViewPlatformSettings(platform)[getActivatedUI().networkIdentifier], + ) + + // TODO: + // support multiple currencies + const { value: currencies = [], loading, error } = useAsync( + () => Services.Plugin.invokePlugin('maskbook.trader', 'getLimitedCurrenies', platform), + [platform], + ) + + 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]) + + return { + value: currency, + loading, + error, + } +} diff --git a/src/plugins/Trader/hooks/useCurrentPlatform.ts b/src/plugins/Trader/hooks/useCurrentPlatform.ts new file mode 100644 index 00000000000..27d3278f6ec --- /dev/null +++ b/src/plugins/Trader/hooks/useCurrentPlatform.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react' +import { Platform } from '../type' +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], + ) + + // sync platform + useEffect(() => { + console.log(`DEBUG: useCurrentPlatform - ${String(platform)} - ${trendingPlatformSettings}`) + if (String(platform) === trendingPlatformSettings) return + if (trendingPlatformSettings === String(Platform.COIN_GECKO)) setPlatform(Platform.COIN_GECKO) + else if (trendingPlatformSettings === String(Platform.COIN_MARKET_CAP)) setPlatform(Platform.COIN_MARKET_CAP) + }, [platform, trendingPlatformSettings]) + return platform +} diff --git a/src/plugins/Trader/hooks/useTrending.ts b/src/plugins/Trader/hooks/useTrending.ts index 5c171112921..ddf7ce85113 100644 --- a/src/plugins/Trader/hooks/useTrending.ts +++ b/src/plugins/Trader/hooks/useTrending.ts @@ -1,56 +1,18 @@ -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' +import { useCurrentCurrency } from './useCurrentCurrency' +import type { Platform } from '../type' -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', 'getLimitedCurrenies', 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 +export function useTrending(keyword: string, platform: Platform) { + const { value: currency, loading: loadingCurrency, error: errorCurrency } = useCurrentCurrency(platform) 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, + loading: loadingCurrency || loadingTrending, + error: errorCurrency || errorTrending, } } diff --git a/src/plugins/Trader/settings.ts b/src/plugins/Trader/settings.ts index 0442af59263..e9f9a89015a 100644 --- a/src/plugins/Trader/settings.ts +++ b/src/plugins/Trader/settings.ts @@ -1,4 +1,11 @@ import { createNetworkSettings } from '../../settings/createSettings' +import { Platform } from './type' -export const currentTrendingViewSettings = createNetworkSettings('currentTrendingViewSettings') export const currentTrendingViewPlatformSettings = createNetworkSettings('currentTrendingViewPlatformSettings') + +const coinGeckoSettings = createNetworkSettings('currentTrendingViewPlatformCoinGeckoSettings') +const coinMarketCapSettings = createNetworkSettings('currentTrendingViewPlatformCoinMarketCapSettings') + +export function getCurrentTrendingViewPlatformSettings(platform: Platform) { + return platform === Platform.COIN_GECKO ? coinGeckoSettings : coinMarketCapSettings +} diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/type.ts index dea672987ef..810cb7a473f 100644 --- a/src/plugins/Trader/type.ts +++ b/src/plugins/Trader/type.ts @@ -57,7 +57,7 @@ export interface Trending { tickers: Ticker[] } -export type Stat = [number, number] +export type Stat = [number | string, number] export function resolveCurrencyName(currency: Currency) { return [ @@ -78,6 +78,17 @@ export function resolvePlatformName(platform: Platform) { } } +export function resolvePlatformLink(platform: Platform) { + switch (platform) { + case Platform.COIN_GECKO: + return 'https://www.coingecko.com/' + case Platform.COIN_MARKET_CAP: + return 'https://coinmarketcap.com/' + default: + return '' + } +} + export function resolveDaysName(days: number) { if (days >= 365) return `${Math.floor(days / 365)}y` if (days >= 30) return `${Math.floor(days / 30)}m` From 8a6c4e0d23cfc7102fbc44394586bb6934245649 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 28 Aug 2020 16:50:10 +0800 Subject: [PATCH 09/16] refactor: better UX --- .../TypedMessageRenderer.tsx | 8 ++--- src/plugins/Trader/UI/PriceChangedTable.tsx | 2 +- src/plugins/Trader/UI/PriceChart.tsx | 2 +- .../Trader/UI/PriceChartDaysControl.tsx | 2 +- src/plugins/Trader/UI/SettingsDialog.tsx | 2 +- src/plugins/Trader/UI/TickersTable.tsx | 2 +- src/plugins/Trader/UI/TrendingPopper.tsx | 10 ++++-- src/plugins/Trader/UI/TrendingView.tsx | 32 +++++++++++-------- src/plugins/Trader/apis/coingecko/index.ts | 4 +-- .../Trader/apis/coinmarketcap/index.ts | 1 + src/plugins/Trader/apis/index.ts | 6 ++-- src/plugins/Trader/define.tsx | 4 +-- .../Trader/hooks/useCurrentCurrency.ts | 6 ++-- .../Trader/hooks/useCurrentPlatform.ts | 3 +- src/plugins/Trader/hooks/usePriceStats.ts | 2 +- src/plugins/Trader/hooks/useTrending.ts | 11 +++---- src/plugins/Trader/messages.ts | 2 +- .../messages/TypedMessageCashTrending.tsx | 2 +- src/plugins/Trader/settings.ts | 2 +- src/plugins/Trader/{type.ts => types.ts} | 0 .../twitter.com/ui/custom.ts | 3 ++ .../twitter.com/utils/theme.ts | 1 - 22 files changed, 56 insertions(+), 51 deletions(-) rename src/plugins/Trader/{type.ts => types.ts} (100%) diff --git a/src/components/InjectedComponents/TypedMessageRenderer.tsx b/src/components/InjectedComponents/TypedMessageRenderer.tsx index 0739c7b1f35..8a9841fb18f 100644 --- a/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ b/src/components/InjectedComponents/TypedMessageRenderer.tsx @@ -46,11 +46,7 @@ export const DefaultTypedMessageTextRenderer = React.memo(function DefaultTypedM ) { return renderWithMetadata( props, - + , ) @@ -67,7 +63,7 @@ export const DefaultTypedMessageAnchorRenderer = React.memo(function DefaultType const { content, href } = props.message return renderWithMetadata( props, - + {content} diff --git a/src/plugins/Trader/UI/PriceChangedTable.tsx b/src/plugins/Trader/UI/PriceChangedTable.tsx index a93f8916c97..4832af8165e 100644 --- a/src/plugins/Trader/UI/PriceChangedTable.tsx +++ b/src/plugins/Trader/UI/PriceChangedTable.tsx @@ -10,7 +10,7 @@ import { TableCell, TableBody, } from '@material-ui/core' -import type { Market } from '../type' +import type { Market } from '../types' import { PriceChanged } from './PriceChanged' const useStyles = makeStyles((theme: Theme) => diff --git a/src/plugins/Trader/UI/PriceChart.tsx b/src/plugins/Trader/UI/PriceChart.tsx index 3836e536376..553ee1df0ac 100644 --- a/src/plugins/Trader/UI/PriceChart.tsx +++ b/src/plugins/Trader/UI/PriceChart.tsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect } from 'react' import * as d3 from 'd3' -import type { Stat } from '../type' +import type { Stat } from '../types' import { makeStyles, Theme, createStyles, CircularProgress, Typography } from '@material-ui/core' const DEFAULT_WIDTH = 460 diff --git a/src/plugins/Trader/UI/PriceChartDaysControl.tsx b/src/plugins/Trader/UI/PriceChartDaysControl.tsx index a3b44a24149..a6c01d06cc0 100644 --- a/src/plugins/Trader/UI/PriceChartDaysControl.tsx +++ b/src/plugins/Trader/UI/PriceChartDaysControl.tsx @@ -1,6 +1,6 @@ import React from 'react' import { makeStyles, Theme, createStyles, Link, Typography } from '@material-ui/core' -import { resolveDaysName } from '../type' +import { resolveDaysName } from '../types' const useStyles = makeStyles((theme: Theme) => createStyles({ diff --git a/src/plugins/Trader/UI/SettingsDialog.tsx b/src/plugins/Trader/UI/SettingsDialog.tsx index b83c470ab54..033c2448bbb 100644 --- a/src/plugins/Trader/UI/SettingsDialog.tsx +++ b/src/plugins/Trader/UI/SettingsDialog.tsx @@ -20,7 +20,7 @@ import { } from '@material-ui/core' import ShadowRootDialog from '../../../utils/jss/ShadowRootDialog' import { DialogDismissIconUI } from '../../../components/InjectedComponents/DialogDismissIcon' -import { Currency, Platform, resolveCurrencyName, resolvePlatformName } from '../type' +import { Currency, Platform, resolveCurrencyName, resolvePlatformName } from '../types' import { useStylesExtends } from '../../../components/custom-ui-helper' import { getActivatedUI } from '../../../social-network/ui' import { diff --git a/src/plugins/Trader/UI/TickersTable.tsx b/src/plugins/Trader/UI/TickersTable.tsx index 35b9535f470..14ba2b1c732 100644 --- a/src/plugins/Trader/UI/TickersTable.tsx +++ b/src/plugins/Trader/UI/TickersTable.tsx @@ -12,7 +12,7 @@ import { Link, Typography, } from '@material-ui/core' -import type { Ticker, Platform } from '../type' +import type { Ticker, Platform } from '../types' import { formatCurrency, formatEthAddress } from '../../Wallet/formatter' const useStyles = makeStyles((theme: Theme) => diff --git a/src/plugins/Trader/UI/TrendingPopper.tsx b/src/plugins/Trader/UI/TrendingPopper.tsx index 45d7fe6e25d..cf3d8e37d2d 100644 --- a/src/plugins/Trader/UI/TrendingPopper.tsx +++ b/src/plugins/Trader/UI/TrendingPopper.tsx @@ -1,13 +1,16 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef, useMemo } from 'react' +import type PopperJs from 'popper.js' import { Popper, ClickAwayListener, PopperProps } from '@material-ui/core' import { MessageCenter, ObserveCashTagEvent } from '../messages' +import { useInterval } from 'react-use' export interface TrendingPopperProps { - children?: (name: string) => React.ReactNode + children?: (name: string, reposition?: () => void) => React.ReactNode PopperProps?: Partial } export function TrendingPopper(props: TrendingPopperProps) { + const popperRef = useRef(null) const [name, setName] = useState('') const [anchorEl, setAnchorEl] = useState(null) @@ -31,8 +34,9 @@ export function TrendingPopper(props: TrendingPopperProps) { disablePortal transition style={{ zIndex: 1, marginTop: 8 }} + popperRef={popperRef} {...props.PopperProps}> - {props.children?.(name)} + {props.children?.(name, () => setTimeout(() => popperRef.current?.scheduleUpdate(), 0))} ) diff --git a/src/plugins/Trader/UI/TrendingView.tsx b/src/plugins/Trader/UI/TrendingView.tsx index 3cf68337191..2f875074f20 100644 --- a/src/plugins/Trader/UI/TrendingView.tsx +++ b/src/plugins/Trader/UI/TrendingView.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { makeStyles, Avatar, @@ -15,7 +15,7 @@ import { Tab, Tabs, } from '@material-ui/core' -import { resolvePlatformName, Platform } from '../type' +import { resolvePlatformName, Platform } from '../types' import { getActivatedUI } from '../../../social-network/ui' import { formatCurrency } from '../../Wallet/formatter' import { useTrending } from '../hooks/useTrending' @@ -42,7 +42,13 @@ const useStyles = makeStyles((theme: Theme) => { display: 'none', }, ...(internalName === 'twitter' - ? { border: `1px solid ${theme.palette.type === 'dark' ? '#2f3336' : '#ccd6dd'}` } + ? { + boxShadow: `${ + theme.palette.type === 'dark' + ? 'rgba(255, 255, 255, 0.2) 0px 0px 15px, rgba(255, 255, 255, 0.15) 0px 0px 3px 1px' + : 'rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px' + }`, + } : null), }, header: { @@ -93,25 +99,19 @@ const useStyles = makeStyles((theme: Theme) => { export interface TrendingViewProps extends withClasses> { name: string + onUpdate?: () => void } export function TrendingView(props: TrendingViewProps) { const classes = useStyles() const [tabIndex, setTabIndex] = useState(0) + //#region trending const platform = useCurrentPlatform(Platform.COIN_GECKO) const { value: currency, loading: loadingCurrency } = useCurrentCurrency(platform) - - //#region trending - const { value: trending, loading: loadingTrending } = useTrending(props.name, platform) + const { value: trending, loading: loadingTrending } = useTrending(props.name, platform, currency) //#endregion - console.log('DEBUG: TrendingView') - console.log({ - currency, - trending, - }) - //#region stats const [days, setDays] = useState(365) const { value: stats = [], loading: loadingStats } = usePriceStats({ @@ -122,6 +122,12 @@ export function TrendingView(props: TrendingViewProps) { }) //#endregion + //#region api ready callback + useEffect(() => { + props.onUpdate?.() + }, [tabIndex, loadingCurrency, loadingTrending]) + //#endregion + //#region display loading skeleton if (loadingCurrency || loadingTrending) return ( @@ -133,7 +139,7 @@ export function TrendingView(props: TrendingViewProps) { /> - + diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 0beb112c831..585277e63b3 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -4,7 +4,7 @@ const CHART_BASE_URL = 'https://www' //#region get currency export async function getAllCurrenies() { - const response = await fetch(`${BASE_URL}/simple/supported_vs_currencies`) + const response = await fetch(`${BASE_URL}/simple/supported_vs_currencies`, { cache: 'force-cache' }) return response.json() as Promise } //#endregion @@ -17,7 +17,7 @@ export interface Coin { } export async function getAllCoins() { - const response = await fetch(`${BASE_URL}/coins/list`) + const response = await fetch(`${BASE_URL}/coins/list`, { cache: 'force-cache' }) return response.json() as Promise } //#endregion diff --git a/src/plugins/Trader/apis/coinmarketcap/index.ts b/src/plugins/Trader/apis/coinmarketcap/index.ts index 7e5dc53f7b7..a82aaeac8d4 100644 --- a/src/plugins/Trader/apis/coinmarketcap/index.ts +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -43,6 +43,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`, + { cache: 'force-cache' }, ) return response.json() as Promise<{ data: Coin[] diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 3bdfd981b0c..9ad2817920a 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -1,4 +1,4 @@ -import { Platform, Currency, Coin, Trending, Market, Stat } from '../type' +import { Platform, Currency, Coin, Trending, Market, Stat } from '../types' import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' @@ -98,7 +98,9 @@ export async function getCoinInfo(id: string, platform: Platform, currency: Curr market: { current_price: info.quotes[currencyName].price, total_volume: info.quotes[currencyName].volume_24h, - price_change_percentage_24h: info.quotes[currencyName].percent_change_24h, + price_change_percentage_1h_in_currency: info.quotes[currencyName].percent_change_1h, + price_change_percentage_24h_in_currency: info.quotes[currencyName].percent_change_24h, + price_change_percentage_7d_in_currency: info.quotes[currencyName].percent_change_7d, }, tickers: market.market_pairs.map((pair) => ({ logo_url: '', diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index 222cbb0d12d..02d3b31df9a 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -25,9 +25,7 @@ export const TraderPluginDefine: PluginConfig = { pageInspector() { return ( - {(name: string) => { - return - }} + {(name: string, reposition?: () => void) => } ) }, diff --git a/src/plugins/Trader/hooks/useCurrentCurrency.ts b/src/plugins/Trader/hooks/useCurrentCurrency.ts index b1ae80e0260..68319433d56 100644 --- a/src/plugins/Trader/hooks/useCurrentCurrency.ts +++ b/src/plugins/Trader/hooks/useCurrentCurrency.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useAsync } from 'react-use' -import type { Platform, Currency, Settings } from '../type' +import type { Platform, Currency, Settings } from '../types' import Services from '../../../extension/service' import { useValueRef } from '../../../utils/hooks/useValueRef' import { getActivatedUI } from '../../../social-network/ui' @@ -28,10 +28,10 @@ export function useCurrentCurrency(platform: Platform) { } catch (e) { setCurrency(null) } - }, [trendingSettings, currencies.length]) + }, [platform, trendingSettings, currencies.map((x) => x.id).join()]) return { - value: currency, + value: loading ? null : currency, loading, error, } diff --git a/src/plugins/Trader/hooks/useCurrentPlatform.ts b/src/plugins/Trader/hooks/useCurrentPlatform.ts index 27d3278f6ec..81c08809239 100644 --- a/src/plugins/Trader/hooks/useCurrentPlatform.ts +++ b/src/plugins/Trader/hooks/useCurrentPlatform.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { Platform } from '../type' +import { Platform } from '../types' import { getActivatedUI } from '../../../social-network/ui' import { useValueRef } from '../../../utils/hooks/useValueRef' import { currentTrendingViewPlatformSettings } from '../settings' @@ -12,7 +12,6 @@ export function useCurrentPlatform(defaultPlatform: Platform) { // sync platform useEffect(() => { - console.log(`DEBUG: useCurrentPlatform - ${String(platform)} - ${trendingPlatformSettings}`) if (String(platform) === trendingPlatformSettings) return if (trendingPlatformSettings === String(Platform.COIN_GECKO)) setPlatform(Platform.COIN_GECKO) else if (trendingPlatformSettings === String(Platform.COIN_MARKET_CAP)) setPlatform(Platform.COIN_MARKET_CAP) diff --git a/src/plugins/Trader/hooks/usePriceStats.ts b/src/plugins/Trader/hooks/usePriceStats.ts index ce364ba6a70..c65dfa277b5 100644 --- a/src/plugins/Trader/hooks/usePriceStats.ts +++ b/src/plugins/Trader/hooks/usePriceStats.ts @@ -1,6 +1,6 @@ import { useAsync } from 'react-use' import Services from '../../../extension/service' -import type { Currency, Platform } from '../type' +import type { Currency, Platform } from '../types' import { isUndefined } from 'lodash-es' interface Options { diff --git a/src/plugins/Trader/hooks/useTrending.ts b/src/plugins/Trader/hooks/useTrending.ts index ddf7ce85113..dbad1e51c24 100644 --- a/src/plugins/Trader/hooks/useTrending.ts +++ b/src/plugins/Trader/hooks/useTrending.ts @@ -1,18 +1,15 @@ import { useAsync } from 'react-use' import Services from '../../../extension/service' -import { useCurrentCurrency } from './useCurrentCurrency' -import type { Platform } from '../type' +import type { Platform, Currency } from '../types' -export function useTrending(keyword: string, platform: Platform) { - const { value: currency, loading: loadingCurrency, error: errorCurrency } = useCurrentCurrency(platform) +export function useTrending(keyword: string, platform: Platform, currency: Currency | null) { 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]) - return { value: trending, - loading: loadingCurrency || loadingTrending, - error: errorCurrency || errorTrending, + loading: loadingTrending, + error: errorTrending, } } diff --git a/src/plugins/Trader/messages.ts b/src/plugins/Trader/messages.ts index 5f36af87797..155fbf83974 100644 --- a/src/plugins/Trader/messages.ts +++ b/src/plugins/Trader/messages.ts @@ -1,4 +1,4 @@ -import type { Currency, Platform } from './type' +import type { Currency, Platform } from './types' import { BatchedMessageCenter } from '../../utils/messages' export interface SettingsEvent { diff --git a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx index a278fad70f5..ff813ce7bec 100644 --- a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -32,7 +32,7 @@ function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProp } return ( - + {props.message.content} diff --git a/src/plugins/Trader/settings.ts b/src/plugins/Trader/settings.ts index e9f9a89015a..b5ab64adca0 100644 --- a/src/plugins/Trader/settings.ts +++ b/src/plugins/Trader/settings.ts @@ -1,5 +1,5 @@ import { createNetworkSettings } from '../../settings/createSettings' -import { Platform } from './type' +import { Platform } from './types' export const currentTrendingViewPlatformSettings = createNetworkSettings('currentTrendingViewPlatformSettings') diff --git a/src/plugins/Trader/type.ts b/src/plugins/Trader/types.ts similarity index 100% rename from src/plugins/Trader/type.ts rename to src/plugins/Trader/types.ts diff --git a/src/social-network-provider/twitter.com/ui/custom.ts b/src/social-network-provider/twitter.com/ui/custom.ts index 750107dfdbd..32f68831729 100644 --- a/src/social-network-provider/twitter.com/ui/custom.ts +++ b/src/social-network-provider/twitter.com/ui/custom.ts @@ -56,6 +56,9 @@ function useTheme() { dark: toRGB(shade(primaryColorRGB, -10)), }, }, + shape: { + borderRadius: 15, + }, breakpoints: { values: { xs: 0, sm: 687, md: 1024, lg: 1280, xl: 1920 }, }, diff --git a/src/social-network-provider/twitter.com/utils/theme.ts b/src/social-network-provider/twitter.com/utils/theme.ts index afabb2d74e8..01c2a45e6e9 100644 --- a/src/social-network-provider/twitter.com/utils/theme.ts +++ b/src/social-network-provider/twitter.com/utils/theme.ts @@ -58,7 +58,6 @@ export const useTwitterDialog = makeStyles((theme: Theme) => { }, paper: { width: '600px !important', - borderRadius: 14, boxShadow: 'none', [`@media (max-width: ${theme.breakpoints.width('sm')}px)`]: { '&': { From 99b22dbad7f27bb7d9a0ab0bd2953144d598473c Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Mon, 31 Aug 2020 12:12:19 +0800 Subject: [PATCH 10/16] fix: build error --- src/setup.ssr.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/setup.ssr.js b/src/setup.ssr.js index 9274a9acfef..d11a6e5e9fc 100644 --- a/src/setup.ssr.js +++ b/src/setup.ssr.js @@ -12,6 +12,9 @@ globalThis.document = { }, body: { appendChild() {} }, addEventListener() {}, + documentElement: { + onmouseenter() {}, + }, } globalThis.CSSStyleSheet = { name: 'CSSStyleSheet' } globalThis.ShadowRoot = class {} @@ -34,7 +37,6 @@ const restoreKit = modifyPackage('@holoflows/kit', (x) => { './package.json': './package.json', } }) - process.on('uncaughtException', function (err) { cleanup() throw err From 4d06a8b096c5071987a1caafcd35c6cd053c362d Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Mon, 31 Aug 2020 19:51:57 +0800 Subject: [PATCH 11/16] chore: inject post dummy according to the WholePostVisibility settings --- .../InjectedComponents/PostDummy.tsx | 23 ++++++++++++++++--- src/protocols/typed-message/types.ts | 3 +++ .../facebook.com/UI/collectPosts.tsx | 1 + 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx index 120fa5d7022..3120d7cfec8 100644 --- a/src/components/InjectedComponents/PostDummy.tsx +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -2,16 +2,33 @@ import React from 'react' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' import { PluginUI } from '../../plugins/plugin' -import { makeTypedMessageCompound, isTypedMessageSuspended } from '../../protocols/typed-message' +import { makeTypedMessageCompound, isTypedMessageSuspended, isTypedMessageKnown } from '../../protocols/typed-message' +import { useValueRef } from '../../utils/hooks/useValueRef' +import { currentWholePostVisibilitySettings, WholePostVisibility } from '../../settings/settings' export interface PostDummyProps {} export function PostDummy(props: PostDummyProps) { - const postMessage = usePostInfoDetails('parsedPostContent') + 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), - postMessage, + parsedPostContent, + ) + + // render dummy for posts which enhanced by plugins + if ( + wholePostVisibilitySettings === WholePostVisibility.enhancedOnly && + processedPostMessage.items.every(isTypedMessageKnown) ) + return null + + // render dummy for posts which encrypted by maskbook + if (wholePostVisibilitySettings === WholePostVisibility.encryptedOnly && !postPayload.ok) return null + + // render dummy for all posts return ( !isTypedMessageSuspended(x)))} diff --git a/src/protocols/typed-message/types.ts b/src/protocols/typed-message/types.ts index cb6051bb529..88328b0454c 100644 --- a/src/protocols/typed-message/types.ts +++ b/src/protocols/typed-message/types.ts @@ -53,6 +53,9 @@ export function isTypedMessageText(x: TypedMessage): x is TypedMessageText { export function isTypedMessgaeAnchor(x: TypedMessage): x is TypedMessageAnchor { return x.type === 'anchor' } +export function isTypedMessageKnown(x: TypedMessage) { + return ['text', 'anchor', 'compound', 'image', 'empty', 'suspended'].includes(x.type) +} export function isTypedMessageUnknown(x: TypedMessage): x is TypedMessageUnknown { return x.type === 'unknown' } diff --git a/src/social-network-provider/facebook.com/UI/collectPosts.tsx b/src/social-network-provider/facebook.com/UI/collectPosts.tsx index 4bd32d71636..89eb603b342 100644 --- a/src/social-network-provider/facebook.com/UI/collectPosts.tsx +++ b/src/social-network-provider/facebook.com/UI/collectPosts.tsx @@ -84,6 +84,7 @@ export function collectPostsFacebook(this: SocialNetworkUI) { info.postMetadataImages.add(url) nextTypedMessage.push(makeTypedMessageImage(url)) } + // parse post content info.parsedPostContent.value = makeTypedMessageCompound(nextTypedMessage) } collectPostInfo() From fe29811d29d411d482f496bf69b5f2cc130fbceb Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 1 Sep 2020 11:55:20 +0800 Subject: [PATCH 12/16] chore: add MAX options into PriceChart --- .../Trader/UI/PriceChartDaysControl.tsx | 20 ++++++++++++++++++- src/plugins/Trader/UI/TickersTable.tsx | 10 +++------- src/plugins/Trader/apis/coingecko/index.ts | 4 ++-- src/plugins/Trader/apis/index.ts | 13 ++++++++++-- src/plugins/Trader/hooks/usePriceStats.ts | 8 ++++---- src/plugins/Trader/types.ts | 1 + 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/plugins/Trader/UI/PriceChartDaysControl.tsx b/src/plugins/Trader/UI/PriceChartDaysControl.tsx index a6c01d06cc0..988258b5f65 100644 --- a/src/plugins/Trader/UI/PriceChartDaysControl.tsx +++ b/src/plugins/Trader/UI/PriceChartDaysControl.tsx @@ -24,6 +24,16 @@ const useStyles = makeStyles((theme: Theme) => }), ) +export enum Days { + MAX = 0, + ONE_DAY = 1, + ONE_WEEK = 7, + TWO_WEEKS = 14, + ONE_MONTH = 30, + THREE_MONTHS = 90, + ONE_YEAR = 365, +} + export interface PriceChartDaysControlProps { days: number onDaysChange?: (days: number) => void @@ -33,7 +43,15 @@ export function PriceChartDaysControl(props: PriceChartDaysControlProps) { const classes = useStyles() return (
- {[1, 7, 14, 30, 90, 365].map((days) => ( + {[ + Days.ONE_DAY, + Days.ONE_WEEK, + Days.TWO_WEEKS, + Days.ONE_MONTH, + Days.THREE_MONTHS, + Days.ONE_YEAR, + Days.MAX, + ].map((days) => ( props.onDaysChange?.(days)}> ( - - - {ticker.market_name} - - + {ticker.market_name} {(() => { const formated = formatEthAddress(ticker.base_name) return ( - <> + {formated} / {ticker.target_name} - + ) })()} diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 585277e63b3..8fc1768d189 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,6 +1,6 @@ -const BASE_URL = 'https://api.coingecko.com/api/v3' +import { Days } from '../../UI/PriceChartDaysControl' -const CHART_BASE_URL = 'https://www' +const BASE_URL = 'https://api.coingecko.com/api/v3' //#region get currency export async function getAllCurrenies() { diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 9ad2817920a..8247e6e1a4a 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -1,6 +1,7 @@ import { Platform, Currency, Coin, Trending, Market, Stat } from '../types' import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' +import { Days } from '../UI/PriceChartDaysControl' export async function getCurrenies(platform: Platform): Promise { if (platform === Platform.COIN_GECKO) { @@ -127,10 +128,11 @@ export async function getCoinTrendingByKeyword(keyword: string, platform: Platfo export async function getPriceStats(id: string, platform: Platform, currency: Currency, days: number): Promise { if (platform === Platform.COIN_GECKO) { - const stats = await coinGeckoAPI.getPriceStats(id, currency.id, days) + const stats = await coinGeckoAPI.getPriceStats(id, currency.id, days === Days.MAX ? 11430 : days) return stats.prices } const interval = (() => { + if (days === 0) return '1d' // max if (days > 365) return '1d' // 1y if (days > 90) return '2h' // 3m if (days > 30) return '1h' // 1m @@ -140,6 +142,13 @@ export async function getPriceStats(id: string, platform: Platform, currency: Cu const endDate = new Date() const startDate = new Date() startDate.setDate(startDate.getDate() - days) - const stats = await coinMarketCapAPI.getHistorical(id, currency.name.toUpperCase(), startDate, endDate, interval) + const stats = await coinMarketCapAPI.getHistorical( + id, + currency.name.toUpperCase(), + // the bitcoin ledger started at 03 Jan 2009 + days === Days.MAX ? new Date(1230940800000) : startDate, + endDate, + interval, + ) return Object.entries(stats.data).map(([date, x]) => [date, x[currency.name.toUpperCase()][0]]) } diff --git a/src/plugins/Trader/hooks/usePriceStats.ts b/src/plugins/Trader/hooks/usePriceStats.ts index c65dfa277b5..b37885baed5 100644 --- a/src/plugins/Trader/hooks/usePriceStats.ts +++ b/src/plugins/Trader/hooks/usePriceStats.ts @@ -2,18 +2,18 @@ import { useAsync } from 'react-use' import Services from '../../../extension/service' import type { Currency, Platform } from '../types' import { isUndefined } from 'lodash-es' +import { Days } from '../UI/PriceChartDaysControl' interface Options { coinId?: string currency?: Currency - days?: number + days?: Days platform?: Platform } -export function usePriceStats({ coinId, currency, days = 30, platform }: Options) { +export function usePriceStats({ coinId, currency, days = Days.MAX, platform }: Options) { return useAsync(async () => { - if (days <= 0) return [] - if (isUndefined(coinId) || isUndefined(platform) || isUndefined(currency)) return [] + if (isUndefined(days) || isUndefined(coinId) || isUndefined(platform) || isUndefined(currency)) return [] return Services.Plugin.invokePlugin('maskbook.trader', 'getPriceStats', coinId, platform, currency, days) }, [coinId, platform, currency?.id, days]) } diff --git a/src/plugins/Trader/types.ts b/src/plugins/Trader/types.ts index 810cb7a473f..94bc7975d94 100644 --- a/src/plugins/Trader/types.ts +++ b/src/plugins/Trader/types.ts @@ -90,6 +90,7 @@ export function resolvePlatformLink(platform: Platform) { } export function resolveDaysName(days: number) { + if (days === 0) return 'MAX' if (days >= 365) return `${Math.floor(days / 365)}y` if (days >= 30) return `${Math.floor(days / 30)}m` if (days >= 7) return `${Math.floor(days / 7)}w` From 649329892f40965ff6597c9bc8c0d318d81f3d4d Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 1 Sep 2020 13:34:43 +0800 Subject: [PATCH 13/16] chore: support WholePostVisibility --- .../InjectedComponents/PostDummy.tsx | 38 ++++++++++++------- .../twitter.com/ui/injectPostDummy.tsx | 15 +++++++- .../twitter.com/utils/selector.ts | 5 ++- .../defaults/injectPostDummy.tsx | 23 +++++++++-- .../defaults/injectPostInspector.tsx | 3 +- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx index 3120d7cfec8..5820e90d160 100644 --- a/src/components/InjectedComponents/PostDummy.tsx +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' import { PluginUI } from '../../plugins/plugin' @@ -6,7 +6,10 @@ import { makeTypedMessageCompound, isTypedMessageSuspended, isTypedMessageKnown import { useValueRef } from '../../utils/hooks/useValueRef' import { currentWholePostVisibilitySettings, WholePostVisibility } from '../../settings/settings' -export interface PostDummyProps {} +export interface PostDummyProps { + zip?: () => void + unzip?: () => void +} export function PostDummy(props: PostDummyProps) { const parsedPostContent = usePostInfoDetails('parsedPostContent') @@ -17,21 +20,30 @@ export function PostDummy(props: PostDummyProps) { (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) - // render dummy for posts which enhanced by plugins - if ( - wholePostVisibilitySettings === WholePostVisibility.enhancedOnly && - processedPostMessage.items.every(isTypedMessageKnown) - ) - return null + console.log(`DEBUG: postDummyVisible`) + console.log({ + postDummyVisible, + wholePostVisibilitySettings, + }) - // render dummy for posts which encrypted by maskbook - if (wholePostVisibilitySettings === WholePostVisibility.encryptedOnly && !postPayload.ok) return null + // zip original post + useEffect(() => { + if (postDummyVisible) props.zip?.() + else props.unzip?.() + }, [postDummyVisible]) - // render dummy for all posts - return ( + return postDummyVisible ? ( !isTypedMessageSuspended(x)))} /> - ) + ) : null } diff --git a/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx b/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx index c8a64072317..c4360e84037 100644 --- a/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx +++ b/src/social-network-provider/twitter.com/ui/injectPostDummy.tsx @@ -1,6 +1,19 @@ import type { PostInfo } from '../../../social-network/PostInfo' import { injectPostDummyDefault } from '../../../social-network/defaults/injectPostDummy' +function resolveLangNode(node: HTMLElement) { + return node.hasAttribute('lang') ? node : node.querySelector('[lang]') +} + export function injectPostDummyAtTwitter(current: PostInfo) { - return injectPostDummyDefault()(current) + return injectPostDummyDefault({ + zipPost(node) { + const langNode = resolveLangNode(node.current) + if (langNode) langNode.style.display = 'none' + }, + unzipPost(node) { + const langNode = resolveLangNode(node.current) + if (langNode) langNode.style.display = 'unset' + }, + })(current) } diff --git a/src/social-network-provider/twitter.com/utils/selector.ts b/src/social-network-provider/twitter.com/utils/selector.ts index d4e8eddd7c2..a4fabba00ba 100644 --- a/src/social-network-provider/twitter.com/utils/selector.ts +++ b/src/social-network-provider/twitter.com/utils/selector.ts @@ -90,7 +90,10 @@ export const postsContentSelector = () => ].join(), ).concat( querySelectorAll('[data-testid="tweet"] > div:last-child').map( - (x) => x.querySelector('[role="group"]')?.parentElement?.firstElementChild as HTMLDivElement | undefined, + (x) => + x.querySelector('[role="group"]')?.parentElement?.querySelector('div[lang]') as + | HTMLDivElement + | undefined, ), // timeline page for new twitter ) diff --git a/src/social-network/defaults/injectPostDummy.tsx b/src/social-network/defaults/injectPostDummy.tsx index 9cb361b3e63..cbd50a82660 100644 --- a/src/social-network/defaults/injectPostDummy.tsx +++ b/src/social-network/defaults/injectPostDummy.tsx @@ -4,22 +4,34 @@ import { PostInfoContext } from '../../components/DataSource/usePostInfo' import { PostDummy, PostDummyProps } from '../../components/InjectedComponents/PostDummy' 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 = () => ({}), useCustomStyles: (props?: any) => Record = makeStyles({}) as any, ) { - const PostDummyDefault = React.memo(function PostDummyDefault() { + const PostDummyDefault = React.memo(function PostDummyDefault(props: { + zipPost: PostDummyProps['zip'] + unZipPost: PostDummyProps['unzip'] + }) { const classes = useCustomStyles() const additionalProps = additionalPropsToPostDummy(classes) - return + return }) + const { zipPost, unzipPost } = config + const zipPostF = zipPost || noop + const unzipPostF = unzipPost || noop return function injectPostDummy(current: PostInfo) { return renderInShadowRoot( - + zipPostF(current.rootNodeProxy)} + unZipPost={() => unzipPostF(current.rootNodeProxy)} + {...current} + /> , { shadow: () => current.rootNodeProxy.afterShadow, @@ -30,4 +42,7 @@ export function injectPostDummyDefault( } } -interface InjectPostDummyDefaultConfig {} +interface InjectPostDummyDefaultConfig { + zipPost?(node: DOMProxy): void + unzipPost?(node: DOMProxy): void +} diff --git a/src/social-network/defaults/injectPostInspector.tsx b/src/social-network/defaults/injectPostInspector.tsx index 2ea8d7634c9..904f64b90d1 100644 --- a/src/social-network/defaults/injectPostInspector.tsx +++ b/src/social-network/defaults/injectPostInspector.tsx @@ -5,6 +5,7 @@ import { renderInShadowRoot } from '../../utils/jss/renderInShadowRoot' import { PostInspector, PostInspectorProps } from '../../components/InjectedComponents/PostInspector' import { makeStyles } from '@material-ui/core' import { PostInfoContext, usePostInfoDetails } from '../../components/DataSource/usePostInfo' +import { noop } from 'lodash-es' export function injectPostInspectorDefault( config: InjectPostInspectorDefaultConfig = {}, @@ -29,7 +30,7 @@ export function injectPostInspectorDefault( }) const { zipPost } = config - const zipPostF = zipPost || (() => {}) + const zipPostF = zipPost || noop return function injectPostInspector(current: PostInfo) { return renderInShadowRoot( From 8521e9dbbaa498b8b4377f40c6a131bfafe10e62 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 1 Sep 2020 15:19:52 +0800 Subject: [PATCH 14/16] chore: check availability before displaying TrendingView --- .../InjectedComponents/PostDummy.tsx | 6 ---- src/plugins/Trader/README.md | 31 ++++++++++++++++ src/plugins/Trader/UI/TrendingPopper.tsx | 1 - src/plugins/Trader/apis/index.ts | 36 ++++++++++++++++++- src/plugins/Trader/constants.ts | 2 ++ src/plugins/Trader/define.tsx | 12 +++++-- .../messages/TypedMessageCashTrending.tsx | 16 +++++---- 7 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 src/plugins/Trader/README.md create mode 100644 src/plugins/Trader/constants.ts diff --git a/src/components/InjectedComponents/PostDummy.tsx b/src/components/InjectedComponents/PostDummy.tsx index 5820e90d160..1effd5217c1 100644 --- a/src/components/InjectedComponents/PostDummy.tsx +++ b/src/components/InjectedComponents/PostDummy.tsx @@ -29,12 +29,6 @@ export function PostDummy(props: PostDummyProps) { // render dummy for posts which encrypted by maskbook (wholePostVisibilitySettings === WholePostVisibility.encryptedOnly && postPayload.ok) - console.log(`DEBUG: postDummyVisible`) - console.log({ - postDummyVisible, - wholePostVisibilitySettings, - }) - // zip original post useEffect(() => { if (postDummyVisible) props.zip?.() diff --git a/src/plugins/Trader/README.md b/src/plugins/Trader/README.md new file mode 100644 index 00000000000..974a60e1a5d --- /dev/null +++ b/src/plugins/Trader/README.md @@ -0,0 +1,31 @@ +# Plugin: Trader + +## Feature Set + +- [x] View trending info of a cryptocurrency with cash tag +- [ ] \[TODO\] View trending info of a stock with cash tag +- [ ] \[TODO\] Do trading between different cryptocurrencies + +## Files + +- `./apis/` - data vendor APIs +- `./hooks/` - customized hooks +- `./messages/` - customized typed messags +- `./UI/` - UI related stuff +- `./services.ts` - A plugin specific messages center +- `./settings.ts` - Some plugin specific settings +- `./types.ts` - Some plugin specific TypeScript typings +- `./constants.ts` - Some plugin specific constants +- `./define.ts` - The definition of the plugin + +## Related discussion + +- + +## Pull requests + +- + +## Prototype + +- \ No newline at end of file diff --git a/src/plugins/Trader/UI/TrendingPopper.tsx b/src/plugins/Trader/UI/TrendingPopper.tsx index cf3d8e37d2d..eedffcfccb7 100644 --- a/src/plugins/Trader/UI/TrendingPopper.tsx +++ b/src/plugins/Trader/UI/TrendingPopper.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef, useMemo } from 'react' import type PopperJs from 'popper.js' import { Popper, ClickAwayListener, PopperProps } from '@material-ui/core' import { MessageCenter, ObserveCashTagEvent } from '../messages' -import { useInterval } from 'react-use' export interface TrendingPopperProps { children?: (name: string, reposition?: () => void) => React.ReactNode diff --git a/src/plugins/Trader/apis/index.ts b/src/plugins/Trader/apis/index.ts index 8247e6e1a4a..86b747a498a 100644 --- a/src/plugins/Trader/apis/index.ts +++ b/src/plugins/Trader/apis/index.ts @@ -1,7 +1,8 @@ -import { Platform, Currency, Coin, Trending, Market, Stat } from '../types' +import { Platform, Currency, Coin, Trending, Stat } from '../types' import * as coinGeckoAPI from './coingecko' import * as coinMarketCapAPI from './coinmarketcap' import { Days } from '../UI/PriceChartDaysControl' +import { getEnumAsArray } from '../../../utils/enum' export async function getCurrenies(platform: Platform): Promise { if (platform === Platform.COIN_GECKO) { @@ -46,6 +47,39 @@ export async function getCoins(platform: Platform): Promise { })) } +//#region check a specific coin is available on specific platform +const availabilityCache = new Map< + Platform, + { + supported: Set + lastUpdated: Date + } +>() + +export async function checkAvailabilityAtPlatform(platform: Platform, keyword: string) { + if ( + // cache never built before + !availabilityCache.has(platform) || + // cache expired in 24h + new Date().getTime() - (availabilityCache.get(platform)?.lastUpdated.getTime() ?? 0) > + 24 /* hours */ * 60 /* minutes */ * 60 /* seconds */ * 1000 /* milliseconds */ + ) { + const coins = await getCoins(platform) + availabilityCache.set(platform, { + supported: new Set(coins.map((x) => x.symbol.toLowerCase())), + lastUpdated: new Date(), + }) + } + return availabilityCache.get(platform)?.supported.has(keyword.toLowerCase()) ?? false +} + +export async function checkAvailability(keyword: string) { + return (await Promise.all(getEnumAsArray(Platform).map((x) => checkAvailabilityAtPlatform(x.value, keyword)))).some( + Boolean, + ) +} +//#endregion + export async function getCoinInfo(id: string, platform: Platform, currency: Currency): Promise { if (platform === Platform.COIN_GECKO) { const info = await coinGeckoAPI.getCoinInfo(id) diff --git a/src/plugins/Trader/constants.ts b/src/plugins/Trader/constants.ts new file mode 100644 index 00000000000..c71de33a4aa --- /dev/null +++ b/src/plugins/Trader/constants.ts @@ -0,0 +1,2 @@ +export const PLUGIN_IDENTIFIER = 'co.maskbook.trader' +export const PLUGIN_METADATA_KEY = 'com.maskbook.trader:1' diff --git a/src/plugins/Trader/define.tsx b/src/plugins/Trader/define.tsx index 02d3b31df9a..142c9714fa1 100644 --- a/src/plugins/Trader/define.tsx +++ b/src/plugins/Trader/define.tsx @@ -9,13 +9,17 @@ import { import { makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' 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' export const TraderPluginDefine: PluginConfig = { pluginName: 'Trader', - identifier: 'co.maskbook.trader', - postDialogMetadataBadge: new Map([['com.maskbook.trader:1', (meta) => 'no metadata']]), + identifier: PLUGIN_IDENTIFIER, + postDialogMetadataBadge: new Map([[PLUGIN_METADATA_KEY, (meta) => 'no metadata']]), postMessageProcessor(message: TypedMessageCompound) { return { ...message, @@ -23,6 +27,10 @@ export const TraderPluginDefine: PluginConfig = { } }, pageInspector() { + // build availability cache in the background page + getEnumAsArray(Platform).forEach((p) => + Services.Plugin.invokePlugin('maskbook.trader', 'checkAvailability', 'BTC'), + ) return ( {(name: string, reposition?: () => void) => } diff --git a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx index ff813ce7bec..2faef19a735 100644 --- a/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -3,6 +3,7 @@ import { TypedMessageAnchor, registerTypedMessageRenderer } from '../../../proto import { Link, Typography } from '@material-ui/core' import type { TypedMessageRendererProps } from '../../../components/InjectedComponents/TypedMessageRenderer' import { MessageCenter } from '../messages' +import Services from '../../../extension/service' export interface TypedMessageCashTrending extends Omit { readonly type: 'anchor/cash_trending' @@ -24,13 +25,16 @@ registerTypedMessageRenderer('anchor/cash_trending', { }) function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { - const onHoverCashTag = (ev: React.MouseEvent) => { - MessageCenter.emit('cashTagObserved', { - name: props.message.name, - element: ev.currentTarget, - }) + const onHoverCashTag = async (ev: React.MouseEvent) => { + // should cache before 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, + }) + } } - return ( From b9c9619e822a7ca6800dadd955b12d853ce53a2b Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 1 Sep 2020 19:07:18 +0800 Subject: [PATCH 15/16] refactor: use URLSearchParams --- src/plugins/Trader/apis/coingecko/index.ts | 8 ++++-- .../Trader/apis/coinmarketcap/index.ts | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/plugins/Trader/apis/coingecko/index.ts b/src/plugins/Trader/apis/coingecko/index.ts index 8fc1768d189..7745c81245e 100644 --- a/src/plugins/Trader/apis/coingecko/index.ts +++ b/src/plugins/Trader/apis/coingecko/index.ts @@ -1,5 +1,3 @@ -import { Days } from '../../UI/PriceChartDaysControl' - const BASE_URL = 'https://api.coingecko.com/api/v3' //#region get currency @@ -118,7 +116,11 @@ export async function getCoinInfo(coinId: string) { export type Stat = [number, number] export async function getPriceStats(coinId: string, currencyId: string, days: number) { - const response = await fetch(`${BASE_URL}/coins/${coinId}/market_chart?vs_currency=${currencyId}&days=${days}`) + const params = new URLSearchParams() + params.append('vs_currency', currencyId) + params.append('days', String(days)) + + const response = await fetch(`${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 a82aaeac8d4..38b9e3d4bd2 100644 --- a/src/plugins/Trader/apis/coinmarketcap/index.ts +++ b/src/plugins/Trader/apis/coinmarketcap/index.ts @@ -77,7 +77,10 @@ export interface CoinInfo { } export async function getCoinInfo(id: string, currency: string) { - const response = await fetch(`${WIDGET_BASE_URL}/ticker/${id}/?ref=widget&convert=${currency}`) + const params = new URLSearchParams('ref=widget') + params.append('convert', currency) + + const response = await fetch(`${WIDGET_BASE_URL}/ticker/${id}/?${params.toString()}`) return response.json() as Promise<{ data: CoinInfo status: Status @@ -95,12 +98,15 @@ export async function getHistorical( endDate: Date, interval: string = '1d', ) { - const toUnixTimestamp = (d: Date) => Math.floor(d.getTime() / 1000) - const response = await fetch( - `${BASE_URL_v1_1}/cryptocurrency/quotes/historical?convert=${currency}&format=chart_crypto_details&id=${id}&interval=${interval}&time_end=${toUnixTimestamp( - endDate, - )}&time_start=${toUnixTimestamp(startDate)}`, - ) + const toUnixTimestamp = (d: Date) => String(Math.floor(d.getTime() / 1000)) + const params = new URLSearchParams('format=chart_crypto_details') + params.append('convert', currency) + params.append('id', id) + params.append('interval', interval) + 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()}`) return response.json() as Promise<{ data: Record> status: Status @@ -152,9 +158,13 @@ export interface Pair { } } export async function getLatestMarketPairs(id: string, currency: string) { - const response = await fetch( - `${BASE_URL_v1}/cryptocurrency/market-pairs/latest?aux=num_market_pairs,market_url,price_quote,effective_liquidity,market_score,market_reputation&convert=${currency}&id=${id}&limit=40&sort=cmc_rank&start=1`, + const params = new URLSearchParams( + 'aux=num_market_pairs,market_url,price_quote,effective_liquidity,market_score,market_reputation&limit=40&sort=cmc_rank&start=1', ) + params.append('convert', currency) + params.append('id', id) + + const response = await fetch(`${BASE_URL_v1}/cryptocurrency/market-pairs/latest?${params.toString()}`) return response.json() as Promise<{ data: { id: number From da8db3aae8f4e14384092a9bbbce94508809f896 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 4 Sep 2020 10:42:21 +0800 Subject: [PATCH 16/16] refactor: resolve reviews --- package.json | 2 +- src/_locales/en/messages.json | 18 +- src/_locales/ja/messages.json | 18 +- src/_locales/zh/messages.json | 18 +- .../AdditionalPostContent.tsx | 2 +- .../InjectedComponents/PostDummy.tsx | 43 ----- .../InjectedComponents/PostReplacer.tsx | 57 ++++++ .../TypedMessageRenderer.tsx | 2 +- .../DashboardRouters/Settings.tsx | 31 ++- src/plugins/Trader/UI/PriceChanged.tsx | 19 +- src/plugins/Trader/UI/PriceChangedTable.tsx | 10 +- src/plugins/Trader/UI/PriceChart.tsx | 115 +++-------- src/plugins/Trader/UI/ScreenModal.tsx | 5 - src/plugins/Trader/UI/SettingsDialog.tsx | 181 ------------------ src/plugins/Trader/UI/TickersTable.tsx | 22 ++- src/plugins/Trader/UI/TrendingPopper.tsx | 62 ++++-- src/plugins/Trader/UI/TrendingView.tsx | 77 ++++---- src/plugins/Trader/apis/coingecko/index.ts | 14 +- .../Trader/apis/coinmarketcap/index.ts | 17 +- src/plugins/Trader/apis/index.ts | 12 +- src/plugins/Trader/constants.ts | 21 ++ src/plugins/Trader/define.tsx | 13 +- src/plugins/Trader/graphs/useDimension.ts | 18 ++ .../Trader/graphs/usePriceLineChart.ts | 84 ++++++++ .../Trader/hooks/useCurrentCurrency.ts | 6 +- .../Trader/hooks/useCurrentPlatform.ts | 5 +- src/plugins/Trader/messages.ts | 8 +- .../messages/TypedMessageCashTrending.tsx | 42 ++-- src/plugins/Trader/settings.ts | 16 +- src/plugins/Trader/types.ts | 2 +- src/plugins/Wallet/formatter.ts | 4 +- src/plugins/plugin.ts | 10 +- src/protocols/typed-message/helpers.ts | 13 +- src/protocols/typed-message/types.ts | 2 +- src/settings/settings.ts | 12 +- .../facebook.com/UI/collectPosts.tsx | 2 +- .../facebook.com/UI/injectPostDummy.tsx | 3 - .../facebook.com/UI/injectPostReplacer.tsx | 3 + .../facebook.com/ui-provider.ts | 4 +- .../twitter.com/ui/fetch.ts | 11 +- .../twitter.com/ui/inject.tsx | 4 +- ...ctPostDummy.tsx => injectPostReplacer.tsx} | 6 +- .../twitter.com/utils/fetch.ts | 14 +- src/social-network/PostInfo.ts | 14 +- .../defaults/emptyDefinition.ts | 2 +- ...ctPostDummy.tsx => injectPostReplacer.tsx} | 24 +-- src/social-network/ui.ts | 10 +- src/utils/flags.ts | 1 + 48 files changed, 545 insertions(+), 534 deletions(-) delete mode 100644 src/components/InjectedComponents/PostDummy.tsx create mode 100644 src/components/InjectedComponents/PostReplacer.tsx delete mode 100644 src/plugins/Trader/UI/ScreenModal.tsx delete mode 100644 src/plugins/Trader/UI/SettingsDialog.tsx create mode 100644 src/plugins/Trader/graphs/useDimension.ts create mode 100644 src/plugins/Trader/graphs/usePriceLineChart.ts delete mode 100644 src/social-network-provider/facebook.com/UI/injectPostDummy.tsx create mode 100644 src/social-network-provider/facebook.com/UI/injectPostReplacer.tsx rename src/social-network-provider/twitter.com/ui/{injectPostDummy.tsx => injectPostReplacer.tsx} (73%) rename src/social-network/defaults/{injectPostDummy.tsx => injectPostReplacer.tsx} (61%) 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