diff --git a/.eslintrc.json b/.eslintrc.json index 51b08c417f4d..572a71cd1d7b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,7 +28,12 @@ { "name": "lodash", "message": "Please use lodash-unified instead." }, { "name": "lodash-es", "message": "Please use lodash-unified instead." }, { "name": "date-fns", "message": "Please use date-fns/{submodule} instead." }, - { "name": "date-fns/esm", "message": "Please use date-fns/{submodule} instead." } + { "name": "date-fns/esm", "message": "Please use date-fns/{submodule} instead." }, + { "name": "@masknet/typed-message/base", "message": "Please use @masknet/typed-message instead." }, + { + "name": "@dimensiondev/holoflows-kit/es", + "message": "Please use @dimensiondev/holoflows-kit instead." + } ] } ], diff --git a/jest.config.ts b/jest.config.ts index d089e951e89a..13f9a91e7ee6 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -24,6 +24,7 @@ const config: InitialOptionsTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], moduleNameMapper: { '@masknet/shared-base': '/packages/shared-base/src/index.ts', + '@masknet/typed-message': '/packages/typed-message/base/index.ts', 'jest-websocket-mock': '/packages/web3-shared/base/node_modules/jest-websocket-mock', 'reconnecting-websocket': '/packages/web3-shared/base/node_modules/reconnecting-websocket', 'date-fns/(.*)': '/packages/web3-shared/base/node_modules/date-fns/$1', diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index 11f3646509b6..9d32584cedf7 100644 --- a/packages/.eslintrc.json +++ b/packages/.eslintrc.json @@ -19,7 +19,12 @@ { "name": "date-fns", "message": "Please use date-fns/{submodule} instead." }, { "name": "date-fns/esm", "message": "Please use date-fns/{submodule} instead." }, { "name": "idb", "message": "Please use idb/with-async-ittr instead." }, - { "name": "idb/with-async-ittr-cjs", "message": "Please use idb/with-async-ittr instead." } + { "name": "idb/with-async-ittr-cjs", "message": "Please use idb/with-async-ittr instead." }, + { "name": "@masknet/typed-message/base", "message": "Please use @masknet/typed-message instead." }, + { + "name": "@dimensiondev/holoflows-kit/es", + "message": "Please use @dimensiondev/holoflows-kit instead." + } ] } ], diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 4a3de2bc83c8..868ca06bf859 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -7,6 +7,7 @@ "dependencies": { "@dimensiondev/stego-js": "0.11.1-20201027083223-8ab41be", "@masknet/shared-base": "workspace:*", + "@masknet/typed-message": "workspace:*", "@msgpack/msgpack": "^2.7.2" } } diff --git a/packages/encryption/src/encryption/Decryption.ts b/packages/encryption/src/encryption/Decryption.ts index e88eb168adb8..d4d9543830a4 100644 --- a/packages/encryption/src/encryption/Decryption.ts +++ b/packages/encryption/src/encryption/Decryption.ts @@ -1,11 +1,6 @@ import { unreachable } from '@dimensiondev/kit' -import { - decodeTypedMessageFromDocument, - decodeTypedMessageV38ToV40Format, - AESCryptoKey, - EC_Public_CryptoKey, - andThenAsync, -} from '@masknet/shared-base' +import { decodeTypedMessageFromDocument, decodeTypedMessageV38ToV40Format } from '@masknet/typed-message' +import { AESCryptoKey, EC_Public_CryptoKey, andThenAsync } from '@masknet/shared-base' import { None, Result } from 'ts-results' import { AESAlgorithmEnum, PayloadParseResult } from '../payload' import { decryptWithAES, importAESFromJWK } from '../utils' diff --git a/packages/encryption/src/encryption/DecryptionTypes.ts b/packages/encryption/src/encryption/DecryptionTypes.ts index 882ad19b8451..e7c955c99f44 100644 --- a/packages/encryption/src/encryption/DecryptionTypes.ts +++ b/packages/encryption/src/encryption/DecryptionTypes.ts @@ -1,4 +1,5 @@ -import type { TypedMessage, ProfileIdentifier, AESCryptoKey, EC_Public_CryptoKey } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' +import type { ProfileIdentifier, AESCryptoKey, EC_Public_CryptoKey } from '@masknet/shared-base' import type { PayloadParseResult } from '../payload' export interface DecryptOptions { diff --git a/packages/encryption/src/encryption/Encryption.ts b/packages/encryption/src/encryption/Encryption.ts index e7e1307085ed..2971412dd1c8 100644 --- a/packages/encryption/src/encryption/Encryption.ts +++ b/packages/encryption/src/encryption/Encryption.ts @@ -1,7 +1,7 @@ +import type { TypedMessage } from '@masknet/typed-message' import type { PersonaIdentifier, ProfileIdentifier, - TypedMessage, AESCryptoKey, PostIVIdentifier, EC_Public_CryptoKey, diff --git a/packages/encryption/tsconfig.json b/packages/encryption/tsconfig.json index 23e1c2c100ae..bd9d91161be8 100644 --- a/packages/encryption/tsconfig.json +++ b/packages/encryption/tsconfig.json @@ -6,5 +6,5 @@ "tsBuildInfoFile": "./dist/.tsbuildinfo" }, "include": ["./src/**/*"], - "references": [{ "path": "../shared-base" }] + "references": [{ "path": "../shared-base" }, { "path": "../typed-message/base" }] } diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index 00454c9c7169..7f0f81194fb4 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -93,6 +93,8 @@ export function createConfiguration(rawFlags: BuildFlags): Configuration { '@masknet/sdk': join(__dirname, '../../mask-sdk/server/'), '@masknet/backup-format': join(__dirname, '../../backup-format/src/'), '@masknet/encryption': join(__dirname, '../../encryption/src'), + '@masknet/typed-message/dom$': require.resolve('../../typed-message/dom/index.ts'), + '@masknet/typed-message$': require.resolve('../../typed-message/base/index.ts'), // @masknet/scripts: insert-here '@uniswap/v3-sdk': require.resolve('@uniswap/v3-sdk/dist/index.js'), } diff --git a/packages/mask/package.json b/packages/mask/package.json index 64082c20bf0e..e3a0a58d3bce 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -34,6 +34,7 @@ "@masknet/shared": "workspace:*", "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", + "@masknet/typed-message": "workspace:*", "@masknet/web3-contracts": "workspace:*", "@masknet/web3-providers": "workspace:*", "@masknet/web3-shared-base": "workspace:*", @@ -90,7 +91,6 @@ "jsbi": "3.1.4", "json-stable-stringify": "^1.0.1", "json2csv": "^5.0.6", - "jsx-jsonml-devtools-renderer": "^1.4.3", "millify": "^4.0.0", "next-tick": "^1.0.0", "opensea-js": "^1.2.7", diff --git a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx new file mode 100644 index 000000000000..10c4d5091c1e --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -0,0 +1,37 @@ +import { memo, useCallback } from 'react' +import { Typography, Link as MaterialLink } from '@mui/material' +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' +import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' + +export const Container = memo(function Container(props) { + return ( + + {props.children} + + ) +}) + +export const Link = memo(function Anchor(props: RenderFragmentsContextType.LinkProps) { + return +}) + +export function useTagEnhancer(kind: 'hash' | 'cash', content: string) { + const plugin = useActivatedPluginsSNSAdaptor(false) + .filter((x) => x.enhanceTag) + .at(0) + + const onClick: React.EventHandler> = useCallback( + (event) => { + plugin?.enhanceTag?.onClick?.(kind, content, event) + }, + [plugin], + ) + const onMouseEnter: React.EventHandler> = useCallback( + (event) => { + const cancel = plugin?.enhanceTag?.onHover?.(kind, content, event) + event.currentTarget.addEventListener('mouseleave', () => cancel?.(), { once: true }) + }, + [plugin], + ) + return { onClick, onMouseEnter, hasMatch: !!plugin } +} diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx new file mode 100644 index 000000000000..06cd69a1fb7b --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -0,0 +1,42 @@ +import { emptyTransformationContext, TransformationContext } from '@masknet/typed-message' +import { + RenderFragmentsContext, + type RenderFragmentsContextType, + MetadataRenderProps, + RegistryContext, + TransformerProvider, + TransformationContextProvider, +} from '@masknet/typed-message/dom' +import { TypedMessageRenderRegistry } from './registry' +import { useSubscription } from 'use-subscription' +import { useMemo } from 'react' +import { Container, Link } from './Components/Text' +import { TypedMessageTransformers } from './transformer' + +export interface TypedMessageRenderContextProps extends React.PropsWithChildren<{}> { + context?: TransformationContext + metadataRender?: React.ComponentType + renderFragments?: RenderFragmentsContextType +} + +export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) { + const registry = useSubscription(TypedMessageRenderRegistry.subscription) + const transformerFunction = useSubscription(TypedMessageTransformers.subscription) + const Provider = useMemo((): RenderFragmentsContextType => { + return { Container, Link, Metadata: props.metadataRender, ...props.renderFragments } + }, [props.metadataRender, props.renderFragments]) + + return ( + // basic render fragments provider: Text, Link, Image and Metadata + + {/* transformer pipeline */} + + {/* transformation context */} + + {/* components provider */} + {props.children} + + + + ) +} diff --git a/packages/mask/shared-ui/TypedMessageRender/registry.ts b/packages/mask/shared-ui/TypedMessageRender/registry.ts new file mode 100644 index 000000000000..63289e89f714 --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/registry.ts @@ -0,0 +1,2 @@ +import { createTypedMessageRenderRegistry } from '@masknet/typed-message/dom' +export const TypedMessageRenderRegistry = createTypedMessageRenderRegistry() diff --git a/packages/mask/shared-ui/TypedMessageRender/transformer.ts b/packages/mask/shared-ui/TypedMessageRender/transformer.ts new file mode 100644 index 000000000000..e1816712c018 --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/transformer.ts @@ -0,0 +1,44 @@ +import { + composeTransformers, + FlattenTypedMessage, + isTypedMessageAnchor, + makeTypedMessageAnchor, + ParseLinkTransformer, + visitEachTypedMessageChild, +} from '@masknet/typed-message' + +export enum Order { + Flatten = 1000, + ParseLink = 900, + // MaskPayload = 800, + /** @deprecated */ + PayloadReplacer = 700, + Plugins = 500, +} +/** + * Message transformation order: + * + * sns_parsed_message + * |> flatten + * |> parse links and tags (@user, #tag, $NAME) + * |> Mask Payload parser (installed in SNS Adaptor starts) + * |> plugins + */ +export const TypedMessageTransformers = composeTransformers() +export const TypedMessagePluginTransformers = composeTransformers() +TypedMessageTransformers.addTransformer(FlattenTypedMessage, Order.Flatten) +TypedMessageTransformers.addTransformer(ParseLinkTransformer, Order.ParseLink) +TypedMessageTransformers.addTransformer(TypedMessagePluginTransformers.subscription, Order.Plugins) + +// We will use MaskPayload transformer to replace it in the future PR. +const matcher = /^https?:\/\/mask(\.io|book\.com)/i +TypedMessageTransformers.addTransformer(function visitor(message, context) { + if ( + isTypedMessageAnchor(message) && + message.href && + (matcher.test(message.href) || matcher.test(message.content)) + ) { + return makeTypedMessageAnchor('normal', 'https://mask.io', 'Mask') + } + return visitEachTypedMessageChild(message, visitor, context) +}, Order.PayloadReplacer) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index e1a3d42418e9..94ca6c7f2b53 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -24,7 +24,6 @@ "contract": "Contract", "initializing": "Initializing...", "redirect_alert": "If your browser does not redirect, please click here.", - "typed_message_can_not_be_rendered": "The TypedMessage has a circular structure so it can't be rendered on the screen.", "typed_message_text_alert": "Only TypedMessageText is supported currently.", "badge_renderer_provided_by_plugin": "Provided by plugin", "add_to_key_store_success": "The public key has been added to the Keystore", @@ -60,8 +59,6 @@ "create": "Create", "copied": "Copied", "daily": "Daily", - "unknown": "Unknown", - "message": "Message", "dashboard_tab_collectibles": "Collectibles", "dashboard_no_collectible_found": "No collectible found.", "dashboard_collectible_menu_all": "All ({{count}})", @@ -130,7 +127,6 @@ "post_dialog__title": "Mask: Compose", "post_dialog_enable_paste_auto": "Enable auto paste", "post_modal_hint__button": "Compose encrypted post", - "post_substitute_label": "Mask", "hide": "Hide", "reset": "Reset", "editor": "Editor", @@ -160,8 +156,6 @@ "settings_language": "Language", "settings_language_secondary": "Select the language you would like to use.", "settings_choose_eth_network": "Choose Ethereum Network", - "settings_post_replacement": "Substitute Post", - "settings_post_replacement_desc": "All posts on the web-pages will be rendered by Mask, including those not created by Mask.", "settings_launch_page": "Launch Page", "settings_launch_page_secondary": "Select the default page to open when launch the app", "speed_up": "Speed up", diff --git a/packages/mask/shared-ui/locales/ja-JP.json b/packages/mask/shared-ui/locales/ja-JP.json index b817b9b6e181..761cfb08fae0 100644 --- a/packages/mask/shared-ui/locales/ja-JP.json +++ b/packages/mask/shared-ui/locales/ja-JP.json @@ -81,8 +81,6 @@ "settings_language": "言語を設定", "settings_language_secondary": "ご利用になりたい言語をお選びください。", "settings_choose_eth_network": "Ethereum ネットワークを選択してください", - "settings_post_replacement": "代替ポスト", - "settings_post_replacement_desc": "Web ページ上のすべての投稿は、Mask で作成されていない投稿を含め、Mask によってレンダリングされます。", "settings_launch_page": "初期画面", "settings_launch_page_secondary": "アプリを開いたときに最初に開かれる画面を選んでください。", "skip": "スキップ", diff --git a/packages/mask/shared-ui/locales/ko-KR.json b/packages/mask/shared-ui/locales/ko-KR.json index c3c6345e49df..4e05eb84a169 100644 --- a/packages/mask/shared-ui/locales/ko-KR.json +++ b/packages/mask/shared-ui/locales/ko-KR.json @@ -24,7 +24,6 @@ "contract": "컨트랙트", "initializing": "초기화 중...", "redirect_alert": "브라우저가 리다이렉트하지 않으면 여기 클릭", - "typed_message_can_not_be_rendered": "The TypedMessage has a circular structure so it can't be rendered on the screen.", "typed_message_text_alert": "현재 TypedMessageText만 지원합니다.", "badge_renderer_provided_by_plugin": "플러그인으로 제공", "add_to_key_store_success": "해당 공개 키가 이미 키스토어에서 추가되었습니다", @@ -58,8 +57,6 @@ "create": "만들기", "copied": "복사됨", "daily": "일일", - "unknown": "알 수 없음", - "message": "메시지", "dashboard_tab_collectibles": "수집품", "dashboard_no_collectible_found": "수집품이 없습니다.", "days": "{{days}} 일마다", @@ -114,7 +111,6 @@ "post_dialog__title": "Mask: 편집", "post_dialog_enable_paste_auto": "자동 붙이기 지원", "post_modal_hint__button": "암호화된 메시지를 편집하기", - "post_substitute_label": "Mask", "hide": "숨기기", "reset": "초기화", "editor": "에디터", @@ -144,8 +140,6 @@ "settings_language": "언어", "settings_language_secondary": "이용하고 싶은 언어를 선택하세요.", "settings_choose_eth_network": "이더리움 네트워크를 선택하기", - "settings_post_replacement": "대체 게시물", - "settings_post_replacement_desc": "Mask가 이 웹페이지에 모든 게시물을 렌더링합니다. Mask로 만들지 않는 것도 포함됩니다.", "settings_launch_page": "시작 페이지", "settings_launch_page_secondary": "앱을 열을 때 디폴트 페이지를 선택하세요.", "speed_up": "가속하기", diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index 8b013ddfe44f..ce7ea216a408 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -24,7 +24,6 @@ "contract": "crwdns10109:0crwdne10109:0", "initializing": "crwdns10111:0crwdne10111:0", "redirect_alert": "crwdns10113:0crwdne10113:0", - "typed_message_can_not_be_rendered": "crwdns10115:0crwdne10115:0", "typed_message_text_alert": "crwdns10117:0crwdne10117:0", "badge_renderer_provided_by_plugin": "crwdns10119:0crwdne10119:0", "add_to_key_store_success": "crwdns3981:0crwdne3981:0", @@ -60,8 +59,6 @@ "create": "crwdns4047:0crwdne4047:0", "copied": "crwdns9361:0crwdne9361:0", "daily": "crwdns4057:0crwdne4057:0", - "unknown": "crwdns10121:0crwdne10121:0", - "message": "crwdns10123:0crwdne10123:0", "dashboard_tab_collectibles": "crwdns4063:0crwdne4063:0", "dashboard_no_collectible_found": "crwdns4105:0crwdne4105:0", "dashboard_collectible_menu_all": "crwdns14532:0{{count}}crwdne14532:0", @@ -130,7 +127,6 @@ "post_dialog__title": "crwdns4275:0crwdne4275:0", "post_dialog_enable_paste_auto": "crwdns10129:0crwdne10129:0", "post_modal_hint__button": "crwdns4277:0crwdne4277:0", - "post_substitute_label": "crwdns4279:0crwdne4279:0", "hide": "crwdns4293:0crwdne4293:0", "reset": "crwdns4301:0crwdne4301:0", "editor": "crwdns10131:0crwdne10131:0", @@ -160,8 +156,6 @@ "settings_language": "crwdns4441:0crwdne4441:0", "settings_language_secondary": "crwdns4443:0crwdne4443:0", "settings_choose_eth_network": "crwdns4445:0crwdne4445:0", - "settings_post_replacement": "crwdns4447:0crwdne4447:0", - "settings_post_replacement_desc": "crwdns4449:0crwdne4449:0", "settings_launch_page": "crwdns4457:0crwdne4457:0", "settings_launch_page_secondary": "crwdns4459:0crwdne4459:0", "speed_up": "crwdns9393:0crwdne9393:0", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index b16ef816b557..d01e81b66a4d 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -24,7 +24,6 @@ "contract": "合约", "initializing": "初始化中...", "redirect_alert": "如果您的浏览器没有重新跳转,请 点击这里。", - "typed_message_can_not_be_rendered": "输入的消息存在循环结构,因此无法在屏幕上呈现。", "typed_message_text_alert": "当前只支持输入消息文本。", "badge_renderer_provided_by_plugin": "插件提供:", "add_to_key_store_success": "公钥已添加到密钥库", @@ -60,8 +59,6 @@ "create": "创建", "copied": "已复制", "daily": "每日", - "unknown": "未知", - "message": "信息", "dashboard_tab_collectibles": "收藏品", "dashboard_no_collectible_found": "未找到任何收藏品", "days": "每隔 {{days}} 天", @@ -128,7 +125,6 @@ "post_dialog__title": "Mask:编辑", "post_dialog_enable_paste_auto": "启用自动粘贴", "post_modal_hint__button": "编辑加密贴文", - "post_substitute_label": "Mask", "hide": "隐藏", "reset": "重置", "editor": "编辑器", @@ -158,8 +154,6 @@ "settings_language": "语言", "settings_language_secondary": "选择您要使用的语言.", "settings_choose_eth_network": "选择以太坊网络", - "settings_post_replacement": "替换贴文", - "settings_post_replacement_desc": "所有网页上的贴文将由Mask提供,包括不是由Mask创建的贴文。", "settings_launch_page": "启动页面", "settings_launch_page_secondary": "选择启动应用程序时要打开的默认页面", "speed_up": "加速", diff --git a/packages/mask/shared-ui/locales/zh-TW.json b/packages/mask/shared-ui/locales/zh-TW.json index 1eab22e93219..18a4776aea59 100644 --- a/packages/mask/shared-ui/locales/zh-TW.json +++ b/packages/mask/shared-ui/locales/zh-TW.json @@ -45,7 +45,6 @@ "connecting": "正在連接…", "create": "建立", "copied": "已複製", - "message": "訊息", "dashboard_tab_collectibles": "收藏", "dashboard_no_collectible_found": "找不到收藏品", "decrypted_postbox_add_recipients": "新增收件人", @@ -114,8 +113,6 @@ "settings_language": "語言", "settings_language_secondary": "選擇你想使用的語言。", "settings_choose_eth_network": "選擇乙太坊網路", - "settings_post_replacement": "取代貼文", - "settings_post_replacement_desc": "網頁上的所有貼文都將通過 Mask 呈現,包括那些不是由 Mask 建立的貼文。", "settings_launch_page": "啟動頁面", "settings_launch_page_secondary": "選擇啟動程式時要打開的預設頁面", "speed_up": "加速", diff --git a/packages/mask/shared-ui/tsconfig.json b/packages/mask/shared-ui/tsconfig.json index 1160c7b38640..286bed574598 100644 --- a/packages/mask/shared-ui/tsconfig.json +++ b/packages/mask/shared-ui/tsconfig.json @@ -8,5 +8,13 @@ "lib": ["ES2021"] }, "include": ["./", "./**/*.json"], - "references": [{ "path": "../../shared-base" }, { "path": "../../shared/" }] + "references": [ + { "path": "../utils-pure" }, + { "path": "../../shared-base" }, + { "path": "../../shared/" }, + { "path": "../../typed-message/base" }, + { "path": "../../typed-message/dom" }, + { "path": "../../theme" }, + { "path": "../../plugin-infra" } + ] } diff --git a/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx b/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx index cb1cdabca5b5..d5be53533851 100644 --- a/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx +++ b/packages/mask/src/components/CompositionDialog/BadgeRenderer.tsx @@ -1,6 +1,6 @@ import { useActivatedPluginsSNSAdaptor, Plugin, usePluginI18NField } from '@masknet/plugin-infra' import { makeStyles, ShadowRootTooltip } from '@masknet/theme' -import type { TypedMessage } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import { Box, Chip } from '@mui/material' import { useI18N } from '../../utils' diff --git a/packages/mask/src/components/CompositionDialog/CompositionUI.tsx b/packages/mask/src/components/CompositionDialog/CompositionUI.tsx index 83cb81f519c3..7cb00b1b1bc9 100644 --- a/packages/mask/src/components/CompositionDialog/CompositionUI.tsx +++ b/packages/mask/src/components/CompositionDialog/CompositionUI.tsx @@ -1,7 +1,7 @@ import { forwardRef, useImperativeHandle, useMemo, useRef, useState, startTransition, useCallback } from 'react' import { Typography, Chip, Button } from '@mui/material' import { LoadingButton } from '@mui/lab' -import type { TypedMessage } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import { useValueRef } from '@masknet/shared' import { makeStyles } from '@masknet/theme' import { ImagePayloadIcon } from '@masknet/icons' diff --git a/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx b/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx index 801716b65111..dced75e264e9 100644 --- a/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx +++ b/packages/mask/src/components/CompositionDialog/TypedMessageEditor.tsx @@ -4,7 +4,7 @@ import { makeTypedMessageText, TypedMessage, editTypedMessageMeta, -} from '@masknet/shared-base' +} from '@masknet/typed-message' import { makeStyles } from '@masknet/theme' import { InputBase, Alert, Button } from '@mui/material' import { useCallback, useImperativeHandle, useState, useRef, forwardRef, memo } from 'react' diff --git a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx index 1706c80ee31b..426cbf2eb80b 100644 --- a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -1,21 +1,21 @@ import { Typography, Card, Box, CircularProgress, CircularProgressProps } from '@mui/material' -import { makeStyles, useStylesExtends } from '@masknet/theme' +import { makeStyles } from '@masknet/theme' import classNames from 'classnames' -import { TypedMessage, makeTypedMessageText } from '@masknet/shared-base' -import { TypedMessageRendererProps, DefaultTypedMessageRenderer } from './TypedMessageRenderer' +import { TypedMessage, makeTypedMessageText } from '@masknet/typed-message' +import { TextResizeContext, TypedMessageRender } from '@masknet/typed-message/dom' +import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context' import CheckIcon from '@mui/icons-material/Check' import CloseIcon from '@mui/icons-material/Close' import green from '@mui/material/colors/green' import { MaskIcon } from '../../resources/MaskIcon' import { memo, useCallback, useMemo } from 'react' +import { activatedSocialNetworkUI } from '../../social-network/ui' export enum AdditionalIcon { check = 'check', error = 'error', } -export interface AdditionalContentProps - extends withClasses, - Omit, 'message'> { +export interface AdditionalContentProps { title: string titleIcon?: keyof typeof AdditionalIcon headerActions?: React.ReactNode @@ -34,7 +34,7 @@ const useStyles = makeStyles()((theme) => ({ })) export const AdditionalContent = memo(function AdditionalContent(props: AdditionalContentProps): JSX.Element { - const classes = useStylesExtends(useStyles(), props) + const { classes } = useStyles() const stop = useCallback((ev: React.MouseEvent) => ev.stopPropagation(), []) const { progress, title, message } = props const ProgressJSX = !progress ? null : progress === true ? ( @@ -76,7 +76,14 @@ export const AdditionalContent = memo(function AdditionalContent(props: Addition
{header}
{message ? (
- + + + + +
) : null} diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx index 3dee957c9a9d..89501b91242f 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -1,5 +1,6 @@ import { Fragment, useEffect, useMemo, useReducer } from 'react' -import { makeTypedMessageTuple, TypedMessageTuple, type ProfileIdentifier, type Payload } from '@masknet/shared-base' +import { makeTypedMessageTuple, TypedMessageTuple } from '@masknet/typed-message' +import type { ProfileIdentifier, Payload } from '@masknet/shared-base' import { or } from '@masknet/theme' import { ServicesWithProgress } from '../../../extension/service' diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx index 8183157a3f42..ddb8df70f05e 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx @@ -5,35 +5,11 @@ import { useShareMenu } from '../SelectPeopleDialog' import { makeStyles, useStylesExtends } from '@masknet/theme' import { Link } from '@mui/material' import type { Profile } from '../../../database' -import { extractTextFromTypedMessage } from '@masknet/shared-base' -import type { TypedMessage, ProfileIdentifier } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' +import type { ProfileIdentifier } from '@masknet/shared-base' import { wrapAuthorDifferentMessage } from './authorDifferentMessage' -import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -import type { MetadataRendererProps } from '../TypedMessageRenderer' -import { - useDisabledPluginSuggestionFromMeta, - useDisabledPluginSuggestionFromPost, - PossiblePluginSuggestionUI, -} from '../DisabledPluginSuggestion' -import { MaskPostExtraPluginWrapper } from '../../../plugins/MaskPluginWrapper' +import { DecryptedUI_PluginRendererWithSuggestion } from '../DecryptedPostMetadataRender' -const PluginRenderer = createInjectHooksRenderer( - useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, - (x) => x.DecryptedInspector, - MaskPostExtraPluginWrapper, -) -function PluginRendererWithSuggestion(props: MetadataRendererProps) { - const a = useDisabledPluginSuggestionFromMeta(props.metadata || new Map()) - const b = useDisabledPluginSuggestionFromPost(extractTextFromTypedMessage(props.message), []) - - const suggest = Array.from(new Set(a.concat(b))) - return ( - <> - - - - ) -} export interface DecryptPostSuccessProps extends withClasses { data: { content: TypedMessage } requestAppendRecipients?(to: Profile[]): Promise @@ -78,11 +54,11 @@ export const DecryptPostSuccess = memo(function DecryptPostSuccess(props: Decryp <> {shareMenu.ShareMenu} + ) }) diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx new file mode 100644 index 000000000000..66a5914878b1 --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx @@ -0,0 +1,27 @@ +import { createInjectHooksRenderer, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' +import type { MetadataRenderProps } from '@masknet/typed-message/dom' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { + PossiblePluginSuggestionUI, + useDisabledPluginSuggestionFromMeta, + useDisabledPluginSuggestionFromPost, +} from './DisabledPluginSuggestion' +import { MaskPostExtraPluginWrapper } from '../../plugins/MaskPluginWrapper' + +const Decrypted = createInjectHooksRenderer( + useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, + (x) => x.DecryptedInspector, + MaskPostExtraPluginWrapper, +) +export function DecryptedUI_PluginRendererWithSuggestion(props: MetadataRenderProps) { + const a = useDisabledPluginSuggestionFromMeta(props.metadata) + const b = useDisabledPluginSuggestionFromPost(extractTextFromTypedMessage(props.message), []) + + const suggest = Array.from(new Set(a.concat(b))) + return ( + <> + + + + ) +} diff --git a/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx b/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx index a57c0b413ac8..dafa9d1aa00e 100644 --- a/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx +++ b/packages/mask/src/components/InjectedComponents/DisabledPluginSuggestion.tsx @@ -6,7 +6,7 @@ import { Plugin, usePluginI18NField, } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' import { Switch } from '@mui/material' import Services from '../../extension/service' import MaskPostExtraInfoWrapper from '../../plugins/MaskPluginWrapper' @@ -33,8 +33,9 @@ export function useDisabledPluginSuggestionFromPost(postContent: Option, return matches } -export function useDisabledPluginSuggestionFromMeta(meta: ReadonlyMap) { +export function useDisabledPluginSuggestionFromMeta(meta: undefined | ReadonlyMap) { const disabled = useDisabledPlugins().filter((x) => x.contribution?.metadataKeys) + if (!meta) return [] const keys = [...meta.keys()] const matches = disabled.filter((x) => { diff --git a/packages/mask/src/components/InjectedComponents/PayloadReplacer.tsx b/packages/mask/src/components/InjectedComponents/PayloadReplacer.tsx deleted file mode 100644 index a963f5e8b300..000000000000 --- a/packages/mask/src/components/InjectedComponents/PayloadReplacer.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Typography, Link } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { useI18N } from '../../utils' -const useStyle = makeStyles()((theme) => ({ - text: { - fontSize: 'inherit', - marginLeft: theme.spacing(0.5), - marginRight: theme.spacing(0.5), - }, -})) - -export interface PayloadReplacerProps { - payload: string -} - -export function PayloadReplacer({ payload }: PayloadReplacerProps) { - const { t } = useI18N() - const { classes } = useStyle() - return ( - - - {t('post_substitute_label')} - - - ) -} diff --git a/packages/mask/src/components/InjectedComponents/PostComments.tsx b/packages/mask/src/components/InjectedComponents/PostComments.tsx index c6168fad225e..cf9b049449d4 100644 --- a/packages/mask/src/components/InjectedComponents/PostComments.tsx +++ b/packages/mask/src/components/InjectedComponents/PostComments.tsx @@ -7,7 +7,7 @@ import Lock from '@mui/icons-material/Lock' import { useEffect } from 'react' import { useAsync } from 'react-use' import Services from '../../extension/service' -import { extractTextFromTypedMessage } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' import { usePostInfoDetails } from '../DataSource/usePostInfo' const useStyle = makeStyles()({ diff --git a/packages/mask/src/components/InjectedComponents/PostInspector.tsx b/packages/mask/src/components/InjectedComponents/PostInspector.tsx index 32ccfc6e9f82..cd44921ee838 100644 --- a/packages/mask/src/components/InjectedComponents/PostInspector.tsx +++ b/packages/mask/src/components/InjectedComponents/PostInspector.tsx @@ -3,12 +3,8 @@ import { useAsync } from 'react-use' import { DecryptPost } from './DecryptedPost/DecryptedPost' import { AddToKeyStore } from './AddToKeyStore' import Services from '../../extension/service' -import { - ProfileIdentifier, - type TypedMessageTuple, - type PayloadAlpha40_Or_Alpha39, - type PayloadAlpha38, -} from '@masknet/shared-base' +import { ProfileIdentifier, type PayloadAlpha40_Or_Alpha39, type PayloadAlpha38 } from '@masknet/shared-base' +import type { TypedMessageTuple } from '@masknet/typed-message' import type { Profile } from '../../database' import { useCurrentIdentity, useFriendsList } from '../DataSource/useActivatedUI' import { useValueRef } from '@masknet/shared' diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index 2d3145494b9a..0308ca2a2fb6 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,17 +1,20 @@ -import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -import { useValueRef } from '@masknet/shared' import { - isTypedMessagePromise, - isTypedMessageTuple, - isWellKnownTypedMessages, - makeTypedMessageTuple, -} from '@masknet/shared-base' + type TransformationContext, + type TypedMessage, + isTypedMessageEqual, + emptyTransformationContext, + FlattenTypedMessage, + forEachTypedMessageChild, + isTypedMessageAnchor, +} from '@masknet/typed-message' +import { TextResizeContext, TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' import { makeStyles } from '@masknet/theme' import { useEffect, useMemo } from 'react' -import { Result } from 'ts-results' -import { allPostReplacementSettings } from '../../settings/settings' import { usePostInfoDetails } from '../DataSource/usePostInfo' -import { DefaultTypedMessageRenderer } from './TypedMessageRenderer' +import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context' +import { useCurrentIdentity } from '../DataSource/useActivatedUI' +import { activatedSocialNetworkUI } from '../../social-network/ui' + const useStyles = makeStyles()({ root: { overflowWrap: 'break-word', @@ -26,42 +29,63 @@ export interface PostReplacerProps { export function PostReplacer(props: PostReplacerProps) { const { classes } = useStyles() const postMessage = usePostInfoDetails.rawMessage() - const postPayload = usePostInfoDetails.containingMaskPayload() - const allPostReplacement = useValueRef(allPostReplacementSettings) - const plugins = useActivatedPluginsSNSAdaptor(false) - const processedPostMessage = useMemo( - () => - plugins.reduce((x, plugin) => { - const result = Result.wrap(() => plugin.typedMessageTransformer?.(x) ?? x).unwrapOr(x) - if (isTypedMessageTuple(result)) return result - console.warn( - '[TypedMessage] typedMessageTransformer that return a non TypedMessageTuple is not supported yet. This transform is ignored', - result, - ) - return x - }, postMessage), - [plugins.map((x) => x.ID).join(), postMessage], - ) - const shouldReplacePost = - // replace all posts - allPostReplacement || - // replace posts which enhanced by plugins - processedPostMessage.items.some((x) => !isWellKnownTypedMessages(x)) || - // replace posts which encrypted by Mask - postPayload.ok + const author = usePostInfoDetails.author() + const currentProfile = useCurrentIdentity()?.identifier + const url = usePostInfoDetails.url() - // zip/unzip original post - useEffect(() => { - if (shouldReplacePost) props.zip?.() - else props.unzip?.() - }, [shouldReplacePost]) + const initialTransformationContext = useMemo((): TransformationContext => { + return { + authorHint: author, + currentProfile, + postURL: url?.toString(), + } + }, [author, currentProfile, url]) - return shouldReplacePost ? ( + return ( - !isTypedMessagePromise(x)))} - /> + + + + + - ) : null + ) +} + +function Transformer({ message, unzip, zip }: { message: TypedMessage } & PostReplacerProps) { + const after = useTransformedValue(message) + + const shouldReplace = useMemo(() => { + const flatten = FlattenTypedMessage(message, emptyTransformationContext) + if (!isTypedMessageEqual(flatten, after)) return true + if (hasCashOrHashTag(after)) return true + return false + }, [message, after]) + + useEffect(() => { + if (shouldReplace) zip?.() + else unzip?.() + + return () => unzip?.() + }, []) + + if (shouldReplace) return + return null +} +function hasCashOrHashTag(message: TypedMessage): boolean { + let result = false + function visitor(node: TypedMessage): 'stop' | void { + if (isTypedMessageAnchor(node)) { + if (node.category === 'cash' || node.category === 'hash') { + result = true + return 'stop' + } + } else forEachTypedMessageChild(node, visitor) + } + visitor(message) + forEachTypedMessageChild(message, visitor) + return result } diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx index 81801277e8c4..b2096114bb72 100644 --- a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx +++ b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx @@ -8,13 +8,8 @@ import { useI18N, MaskMessages } from '../../utils' import { activatedSocialNetworkUI, SocialNetworkUI } from '../../social-network' import { currentSetupGuideStatus, dismissPinExtensionTip, userGuideStatus } from '../../settings/settings' import type { SetupGuideCrossContextStatus } from '../../settings/types' -import { - PersonaIdentifier, - ProfileIdentifier, - Identifier, - ECKeyIdentifier, - makeTypedMessageText, -} from '@masknet/shared-base' +import { PersonaIdentifier, ProfileIdentifier, Identifier, ECKeyIdentifier } from '@masknet/shared-base' +import { makeTypedMessageText } from '@masknet/typed-message' import Services from '../../extension/service' import { useLastRecognizedIdentity } from '../DataSource/useActivatedUI' import { MaskIcon } from '../../resources/MaskIcon' diff --git a/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx b/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx deleted file mode 100644 index 9980572d87b9..000000000000 --- a/packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { Fragment, memo, createElement } from 'react' -import { Typography, Link } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import anchorme from 'anchorme' -import classNames from 'classnames' -import { - TypedMessage, - TypedMessageText, - TypedMessageAnchor, - TypedMessageImage, - TypedMessageTuple, - TypedMessageUnknown, - TypedMessagePromise, - TypedMessageEmpty, - makeTypedMessageText, -} from '@masknet/shared-base' -import { Image } from '../shared/Image' -import { useAsync } from 'react-use' -import { getRendererOfTypedMessage, registerTypedMessageRenderer } from '../../protocols/typed-message' -import { deconstructPayload } from '../../utils/type-transform/Payload' -import { PayloadReplacer } from './PayloadReplacer' -import { useI18N } from '../../utils' - -export interface MetadataRendererProps { - metadata: TypedMessage['meta'] - message: TypedMessage -} -export interface TypedMessageRendererProps { - /** - * The TypedMessage - */ - message: T - allowTextEnlarge?: boolean - afterPreviousMetadata?: React.ReactNode - beforeLatterMetadata?: React.ReactNode - metadataRenderer?: { - before?: React.ComponentType - after?: React.ComponentType - } -} - -export const DefaultTypedMessageRenderer = memo(function DefaultTypedMessageRenderer( - props: TypedMessageRendererProps, -) { - const Renderer = getRendererOfTypedMessage(props.message)[0]?.component || DefaultTypedMessageUnknownRenderer - return -}) - -export const DefaultTypedMessageTextRenderer = memo(function DefaultTypedMessageTextRenderer( - props: TypedMessageRendererProps, -) { - const { content } = props.message - const deconstructed = deconstructPayload(content, null) - return renderWithMetadata( - props, - - {deconstructed.ok ? ( - - ) : ( - - )} - , - ) -}) -registerTypedMessageRenderer('text', { - component: DefaultTypedMessageTextRenderer, - id: 'mask.text', - priority: 0, -}) - -export const DefaultTypedMessageAnchorRenderer = memo(function DefaultTypedMessageAnchorRenderer( - props: TypedMessageRendererProps, -) { - const { content, href } = props.message - const deconstructed = deconstructPayload(content, null) - return renderWithMetadata( - props, - - {deconstructed.ok ? ( - - ) : ( - // TODO: shrink link size - - {content} - - )} - , - ) -}) -registerTypedMessageRenderer('x-anchor', { - component: DefaultTypedMessageAnchorRenderer, - id: 'mask.anchor', - priority: 0, -}) - -export const DefaultTypedMessageImageRenderer = memo(function DefaultTypedMessageImageRenderer( - props: TypedMessageRendererProps, -) { - const { image, width, height } = props.message - return renderWithMetadata( - props, - - - , - ) -}) -registerTypedMessageRenderer('image', { - component: DefaultTypedMessageImageRenderer, - id: 'mask.image', - priority: 0, -}) - -export const DefaultTypedMessageTupleRenderer = memo(function DefaultTypedMessageTupleRenderer( - props: TypedMessageRendererProps, -) { - const { t } = useI18N() - try { - JSON.stringify(props.message.items) - } catch (error) { - if (error instanceof Error && error.message.includes('circular structure')) { - return {t('typed_message_can_not_be_rendered')} - } - } - return ( - <> - {props.message.items.map((x, index) => ( - - ))} - - ) -}) -registerTypedMessageRenderer('tuple', { - component: DefaultTypedMessageTupleRenderer, - id: 'mask.compound', - priority: 0, -}) - -export const DefaultTypedMessageEmptyRenderer = memo(function DefaultTypedMessageEmptyRenderer( - props: TypedMessageRendererProps, -) { - return renderWithMetadata(props, null) -}) -registerTypedMessageRenderer('empty', { - component: DefaultTypedMessageEmptyRenderer, - id: 'mask.empty', - priority: 0, -}) - -export const DefaultTypedMessageUnknownRenderer = memo(function DefaultTypedMessageUnknownRenderer( - props: TypedMessageRendererProps, -) { - const { t } = useI18N() - - return renderWithMetadata( - props, - - {t('unknown')} {t('message')} - , - ) -}) -registerTypedMessageRenderer('unknown', { - component: DefaultTypedMessageUnknownRenderer, - id: 'mask.unknown', - priority: 0, -}) - -export const DefaultTypedMessageSuspendedRenderer = memo(function DefaultTypedMessageSuspendedRenderer( - props: TypedMessageRendererProps, -) { - const { promise } = props.message - const { loading, error, value } = useAsync(() => promise, [promise]) - - return renderWithMetadata( - props, - loading ? ( - - ) : error ? ( - - ) : ( - - ), - ) -}) -registerTypedMessageRenderer('promise', { - component: DefaultTypedMessageSuspendedRenderer, - id: 'mask.suspended', - priority: 0, -}) - -function DefaultMetadataRender() { - return null -} - -function renderWithMetadata(props: TypedMessageRendererProps, jsx: React.ReactNode) { - const Before = props.metadataRenderer?.before || DefaultMetadataRender - const After = props.metadataRenderer?.after || DefaultMetadataRender - return ( - <> - - {props.afterPreviousMetadata} - {jsx} - {props.beforeLatterMetadata} - - - ) -} - -const RenderText = memo(function RenderText(props: { text: string; allowTextEnlarge: boolean }) { - return createElement(Fragment, {}, ...parseText(props.text, props.allowTextEnlarge)) -}) - -interface ParseTextProps { - text: string - fontSize: number -} - -interface ParseTextLinkProps extends ParseTextProps { - link: string -} - -const useStyle = makeStyles()((theme, fontSize) => ({ - link: { - color: theme.palette.primary.main, - }, - text: { - fontSize: Number(fontSize) + 'rem', - }, -})) - -const ParseTextLink = memo(function ParseTextLink({ link, text, fontSize }: ParseTextLinkProps) { - const { classes } = useStyle(fontSize) - return ( - - {text} - - ) -}) - -const ParseText = memo(function ParseText({ text, fontSize }: ParseTextProps) { - const { classes } = useStyle(fontSize) - return {text} -}) - -function parseText(string: string, allowTextEnlarge: boolean) { - const links = anchorme.list(string) - let current = string - const fontSize = - allowTextEnlarge && Array.from(current).length < 45 - ? 1.5 - : allowTextEnlarge && Array.from(current).length < 85 - ? 1.2 - : 1 - - const result = [] - while (current.length) { - const search1 = current.indexOf('\n') - const search2 = links[0] ? current.indexOf(links[0].string) : -1 - // ? if rest is normal - if (search1 === -1 && search2 === -1) { - result.push() - break - } - // ? if rest have \n but no links - if ((search1 < search2 && search1 !== -1) || search2 === -1) { - result.push(,
) - current = current.substring(search1 + 1) - } - // ? if rest have links but no \n - if ((search2 < search1 && search2 !== -1) || search1 === -1) { - let link = links[0].string - if (!links[0].protocol) link = 'http://' + link - result.push( - , - , - ) - current = current.substring(search2 + links[0].string.length) - links.shift() - } - } - return result -} diff --git a/packages/mask/src/components/shared/DebugMetadataInspector.tsx b/packages/mask/src/components/shared/DebugMetadataInspector.tsx index fb7425c0b84f..c9fe24d2e9f1 100644 --- a/packages/mask/src/components/shared/DebugMetadataInspector.tsx +++ b/packages/mask/src/components/shared/DebugMetadataInspector.tsx @@ -14,7 +14,7 @@ import { } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { InjectedDialog } from './InjectedDialog' -import { isDataMatchJSONSchema, getKnownMetadataKeys, getMetadataSchema } from '@masknet/shared-base' +import { isDataMatchJSONSchema, getKnownMetadataKeys, getMetadataSchema } from '@masknet/typed-message' import { ShadowRootPopper } from '@masknet/theme' import { useState } from 'react' import { useI18N } from '../../utils' diff --git a/packages/mask/src/crypto/crypto-alpha-38.ts b/packages/mask/src/crypto/crypto-alpha-38.ts index d690293aea41..ff4554f2fd6d 100644 --- a/packages/mask/src/crypto/crypto-alpha-38.ts +++ b/packages/mask/src/crypto/crypto-alpha-38.ts @@ -1,5 +1,6 @@ /* eslint @dimensiondev/unicode-specific-set: ["error", { "only": "code" }] */ -import { TypedMessage, makeTypedMessageText, isTypedMessageText, type AESJsonWebKey } from '@masknet/shared-base' +import { TypedMessage, makeTypedMessageText, isTypedMessageText } from '@masknet/typed-message' +import type { AESJsonWebKey } from '@masknet/shared-base' export * from './crypto-alpha-39' // @ts-ignore diff --git a/packages/mask/src/crypto/crypto-alpha-40.ts b/packages/mask/src/crypto/crypto-alpha-40.ts index 475e0ecdec93..b9183f0bf5da 100644 --- a/packages/mask/src/crypto/crypto-alpha-40.ts +++ b/packages/mask/src/crypto/crypto-alpha-40.ts @@ -13,7 +13,8 @@ import { } from '@dimensiondev/kit' import { i18n } from '../../shared-ui/locales_legacy' import { CryptoWorker } from '../modules/workers' -import { EC_Private_JsonWebKey, EC_Public_JsonWebKey, AESJsonWebKey, makeTypedMessageText } from '@masknet/shared-base' +import type { EC_Private_JsonWebKey, EC_Public_JsonWebKey, AESJsonWebKey } from '@masknet/shared-base' +import { makeTypedMessageText } from '@masknet/typed-message' import { derive_AES_GCM_256_Key_From_PBKDF2, derive_AES_GCM_256_Key_From_ECDH_256k1_Keys, diff --git a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts index 12308a80d8cb..585ed0f49f97 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/decryptFrom.ts @@ -14,7 +14,8 @@ import { publicSharedAESKey } from '../../../crypto/crypto-alpha-38' import { DecryptFailedReason } from '../../../utils/constants' import { asyncIteratorWithResult, memorizeAsyncGenerator } from '../../../utils/type-transform/asyncIteratorHelpers' import { DecryptStaticECDH_PostKey, steganographyDecodeImageUrl } from '@masknet/encryption' -import type { TypedMessage, AESJsonWebKey, Payload } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' +import type { AESJsonWebKey, Payload } from '@masknet/shared-base' import stringify from 'json-stable-stringify' import { MaskMessages } from '../../../utils/messages' import { Err, Ok, Result } from 'ts-results' diff --git a/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts b/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts index 2b0c03da19e6..baee77a242fa 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/encryptTo.ts @@ -3,15 +3,8 @@ import { publishPostAESKey_version39Or38 } from '../../../../background/network/ import { decodeArrayBuffer, encodeArrayBuffer } from '@dimensiondev/kit' import { constructAlpha38 } from '../../../utils/type-transform/Payload' import { queryPrivateKey, queryLocalKey, queryProfile } from '../../../database' -import { - ProfileIdentifier, - PostIVIdentifier, - isTypedMessageText, - TypedMessage, - TypedMessageText, - PayloadLatest, - compressSecp256k1Key, -} from '@masknet/shared-base' +import { ProfileIdentifier, PostIVIdentifier, PayloadLatest, compressSecp256k1Key } from '@masknet/shared-base' +import { isTypedMessageText, TypedMessage, TypedMessageText } from '@masknet/typed-message' import { prepareRecipientDetail } from './prepareRecipientDetail' import { getNetworkWorker } from '../../../social-network/worker' import { createPostDB, PostRecord } from '../../../../background/database/post' diff --git a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx index 8b3e0ecef75b..aa1c69861973 100644 --- a/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/ArtBlocks/SNSAdaptor/index.tsx @@ -3,7 +3,7 @@ import { uniq } from 'lodash-unified' import { checkUrl, getAssetInfoFromURL, getRelevantUrl } from '../utils' import { base } from '../base' -import { getTypedMessageContent } from '../../../protocols/typed-message' +import { extractTextFromTypedMessage } from '@masknet/typed-message' import { Collectible } from './Collectible' import type { ChainId } from '@masknet/web3-shared-evm' import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' @@ -19,7 +19,7 @@ const sns: Plugin.SNSAdaptor.Definition = { return asset ? : null }, DecryptedInspector: function Component(props) { - const collectibleUrl = getRelevantUrl(getTypedMessageContent(props.message)) + const collectibleUrl = getRelevantUrl(extractTextFromTypedMessage(props.message).unwrapOr('')) const asset = getAssetInfoFromURL(collectibleUrl) return asset ? : null }, diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx index 5fc6612f35c9..0081e9e71e44 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx @@ -4,7 +4,7 @@ import { PostInspector } from './PostInspector' import { base } from '../base' import { checkUrl, getAssetInfoFromURL, getRelevantUrl } from '../utils' import { PLUGIN_ID } from '../constants' -import { getTypedMessageContent } from '../../../protocols/typed-message' +import { extractTextFromTypedMessage } from '@masknet/typed-message' import { NFTPage } from './NFTPage' import { AddressName, AddressNameType } from '@masknet/web3-shared-evm' @@ -19,7 +19,9 @@ const sns: Plugin.SNSAdaptor.Definition = { return asset ? : null }, DecryptedInspector: function Component(props) { - const collectibleUrl = getRelevantUrl(getTypedMessageContent(props.message)) + const collectibleUrl = getRelevantUrl( + extractTextFromTypedMessage(props.message, { linkAsText: true }).unwrapOr(''), + ) const asset = getAssetInfoFromURL(collectibleUrl) usePluginWrapper(!!asset) return asset ? : null diff --git a/packages/mask/src/plugins/Collectible/helpers.ts b/packages/mask/src/plugins/Collectible/helpers.ts index 45bb0d869b7f..8c59ab041000 100644 --- a/packages/mask/src/plugins/Collectible/helpers.ts +++ b/packages/mask/src/plugins/Collectible/helpers.ts @@ -1,5 +1,5 @@ import type { Asset, WyvernSchemaName } from 'opensea-js/lib/types' -import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/shared-base' +import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/typed-message' import { PLUGIN_META_KEY } from './constants' import type { CollectibleJSON_Payload, CollectibleToken } from './types' import schema from './schema.json' diff --git a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/index.tsx b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/index.tsx index 77d47b296fad..91ee0df8821e 100644 --- a/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/CryptoartAI/SNSAdaptor/index.tsx @@ -3,7 +3,7 @@ import { Plugin, usePostInfoDetails, usePluginWrapper } from '@masknet/plugin-in import { PostInspector } from './PostInspector' import { base } from '../base' import { checkUrl, getAssetInfoFromURL, getRelevantUrl } from '../utils' -import { getTypedMessageContent } from '../../../protocols/typed-message' +import { extractTextFromTypedMessage } from '@masknet/typed-message' const sns: Plugin.SNSAdaptor.Definition = { ...base, @@ -16,7 +16,9 @@ const sns: Plugin.SNSAdaptor.Definition = { return asset ? : null }, DecryptedInspector: function Component(props) { - const collectibleUrl = getRelevantUrl(getTypedMessageContent(props.message)) + const collectibleUrl = getRelevantUrl( + extractTextFromTypedMessage(props.message, { linkAsText: true }).unwrapOr(''), + ) const asset = getAssetInfoFromURL(collectibleUrl) usePluginWrapper(!!asset) return asset ? : null diff --git a/packages/mask/src/plugins/External/SNSAdaptor/index.tsx b/packages/mask/src/plugins/External/SNSAdaptor/index.tsx index ddc825389186..8b2cadea8ec4 100644 --- a/packages/mask/src/plugins/External/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/External/SNSAdaptor/index.tsx @@ -7,7 +7,7 @@ import { ThirdPartyPluginCompositionEntry } from '../components/CompositionEntry import { ExternalPluginMessages } from '../messages' import { isLocalContext } from '../sns-context' import { MaskMessages } from '../../../utils' -import { makeTypedMessageText } from '@masknet/shared-base' +import { makeTypedMessageText } from '@masknet/typed-message' const sns: Plugin.SNSAdaptor.Definition = { ...base, diff --git a/packages/mask/src/plugins/External/messages.ts b/packages/mask/src/plugins/External/messages.ts index 650bc6c25a79..2dc94908ffe9 100644 --- a/packages/mask/src/plugins/External/messages.ts +++ b/packages/mask/src/plugins/External/messages.ts @@ -1,5 +1,6 @@ import { createPluginMessage } from '@masknet/plugin-infra' -import { TypedMessage, serializer } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' +import { serializer } from '@masknet/shared-base' import type { MaskSDK_SNS_ContextIdentifier } from './sns-context' // Make TS happy diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx index 3e854c076bb2..93b229a4f97e 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx @@ -3,7 +3,8 @@ import { useMemo, Suspense } from 'react' import { Skeleton } from '@mui/material' import { makeStyles } from '@masknet/theme' import { type Plugin, usePostInfoDetails, usePluginWrapper } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { PostInspector } from './PostInspector' const useStyles = makeStyles()((theme) => { diff --git a/packages/mask/src/plugins/Furucombo/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Furucombo/SNSAdaptor/index.tsx index 500bc0944c02..1c7c198ce206 100644 --- a/packages/mask/src/plugins/Furucombo/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Furucombo/SNSAdaptor/index.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react' import { Plugin, usePluginWrapper, usePostInfoDetails } from '@masknet/plugin-infra' import { base } from '../base' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { FurucomboView } from '../UI/FurucomboView' import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' diff --git a/packages/mask/src/plugins/Gitcoin/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Gitcoin/SNSAdaptor/index.tsx index 9f08826461d5..cf5cf52711a0 100644 --- a/packages/mask/src/plugins/Gitcoin/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Gitcoin/SNSAdaptor/index.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react' import { ChainId } from '@masknet/web3-shared-evm' import { NetworkPluginID, Plugin, useChainId, usePluginWrapper } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { usePostInfoDetails } from '../../../components/DataSource/usePostInfo' import { PreviewCard } from './PreviewCard' import { base } from '../base' diff --git a/packages/mask/src/plugins/GoodGhosting/SNSAdaptor/index.tsx b/packages/mask/src/plugins/GoodGhosting/SNSAdaptor/index.tsx index c490ee7945af..13d7750ee42c 100644 --- a/packages/mask/src/plugins/GoodGhosting/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/GoodGhosting/SNSAdaptor/index.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { Plugin, usePluginWrapper } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { usePostInfoDetails } from '../../../components/DataSource/usePostInfo' import { PreviewCard } from '../UI/PreviewCard' import { ChainId } from '@masknet/web3-shared-evm' diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/helpers.ts b/packages/mask/src/plugins/ITO/SNSAdaptor/helpers.ts index 172c80e7bfb2..b940d93d807d 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/helpers.ts +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/helpers.ts @@ -2,7 +2,7 @@ import { omit } from 'lodash-unified' import type BigNumber from 'bignumber.js' import type { Result } from 'ts-results' import { ChainId, isNativeTokenAddress } from '@masknet/web3-shared-evm' -import { createRenderWithMetadata, createTypedMessageMetadataReader, type TypedMessage } from '@masknet/shared-base' +import { createRenderWithMetadata, createTypedMessageMetadataReader, type TypedMessage } from '@masknet/typed-message' import { ITO_MetaKey_1, ITO_MetaKey_2 } from '../constants' import type { JSON_PayloadInMask, JSON_PayloadOutMask } from '../types' import schemaV1 from '../schema-v1.json' diff --git a/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx b/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx index 3e31f89f3edc..7fa3108da7bb 100644 --- a/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/MaskBox/SNSAdaptor/index.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react' import { Plugin, usePluginWrapper, usePostInfoDetails } from '@masknet/plugin-infra' import { base } from '../base' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' import { PreviewCard } from './components/PreviewCard' import { Context } from '../hooks/useContext' diff --git a/packages/mask/src/plugins/Polls/SNSAdaptor/PollsInPost.tsx b/packages/mask/src/plugins/Polls/SNSAdaptor/PollsInPost.tsx index 0d7b57543361..7248eebe3949 100644 --- a/packages/mask/src/plugins/Polls/SNSAdaptor/PollsInPost.tsx +++ b/packages/mask/src/plugins/Polls/SNSAdaptor/PollsInPost.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import type { TypedMessage } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import { PluginPollRPC } from '../messages' import { renderWithPollMetadata, PollMetadataReader } from '../helpers' import type { PollGunDB } from '../Services' diff --git a/packages/mask/src/plugins/Polls/helpers.ts b/packages/mask/src/plugins/Polls/helpers.ts index 9a2e2d4c98d2..bef623d9e62b 100644 --- a/packages/mask/src/plugins/Polls/helpers.ts +++ b/packages/mask/src/plugins/Polls/helpers.ts @@ -1,4 +1,4 @@ -import { createTypedMessageMetadataReader, createRenderWithMetadata } from '@masknet/shared-base' +import { createTypedMessageMetadataReader, createRenderWithMetadata } from '@masknet/typed-message' import type { PollMetaData } from './types' import { PLUGIN_META_KEY } from './constants' import schema from './schema.json' diff --git a/packages/mask/src/plugins/PoolTogether/SNSAdaptor/index.tsx b/packages/mask/src/plugins/PoolTogether/SNSAdaptor/index.tsx index c642cb0d0ccd..78666abfe10e 100644 --- a/packages/mask/src/plugins/PoolTogether/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/PoolTogether/SNSAdaptor/index.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { Plugin, usePluginWrapper, usePostInfoDetails } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { ChainId } from '@masknet/web3-shared-evm' import { base } from '../base' import { DepositDialog } from '../UI/DepositDialog' diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/helpers.ts b/packages/mask/src/plugins/RedPacket/SNSAdaptor/helpers.ts index 1bb748a2330c..6b6992ad9d87 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/helpers.ts +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/helpers.ts @@ -1,4 +1,4 @@ -import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/shared-base' +import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/typed-message' import { RedPacketMetaKey, RedPacketNftMetaKey } from '../constants' import type { RedPacketJSONPayload, RedPacketNftJSONPayload } from '../types' import schema from '../schema.json' diff --git a/packages/mask/src/plugins/Snapshot/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Snapshot/SNSAdaptor/index.tsx index a01dc5eaea71..d49f3c680bf9 100644 --- a/packages/mask/src/plugins/Snapshot/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Snapshot/SNSAdaptor/index.tsx @@ -5,7 +5,8 @@ import { Skeleton } from '@mui/material' import { makeStyles } from '@masknet/theme' import { PostInspector } from './PostInspector' import { usePostInfoDetails } from '../../../components/DataSource/usePostInfo' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' const useStyles = makeStyles()((theme) => { return { diff --git a/packages/mask/src/plugins/Trader/Dashboard/index.tsx b/packages/mask/src/plugins/Trader/Dashboard/index.tsx index 5e0c1dbbb391..e021aaa09f44 100644 --- a/packages/mask/src/plugins/Trader/Dashboard/index.tsx +++ b/packages/mask/src/plugins/Trader/Dashboard/index.tsx @@ -1,11 +1,9 @@ import type { Plugin } from '@masknet/plugin-infra' import { base } from '../base' -import { baseDeferred } from '../base-deferred' import { TraderDialog } from '../SNSAdaptor/trader/TraderDialog' const dashboard: Plugin.Dashboard.Definition = { ...base, - ...baseDeferred, init(signal) {}, GlobalInjection: function Component() { return ( diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/cashTag.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/cashTag.tsx new file mode 100644 index 000000000000..83ba0f48be8d --- /dev/null +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/cashTag.tsx @@ -0,0 +1,23 @@ +import { PluginTraderMessages, PluginTraderRPC } from '../messages' +import { TagType } from '../types' +import type { Plugin } from '@masknet/plugin-infra' + +export const enhanceTag: Plugin.SNSAdaptor.Definition['enhanceTag'] = { + onHover(kind, content, event) { + const element = event.currentTarget + const timer = setTimeout(async () => { + const type = kind === 'cash' ? TagType.CASH : TagType.HASH + const dataProviders = await PluginTraderRPC.getAvailableDataProviders(type, content) + if (!dataProviders.length) return + PluginTraderMessages.cashTagObserved.sendToLocal({ + name: content, + type, + element, + dataProviders, + }) + }, 500) + return () => { + clearTimeout(timer) + } + }, +} diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/index.tsx index cc581ccd87d2..29c369c34569 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/index.tsx @@ -1,13 +1,12 @@ import type { Plugin } from '@masknet/plugin-infra' import { base } from '../base' -import { baseDeferred } from '../base-deferred' import { TraderDialog } from './trader/TraderDialog' import { SearchResultInspector } from './trending/SearchResultInspector' import { TagInspector } from './trending/TagInspector' +import { enhanceTag } from './cashTag' const sns: Plugin.SNSAdaptor.Definition = { ...base, - ...baseDeferred, init(signal) {}, SearchResultBox: SearchResultInspector, GlobalInjection: function Component() { @@ -18,6 +17,7 @@ const sns: Plugin.SNSAdaptor.Definition = { ) }, + enhanceTag, } export default sns diff --git a/packages/mask/src/plugins/Trader/Worker/index.ts b/packages/mask/src/plugins/Trader/Worker/index.ts index 34b445ef211d..824d930b57e1 100644 --- a/packages/mask/src/plugins/Trader/Worker/index.ts +++ b/packages/mask/src/plugins/Trader/Worker/index.ts @@ -1,11 +1,9 @@ import type { Plugin } from '@masknet/plugin-infra' import { base } from '../base' -import { baseDeferred } from '../base-deferred' import '../messages' const worker: Plugin.Worker.Definition = { ...base, - ...baseDeferred, init(signal) {}, } export default worker diff --git a/packages/mask/src/plugins/Trader/base-deferred.ts b/packages/mask/src/plugins/Trader/base-deferred.ts deleted file mode 100644 index c7e987101372..000000000000 --- a/packages/mask/src/plugins/Trader/base-deferred.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Plugin } from '@masknet/plugin-infra' -import type { TypedMessage, TypedMessageTuple } from '@masknet/shared-base' -import { isCashTagMessage, makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' - -export const baseDeferred: Plugin.Shared.Utilities = { - typedMessageTransformer(message) { - return { - ...message, - items: message.items.map((m: TypedMessage) => (isCashTagMessage(m) ? makeTypedMessageCashTrending(m) : m)), - } as TypedMessageTuple - }, -} diff --git a/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx deleted file mode 100644 index f3fdff25fda2..000000000000 --- a/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useState } from 'react' -import { useChainId } from '@masknet/web3-shared-evm' -import { Link, Typography } from '@mui/material' -import { TypedMessageAnchor, TypedMessage, isTypedMessageAnchor } from '@masknet/shared-base' -import type { TypedMessageRendererProps } from '../../../components/InjectedComponents/TypedMessageRenderer' -import { PluginTraderMessages, PluginTraderRPC } from '../messages' -import { TagType } from '../types' -import { registerTypedMessageRenderer } from '../../../protocols/typed-message' - -export interface TypedMessageCashTrending extends Omit { - readonly type: 'x-cash-trending' - readonly name: string -} - -export const isCashTagMessage = (m: TypedMessage): m is TypedMessageAnchor => - isTypedMessageAnchor(m) && ['cash', 'hash'].includes(m.category) && !/#\w+lbp$/i.test(m.content) -export function makeTypedMessageCashTrending(message: TypedMessageAnchor) { - return { - ...message, - type: 'x-cash-trending', - name: message.content.substr(1).toLowerCase(), - } as TypedMessageCashTrending -} - -registerTypedMessageRenderer('x-cash-trending', { - component: DefaultTypedMessageCashTrendingRenderer, - id: 'com.maskbook.trader.x-cash-trending', - priority: 0, -}) - -function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { - const chainId = useChainId() - const [openTimer, setOpenTimer] = useState | null>(null) - const onMouseOver = (ev: React.MouseEvent) => { - // cache for async operations - const element = ev.currentTarget - if (openTimer !== null) clearTimeout(openTimer) - setOpenTimer( - setTimeout(async () => { - if (props.message.category !== 'cash' && props.message.category !== 'hash') return - const { name, category } = props.message - const type = category === 'cash' ? TagType.CASH : TagType.HASH - const dataProviders = await PluginTraderRPC.getAvailableDataProviders(type, name) - const tradeProviders = await PluginTraderRPC.getAvailableTraderProviders(chainId) - if (!dataProviders.length) return - PluginTraderMessages.cashTagObserved.sendToLocal({ - name, - type, - element, - dataProviders, - }) - }, 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/packages/mask/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolInPost.tsx b/packages/mask/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolInPost.tsx index 55e5d8f836e4..d7af53613db4 100644 --- a/packages/mask/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolInPost.tsx +++ b/packages/mask/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolInPost.tsx @@ -1,4 +1,4 @@ -import type { TypedMessage } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import { Button, Typography } from '@mui/material' import React, { useEffect, useState } from 'react' import { useI18N } from '../../../utils' diff --git a/packages/mask/src/plugins/UnlockProtocol/helpers.ts b/packages/mask/src/plugins/UnlockProtocol/helpers.ts index 3cf88ca1d894..9ed47de37d44 100644 --- a/packages/mask/src/plugins/UnlockProtocol/helpers.ts +++ b/packages/mask/src/plugins/UnlockProtocol/helpers.ts @@ -1,4 +1,4 @@ -import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/shared-base' +import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/typed-message' import { pluginMetaKey } from './constants' import type { UnlockProtocolMetadata } from './types' import schema from './schema.json' diff --git a/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx b/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx index 44ce0c781396..7bc8e95ed740 100644 --- a/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/dHEDGE/SNSAdaptor/index.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { Plugin, usePostInfoDetails, usePluginWrapper } from '@masknet/plugin-infra' -import { extractTextFromTypedMessage, parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' +import { parseURL } from '@masknet/shared-base' import { base } from '../base' import { PoolView } from '../UI/PoolView' import { InvestDialog } from '../UI/InvestDialog' diff --git a/packages/mask/src/protocols/typed-message/debugger.tsx b/packages/mask/src/protocols/typed-message/debugger.tsx deleted file mode 100644 index 1c637d225506..000000000000 --- a/packages/mask/src/protocols/typed-message/debugger.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-ignore -const React = require('jsx-jsonml-devtools-renderer') as any -import { - TypedMessage, - TypedMessageTuple, - TypedMessageImage, - TypedMessageText, - isTypedMessageTuple, - isTypedMessageText, - isTypedMessageImage, -} from '@masknet/shared-base' - -class TypedMessageFormatter { - isTypedMessage(obj: unknown): obj is TypedMessage { - if (typeof obj !== 'object' || obj === null) return false - if (!('version' in obj)) return false - if (!('meta' in obj)) return false - if (!('type' in obj)) return false - return true - } - hasBody(obj: unknown) { - if (!this.isTypedMessage(obj)) return false - if (obj.type === 'empty') return false - return true - } - compound(obj: TypedMessageTuple) { - return ( -
-
    - {obj.items.map((x, i) => ( -
  1. {display(x)}
  2. - ))} -
-
- ) - } - fields(obj: TypedMessage) { - return ( - - - - - - {Object.keys(obj) - .filter((x) => x !== 'type' && x !== 'meta' && x !== 'version') - .map((x, i) => ( - - - {display((obj as any)[x])} - - ))} -
FieldValue
{x}
- ) - } - text(obj: TypedMessageText) { - return {obj.content} - } - image(obj: TypedMessageImage) { - if (typeof obj.image === 'string') - return - return this.fields(obj) - } - body(obj: TypedMessage) { - if (isTypedMessageTuple(obj)) return this.compound(obj) - if (isTypedMessageText(obj)) return this.text(obj) - if (isTypedMessageImage(obj)) return this.image(obj) - return this.fields(obj) - } - header(obj: unknown) { - if (!this.isTypedMessage(obj)) return null - return ( -
- TypedMessage({obj.type}) {(obj.meta?.size || 0) > 0 ? <>(with meta {display(obj.meta)}) : ''} -
- ) - } -} -export function enhanceTypedMessageDebugger() { - React.installCustomObjectFormatter(new TypedMessageFormatter()) -} -function display(obj: unknown) { - switch (typeof obj) { - case 'string': - return obj - default: - // @ts-ignore - return - } -} diff --git a/packages/mask/src/protocols/typed-message/index.ts b/packages/mask/src/protocols/typed-message/index.ts deleted file mode 100644 index dcf8cbd66ff4..000000000000 --- a/packages/mask/src/protocols/typed-message/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types' -export * from './render-registry' diff --git a/packages/mask/src/protocols/typed-message/render-registry.tsx b/packages/mask/src/protocols/typed-message/render-registry.tsx deleted file mode 100644 index 277296a4f171..000000000000 --- a/packages/mask/src/protocols/typed-message/render-registry.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { TypedMessage } from '@masknet/shared-base' -import type { TypedMessageRendererProps } from '../../components/InjectedComponents/TypedMessageRenderer' -type Renderer = React.ComponentType> - -interface RendererConfig { - component: Renderer - priority: number - id: string -} -const registry = new Map>>() -// TODO: before metadata, after metadata, ... -export function registerTypedMessageRenderer(matching: T['type'], config: RendererConfig) { - const set = registry.get(matching) || new Set() - // No overwrite in production - if (process.env.NODE_ENV === 'development') { - for (const item of set) - if (item.id === config.id) { - set.delete(item) - break - } - } - set.add(config) - registry.set(matching, set) -} -// TODO: add settings for "selected" renderer -export function getRendererOfTypedMessage(message: T): RendererConfig[] { - const list = [...(registry.get(message.type) || []), ...(registry.get('*') || [])].sort( - (x, y) => x.priority - y.priority, - ) - return list as any -} diff --git a/packages/mask/src/protocols/typed-message/types.ts b/packages/mask/src/protocols/typed-message/types.ts deleted file mode 100644 index b1a618abc607..000000000000 --- a/packages/mask/src/protocols/typed-message/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isTypedMessageAnchor, isTypedMessageText, TypedMessage } from '@masknet/shared-base' - -export function getTypedMessageContent(message: TypedMessage): string { - if (isTypedMessageText(message)) return message.content - if (isTypedMessageAnchor(message)) return message.href - return '' -} diff --git a/packages/mask/src/settings/settings.ts b/packages/mask/src/settings/settings.ts index 7f62d51f97b8..ae35a9857532 100644 --- a/packages/mask/src/settings/settings.ts +++ b/packages/mask/src/settings/settings.ts @@ -14,14 +14,6 @@ export const debugModeSetting = createGlobalSettings('debugMode', false secondary: () => i18n.t('settings_enable_debug_desc'), }) -/** - * Whether if create substitute post for all posts - */ -export const allPostReplacementSettings = createGlobalSettings('post replacement all', false, { - primary: () => i18n.t('settings_post_replacement'), - secondary: () => i18n.t('settings_post_replacement_desc'), -}) - // #region appearance export const appearanceSettings = createGlobalSettings('appearance', Appearance.default, { primary: () => i18n.t('settings_appearance'), diff --git a/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts index bf623963d3cf..2959000832eb 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/facebook.com/automation/openComposeBox.ts @@ -1,7 +1,7 @@ import { LiveSelector } from '@dimensiondev/holoflows-kit' import { MaskMessages, CompositionRequest } from '../../../utils/messages' import { i18n } from '../../../../shared-ui/locales_legacy' -import { makeTypedMessageText, TypedMessage } from '@masknet/shared-base' +import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' import { delay, waitDocumentReadyState } from '@dimensiondev/kit' const nativeComposeButtonSelector = () => diff --git a/packages/mask/src/social-network-adaptor/facebook.com/collecting/posts.tsx b/packages/mask/src/social-network-adaptor/facebook.com/collecting/posts.tsx index 98f7d681ad29..efab2e3ed248 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/collecting/posts.tsx +++ b/packages/mask/src/social-network-adaptor/facebook.com/collecting/posts.tsx @@ -3,7 +3,12 @@ import type { SocialNetworkUI as Next } from '../../../social-network/types' import { creator } from '../../../social-network/utils' import { isMobileFacebook } from '../utils/isMobile' import { getProfileIdentifierAtFacebook } from '../utils/getProfileIdentifier' -import { TypedMessage, makeTypedMessageText, makeTypedMessageImage, makeTypedMessageTuple } from '@masknet/shared-base' +import { + TypedMessage, + makeTypedMessageText, + makeTypedMessageImage, + makeTypedMessageTuple, +} from '@masknet/typed-message' import { clickSeeMore } from '../injection/PostInspector' import { startWatch } from '../../../utils/watcher' import { facebookShared } from '../shared' diff --git a/packages/mask/src/social-network-adaptor/facebook.com/customization/render-fragments.tsx b/packages/mask/src/social-network-adaptor/facebook.com/customization/render-fragments.tsx new file mode 100644 index 000000000000..c20a0e2b5133 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/facebook.com/customization/render-fragments.tsx @@ -0,0 +1,17 @@ +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' +import { memo } from 'react' +import { Link } from '@mui/material' +import { useTagEnhancer } from '../../../../shared-ui/TypedMessageRender/Components/Text' + +function Hash(props: RenderFragmentsContextType.HashLinkProps | RenderFragmentsContextType.CashLinkProps) { + const text = props.children.slice(1) + const target = `/hashtag/${encodeURIComponent(text)}` + const { hasMatch, ...events } = useTagEnhancer('hash', text) + return +} +export const FacebookRenderFragments: RenderFragmentsContextType = { + // AtLink: not supported + HashLink: memo(Hash), + // Facebook has no native cashtag support. Treat it has a hash. + CashLink: memo(Hash), +} diff --git a/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts b/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts index ee64a7ce474e..a05d6721148a 100644 --- a/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts +++ b/packages/mask/src/social-network-adaptor/facebook.com/ui-provider.ts @@ -29,6 +29,7 @@ import { injectProfileNFTAvatarInFaceBook } from './injection/NFT/ProfileNFTAvat import { injectNFTAvatarInFacebook } from './injection/NFT/NFTAvatarInFacebook' import { injectUserNFTAvatarAtFacebook } from './injection/NFT/NFTAvatarInTimeline' import { injectOpenNFTAvatarEditProfileButton } from './injection/NFT/NFTAvatarEditProfile' +import { FacebookRenderFragments } from './customization/render-fragments' const useInjectedDialogClassesOverwriteFacebook = makeStyles()((theme) => { const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)` @@ -142,6 +143,7 @@ const facebookUI: SocialNetworkUI.Definition = { InjectedDialog: { classes: useInjectedDialogClassesOverwriteFacebook, }, + RenderFragments: FacebookRenderFragments, }, useTheme: useThemeFacebookVariant, }, diff --git a/packages/mask/src/social-network-adaptor/instagram.com/collecting/posts.ts b/packages/mask/src/social-network-adaptor/instagram.com/collecting/posts.ts index adbe00ad9db8..27e51a4f6a96 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/collecting/posts.ts +++ b/packages/mask/src/social-network-adaptor/instagram.com/collecting/posts.ts @@ -1,7 +1,8 @@ import { DOMProxy, LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-kit' import type { SocialNetworkUI } from '../../../social-network/types' import { creator } from '../../../social-network/utils' -import { TypedMessage, makeTypedMessageImage, makeTypedMessageTuple, ProfileIdentifier } from '@masknet/shared-base' +import { TypedMessage, makeTypedMessageImage, makeTypedMessageTuple } from '@masknet/typed-message' +import { ProfileIdentifier } from '@masknet/shared-base' import { startWatch } from '../../../utils/watcher' import { instagramBase } from '../base' import { createRefsForCreatePostContext } from '../../../social-network/utils/create-post-context' diff --git a/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts index d912933678b5..cc58f93bfb84 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/automation/openComposeBox.ts @@ -1,5 +1,5 @@ +import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' import { delay, waitDocumentReadyState } from '@dimensiondev/kit' -import { makeTypedMessageText, TypedMessage } from '@masknet/shared-base' import { i18n } from '../../../../shared-ui/locales_legacy' import { MaskMessages, CompositionRequest } from '../../../utils/messages' import { composeButtonSelector, composeDialogIndicatorSelector, composeTextareaSelector } from '../utils/selector' diff --git a/packages/mask/src/social-network-adaptor/minds.com/collecting/post.ts b/packages/mask/src/social-network-adaptor/minds.com/collecting/post.ts index f21b4f9b2253..5d556050ae71 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/collecting/post.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/collecting/post.ts @@ -4,9 +4,9 @@ import { makeTypedMessagePromise, makeTypedMessageTuple, makeTypedMessageTupleFromList, - ProfileIdentifier, makeTypedMessageImage, -} from '@masknet/shared-base' +} from '@masknet/typed-message' +import { ProfileIdentifier } from '@masknet/shared-base' import type { SocialNetworkUI as Next } from '../../../social-network/types' import { creator } from '../../../social-network/utils' import { createRefsForCreatePostContext } from '../../../social-network/utils/create-post-context' diff --git a/packages/mask/src/social-network-adaptor/minds.com/utils/fetch.ts b/packages/mask/src/social-network-adaptor/minds.com/utils/fetch.ts index f9a31fc56401..00e8dc736a46 100644 --- a/packages/mask/src/social-network-adaptor/minds.com/utils/fetch.ts +++ b/packages/mask/src/social-network-adaptor/minds.com/utils/fetch.ts @@ -7,7 +7,7 @@ import { makeTypedMessageText, TypedMessage, TypedMessageText, -} from '@masknet/shared-base' +} from '@masknet/typed-message' import { assertNonNull } from '@dimensiondev/kit' const parseNameArea = (nameArea: HTMLAnchorElement) => { diff --git a/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts b/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts index c95866501c7a..2d9ccb7cb236 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/automation/openComposeBox.ts @@ -1,5 +1,5 @@ import { MaskMessages, CompositionRequest } from '../../../utils/messages' -import { makeTypedMessageText, TypedMessage } from '@masknet/shared-base' +import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' export function openComposeBoxTwitter(content: string | TypedMessage, options?: CompositionRequest['options']) { MaskMessages.events.requestComposition.sendToLocal({ diff --git a/packages/mask/src/social-network-adaptor/twitter.com/collecting/post.ts b/packages/mask/src/social-network-adaptor/twitter.com/collecting/post.ts index c183b8737419..93b773497197 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/collecting/post.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/collecting/post.ts @@ -7,14 +7,14 @@ import { postIdParser, postParser, postImagesParser } from '../utils/fetch' import { memoize, noop } from 'lodash-unified' import Services from '../../../extension/service' import { injectMaskIconToPostTwitter } from '../injection/MaskIcon' +import { ProfileIdentifier } from '@masknet/shared-base' import { - ProfileIdentifier, makeTypedMessageImage, makeTypedMessageTupleFromList, makeTypedMessageEmpty, makeTypedMessagePromise, makeTypedMessageTuple, -} from '@masknet/shared-base' +} from '@masknet/typed-message' import { untilElementAvailable } from '../../../utils/dom' import { twitterBase } from '../base' import { twitterShared } from '../shared' diff --git a/packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx b/packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx new file mode 100644 index 000000000000..ecfff4710ec9 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx @@ -0,0 +1,21 @@ +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' +import { memo } from 'react' +import { Link } from '@mui/material' +import { useTagEnhancer } from '../../../../shared-ui/TypedMessageRender/Components/Text' +export const TwitterRenderFragments: RenderFragmentsContextType = { + AtLink: memo(function (props) { + const target = '/' + props.children.slice(1) + return + }), + HashLink: memo(function (props) { + const text = props.children.slice(1) + const target = `/hashtag/${encodeURIComponent(text)}?src=hashtag_click` + const { hasMatch, ...events } = useTagEnhancer('hash', text) + return + }), + CashLink: memo(function (props) { + const target = `/search?q=${encodeURIComponent(props.children)}&src=cashtag_click` + const { hasMatch, ...events } = useTagEnhancer('cash', props.children.slice(1)) + return + }), +} diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/PostDialogHint.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/PostDialogHint.tsx index 532be145b54c..c4b5efbb4f9a 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/PostDialogHint.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/PostDialogHint.tsx @@ -10,7 +10,7 @@ import { makeStyles, MaskColorVar } from '@masknet/theme' import { alpha } from '@mui/material' import { twitterBase } from '../base' import { sayHelloShowed } from '../../../settings/settings' -import { makeTypedMessageText } from '@masknet/shared-base' +import { makeTypedMessageText } from '@masknet/typed-message' import { useI18N } from '../../../utils' const useStyles = makeStyles()((theme) => ({ diff --git a/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts b/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts index 0e225f50731f..d14dee9b94c2 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts @@ -36,6 +36,7 @@ import { injectOpenNFTAvatarEditProfileButton } from './injection/NFT/NFTAvatarE import { injectUserNFTAvatarAtTweet } from './injection/NFT/TweetNFTAvatar' import { injectNFTContractAtTwitter } from './injection/NFT/NFTContract' import { injectNFTAvatarClipInTwitter } from './injection/NFT/NFTAvatarClip' +import { TwitterRenderFragments } from './customization/render-fragments' const useInjectedDialogClassesOverwriteTwitter = makeStyles()((theme) => { const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)` @@ -137,6 +138,7 @@ const twitterUI: SocialNetworkUI.Definition = { InjectedDialog: { classes: useInjectedDialogClassesOverwriteTwitter, }, + RenderFragments: TwitterRenderFragments, }, useTheme: useThemeTwitterVariant, i18nOverwrite: i18NOverwriteTwitter, diff --git a/packages/mask/src/social-network-adaptor/twitter.com/utils/fetch.ts b/packages/mask/src/social-network-adaptor/twitter.com/utils/fetch.ts index ce6fcc543261..ea76b001e55b 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/utils/fetch.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/utils/fetch.ts @@ -10,7 +10,7 @@ import { isTypedMessageEmpty, isTypedMessageText, TypedMessageText, -} from '@masknet/shared-base' +} from '@masknet/typed-message' import { collectNodeText, collectTwitterEmoji } from '../../../utils' /** diff --git a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx index 4a8f9893f5d8..31e859c05f0e 100644 --- a/packages/mask/src/social-network/defaults/inject/CommentBox.tsx +++ b/packages/mask/src/social-network/defaults/inject/CommentBox.tsx @@ -9,7 +9,7 @@ import { usePostInfoDetails, usePostInfo, PostInfoProvider } from '../../../comp import { noop } from 'lodash-unified' import { MaskMessages } from '../../../utils/messages' import { startWatch } from '../../../utils/watcher' -import { extractTextFromTypedMessage } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' const defaultOnPasteToCommentBox = async ( encryptedComment: string, diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index fe08c2ee3b50..6a98ad910db8 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -8,8 +8,9 @@ import type { PostIdentifier, ProfileIdentifier, ReadonlyIdentifierMap, - TypedMessage, } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' import type { PaletteMode, Theme } from '@mui/material' import type { Subscription } from 'use-subscription' import type { InjectedDialogClassKey, InjectedDialogProps } from '../components/shared/InjectedDialog' @@ -273,6 +274,7 @@ export namespace SocialNetworkUI { } export interface ComponentOverwrite { InjectedDialog?: ComponentOverwriteConfig + RenderFragments?: RenderFragmentsContextType } export interface ComponentOverwriteConfig { classes?: () => { classes: Partial> } diff --git a/packages/mask/src/social-network/utils/create-post-context.ts b/packages/mask/src/social-network/utils/create-post-context.ts index 320523d12a9e..f838e01279c0 100644 --- a/packages/mask/src/social-network/utils/create-post-context.ts +++ b/packages/mask/src/social-network/utils/create-post-context.ts @@ -1,18 +1,20 @@ import { ValueRef } from '@dimensiondev/holoflows-kit' import type { PostContext, PostContextAuthor, PostContextCreation, PostContextSNSActions } from '@masknet/plugin-infra' import { - ALL_EVENTS, extractTextFromTypedMessage, isTypedMessageEqual, makeTypedMessageTupleFromList, + type TypedMessage, + type TypedMessageTuple, +} from '@masknet/typed-message' +import { + ALL_EVENTS, ObservableMap, ObservableSet, parseURL, Payload, PostIdentifier, ProfileIdentifier, - TypedMessage, - TypedMessageTuple, SubscriptionFromValueRef, SubscriptionDebug as debug, mapSubscription, diff --git a/packages/mask/src/tsconfig.json b/packages/mask/src/tsconfig.json index b038873061a0..861dca90a047 100644 --- a/packages/mask/src/tsconfig.json +++ b/packages/mask/src/tsconfig.json @@ -17,6 +17,8 @@ { "path": "../../public-api" }, { "path": "../../shared" }, { "path": "../../shared-base" }, + { "path": "../../typed-message/base" }, + { "path": "../../typed-message/dom" }, { "path": "../../web3-shared/base" }, { "path": "../../web3-shared/evm" }, { "path": "../../web3-shared/flow" }, diff --git a/packages/mask/src/utils/debug/ui.ts b/packages/mask/src/utils/debug/ui.ts index e3b33c58b8ab..196831b807fe 100644 --- a/packages/mask/src/utils/debug/ui.ts +++ b/packages/mask/src/utils/debug/ui.ts @@ -1,9 +1,7 @@ import { LiveSelector, Watcher, DOMProxy } from '@dimensiondev/holoflows-kit' -import { enhanceTypedMessageDebugger } from '../../protocols/typed-message/debugger' -if (typeof window === 'object') { +if (typeof window === 'object' && process.env.NODE_ENV === 'development') { LiveSelector.enhanceDebugger() Watcher.enhanceDebugger() DOMProxy.enhanceDebugger() - enhanceTypedMessageDebugger() } diff --git a/packages/mask/src/utils/type-transform/BackupFormat/JSON/DBRecord-JSON/PostRecord.ts b/packages/mask/src/utils/type-transform/BackupFormat/JSON/DBRecord-JSON/PostRecord.ts index 73d5bc29ba73..53f082dbcbfa 100644 --- a/packages/mask/src/utils/type-transform/BackupFormat/JSON/DBRecord-JSON/PostRecord.ts +++ b/packages/mask/src/utils/type-transform/BackupFormat/JSON/DBRecord-JSON/PostRecord.ts @@ -3,8 +3,8 @@ import type { BackupJSONFileLatest } from '../latest' import type { RecipientReasonJSON } from '../version-2' import { encodeArrayBuffer, decodeArrayBuffer, unreachable } from '@dimensiondev/kit' import { encode, decode } from '@msgpack/msgpack' +import type { TypedMessage } from '@masknet/typed-message' import { - type TypedMessage, IdentifierMap, ECKeyIdentifier, GroupIdentifier, diff --git a/packages/plugin-infra/package.json b/packages/plugin-infra/package.json index 287173d91088..d14f8b63df7f 100644 --- a/packages/plugin-infra/package.json +++ b/packages/plugin-infra/package.json @@ -10,6 +10,7 @@ "@masknet/shared": "workspace:*", "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", + "@masknet/typed-message": "workspace:*", "@masknet/web3-shared-base": "workspace:*", "@servie/events": "^3.0.0", "@types/use-subscription": "^1.0.0", diff --git a/packages/plugin-infra/src/PostContext.ts b/packages/plugin-infra/src/PostContext.ts index 68fb7cf30f20..f8acdf0525f1 100644 --- a/packages/plugin-infra/src/PostContext.ts +++ b/packages/plugin-infra/src/PostContext.ts @@ -5,8 +5,8 @@ import { type Payload, type PostIdentifier, type ProfileIdentifier, - type TypedMessageTuple, } from '@masknet/shared-base' +import type { TypedMessageTuple } from '@masknet/typed-message' import { ValueRef, LiveSelector, DOMProxy } from '@dimensiondev/holoflows-kit' import type { Result } from 'ts-results' import { Context, createContext, createElement, memo, useContext } from 'react' diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index d4cbe0e3cec1..a64489ff39c2 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -1,13 +1,8 @@ /* eslint @dimensiondev/unicode-specific-set: ["error", { "only": "code" }] */ import type React from 'react' import type { Option, Result } from 'ts-results' -import type { - TypedMessage, - TypedMessageTuple, - ScopedStorage, - ProfileIdentifier, - PersonaIdentifier, -} from '@masknet/shared-base' +import type { TypedMessage, TypedMessageTuple } from '@masknet/typed-message' +import type { ScopedStorage, ProfileIdentifier, PersonaIdentifier } from '@masknet/shared-base' import type { Emitter } from '@servie/events' import type { Web3Plugin } from './web3-types' import type { Subscription } from 'use-subscription' @@ -145,12 +140,7 @@ export namespace Plugin.Shared { */ init(signal: AbortSignal, context: Context): void | Promise } - export interface Utilities { - /** - * A pure function that convert a TypedMessage into another one - */ - typedMessageTransformer?: TypedMessageTransformer - } + export interface Utilities {} export type TypedMessageTransformer = (message: TypedMessageTuple) => TypedMessageTuple /** The publisher of the plugin */ export interface Publisher { @@ -261,6 +251,13 @@ export namespace Plugin.SNSAdaptor { ProfileSliders?: ProfileSlider[] /** This UI will be rendered as tabs on the profile page */ ProfileTabs?: ProfileTab[] + /** + * A hook for if this plugin can enhance the #hash or $cash tag. + */ + enhanceTag?: { + onClick?: (kind: 'cash' | 'hash', content: string, event: React.MouseEvent) => void + onHover?: (kind: 'cash' | 'hash', content: string, event: React.MouseEvent) => () => void + } } // #region Composition entry /** diff --git a/packages/plugin-infra/tsconfig.json b/packages/plugin-infra/tsconfig.json index 7b702f0b8e10..d754a36a1237 100644 --- a/packages/plugin-infra/tsconfig.json +++ b/packages/plugin-infra/tsconfig.json @@ -10,7 +10,8 @@ { "path": "../shared/" }, { "path": "../shared-base" }, { "path": "../theme" }, - { "path": "../web3-shared/base" } + { "path": "../web3-shared/base" }, + { "path": "../typed-message/base" } ], "ts-node": { "swc": true, diff --git a/packages/plugins/CyberConnect/src/SNSAdaptor/index.tsx b/packages/plugins/CyberConnect/src/SNSAdaptor/index.tsx index 500b16b676b2..005ea38c9ae7 100644 --- a/packages/plugins/CyberConnect/src/SNSAdaptor/index.tsx +++ b/packages/plugins/CyberConnect/src/SNSAdaptor/index.tsx @@ -1,21 +1,10 @@ import { Plugin, usePostInfoDetails, usePluginWrapper } from '@masknet/plugin-infra' import { base } from '../base' import { useMemo } from 'react' -import { makeStyles } from '@masknet/theme' -import { parseURL, extractTextFromTypedMessage } from '@masknet/shared-base' +import { parseURL } from '@masknet/shared-base' +import { extractTextFromTypedMessage } from '@masknet/typed-message' import Profile from './Profile' -const useStyles = makeStyles()((theme) => { - return { - skeleton: { - margin: theme.spacing(2), - '&:first-child': { - marginTop: theme.spacing(3), - }, - }, - } -}) - const isCyberConnectUrl = (x: string): boolean => x.includes('app.cyberconnect.me') function Renderer({ url }: { url: string }) { diff --git a/packages/plugins/CyberConnect/tsconfig.json b/packages/plugins/CyberConnect/tsconfig.json index d213b29f4f7e..4d052cd2e340 100644 --- a/packages/plugins/CyberConnect/tsconfig.json +++ b/packages/plugins/CyberConnect/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../../plugin-infra" }, { "path": "../../theme" }, { "path": "../../shared-base" }, - { "path": "../../web3-shared/evm" } + { "path": "../../web3-shared/evm" }, + { "path": "../../typed-message/base" } ] } diff --git a/packages/plugins/FileService/package.json b/packages/plugins/FileService/package.json index dc1d7aeae8cb..e61294648213 100644 --- a/packages/plugins/FileService/package.json +++ b/packages/plugins/FileService/package.json @@ -12,6 +12,7 @@ "@masknet/shared": "workspace:*", "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", + "@masknet/typed-message": "workspace:*", "@msgpack/msgpack": "^2.7.2", "@masknet/web3-kit": "workspace:*", "arweave": "^1.10.3", diff --git a/packages/plugins/FileService/src/helpers.ts b/packages/plugins/FileService/src/helpers.ts index 32ba38fdac51..010644730ee6 100644 --- a/packages/plugins/FileService/src/helpers.ts +++ b/packages/plugins/FileService/src/helpers.ts @@ -1,4 +1,4 @@ -import { type TypedMessage, createTypedMessageMetadataReader } from '@masknet/shared-base' +import { type TypedMessage, createTypedMessageMetadataReader } from '@masknet/typed-message' import { META_KEY_1, META_KEY_2 } from './constants' import { FileInfo, FileInfoV1, Provider } from './types' import schemaV1 from './schema-v1.json' diff --git a/packages/plugins/FileService/tsconfig.json b/packages/plugins/FileService/tsconfig.json index 021acd11ab64..13e20419cc5b 100644 --- a/packages/plugins/FileService/tsconfig.json +++ b/packages/plugins/FileService/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../../theme" }, { "path": "../../plugin-infra" }, { "path": "../../shared-base" }, + { "path": "../../typed-message/base" }, { "path": "../../shared" }, { "path": "../../web3-kit" }, { "path": "../../icons" }, diff --git a/packages/scripts/src/commands/new-package.ts b/packages/scripts/src/commands/new-package.ts index d8c972f1f6c9..52e8a076f06c 100644 --- a/packages/scripts/src/commands/new-package.ts +++ b/packages/scripts/src/commands/new-package.ts @@ -152,7 +152,7 @@ async function createNewPackage({ path, npmName, type, pluginID }: PackageOption .replace(INSERT_HERE + ' 1', `${INSERT_HERE} 1\n { "path": "./${path}/tsconfig.tests.json" },`) .replace(INSERT_HERE + ' 2', `"${npmName}": ["./${path}/src"],\n ${INSERT_HERE} 2`), ) - await changeFile(resolve(packagePath, 'README.md'), () => `# ${npmName}`) + await changeFile(resolve(packagePath, 'README.md'), () => `# ${npmName}\n`) } // regenerate lockfile and install dependencies for newly installed packages diff --git a/packages/shared-base/package.json b/packages/shared-base/package.json index a0666cacc7de..036aee5c5203 100644 --- a/packages/shared-base/package.json +++ b/packages/shared-base/package.json @@ -6,6 +6,7 @@ "sideEffect": false, "dependencies": { "@dimensiondev/holoflows-kit": "^0.9.0-20210902104757-7c3d0d0", + "@masknet/typed-message": "workspace:*", "@msgpack/msgpack": "^2.7.2", "@mui/icons-material": "link:..\\empty", "@mui/lab": "link:..\\empty", diff --git a/packages/shared-base/src/Messages/Mask.ts b/packages/shared-base/src/Messages/Mask.ts index a3a96e21a0ad..46bd027f32ac 100644 --- a/packages/shared-base/src/Messages/Mask.ts +++ b/packages/shared-base/src/Messages/Mask.ts @@ -1,4 +1,4 @@ -import type { TypedMessage } from '../TypedMessage' +import type { TypedMessage } from '@masknet/typed-message' import type { ProfileIdentifier, PersonaIdentifier } from '../Identifier/type' import type { RelationFavor } from '../Persona/type' import type { Appearance, LanguageOptions, DataProvider } from '../../../public-api/src/web' diff --git a/packages/shared-base/src/TypedMessage/README.md b/packages/shared-base/src/TypedMessage/README.md deleted file mode 100644 index a3810ee59959..000000000000 --- a/packages/shared-base/src/TypedMessage/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# TypedMessage - -TypedMessage represents a structured message. diff --git a/packages/shared-base/src/TypedMessage/binary-encode/type.ts b/packages/shared-base/src/TypedMessage/binary-encode/type.ts deleted file mode 100644 index b50b2666bbfb..000000000000 --- a/packages/shared-base/src/TypedMessage/binary-encode/type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum TypedMessageTypeEnum { - Tuple = 0, - Text = 1, -} diff --git a/packages/shared-base/src/TypedMessage/core.ts b/packages/shared-base/src/TypedMessage/core.ts deleted file mode 100644 index 5a576289d799..000000000000 --- a/packages/shared-base/src/TypedMessage/core.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - SerializableTypedMessage, - SerializableTypedMessages, - NonSerializableTypedMessage, - TypedMessage, -} from './base' -import { isSerializableTypedMessage } from './utils' -import { composeEvery, composeSome, createIsType } from './utils/internal' -// Don't forget to add to isWellKnownCoreTypedMessages also -type Meta = TypedMessage['meta'] -export type WellKnownCoreTypedMessages = - | TypedMessageText - | TypedMessageImage - | TypedMessageTuple - | TypedMessageTupleSerializable - | TypedMessageUnknown - | TypedMessageEmpty - | TypedMessagePromise -// #region Text -export type TypedMessageText = TypedMessageTextV1 -/** A text message */ -export interface TypedMessageTextV1 extends SerializableTypedMessage<1> { - readonly type: 'text' - readonly content: string -} -export const isTypedMessageTextV1 = createIsType('text', 1) -export const isTypedMessageText = composeSome(isTypedMessageTextV1) as (x: TypedMessage) => x is TypedMessageText -export function makeTypedMessageText(text: string, meta?: Meta): TypedMessageTextV1 { - return { type: 'text', version: 1, serializable: true, content: text, meta } -} -// #endregion -// #region Image -export type TypedMessageImage = TypedMessageImageV1 -/** - * A single image - * TODO: it should be serializable but still not decided how to do that yet. - */ -export interface TypedMessageImageV1 extends NonSerializableTypedMessage { - readonly type: 'image' - readonly image: string | Blob - readonly width?: number - readonly height?: number -} -export const isTypedMessageImageV1 = createIsType('image') -export const isTypedMessageImage = composeSome(isTypedMessageImageV1) as (x: TypedMessage) => x is TypedMessageImage -export function makeTypedMessageImage( - image: TypedMessageImageV1['image'], - size?: { width: number; height: number }, - meta?: Meta, -): TypedMessageImageV1 { - return { type: 'image', serializable: false, image, width: size?.width, height: size?.height, meta } -} -// #endregion -// #region Tuple - -/** Multiple TypedMessages (ordered) */ -export interface TypedMessageTuple extends TypedMessage { - readonly type: 'tuple' - readonly items: T -} -export interface TypedMessageTupleSerializable< - T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], -> extends SerializableTypedMessage<1>, - TypedMessageTuple { - readonly type: 'tuple' -} -export const isTypedMessageTuple = (x: TypedMessage): x is TypedMessageTuple | TypedMessageTupleSerializable => - x.type === 'tuple' -export const isTypedMessageTupleSerializable: (x: TypedMessage) => x is TypedMessageTupleSerializable = composeEvery( - isTypedMessageTuple, - isSerializableTypedMessage, -) as any -export function makeTypedMessageTuple( - items: T, - meta?: Meta, -): TypedMessageTuple { - return { type: 'tuple', items, meta } -} -export function makeTypedMessageTupleFromList( - ...args: T -): TypedMessageTuple { - return { type: 'tuple', items: args } -} -export function makeTypedMessageTupleSerializable< - T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], ->(items: T, meta?: Meta): TypedMessageTupleSerializable { - return { type: 'tuple', version: 1, serializable: true, items, meta } -} -export function makeTypedMessageSerializableTupleFromList< - T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], ->(...args: T): TypedMessageTupleSerializable { - return { type: 'tuple', version: 1, items: args, serializable: true } -} -// #endregion -// #region unknown and empty -export interface TypedMessageUnknown extends NonSerializableTypedMessage { - readonly type: 'unknown' - /** The unrecognized data */ - readonly raw?: unknown -} -export const isTypedMessageUnknown = createIsType('unknown') -export function makeTypedMessageUnknown(raw?: unknown, meta?: Meta): TypedMessageUnknown { - return { type: 'unknown', serializable: false, meta, raw } -} - -export interface TypedMessageEmpty extends SerializableTypedMessage<1> { - readonly type: 'empty' -} -export const isTypedMessageEmpty = createIsType('empty') -export function makeTypedMessageEmpty(meta?: Meta): TypedMessageEmpty { - return { type: 'empty', serializable: true, version: 1, meta } -} -// #endregion -// #region Promise -export interface TypedMessagePromise extends NonSerializableTypedMessage { - readonly type: 'promise' - readonly promise: Promise - readonly value?: T -} -export const isTypedMessagePromise = createIsType('promise') -export function makeTypedMessagePromise( - promise: Promise, -): TypedMessagePromise { - const x: TypedMessagePromise = { type: 'promise', serializable: false, promise } - promise.then((y) => ((x as any).value = y)) - return x -} -// #endregion -export const isWellKnownCoreTypedMessages = composeSome( - isTypedMessageText, - isTypedMessageImage, - isTypedMessageTuple, - isTypedMessageUnknown, - isTypedMessagePromise, -) as (x: TypedMessage) => x is WellKnownCoreTypedMessages diff --git a/packages/shared-base/src/TypedMessage/index.ts b/packages/shared-base/src/TypedMessage/index.ts deleted file mode 100644 index b6e7ef53fea2..000000000000 --- a/packages/shared-base/src/TypedMessage/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './base' -export * from './extension' -export * from './utils' -export * from './core' -export * from './metadata' -import { isWellKnownExtensionTypedMessages, WellKnownExtensionTypedMessages } from './extension' -import { composeSome } from './utils/internal' -import { isWellKnownCoreTypedMessages, WellKnownCoreTypedMessages } from './core' -export type WellKnownTypedMessages = WellKnownCoreTypedMessages | WellKnownExtensionTypedMessages -export const isWellKnownTypedMessages = composeSome(isWellKnownCoreTypedMessages, isWellKnownExtensionTypedMessages) -export { encodeTypedMessageToDocument } from './binary-encode/encode' -export { decodeTypedMessageFromDocument } from './binary-encode/decode' -export * from './deprecated-encode' diff --git a/packages/shared-base/src/TypedMessage/transforms/flatten.ts b/packages/shared-base/src/TypedMessage/transforms/flatten.ts deleted file mode 100644 index 678a5d658790..000000000000 --- a/packages/shared-base/src/TypedMessage/transforms/flatten.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - isTypedMessagePromise, - isTypedMessageText, - isTypedMessageTuple, - makeTypedMessageEmpty, - makeTypedMessageText, - makeTypedMessageTuple, - TypedMessage, -} from '..' -export function flattenTypedMessage(x: TypedMessage): TypedMessage { - // Promise are always dropped after resolve even it has meta. - if (isTypedMessagePromise(x) && x.value) return flattenTypedMessage(x.value) - if (isTypedMessageTuple(x)) { - if (x.meta) return x - if (x.items.length === 0) return makeTypedMessageEmpty() - if (x.items.length === 1) return flattenTypedMessage(x.items[0]) - const nextItem = x.items - .map(flattenTypedMessage) - .flatMap((x) => (isTypedMessageTuple(x) && !x.meta ? x.items : x)) - .reduce((prev, current) => { - const lastItem = prev.at(-1) - if (!lastItem || lastItem.meta || current.meta) return prev.concat(current) - if (!isTypedMessageText(current) || !isTypedMessageText(lastItem)) return prev.concat(current) - // Only concat when last one and current one are both text and have no meta. - prev.pop() - prev.push(makeTypedMessageText(`${lastItem.content} ${current.content}`)) - return prev - }, []) - if (nextItem.length === 1) return nextItem[0] - return makeTypedMessageTuple(nextItem) - } - return x -} diff --git a/packages/shared-base/src/TypedMessage/transforms/promise.ts b/packages/shared-base/src/TypedMessage/transforms/promise.ts deleted file mode 100644 index 26a225d06f4a..000000000000 --- a/packages/shared-base/src/TypedMessage/transforms/promise.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { flattenTypedMessage, isTypedMessagePromise, isTypedMessageTuple, TypedMessage } from '..' - -export function hasPromise(x: TypedMessage) { - if (isTypedMessagePromise(x)) return true - if (isTypedMessageTuple(x)) return x.items.some(hasPromise) - return false -} - -export function collectTypedMessagePromise( - x: TypedMessage, - result: Promise[] = [], -): Promise[] { - if (isTypedMessagePromise(x)) return result.concat(x.promise.then(flattenTypedMessage)) - if (isTypedMessageTuple(x)) { - return result.concat(x.items.flatMap((x) => collectTypedMessagePromise(x))) - } - return result -} diff --git a/packages/shared-base/src/TypedMessage/utils/index.ts b/packages/shared-base/src/TypedMessage/utils/index.ts deleted file mode 100644 index 7719a1ff5d21..000000000000 --- a/packages/shared-base/src/TypedMessage/utils/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isTypedMessageAnchor } from '../extension' -import { isTypedMessageText, isTypedMessageTuple, TypedMessageTuple } from '../core' -import type { - NonSerializableWithAltTypedMessage, - SerializableTypedMessage, - SerializableTypedMessages, - TypedMessage, -} from '../base' -import { eq } from 'lodash-unified' -import { Option, Some, None } from 'ts-results' -import { isTypedMessageImage } from '..' - -export function isSerializableTypedMessage(x: TypedMessage): x is SerializableTypedMessages { - if ((x as SerializableTypedMessage).serializable) return true - const y = x as NonSerializableWithAltTypedMessage - if (y.serializable === false && y.alt) return true - return false -} - -/** - * This is a tree compare algorithm, may need to find a more efficient one from NPM - */ -export function isTypedMessageEqual(message1: TypedMessage, message2: TypedMessage): boolean { - if (message1.type !== message2.type) return false - if (message1.meta !== message2.meta) return false - if (isTypedMessageTuple(message1)) { - const msg1 = message1 as TypedMessageTuple - const msg2 = message2 as TypedMessageTuple - if (msg1.items.length !== msg2.items.length) return false - return msg1.items.every((item, index) => isTypedMessageEqual(item, msg2.items[index])) - } - return eq(message1, message2) -} - -/** - * Get inner text from a TypedMessage - * @param message message - */ -export function extractTextFromTypedMessage(message: TypedMessage | null): Option { - if (!message) return None - if (isTypedMessageText(message)) return Some(message.content) - if (isTypedMessageAnchor(message)) return Some(message.content) - if (isTypedMessageTuple(message)) { - const str: string[] = [] - for (const item of message.items) { - const text = extractTextFromTypedMessage(item) - if (text.some) str.push(text.val) - } - if (str.length) return Some(str.join(' ')) - return None - } - return None -} -export function extractImageFromTypedMessage( - message: TypedMessage | null, - result: (string | Blob)[] = [], -): (string | Blob)[] { - if (!message) return result - if (isTypedMessageImage(message)) return result.concat(message.image) - if (isTypedMessageTuple(message)) - return result.concat(message.items.flatMap((x) => extractImageFromTypedMessage(x))) - return result -} -export * from '../transforms/flatten' -export * from '../transforms/promise' diff --git a/packages/shared-base/src/index.ts b/packages/shared-base/src/index.ts index 347dfe9de26b..f4038d746655 100644 --- a/packages/shared-base/src/index.ts +++ b/packages/shared-base/src/index.ts @@ -4,7 +4,6 @@ export * from './collections' export * from './MaskPayload' export * from './Identifier/type' export * from './i18n' -export * from './TypedMessage' export * from './utils' export * from './kv-storage' export * from './crypto' diff --git a/packages/shared-base/tests/typed-message.ts b/packages/shared-base/tests/typed-message.ts index b20ff5a796b3..1aac5aa1c703 100644 --- a/packages/shared-base/tests/typed-message.ts +++ b/packages/shared-base/tests/typed-message.ts @@ -1,10 +1,11 @@ +// TODO: move this test to @masknet/typed-message package import { test, expect } from '@jest/globals' import { makeTypedMessageText, encodeTypedMessageToDocument, makeTypedMessageSerializableTupleFromList, decodeTypedMessageFromDocument, -} from '../src/TypedMessage' +} from '@masknet/typed-message' const meta = new Map([ ['com.example.test', 'hi'], diff --git a/packages/shared-base/tsconfig.json b/packages/shared-base/tsconfig.json index 799a575812fd..398b55ddd11e 100644 --- a/packages/shared-base/tsconfig.json +++ b/packages/shared-base/tsconfig.json @@ -7,5 +7,5 @@ }, "include": ["./src", "./src/**/*.json"], // Only type-level dependency. - "references": [{ "path": "../public-api" }, { "path": "../web3-shared/evm" }] + "references": [{ "path": "../public-api" }, { "path": "../web3-shared/evm" }, { "path": "../typed-message/base" }] } diff --git a/packages/shared-base/tsconfig.tests.json b/packages/shared-base/tsconfig.tests.json index 5c696bf78dab..27d0caa72bca 100644 --- a/packages/shared-base/tsconfig.tests.json +++ b/packages/shared-base/tsconfig.tests.json @@ -6,5 +6,5 @@ "types": ["node"] }, "include": ["./tests"], - "references": [{ "path": "./tsconfig.json" }] + "references": [{ "path": "./tsconfig.json" }, { "path": "../typed-message/base" }] } diff --git a/packages/typed-message/README.md b/packages/typed-message/README.md new file mode 100644 index 000000000000..8862445e634d --- /dev/null +++ b/packages/typed-message/README.md @@ -0,0 +1 @@ +# @masknet/typed-message diff --git a/packages/shared-base/src/TypedMessage/base.ts b/packages/typed-message/base/base.ts similarity index 96% rename from packages/shared-base/src/TypedMessage/base.ts rename to packages/typed-message/base/base.ts index 34adb8995404..5167aba19870 100644 --- a/packages/shared-base/src/TypedMessage/base.ts +++ b/packages/typed-message/base/base.ts @@ -1,3 +1,4 @@ +export type Meta = ReadonlyMap /** * The basic structure that contains */ diff --git a/packages/shared-base/src/TypedMessage/binary-encode/decode.ts b/packages/typed-message/base/binary-encode/decode.ts similarity index 85% rename from packages/shared-base/src/TypedMessage/binary-encode/decode.ts rename to packages/typed-message/base/binary-encode/decode.ts index 73bbd829abdb..45fbac7f78a5 100644 --- a/packages/shared-base/src/TypedMessage/binary-encode/decode.ts +++ b/packages/typed-message/base/binary-encode/decode.ts @@ -1,9 +1,10 @@ import { decode } from '@msgpack/msgpack' import { Result } from 'ts-results' -import { makeTypedMessageText, makeTypedMessageTuple, makeTypedMessageUnknown, TypedMessage } from '..' -import { TypedMessageTypeEnum } from './type' +import type { TypedMessage } from '../base' +import { makeTypedMessageText, makeTypedMessageTuple, makeTypedMessageUnknown } from '../core' +import { TypedMessageBinaryEncodingTypeEnum } from './type' -const HEAD = '[@masknet/shared-base] TypedMessage: ' +const HEAD = '[@masknet/typed-message] ' export function decodeTypedMessageFromDocument(bin: Uint8Array) { return Result.wrap(() => { const doc = decode(bin) @@ -19,9 +20,9 @@ export function decodeTypedMessageFromDocument(bin: Uint8Array) { function decodeTypedMessage(tm: readonly unknown[]): TypedMessage { const [type] = tm if (typeof type !== 'string' && typeof type !== 'number') throw new TypeError(`${HEAD}Invalid TypedMessage`) - if (type === TypedMessageTypeEnum.Text) return decodeTypedMessageText(tm) - if (type === TypedMessageTypeEnum.Tuple) return decodeTypedMessageTuple(tm) - return makeTypedMessageUnknown(tm, decodeMetadata(tm[1])) + if (type === TypedMessageBinaryEncodingTypeEnum.Text) return decodeTypedMessageText(tm) + if (type === TypedMessageBinaryEncodingTypeEnum.Tuple) return decodeTypedMessageTuple(tm) + return makeTypedMessageUnknown(tm) } function decodeTypedMessageText([, version, meta, text, format]: readonly unknown[]) { assertNumber(version) diff --git a/packages/shared-base/src/TypedMessage/binary-encode/encode.ts b/packages/typed-message/base/binary-encode/encode.ts similarity index 84% rename from packages/shared-base/src/TypedMessage/binary-encode/encode.ts rename to packages/typed-message/base/binary-encode/encode.ts index 401e5dff5758..81d4d65c5671 100644 --- a/packages/shared-base/src/TypedMessage/binary-encode/encode.ts +++ b/packages/typed-message/base/binary-encode/encode.ts @@ -1,15 +1,14 @@ import { isTypedMessageText, isTypedMessageTupleSerializable, - SerializableTypedMessages, - TypedMessage, TypedMessageText, TypedMessageTupleSerializable, -} from '..' +} from '../core' +import type { SerializableTypedMessages, TypedMessage } from '../base' import { encode } from '@msgpack/msgpack' -import { TypedMessageTypeEnum } from './type' +import { TypedMessageBinaryEncodingTypeEnum } from './type' -const HEAD = '[@masknet/shared-base] TypedMessage: ' +const HEAD = '[@masknet/typed-message] ' export function encodeTypedMessageToDocument(tm: SerializableTypedMessages) { const doc = [0, encodeTypedMessage(tm)] return encode(doc) @@ -25,10 +24,10 @@ function encodeTypedMessage(tm: SerializableTypedMessages): any[] { } function encodeTypedMessageText(tm: TypedMessageText): any[] { // TODO: TextFormat - return [TypedMessageTypeEnum.Text, tm.version, encodeMeta(tm), tm.content] + return [TypedMessageBinaryEncodingTypeEnum.Text, tm.version, encodeMeta(tm), tm.content] } function encodeTypedMessageTuple(tm: TypedMessageTupleSerializable): any[] { - return [TypedMessageTypeEnum.Tuple, tm.version, encodeMeta(tm), tm.items.map(encodeTypedMessage)] + return [TypedMessageBinaryEncodingTypeEnum.Tuple, tm.version, encodeMeta(tm), tm.items.map(encodeTypedMessage)] } function encodeMeta(tm: TypedMessage) { @@ -78,7 +77,7 @@ function collectValue(val: any): any { } return result } catch (err) { - console.warn('[@masknet/shared-base] TypedMessage: When converting value', val, ', an error occurred', err) + console.warn(HEAD, 'When converting value', val, ', an error occurred', err) throw err } } diff --git a/packages/typed-message/base/binary-encode/index.ts b/packages/typed-message/base/binary-encode/index.ts new file mode 100644 index 000000000000..95179f2f0e2d --- /dev/null +++ b/packages/typed-message/base/binary-encode/index.ts @@ -0,0 +1,2 @@ +export { decodeTypedMessageFromDocument } from './decode' +export { encodeTypedMessageToDocument } from './encode' diff --git a/packages/typed-message/base/binary-encode/type.ts b/packages/typed-message/base/binary-encode/type.ts new file mode 100644 index 000000000000..f531a4e0e0ae --- /dev/null +++ b/packages/typed-message/base/binary-encode/type.ts @@ -0,0 +1,5 @@ +/** @internal */ +export enum TypedMessageBinaryEncodingTypeEnum { + Tuple = 0, + Text = 1, +} diff --git a/packages/typed-message/base/core/empty.ts b/packages/typed-message/base/core/empty.ts new file mode 100644 index 000000000000..eaa227eab98e --- /dev/null +++ b/packages/typed-message/base/core/empty.ts @@ -0,0 +1,21 @@ +import type { SerializableTypedMessage } from '../base' +import { createIsType } from '../utils/internal' + +export interface TypedMessageEmpty extends SerializableTypedMessage<1> { + readonly type: 'empty' + readonly meta?: undefined +} + +export const isTypedMessageEmpty = createIsType('empty') + +const empty: TypedMessageEmpty = { + type: 'empty', + serializable: true, + version: 1, + meta: undefined, +} +Object.setPrototypeOf(empty, null) +Object.freeze(empty) +export function makeTypedMessageEmpty(): TypedMessageEmpty { + return empty +} diff --git a/packages/typed-message/base/core/image.ts b/packages/typed-message/base/core/image.ts new file mode 100644 index 000000000000..e3321663eaa7 --- /dev/null +++ b/packages/typed-message/base/core/image.ts @@ -0,0 +1,30 @@ +import type { NonSerializableTypedMessage, Meta } from '../base' +import { createIsType } from '../utils/internal' + +/** + * A single image + * TODO: it should be serializable but still not decided how to do that yet. + */ +export interface TypedMessageImage extends NonSerializableTypedMessage { + readonly type: 'image' + readonly image: string | Blob + readonly width?: number + readonly height?: number +} + +export const isTypedMessageImage = createIsType('image') + +export function makeTypedMessageImage( + image: TypedMessageImage['image'], + size?: { width: number; height: number }, + meta?: Meta, +): TypedMessageImage { + return { + type: 'image', + serializable: false, + image, + width: size?.width, + height: size?.height, + meta, + } +} diff --git a/packages/typed-message/base/core/index.ts b/packages/typed-message/base/core/index.ts new file mode 100644 index 000000000000..015e2047fe1f --- /dev/null +++ b/packages/typed-message/base/core/index.ts @@ -0,0 +1,34 @@ +export * from './empty' +export * from './text' +export * from './image' +export * from './tuple' +export * from './unknown' +export * from './promise' + +import * as empty from './empty' +import * as unknown from './unknown' +import * as tuple from './tuple' +import * as text from './text' +import * as image from './image' +import * as promise from './promise' + +import type { TypedMessage } from '../base' +import { composeSome } from '../utils/internal' + +export type CoreTypedMessages = + | empty.TypedMessageEmpty + | text.TypedMessageText + | image.TypedMessageImage + | tuple.TypedMessageTuple + | tuple.TypedMessageTupleSerializable + | unknown.TypedMessageUnknown + | promise.TypedMessagePromise + +export const isCoreTypedMessages = composeSome( + empty.isTypedMessageEmpty, + text.isTypedMessageText, + image.isTypedMessageImage, + tuple.isTypedMessageTuple, + unknown.isTypedMessageUnknown, + promise.isTypedMessagePromise, +) as (x: TypedMessage) => x is CoreTypedMessages diff --git a/packages/typed-message/base/core/promise.ts b/packages/typed-message/base/core/promise.ts new file mode 100644 index 000000000000..9a2941065525 --- /dev/null +++ b/packages/typed-message/base/core/promise.ts @@ -0,0 +1,26 @@ +import type { TypedMessage, NonSerializableTypedMessage } from '../base' +import { createIsType } from '../utils/internal' + +export interface TypedMessagePromise extends NonSerializableTypedMessage { + readonly type: 'promise' + readonly promise: Promise & { value?: T } + readonly meta?: undefined + /** What to display when the message is not ready. */ + readonly alt?: TypedMessage +} + +export const isTypedMessagePromise = createIsType('promise') + +export function makeTypedMessagePromise( + promise: Promise, + alt?: TypedMessage, +): TypedMessagePromise { + const x: TypedMessagePromise = { + type: 'promise', + serializable: false, + promise: promise.then((y) => (x.promise.value = y)), + alt, + meta: undefined, + } + return x +} diff --git a/packages/typed-message/base/core/text.ts b/packages/typed-message/base/core/text.ts new file mode 100644 index 000000000000..56a5936acd2e --- /dev/null +++ b/packages/typed-message/base/core/text.ts @@ -0,0 +1,24 @@ +import type { SerializableTypedMessage, TypedMessage, Meta } from '../base' +import { createIsType } from '../utils/internal' + +export type TypedMessageText = TypedMessageTextV1 + +/** A text message */ +export interface TypedMessageTextV1 extends SerializableTypedMessage<1> { + readonly type: 'text' + readonly content: string +} + +export const isTypedMessageTextV1 = createIsType('text', 1) + +export const isTypedMessageText: (x: TypedMessage) => x is TypedMessageText = isTypedMessageTextV1 + +export function makeTypedMessageText(text: string, meta?: Meta): TypedMessageTextV1 { + return { + type: 'text', + version: 1, + serializable: true, + content: text, + meta, + } +} diff --git a/packages/typed-message/base/core/tuple.ts b/packages/typed-message/base/core/tuple.ts new file mode 100644 index 000000000000..2b8a1c9f9570 --- /dev/null +++ b/packages/typed-message/base/core/tuple.ts @@ -0,0 +1,47 @@ +import type { SerializableTypedMessage, SerializableTypedMessages, TypedMessage, Meta } from '../base' +import { isSerializableTypedMessage } from '../utils' +import { composeEvery } from '../utils/internal' + +/** Multiple TypedMessages (ordered) */ +export interface TypedMessageTuple extends TypedMessage { + readonly type: 'tuple' + readonly items: T +} +export interface TypedMessageTupleSerializable< + T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], +> extends SerializableTypedMessage<1>, + TypedMessageTuple { + readonly type: 'tuple' +} + +export const isTypedMessageTuple = (x: TypedMessage): x is TypedMessageTuple | TypedMessageTupleSerializable => + x.type === 'tuple' + +export const isTypedMessageTupleSerializable: (x: TypedMessage) => x is TypedMessageTupleSerializable = composeEvery( + isTypedMessageTuple, + isSerializableTypedMessage, +) as any + +export function makeTypedMessageTuple( + items: T, + meta?: Meta, +): TypedMessageTuple { + return { type: 'tuple', items, meta } +} +export function makeTypedMessageTupleFromList( + ...args: T +): TypedMessageTuple { + return { type: 'tuple', items: args } +} + +export function makeTypedMessageTupleSerializable< + T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], +>(items: T, meta?: Meta): TypedMessageTupleSerializable { + return { type: 'tuple', version: 1, serializable: true, items, meta } +} + +export function makeTypedMessageSerializableTupleFromList< + T extends readonly SerializableTypedMessages[] = readonly SerializableTypedMessages[], +>(...args: T): TypedMessageTupleSerializable { + return { type: 'tuple', version: 1, items: args, serializable: true } +} diff --git a/packages/typed-message/base/core/unknown.ts b/packages/typed-message/base/core/unknown.ts new file mode 100644 index 000000000000..8bb8cbe49a7f --- /dev/null +++ b/packages/typed-message/base/core/unknown.ts @@ -0,0 +1,14 @@ +import type { NonSerializableTypedMessage } from '../base' +import { createIsType } from '../utils/internal' + +export interface TypedMessageUnknown extends NonSerializableTypedMessage { + readonly type: 'unknown' + /** The unrecognized data */ + readonly raw?: unknown + readonly meta?: undefined +} +export const isTypedMessageUnknown = createIsType('unknown') + +export function makeTypedMessageUnknown(raw?: unknown): TypedMessageUnknown { + return { type: 'unknown', serializable: false, meta: undefined, raw } +} diff --git a/packages/shared-base/src/TypedMessage/deprecated-encode/index.ts b/packages/typed-message/base/deprecated-encode/index.ts similarity index 93% rename from packages/shared-base/src/TypedMessage/deprecated-encode/index.ts rename to packages/typed-message/base/deprecated-encode/index.ts index ff73a4da28f1..76a7e1ecb195 100644 --- a/packages/shared-base/src/TypedMessage/deprecated-encode/index.ts +++ b/packages/typed-message/base/deprecated-encode/index.ts @@ -1,6 +1,5 @@ import { None, Ok, Result } from 'ts-results' -import type { TypedMessageText } from '..' -import { makeTypedMessageText } from '../core' +import { makeTypedMessageText, TypedMessageText } from '../core' export function encodeTypedMessageV38Format(message: TypedMessageText) { const encoder = new TextEncoder() diff --git a/packages/typed-message/base/extension/MaskPayload.ts b/packages/typed-message/base/extension/MaskPayload.ts new file mode 100644 index 000000000000..4643f37b2feb --- /dev/null +++ b/packages/typed-message/base/extension/MaskPayload.ts @@ -0,0 +1,22 @@ +import type { Meta, TypedMessage } from '../base' +import { createIsType } from '../utils/internal' + +/** + * This TypedMessage represents an encrypted TypedMessage. + */ +export interface TypedMessageMaskPayload extends TypedMessage { + readonly type: 'x-mask-payload' + readonly message: TypedMessage +} +export const isTypedMessageMaskPayload = createIsType('x-mask-payload') + +export function makeTypedMessageMaskPayload( + message: TypedMessageMaskPayload['message'], + meta?: Meta, +): TypedMessageMaskPayload { + return { + type: 'x-mask-payload', + message, + meta, + } +} diff --git a/packages/shared-base/src/TypedMessage/extension.ts b/packages/typed-message/base/extension/anchor.ts similarity index 64% rename from packages/shared-base/src/TypedMessage/extension.ts rename to packages/typed-message/base/extension/anchor.ts index 1936116847c7..73fb986d5c27 100644 --- a/packages/shared-base/src/TypedMessage/extension.ts +++ b/packages/typed-message/base/extension/anchor.ts @@ -1,7 +1,7 @@ -import type { NonSerializableWithAltTypedMessage, TypedMessage } from './base' -import { composeSome, createIsType } from './utils/internal' -import { makeTypedMessageText, TypedMessageText } from './core' -export type WellKnownExtensionTypedMessages = TypedMessageAnchor +import type { NonSerializableWithAltTypedMessage } from '../base' +import { createIsType } from '../utils/internal' +import { makeTypedMessageText, TypedMessageText } from '../core' + /** It represents a single link */ export interface TypedMessageAnchor extends NonSerializableWithAltTypedMessage { readonly type: 'x-anchor' @@ -10,7 +10,9 @@ export interface TypedMessageAnchor extends NonSerializableWithAltTypedMessage { readonly content: string readonly alt: TypedMessageText } + export const isTypedMessageAnchor = createIsType('x-anchor') + export function makeTypedMessageAnchor( category: TypedMessageAnchor['category'], href: TypedMessageAnchor['href'], @@ -25,7 +27,3 @@ export function makeTypedMessageAnchor( alt: makeTypedMessageText(`[${content}](${href})`), } } -// -export const isWellKnownExtensionTypedMessages = composeSome(isTypedMessageAnchor) as ( - x: TypedMessage, -) => x is WellKnownExtensionTypedMessages diff --git a/packages/typed-message/base/extension/index.ts b/packages/typed-message/base/extension/index.ts new file mode 100644 index 000000000000..2ed1e097eaf2 --- /dev/null +++ b/packages/typed-message/base/extension/index.ts @@ -0,0 +1,12 @@ +export * from './anchor' +export * from './MaskPayload' + +import { composeSome } from '../utils/internal' +import * as anchor from './anchor' +import * as MaskPayload from './MaskPayload' +export type WellKnownExtensionTypedMessages = anchor.TypedMessageAnchor | MaskPayload.TypedMessageMaskPayload + +export const isWellKnownExtensionTypedMessages = composeSome( + anchor.isTypedMessageAnchor, + MaskPayload.isTypedMessageMaskPayload, +) diff --git a/packages/typed-message/base/index.ts b/packages/typed-message/base/index.ts new file mode 100644 index 000000000000..2a7b522050ea --- /dev/null +++ b/packages/typed-message/base/index.ts @@ -0,0 +1,15 @@ +export * from './base' +export * from './core' +export * from './extension' +export * from './binary-encode' +export * from './deprecated-encode' +export * from './utils' +export * from './visitor' +export * from './transformer' + +import { isCoreTypedMessages, CoreTypedMessages } from './core' +import { isWellKnownExtensionTypedMessages, WellKnownExtensionTypedMessages } from './extension' +import { composeSome } from './utils/internal' + +export type WellKnownTypedMessages = WellKnownExtensionTypedMessages | CoreTypedMessages +export const isWellKnownTypedMessages = composeSome(isCoreTypedMessages, isWellKnownExtensionTypedMessages) diff --git a/packages/typed-message/base/transformer/Flatten.ts b/packages/typed-message/base/transformer/Flatten.ts new file mode 100644 index 000000000000..ba5f8f40f2aa --- /dev/null +++ b/packages/typed-message/base/transformer/Flatten.ts @@ -0,0 +1,42 @@ +import { + isTypedMessageEmpty, + isTypedMessagePromise, + isTypedMessageText, + isTypedMessageTuple, + makeTypedMessageEmpty, + makeTypedMessageText, + makeTypedMessageTuple, + makeTypedMessageTupleSerializable, +} from '../core' +import type { TypedMessage } from '../base' +import { visitEachTypedMessageChild } from '../visitor' +import { isSerializableTypedMessage } from '../utils' +import { emptyTransformationContext, TransformationContext } from './context' + +export function FlattenTypedMessage(message: TypedMessage, context: TransformationContext): TypedMessage { + if (isTypedMessagePromise(message) && message.promise.value) return message.promise.value + if (isTypedMessageTuple(message)) { + const next = message.items + .map((x) => FlattenTypedMessage(x, context)) + .flatMap((x) => (isTypedMessageTuple(x) ? (x.meta ? x : x.items) : x)) + .filter((x) => !isTypedMessageEmpty(x)) + .reduce((result, current) => { + const lastItem = result.at(-1) + if (!lastItem || lastItem.meta || current.meta) return result.concat(current) + if (!isTypedMessageText(current) || !isTypedMessageText(lastItem)) return result.concat(current) + // Only concat when last one and current one are both text and have no meta. + result.pop() + result.push(makeTypedMessageText(`${lastItem.content} ${current.content}`)) + return result + }, []) + + if (!message.meta) { + if (next.length === 0) return makeTypedMessageEmpty() + if (next.length === 1) return next[0] + } + if (next.every(isSerializableTypedMessage)) return makeTypedMessageTupleSerializable(next, message.meta) + return makeTypedMessageTuple(next, message.meta) + } + return visitEachTypedMessageChild(message, FlattenTypedMessage, context) +} +FlattenTypedMessage.NoContext = (message: TypedMessage) => FlattenTypedMessage(message, emptyTransformationContext) diff --git a/packages/typed-message/base/transformer/MaskPayload/index.ts b/packages/typed-message/base/transformer/MaskPayload/index.ts new file mode 100644 index 000000000000..7117f5671f73 --- /dev/null +++ b/packages/typed-message/base/transformer/MaskPayload/index.ts @@ -0,0 +1,88 @@ +import { FlattenTypedMessage } from '../Flatten' +import type { TypedMessage } from '../../base' +import { isTypedMessageImage, isTypedMessageText, isTypedMessageTuple, TypedMessageImage } from '../../core' +import { isTypedMessageAnchor, isTypedMessageMaskPayload } from '../../extension' +import { visitEachTypedMessageChild } from '../../visitor' +import type { TransformationContext } from '../context' + +export interface MaskPayloadTransformOptions { + /** + * Text that always before the Mask Payload. This transform will remove them. + * + * ``` + * This message is encrypted by @Mask. _link here_ + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * ``` + * + * This part can be removed by providing the strings into this option. + */ + removePrefixText: string[] + /** + * Text that always after the Mask Payload. This transform will remove them. + * + * ``` + * _link here_ Install to decrypt! + * ~~~~~~~~~~~~~~~~~~~~ + * ``` + * + * This part can be removed by providing the strings into this option. + */ + removePostfixText: string[] + /** + * An example: + * + * TypedMessagePromise (alt = TypedMessageText("decrypting...")) resolves to + * TypedMessagePromise (alt = TypedMessageText("waiting key on gun")) resolves to + * TypedMessageMaskPayload (message = TypedMessageText("hey this is an encrypted message!")) + */ + transformText(payloadLike: string, context: TransformationContext): TypedMessage + transformImage(image: TypedMessageImage, context: TransformationContext): TypedMessage +} +export function createMaskPayloadTransform(options: MaskPayloadTransformOptions) { + return function MaskPayloadTransform(message: TypedMessage, context: TransformationContext) { + if (context.skipMaskPayloadTransform) return message + + message = FlattenTypedMessage(message, context) + if (isTypedMessageMaskPayload(message)) return message + // We don't transform nested message + + // Note: there maybe more than 1 payload in the message. Make sure both of them are handled. + if (isTypedMessageAnchor(message)) { + if (message.content.match(linkPayload)) return options.transformText(message.content, context) + if (message.href.match(linkPayload)) return options.transformText(message.href, context) + } else if (isTypedMessageImage(message)) { + return options.transformImage(message, context) + } else if (isTypedMessageText(message)) { + // TODO: there maybe more than 1 payload to parse. + // Not detect link form here. Only detect raw form (used on FB) in this branch. + if (message.content.match(textPayload)) { + return options.transformText(message.content, context) + } + } else if (isTypedMessageTuple(message)) { + // TODO: there maybe more than 1 payload to parse. + // Visit each child here, when a raw form or link form is detected, + // we should check if the before/after is the text need to be removed. + // For example: + // Tuple( + // Text("Keep this text! This post is encrypted by") + // Anchor("@mask.io"), + // Text(" . Install to decrypt!"), + // Anchor(payload here), + // Text(" more text to be removed! But keep this one!") + // ) + // it should be transformed into + // Tuple( + // Text("Keep this text! "), + // MaskPayload(pending...), + // Text(" But keep this one!"), + // ) + } + return visitEachTypedMessageChild(message, MaskPayloadTransform, context) + } +} +// match the link version payload + +/* cspell:disable-next-line */ +const linkPayload = /^https?:\/\/mask(\.io|book\.com)\/\?postdata_v/i +// match the text version payload +const textPayload = /(\u{1F3BC}[\w+/=|]+:\|\|)/giu diff --git a/packages/typed-message/base/transformer/ParseLink.ts b/packages/typed-message/base/transformer/ParseLink.ts new file mode 100644 index 000000000000..79550fc17009 --- /dev/null +++ b/packages/typed-message/base/transformer/ParseLink.ts @@ -0,0 +1,21 @@ +import type { TypedMessage } from '../base' +import { isTypedMessageText, makeTypedMessageTupleSerializable, makeTypedMessageText } from '../core' +import { makeTypedMessageAnchor } from '../extension' +import { parseLink } from '../utils/parseLink' +import { visitEachTypedMessageChild } from '../visitor' +import type { TransformationContext } from './context' + +export function ParseLinkTransformer(message: TypedMessage, context: TransformationContext): TypedMessage { + if (isTypedMessageText(message)) { + const parsed = parseLink(message.content) + if (parsed.length === 1 && parsed[0].type === 'text') return message + return makeTypedMessageTupleSerializable( + parsed.map((i) => { + if (i.type === 'text') return makeTypedMessageText(i.content) + return makeTypedMessageAnchor(i.category, i.content, i.content) + }), + message.meta, + ) + } + return visitEachTypedMessageChild(message, ParseLinkTransformer, context) +} diff --git a/packages/typed-message/base/transformer/composed.ts b/packages/typed-message/base/transformer/composed.ts new file mode 100644 index 000000000000..e0ef953527e2 --- /dev/null +++ b/packages/typed-message/base/transformer/composed.ts @@ -0,0 +1,57 @@ +import type { TypedMessage } from '../base' +import type { TransformationContext } from './context' +import type { Transformer } from './index' +export interface ComposedTransformers { + subscription: { + getCurrentValue: () => Transformer + subscribe(f: () => void): () => void + } + addTransformer( + transformer: Transformer | ComposedTransformers['subscription'], + priority: number, + signal?: AbortSignal, + ): () => void +} +export function composeTransformers(): ComposedTransformers { + const event = new EventTarget() + const onUpdate = () => event.dispatchEvent(new Event('update')) + const transformers = new Set() + + function composed(message: TypedMessage, context: TransformationContext) { + return [...transformers].sort((a, b) => a[1] - b[1]).reduce((p, [c]) => c(p, context), message) + } + + const subscription = { + getCurrentValue: (): Transformer => (message, context) => composed(message, context), + subscribe(f: () => void) { + event.addEventListener('update', f) + return () => { + event.removeEventListener('update', f) + } + }, + } + return { + subscription, + addTransformer(t, priority, signal) { + const f_priority = [ + typeof t === 'function' + ? t + : (message: TypedMessage, context: TransformationContext) => t.getCurrentValue()(message, context), + priority, + ] as const + transformers.add(f_priority) + onUpdate() + + const cancelSubscription = typeof t === 'function' ? () => {} : t.subscribe(onUpdate) + + const remove = () => { + transformers.delete(f_priority) + cancelSubscription() + onUpdate() + } + + signal?.addEventListener('abort', remove) + return remove + }, + } +} diff --git a/packages/typed-message/base/transformer/context.ts b/packages/typed-message/base/transformer/context.ts new file mode 100644 index 000000000000..dcfeb52d9ed1 --- /dev/null +++ b/packages/typed-message/base/transformer/context.ts @@ -0,0 +1,15 @@ +export interface TransformationContext { + skipMaskPayloadTransform?: boolean + authorHint?: { network: string; userId: string } + currentProfile?: { network: string; userId: string } + postURL?: string + reportDecryptedInfo?( + iv?: Uint8Array, + claimedAuthor?: { network: string; userId: string }, + publicShared?: boolean, + ): void +} +export function createTransformationContext(): TransformationContext { + return Object.freeze({}) +} +export const emptyTransformationContext = createTransformationContext() diff --git a/packages/typed-message/base/transformer/index.ts b/packages/typed-message/base/transformer/index.ts new file mode 100644 index 000000000000..4c9128e560bc --- /dev/null +++ b/packages/typed-message/base/transformer/index.ts @@ -0,0 +1,8 @@ +import type { TypedMessage } from '../base' +import type { TransformationContext } from './context' +export type Transformer = (message: TypedMessage, context: TransformationContext) => TypedMessage +export * from './composed' +export * from './context' +export { FlattenTypedMessage } from './Flatten' +export { ParseLinkTransformer } from './ParseLink' +export { createMaskPayloadTransform } from './MaskPayload' diff --git a/packages/typed-message/base/tsconfig.json b/packages/typed-message/base/tsconfig.json new file mode 100644 index 000000000000..c2405a737449 --- /dev/null +++ b/packages/typed-message/base/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "../base/", + "outDir": "../dist/base", + "tsBuildInfoFile": "../dist/base.tsbuildinfo" + }, + "include": ["./"], + "references": [] +} diff --git a/packages/typed-message/base/utils/extract.ts b/packages/typed-message/base/utils/extract.ts new file mode 100644 index 000000000000..de84bef18d48 --- /dev/null +++ b/packages/typed-message/base/utils/extract.ts @@ -0,0 +1,41 @@ +import type { TypedMessage } from '../base' +import { isTypedMessageText, isTypedMessageImage } from '../core' +import { isTypedMessageAnchor } from '../extension' +import { Option, Some, None } from 'ts-results' +import { forEachTypedMessageChild } from '../visitor' + +/** + * Get inner text from a TypedMessage + * @param message message + */ +export function extractTextFromTypedMessage( + message: TypedMessage | null, + options?: { linkAsText: boolean }, +): Option { + if (!message) return None + + const text: string[] = [] + function visitor(message: TypedMessage) { + if (isTypedMessageText(message)) text.push(message.content) + else if (isTypedMessageAnchor(message)) { + text.push(message.content) + if (options?.linkAsText) text.push(`(${message.href})`) + } else forEachTypedMessageChild(message, visitor) + } + visitor(message) + forEachTypedMessageChild(message, visitor) + if (text.length) return Some(text.join(' ')) + return None +} +export function extractImageFromTypedMessage(message: TypedMessage | null): (string | Blob)[] { + if (!message) return [] + + const image: (string | Blob)[] = [] + function visitor(message: TypedMessage): void { + if (isTypedMessageImage(message)) return void image.push(message.image) + return forEachTypedMessageChild(message, visitor) + } + visitor(message) + forEachTypedMessageChild(message, visitor) + return image +} diff --git a/packages/typed-message/base/utils/index.ts b/packages/typed-message/base/utils/index.ts new file mode 100644 index 000000000000..0e491e29c3c0 --- /dev/null +++ b/packages/typed-message/base/utils/index.ts @@ -0,0 +1,31 @@ +export * from './metadata' +export * from './extract' + +import type { + TypedMessage, + SerializableTypedMessages, + SerializableTypedMessage, + NonSerializableWithAltTypedMessage, +} from '../base' +import { isEqual } from 'lodash-unified' + +export function isNonSerializableTypedMessageWithAlt(x: TypedMessage): x is NonSerializableWithAltTypedMessage { + const y = x as NonSerializableWithAltTypedMessage + if (y.serializable !== false) return false + if (!y.alt) return false + return isSerializableTypedMessage(y.alt) +} +export function isSerializableTypedMessage(x: TypedMessage): x is SerializableTypedMessages { + if ((x as SerializableTypedMessage).serializable) return true + return isNonSerializableTypedMessageWithAlt(x) +} + +/** + * This is a tree compare algorithm, may need to find a more efficient one from NPM + */ +export function isTypedMessageEqual(message1: TypedMessage, message2: TypedMessage): boolean { + if (message1.type !== message2.type) return false + if (message1.meta !== message2.meta) return false + // perform a deep equal + return isEqual(message1, message2) +} diff --git a/packages/shared-base/src/TypedMessage/utils/internal.ts b/packages/typed-message/base/utils/internal.ts similarity index 85% rename from packages/shared-base/src/TypedMessage/utils/internal.ts rename to packages/typed-message/base/utils/internal.ts index 69f16b807b3e..00513963ae3d 100644 --- a/packages/shared-base/src/TypedMessage/utils/internal.ts +++ b/packages/typed-message/base/utils/internal.ts @@ -1,7 +1,7 @@ import type { SerializableTypedMessage, TypedMessage } from '../base' /** @internal */ -export function createIsType(x: string, version?: number) { +export function createIsType(x: T['type'], version?: number) { return (y: TypedMessage): y is T => { if (version !== undefined && (y as SerializableTypedMessage).version !== version) return false return y.type === x @@ -12,6 +12,7 @@ export function composeSome(...fns: ((...args: Args) => bool return (...args: Args) => fns.some((f) => f(...args)) } +/** @internal */ export function composeEvery(...fns: ((...args: Args) => boolean)[]) { return (...args: Args) => fns.every((f) => f(...args)) } diff --git a/packages/shared-base/src/TypedMessage/metadata.ts b/packages/typed-message/base/utils/metadata.ts similarity index 96% rename from packages/shared-base/src/TypedMessage/metadata.ts rename to packages/typed-message/base/utils/metadata.ts index 731bcc89cd43..e535e680c65c 100644 --- a/packages/shared-base/src/TypedMessage/metadata.ts +++ b/packages/typed-message/base/utils/metadata.ts @@ -1,5 +1,5 @@ import { Result, Ok, Err, Some, Option, None } from 'ts-results' -import type { TypedMessage } from './base' +import type { TypedMessage } from '../base' import z_schema from 'z-schema' import draft, { enableMapSet, Draft } from 'immer' enableMapSet() @@ -8,17 +8,20 @@ const metadataSchemaStore = new Map() export function getKnownMetadataKeys() { return [...metadataSchemaStore.keys()] } + export function getMetadataSchema(key: string): Option { return metadataSchemaStore.has(key) ? Some(metadataSchemaStore.get(key)!) : None } + /** - * Register your metadata with a JSON Schema so Mask can validate the schema for you. + * Register your metadata with a JSON Schema so we can validate the schema for you. * @param key Metadata key * @param jsonSchema JSON Schema to validate the metadata */ export function registerMetadataSchema(key: string, jsonSchema: object) { metadataSchemaStore.set(key, jsonSchema) } + /** * Create a TypedMessage metadata reader for your plugin * @param key Metadata key @@ -31,6 +34,7 @@ export function createTypedMessageMetadataReader(key: string, jsonSchema?: ob if (jsonSchema) registerMetadataSchema(key, jsonSchema) return (meta: TypedMessage['meta']) => readTypedMessageMetadataUntyped(meta, key) } + /** * The raw parser of metadata reader * @param meta Metadata object diff --git a/packages/typed-message/base/utils/parseLink.ts b/packages/typed-message/base/utils/parseLink.ts new file mode 100644 index 000000000000..7d4e1333ef43 --- /dev/null +++ b/packages/typed-message/base/utils/parseLink.ts @@ -0,0 +1,50 @@ +import _anchorme from 'anchorme' +import type { TypedMessageAnchor } from '../extension' +// ESM/CJS compat +const anchorme = ((_anchorme as any).default || _anchorme) as typeof _anchorme + +export type ParseLinkResult = + | { + type: 'text' + content: string + } + | { + type: 'link' + content: string + category: TypedMessageAnchor['category'] + } + +export function parseLink(text: string): ParseLinkResult[] { + const parsed = anchorme.list(text) + + const result: ParseLinkResult[] = [] + + let start = 0 + for (const x of parsed) { + if (x.isURL) { + result.push({ type: 'text', content: text.slice(start, x.start) }) + result.push({ type: 'link', content: x.string, category: 'normal' }) + } else { + result.push({ type: 'text', content: text.slice(start, x.end) }) + } + start = x.end + } + result.push({ type: 'text', content: text.slice(start, text.length) }) + return result.filter((x) => x.content).flatMap((x) => (x.type === 'text' ? parseTag(x.content) : x)) +} + +const TagLike = /([#$@][\w\-]+)/g +const map = { + '@': 'user', + '#': 'hash', + $: 'cash', +} as const +function parseTag(x: string): ParseLinkResult[] { + return x + .split(TagLike) + .map((x) => + TagLike.test(x) + ? { type: 'link', content: x, category: map[x[0] as keyof typeof map] || 'normal' } + : { type: 'text', content: x }, + ) +} diff --git a/packages/typed-message/base/visitor/forEachChild.ts b/packages/typed-message/base/visitor/forEachChild.ts new file mode 100644 index 000000000000..db644388bb25 --- /dev/null +++ b/packages/typed-message/base/visitor/forEachChild.ts @@ -0,0 +1,22 @@ +import { isTypedMessagePromise, isTypedMessageTuple } from '../core' +import { isNonSerializableTypedMessageWithAlt } from '../utils' +import type { TypedMessage } from '../base' +import { isTypedMessageMaskPayload } from '../extension' + +export function forEachTypedMessageChild(node: TypedMessage, visitor: (x: TypedMessage) => void | 'stop') { + let stop: void | 'stop' + if (isTypedMessageTuple(node)) { + for (const each of node.items) { + stop = visitor(each) + if (stop) return + } + } else if (isTypedMessagePromise(node)) { + // if Promise has a resolved value, we ignore it's alt. + if (node.promise.value) visitor(node.promise.value) + else if (node.alt) visitor(node.alt) + } else if (isTypedMessageMaskPayload(node)) { + visitor(node.message) + } else if (isNonSerializableTypedMessageWithAlt(node)) { + visitor(node.alt) + } +} diff --git a/packages/typed-message/base/visitor/index.ts b/packages/typed-message/base/visitor/index.ts new file mode 100644 index 000000000000..c5fdbce1dec9 --- /dev/null +++ b/packages/typed-message/base/visitor/index.ts @@ -0,0 +1,2 @@ +export * from './visitEachChild' +export * from './forEachChild' diff --git a/packages/typed-message/base/visitor/visitEachChild.ts b/packages/typed-message/base/visitor/visitEachChild.ts new file mode 100644 index 000000000000..f077226fe6ee --- /dev/null +++ b/packages/typed-message/base/visitor/visitEachChild.ts @@ -0,0 +1,53 @@ +import { + isTypedMessagePromise, + isTypedMessageTuple, + makeTypedMessagePromise, + makeTypedMessageTuple, + makeTypedMessageTupleSerializable, +} from '../core' +import { isNonSerializableTypedMessageWithAlt, isSerializableTypedMessage } from '../utils' +import type { TypedMessage } from '../base' +import { isTypedMessageMaskPayload, makeTypedMessageMaskPayload } from '../extension' +import type { TransformationContext, Transformer } from '../transformer' + +export function visitEachTypedMessageChild( + node: TypedMessage, + visitor: Transformer, + context: TransformationContext, +): TypedMessage { + if (isTypedMessageTuple(node)) { + const after = node.items.flatMap((x) => { + const next = visitor(x, context) + if (isTypedMessageTuple(next)) return next.items + return next + }) + if (after.every(isSerializableTypedMessage)) { + return makeTypedMessageTupleSerializable(after, node.meta) + } + return makeTypedMessageTuple(after, node.meta) + } else if (isTypedMessagePromise(node)) { + // we ignore alt if promise is resolved. + if (node.promise.value) return visitor(node.promise.value, context) + else if (node.alt) return makeTypedMessagePromise(node.promise, visitor(node.alt, context)) + return node + } else if (isTypedMessageMaskPayload(node)) { + const next = visitor(node.message, context) + return makeTypedMessageMaskPayload(next, node.meta) + } else if (isNonSerializableTypedMessageWithAlt(node)) { + const alt = visitor(node.alt, context) + if (!isSerializableTypedMessage(alt)) { + console.warn( + '[@masknet/typed-message] You must return a serializable message in this position. Original:', + node, + 'Transformed child "alt":', + alt, + ) + // ignore the transform result + return node + } + return { ...node, alt } as TypedMessage + } else { + // return node with no child + return node + } +} diff --git a/packages/typed-message/dom/Renderer/Core/Image.tsx b/packages/typed-message/dom/Renderer/Core/Image.tsx new file mode 100644 index 000000000000..441fcbf46749 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Image.tsx @@ -0,0 +1,31 @@ +import { memo, useContext, useEffect, useState } from 'react' +import type { TypedMessageImage } from '../../../base' +import { useMetadataRender } from '../MetadataRender' +import { RenderFragmentsContext, DefaultRenderFragments } from '../utils/RenderFragments' + +export const TypedMessageImageRenderer = memo(function TypedMessageImageRenderer(props: TypedMessageImage) { + const { Image = DefaultRenderFragments.Image } = useContext(RenderFragmentsContext) + const { image, width, height } = props + const [blobSrc, setBlobSrc] = useState(null) + + useEffect(() => { + if (typeof image === 'string') return + const src = URL.createObjectURL(image) + setBlobSrc(src) + return () => { + setBlobSrc(null) + URL.revokeObjectURL(src) + } + }, [image]) + + const finalSrc = blobSrc || image + const meta = useMetadataRender(props) + if (typeof finalSrc !== 'string') return null + + return ( + <> + + {meta} + + ) +}) diff --git a/packages/typed-message/dom/Renderer/Core/Promise.tsx b/packages/typed-message/dom/Renderer/Core/Promise.tsx new file mode 100644 index 000000000000..dc18a89e7eb3 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Promise.tsx @@ -0,0 +1,25 @@ +import { memo, Suspense, useEffect, useState } from 'react' +import type { TypedMessagePromise } from '../../../base' +import { TypedMessageRenderInline } from '../Entry' +import { useTransformedValue } from '../utils/TransformContext' +export const TypedMessagePromiseRenderer = memo(function TypedMessagePromiseRenderer(props: TypedMessagePromise) { + const { promise, alt } = props + const _ = useState(0)[1] + const rerender = () => _(Math.random()) + + useEffect(() => { + promise.then(rerender) + }, [promise, _]) + + const transformedValue = useTransformedValue(promise.value) + if (transformedValue) return + return ( + : null}> + + + ) +}) + +function Await(props: { promise: Promise }): JSX.Element { + throw props.promise +} diff --git a/packages/typed-message/dom/Renderer/Core/Text.tsx b/packages/typed-message/dom/Renderer/Core/Text.tsx new file mode 100644 index 000000000000..5694c585d15a --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Text.tsx @@ -0,0 +1,14 @@ +import { memo } from 'react' +import type { TypedMessageText } from '../../../base' +import { RenderTextFragment } from '../utils/renderText' +import { useMetadataRender } from '../MetadataRender' + +export const TypedMessageTextRenderer = memo(function TypedMessageTextRenderer(props: TypedMessageText) { + const { content } = props + return ( + <> + + {useMetadataRender(props)} + + ) +}) diff --git a/packages/typed-message/dom/Renderer/Core/Tuple.tsx b/packages/typed-message/dom/Renderer/Core/Tuple.tsx new file mode 100644 index 000000000000..5f84d22298e6 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Tuple.tsx @@ -0,0 +1,18 @@ +import { memo, useMemo } from 'react' +import type { TypedMessageTuple } from '../../../base' +import { hasCircular } from '../utils/circularDetect' +import { TypedMessageRenderInline } from '../Entry' +import { useMetadataRender } from '../MetadataRender' + +export const TypedMessageTupleRenderer = memo(function TypedMessageTupleRenderer(props: TypedMessageTuple) { + const meta = useMetadataRender(props) + if (useMemo(() => hasCircular(props.items), [props.items])) return null + return ( + <> + {props.items.map((message, index) => ( + + ))} + {meta} + + ) +}) diff --git a/packages/typed-message/dom/Renderer/Core/Unknown.tsx b/packages/typed-message/dom/Renderer/Core/Unknown.tsx new file mode 100644 index 000000000000..a9c49991fea5 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Unknown.tsx @@ -0,0 +1,15 @@ +import { memo, useRef } from 'react' +import type { TypedMessage } from '../../../base' + +/** @internal */ +export const TypedMessageUnknownRenderer = memo(function TypedMessageUnknownRenderer(props: TypedMessage) { + const warned = useRef(false) + if (!warned.current) { + warned.current = true + console.warn( + '[@masknet/typed-message] Trying to render an unknown TypedMessage (or a known TypedMessage with no renderer) with props', + props, + ) + } + return null +}) diff --git a/packages/typed-message/dom/Renderer/Entry.tsx b/packages/typed-message/dom/Renderer/Entry.tsx new file mode 100644 index 000000000000..722a3a707a84 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Entry.tsx @@ -0,0 +1,45 @@ +import { useContext } from 'react' +import type { TypedMessage } from '../../base' +import { TypedMessageUnknownRenderer as TypedMessageUnknownRender } from './Core/Unknown' +import { RegistryContext } from './utils/RegistryContext' +import { RenderFragmentsContext } from './utils/RenderFragments' +import { useTextResize } from './utils/TextResizerContext' +export interface RenderProps { + message: TypedMessage +} +export function TypedMessageRender(props: RenderProps) { + const { Container = 'p' } = useContext(RenderFragmentsContext) + const isEmpty = props.message.type === 'empty' + const textResize = useTextResize(!isEmpty) + + if (isEmpty) return null + + return ( + + + + + + ) +} + +export function TypedMessageRenderInline(props: RenderProps) { + const { message } = props + + const Registry = useContext(RegistryContext) + if (message.type === 'empty') return null + + const Render = Registry(message.type)?.component || TypedMessageUnknownRender + if (process.env.NODE_ENV === 'development') { + return ( + { + if (ref) (ref as any).message = message + }}> + + + ) + } + return +} diff --git a/packages/typed-message/dom/Renderer/Extension/Anchor.tsx b/packages/typed-message/dom/Renderer/Extension/Anchor.tsx new file mode 100644 index 000000000000..f07c3b70a6f2 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Extension/Anchor.tsx @@ -0,0 +1,7 @@ +import { memo } from 'react' +import type { TypedMessageAnchor } from '../../../base' +import { RenderLinkFragment } from '../utils/renderText' + +export const TypedMessageAnchorRenderer = memo(function TypedMessageAnchorRenderer(props: TypedMessageAnchor) { + return +}) diff --git a/packages/typed-message/dom/Renderer/MetadataRender.tsx b/packages/typed-message/dom/Renderer/MetadataRender.tsx new file mode 100644 index 000000000000..cbbf057b537a --- /dev/null +++ b/packages/typed-message/dom/Renderer/MetadataRender.tsx @@ -0,0 +1,13 @@ +import { useContext } from 'react' +import { RenderFragmentsContext } from './utils/RenderFragments' +import type { TypedMessage } from '../../base' + +export interface MetadataRenderProps { + metadata: TypedMessage['meta'] + message: TypedMessage +} +export function useMetadataRender(message: TypedMessage) { + const { Metadata } = useContext(RenderFragmentsContext) + if (!Metadata || !message.meta) return null + return +} diff --git a/packages/typed-message/dom/Renderer/registry.tsx b/packages/typed-message/dom/Renderer/registry.tsx new file mode 100644 index 000000000000..f6d91cde2c59 --- /dev/null +++ b/packages/typed-message/dom/Renderer/registry.tsx @@ -0,0 +1,93 @@ +import type { + TypedMessage, + TypedMessageText, + TypedMessageImage, + TypedMessageUnknown, + TypedMessageAnchor, + TypedMessageTuple, + TypedMessagePromise, +} from '../../base' +import { TypedMessageTextRenderer } from './Core/Text' +import { TypedMessageImageRenderer } from './Core/Image' +import { TypedMessageTupleRenderer } from './Core/Tuple' +import { TypedMessagePromiseRenderer } from './Core/Promise' +import { TypedMessageUnknownRenderer } from './Core/Unknown' +import { TypedMessageAnchorRenderer } from './Extension/Anchor' + +export interface RenderConfig { + component: React.ComponentType + priority: number + id: symbol +} + +export function createTypedMessageRenderRegistry() { + const registry = new Map>>() + const event = new EventTarget() + + function registerTypedMessageRender(type: T['type'], config: RenderConfig) { + if (!registry.has(type)) registry.set(type, new Map()) + const map = registry.get(type)! + + const id = config.id + map.set(id, config) + event.dispatchEvent(new Event('update')) + return () => { + map.delete(id) + event.dispatchEvent(new Event('update')) + } + } + function getTypedMessageRender(type: T['type']): RenderConfig | undefined { + return Array.from(registry.get(type)?.values() || []).sort((a, b) => b.priority - a.priority)[0] + } + const subscription = { + // generate a new function everytime to make sure old !== new + getCurrentValue: (): typeof getTypedMessageRender => (type) => getTypedMessageRender(type), + subscribe: (f: () => void) => { + event.addEventListener('update', f) + return () => event.removeEventListener('update', f) + }, + } + + registerTypedMessageRender('text', { + component: TypedMessageTextRenderer, + id: Symbol('std.text'), + priority: 0, + }) + + registerTypedMessageRender('image', { + component: TypedMessageImageRenderer, + id: Symbol('std.image'), + priority: 0, + }) + + registerTypedMessageRender('unknown', { + component: TypedMessageUnknownRenderer, + id: Symbol('std.unknown'), + priority: 0, + }) + + registerTypedMessageRender('tuple', { + component: TypedMessageTupleRenderer, + id: Symbol('std.tuple'), + priority: 0, + }) + + registerTypedMessageRender('promise', { + component: TypedMessagePromiseRenderer, + id: Symbol('std.promise'), + priority: 0, + }) + + // Extension + registerTypedMessageRender('x-anchor', { + component: TypedMessageAnchorRenderer, + id: Symbol('std.anchor'), + priority: 0, + }) + + return { + registerTypedMessageRender, + getTypedMessageRender, + subscription, + } +} diff --git a/packages/typed-message/dom/Renderer/utils/RegistryContext.tsx b/packages/typed-message/dom/Renderer/utils/RegistryContext.tsx new file mode 100644 index 000000000000..df5c6e28c092 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/RegistryContext.tsx @@ -0,0 +1,9 @@ +import { createContext } from 'react' +import type { RenderConfig } from '../registry' + +export const RegistryContext = createContext<(type: string) => undefined | RenderConfig>(() => { + console.error( + '[@masknet/typed-message] Please create a TypedMessageRenderRegistry and provide it via RegistryContext', + ) + return undefined +}) diff --git a/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx b/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx new file mode 100644 index 000000000000..7641a3393c7b --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx @@ -0,0 +1,53 @@ +import { createContext, memo } from 'react' +import type { MetadataRenderProps } from '../MetadataRender' + +export const DefaultRenderFragments = { + Text: memo((props: RenderFragmentsContextType.TextProps) => <>{props.children}), + Link: memo((props: RenderFragmentsContextType.LinkProps) => ( + + {props.children} + + )), + Image: memo((props: RenderFragmentsContextType.ImageProps) => + props.width === 0 ? null : , + ), + Metadata: memo(() => null), +} +export interface RenderFragmentsContextType { + Container?: React.ComponentType<{}> + Text?: React.ComponentType + Image?: React.ComponentType + Metadata?: React.ComponentType + Link?: React.ComponentType + /** #topic */ + HashLink?: React.ComponentType + /** @user */ + AtLink?: React.ComponentType + /** $CASH */ + CashLink?: React.ComponentType +} +export namespace RenderFragmentsContextType { + export interface TextProps { + children: string + } + export interface LinkProps { + children: string + href: string + } + export interface AtLinkProps { + children: string + } + export interface CashLinkProps { + children: string + } + export interface HashLinkProps { + children: string + } + export interface ImageProps { + src: string + width?: number + height?: number + aspectRatio?: number + } +} +export const RenderFragmentsContext = createContext(DefaultRenderFragments) diff --git a/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx b/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx new file mode 100644 index 000000000000..aaa260cbd244 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx @@ -0,0 +1,28 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +export const TextResizeContext = createContext(false) +/** @internal */ +export function useTextResize(shouldEnable: boolean) { + const hasTextEnlarge = useContext(TextResizeContext) + const [element, setElement] = useState(null) + const enable = hasTextEnlarge && shouldEnable + + useEffect(() => { + if (!element || !enable) return + + const updateFontSize = () => { + const length = Array.from(element.innerText).length + let fontSize = 1 + if (length < 45) fontSize = 1.5 + else if (length < 80) fontSize = 1.2 + + element.style.fontSize = `${fontSize}rem` + } + updateFontSize() + + // const watcher = new MutationObserver(updateFontSize) + // watcher.observe(element, { subtree: true, childList: true, characterData: true }) + // return () => watcher.disconnect() + }, [enable, element]) + return setElement +} diff --git a/packages/typed-message/dom/Renderer/utils/TransformContext.ts b/packages/typed-message/dom/Renderer/utils/TransformContext.ts new file mode 100644 index 000000000000..144d8d5d4299 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/TransformContext.ts @@ -0,0 +1,14 @@ +import { Transformer, TypedMessage, emptyTransformationContext, TransformationContext } from '../../../base' +import { createContext, useContext, useMemo } from 'react' +export const TransformerProvider = createContext((x) => x) +export const TransformationContextProvider = createContext(emptyTransformationContext) + +export function useTransformedValue(message: TypedMessage): TypedMessage +export function useTransformedValue(message: undefined | TypedMessage): TypedMessage | undefined +export function useTransformedValue(message: undefined | TypedMessage): TypedMessage | undefined { + const transformer = useContext(TransformerProvider) + const context = useContext(TransformationContextProvider) + return useMemo(() => { + return message ? transformer(message, context) : undefined + }, [message, transformer, context]) +} diff --git a/packages/typed-message/dom/Renderer/utils/circularDetect.ts b/packages/typed-message/dom/Renderer/utils/circularDetect.ts new file mode 100644 index 000000000000..25292ac11d1a --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/circularDetect.ts @@ -0,0 +1,13 @@ +export function hasCircular(message: object) { + try { + JSON.stringify(message) + return false + } catch (err) { + console.warn( + '[@masknet/typed-message] TypedMessage', + message, + 'may contains circular structure. Skip rendering.', + ) + return true + } +} diff --git a/packages/typed-message/dom/Renderer/utils/renderText.tsx b/packages/typed-message/dom/Renderer/utils/renderText.tsx new file mode 100644 index 000000000000..92d59c50429b --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -0,0 +1,45 @@ +import { memo, Fragment, createElement, useContext } from 'react' +import { RenderFragmentsContext, RenderFragmentsContextType, DefaultRenderFragments } from './RenderFragments' +import type { TypedMessageAnchor } from '../../../base' +import { parseLink } from '../../../base/utils/parseLink' + +/** @internal */ +export interface RenderTextProps { + text: string +} + +/** @internal */ +export const RenderTextFragment = memo(function RenderText(props: RenderTextProps) { + const { Text = DefaultRenderFragments.Text } = useContext(RenderFragmentsContext) + return createElement(Fragment, {}, ...parseText(props.text, Text)) +}) + +/** @internal */ +export const RenderLinkFragment = memo(function RenderLink( + props: Pick & RenderFragmentsContextType.LinkProps, +) { + const { children, href, category } = props + const context = useContext(RenderFragmentsContext) + const { + Text = DefaultRenderFragments.Text, + Link = DefaultRenderFragments.Link, + AtLink = Text, + CashLink = Text, + HashLink = Text, + } = context + if (category === 'cash') return + if (category === 'hash') return + if (category === 'user') return + return +}) + +function parseText(string: string, Text: NonNullable) { + const links = parseLink(string).flatMap((x) => { + if (x.type === 'text') { + return x.content.split(/(\n)/g).map((x) => (x === '\n' ?
: )) + } + if (x.category === 'normal' && !x.content.match(/^https?:\/\//gi)) x.content = 'http://' + x.content + return + }) + return links +} diff --git a/packages/typed-message/dom/index.ts b/packages/typed-message/dom/index.ts new file mode 100644 index 000000000000..6e86b70a8684 --- /dev/null +++ b/packages/typed-message/dom/index.ts @@ -0,0 +1,22 @@ +// Render +export { TypedMessageRender, TypedMessageRenderInline, type RenderProps } from './Renderer/Entry' +export { useMetadataRender, type MetadataRenderProps } from './Renderer/MetadataRender' + +// Render behavior +export { TextResizeContext } from './Renderer/utils/TextResizerContext' + +// Render Registry +export { type RenderConfig, createTypedMessageRenderRegistry } from './Renderer/registry' +export { + RenderFragmentsContext, + type RenderFragmentsContextType, + DefaultRenderFragments, +} from './Renderer/utils/RenderFragments' +export { RegistryContext } from './Renderer/utils/RegistryContext' + +// Transformation +export { + TransformerProvider, + useTransformedValue, + TransformationContextProvider, +} from './Renderer/utils/TransformContext' diff --git a/packages/typed-message/dom/tsconfig.json b/packages/typed-message/dom/tsconfig.json new file mode 100644 index 000000000000..3e753ffac6e6 --- /dev/null +++ b/packages/typed-message/dom/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "../dom/", + "outDir": "../dist/dom", + "tsBuildInfoFile": "../dist/dom.tsbuildinfo" + }, + "include": ["./"], + "references": [{ "path": "../base/" }] +} diff --git a/packages/typed-message/package.json b/packages/typed-message/package.json new file mode 100644 index 000000000000..a6449cac25e8 --- /dev/null +++ b/packages/typed-message/package.json @@ -0,0 +1,18 @@ +{ + "name": "@masknet/typed-message", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": {}, + "dependencies": { + "@msgpack/msgpack": "^2.7.2", + "@mui/material": "link:..\\empty", + "anchorme": "^2.1.2", + "immer": "^9.0.12", + "z-schema": "^5.0.2" + }, + "exports": { + ".": "./dist/base/index.js", + "./dom": "./dist/dom/index.js" + } +} diff --git a/packages/typed-message/tests/.gitkeep b/packages/typed-message/tests/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/web3-shared/base/tsconfig.json b/packages/web3-shared/base/tsconfig.json index 07353b9b0ada..0645b741df7c 100644 --- a/packages/web3-shared/base/tsconfig.json +++ b/packages/web3-shared/base/tsconfig.json @@ -5,6 +5,6 @@ "outDir": "./dist", "tsBuildInfoFile": "./dist/src.tsbuildinfo" }, - "include": ["./src", "./src/env.d.ts", "./src/**/*.json"], + "include": ["./src", "./src/**/*.json"], "references": [{ "path": "../../injected-script/sdk" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44722d9ab429..4a208fc76a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,10 +252,12 @@ importers: specifiers: '@dimensiondev/stego-js': 0.11.1-20201027083223-8ab41be '@masknet/shared-base': workspace:* + '@masknet/typed-message': workspace:* '@msgpack/msgpack': ^2.7.2 dependencies: '@dimensiondev/stego-js': 0.11.1-20201027083223-8ab41be '@masknet/shared-base': link:../shared-base + '@masknet/typed-message': link:../typed-message '@msgpack/msgpack': 2.7.2 packages/external-plugin-previewer: @@ -326,6 +328,7 @@ importers: '@masknet/shared': workspace:* '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* + '@masknet/typed-message': workspace:* '@masknet/web3-contracts': workspace:* '@masknet/web3-providers': workspace:* '@masknet/web3-shared-base': workspace:* @@ -388,7 +391,6 @@ importers: jsbi: 3.1.4 json-stable-stringify: ^1.0.1 json2csv: ^5.0.6 - jsx-jsonml-devtools-renderer: ^1.4.3 millify: ^4.0.0 next-tick: ^1.0.0 opensea-js: ^1.2.7 @@ -462,6 +464,7 @@ importers: '@masknet/shared': link:../shared '@masknet/shared-base': link:../shared-base '@masknet/theme': link:../theme + '@masknet/typed-message': link:../typed-message '@masknet/web3-contracts': link:../web3-contracts '@masknet/web3-providers': link:../web3-providers '@masknet/web3-shared-base': link:../web3-shared/base @@ -518,7 +521,6 @@ importers: jsbi: 3.1.4 json-stable-stringify: 1.0.1 json2csv: 5.0.6 - jsx-jsonml-devtools-renderer: 1.4.3 millify: 4.0.0 next-tick: 1.1.0 opensea-js: 1.2.7 @@ -591,6 +593,7 @@ importers: '@masknet/shared': workspace:* '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* + '@masknet/typed-message': workspace:* '@masknet/web3-shared-base': workspace:* '@servie/events': ^3.0.0 '@types/use-subscription': ^1.0.0 @@ -605,6 +608,7 @@ importers: '@masknet/shared': link:../shared '@masknet/shared-base': link:../shared-base '@masknet/theme': link:../theme + '@masknet/typed-message': link:../typed-message '@masknet/web3-shared-base': link:../web3-shared/base '@servie/events': 3.0.0 '@types/use-subscription': 1.0.0 @@ -696,6 +700,7 @@ importers: '@masknet/shared': workspace:* '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* + '@masknet/typed-message': workspace:* '@masknet/web3-kit': workspace:* '@msgpack/msgpack': ^2.7.2 arweave: ^1.10.3 @@ -714,6 +719,7 @@ importers: '@masknet/shared': link:../../shared '@masknet/shared-base': link:../../shared-base '@masknet/theme': link:../../theme + '@masknet/typed-message': link:../../typed-message '@masknet/web3-kit': link:../../web3-kit '@msgpack/msgpack': 2.7.2 arweave: 1.10.13 @@ -1002,6 +1008,7 @@ importers: packages/shared-base: specifiers: '@dimensiondev/holoflows-kit': ^0.9.0-20210902104757-7c3d0d0 + '@masknet/typed-message': workspace:* '@msgpack/msgpack': ^2.7.2 '@mui/icons-material': link:..\empty '@mui/lab': link:..\empty @@ -1023,6 +1030,7 @@ importers: z-schema: ^5.0.2 dependencies: '@dimensiondev/holoflows-kit': 0.9.0-20210902104757-7c3d0d0 + '@masknet/typed-message': link:../typed-message '@msgpack/msgpack': 2.7.2 '@mui/icons-material': link:../empty '@mui/lab': link:../empty @@ -1099,6 +1107,20 @@ importers: '@storybook/react': 6.4.18_b824aaa144c344b0886eeabad621db2e babel-loader: 8.2.3_900eac683bef8a6b239b47efcf24e8a9 + packages/typed-message: + specifiers: + '@msgpack/msgpack': ^2.7.2 + '@mui/material': link:..\empty + anchorme: ^2.1.2 + immer: ^9.0.12 + z-schema: ^5.0.2 + dependencies: + '@msgpack/msgpack': 2.7.2 + '@mui/material': link:../empty + anchorme: 2.1.2 + immer: 9.0.12 + z-schema: 5.0.2 + packages/web3-constants: specifiers: {} diff --git a/tsconfig.json b/tsconfig.json index 045f3161e066..8c1c3d37347c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -85,6 +85,8 @@ "@masknet/web3-shared-evm": ["./packages/web3-shared/evm/src"], "@masknet/web3-shared-flow": ["./packages/web3-shared/flow/src"], "@masknet/web3-shared-solana": ["./packages/web3-shared/solana/src"], + "@masknet/typed-message": ["./packages/typed-message/base"], + "@masknet/typed-message/dom": ["./packages/typed-message/dom"], // @masknet/scripts: insert-here 2 // Plugins