From 34d6f5ddf8ca95b23403f6eeb3213e84f915d399 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 21 Feb 2022 11:37:54 +0800 Subject: [PATCH 01/18] refactor: add typed-message package --- packages/mask/.webpack/config.ts | 2 + packages/typed-message/README.md | 1 + packages/typed-message/base/base.ts | 42 +++++++ .../base/binary-encode/decode.ts | 60 +++++++++ .../base/binary-encode/encode.ts | 83 +++++++++++++ .../typed-message/base/binary-encode/index.ts | 2 + .../typed-message/base/binary-encode/type.ts | 5 + packages/typed-message/base/core/empty.ts | 21 ++++ packages/typed-message/base/core/image.ts | 30 +++++ packages/typed-message/base/core/index.ts | 34 ++++++ packages/typed-message/base/core/promise.ts | 26 ++++ packages/typed-message/base/core/text.ts | 24 ++++ packages/typed-message/base/core/tuple.ts | 47 +++++++ packages/typed-message/base/core/unknown.ts | 14 +++ .../base/deprecated-encode/index.ts | 36 ++++++ .../base/extension/MaskPayload.ts | 22 ++++ .../typed-message/base/extension/anchor.ts | 29 +++++ .../typed-message/base/extension/index.ts | 12 ++ packages/typed-message/base/index.ts | 15 +++ .../typed-message/base/transformer/Flatten.ts | 42 +++++++ .../base/transformer/MaskPayload/index.ts | 88 ++++++++++++++ .../base/transformer/ParseLink.ts | 21 ++++ .../base/transformer/composed.ts | 57 +++++++++ .../typed-message/base/transformer/context.ts | 15 +++ .../typed-message/base/transformer/index.ts | 8 ++ packages/typed-message/base/tsconfig.json | 10 ++ packages/typed-message/base/utils/extract.ts | 30 +++++ packages/typed-message/base/utils/index.ts | 38 ++++++ packages/typed-message/base/utils/internal.ts | 18 +++ packages/typed-message/base/utils/metadata.ts | 115 ++++++++++++++++++ .../typed-message/base/utils/parseLink.ts | 50 ++++++++ packages/typed-message/base/utils/promise.ts | 12 ++ .../base/visitor/forEachChild.ts | 18 +++ packages/typed-message/base/visitor/index.ts | 1 + .../base/visitor/visitEachChild.ts | 53 ++++++++ .../typed-message/dom/Renderer/Core/Image.tsx | 30 +++++ .../dom/Renderer/Core/Promise.tsx | 25 ++++ .../typed-message/dom/Renderer/Core/Text.tsx | 14 +++ .../typed-message/dom/Renderer/Core/Tuple.tsx | 17 +++ .../dom/Renderer/Core/Unknown.tsx | 15 +++ packages/typed-message/dom/Renderer/Entry.tsx | 26 ++++ .../dom/Renderer/Extension/Anchor.tsx | 14 +++ .../dom/Renderer/MetadataRender.tsx | 13 ++ .../typed-message/dom/Renderer/registry.tsx | 93 ++++++++++++++ .../dom/Renderer/utils/ComponentsContext.tsx | 40 ++++++ .../dom/Renderer/utils/RegistryContext.tsx | 9 ++ .../dom/Renderer/utils/TransformContext.ts | 15 +++ .../dom/Renderer/utils/circularDetect.ts | 13 ++ .../dom/Renderer/utils/renderText.tsx | 39 ++++++ packages/typed-message/dom/index.ts | 15 +++ packages/typed-message/dom/tsconfig.json | 10 ++ packages/typed-message/package.json | 18 +++ packages/typed-message/tests/.gitkeep | 0 pnpm-lock.yaml | 14 +++ tsconfig.json | 2 + 55 files changed, 1503 insertions(+) create mode 100644 packages/typed-message/README.md create mode 100644 packages/typed-message/base/base.ts create mode 100644 packages/typed-message/base/binary-encode/decode.ts create mode 100644 packages/typed-message/base/binary-encode/encode.ts create mode 100644 packages/typed-message/base/binary-encode/index.ts create mode 100644 packages/typed-message/base/binary-encode/type.ts create mode 100644 packages/typed-message/base/core/empty.ts create mode 100644 packages/typed-message/base/core/image.ts create mode 100644 packages/typed-message/base/core/index.ts create mode 100644 packages/typed-message/base/core/promise.ts create mode 100644 packages/typed-message/base/core/text.ts create mode 100644 packages/typed-message/base/core/tuple.ts create mode 100644 packages/typed-message/base/core/unknown.ts create mode 100644 packages/typed-message/base/deprecated-encode/index.ts create mode 100644 packages/typed-message/base/extension/MaskPayload.ts create mode 100644 packages/typed-message/base/extension/anchor.ts create mode 100644 packages/typed-message/base/extension/index.ts create mode 100644 packages/typed-message/base/index.ts create mode 100644 packages/typed-message/base/transformer/Flatten.ts create mode 100644 packages/typed-message/base/transformer/MaskPayload/index.ts create mode 100644 packages/typed-message/base/transformer/ParseLink.ts create mode 100644 packages/typed-message/base/transformer/composed.ts create mode 100644 packages/typed-message/base/transformer/context.ts create mode 100644 packages/typed-message/base/transformer/index.ts create mode 100644 packages/typed-message/base/tsconfig.json create mode 100644 packages/typed-message/base/utils/extract.ts create mode 100644 packages/typed-message/base/utils/index.ts create mode 100644 packages/typed-message/base/utils/internal.ts create mode 100644 packages/typed-message/base/utils/metadata.ts create mode 100644 packages/typed-message/base/utils/parseLink.ts create mode 100644 packages/typed-message/base/utils/promise.ts create mode 100644 packages/typed-message/base/visitor/forEachChild.ts create mode 100644 packages/typed-message/base/visitor/index.ts create mode 100644 packages/typed-message/base/visitor/visitEachChild.ts create mode 100644 packages/typed-message/dom/Renderer/Core/Image.tsx create mode 100644 packages/typed-message/dom/Renderer/Core/Promise.tsx create mode 100644 packages/typed-message/dom/Renderer/Core/Text.tsx create mode 100644 packages/typed-message/dom/Renderer/Core/Tuple.tsx create mode 100644 packages/typed-message/dom/Renderer/Core/Unknown.tsx create mode 100644 packages/typed-message/dom/Renderer/Entry.tsx create mode 100644 packages/typed-message/dom/Renderer/Extension/Anchor.tsx create mode 100644 packages/typed-message/dom/Renderer/MetadataRender.tsx create mode 100644 packages/typed-message/dom/Renderer/registry.tsx create mode 100644 packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx create mode 100644 packages/typed-message/dom/Renderer/utils/RegistryContext.tsx create mode 100644 packages/typed-message/dom/Renderer/utils/TransformContext.ts create mode 100644 packages/typed-message/dom/Renderer/utils/circularDetect.ts create mode 100644 packages/typed-message/dom/Renderer/utils/renderText.tsx create mode 100644 packages/typed-message/dom/index.ts create mode 100644 packages/typed-message/dom/tsconfig.json create mode 100644 packages/typed-message/package.json create mode 100644 packages/typed-message/tests/.gitkeep diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index ee28d8ebcb70..680d38633c88 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -91,6 +91,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': join(__dirname, '../../packages/typed-message/base'), + '@masknet/typed-message/dom': join(__dirname, '../../packages/typed-message/dom'), // @masknet/scripts: insert-here '@uniswap/v3-sdk': require.resolve('@uniswap/v3-sdk/dist/index.js'), } diff --git a/packages/typed-message/README.md b/packages/typed-message/README.md new file mode 100644 index 000000000000..b168286af627 --- /dev/null +++ b/packages/typed-message/README.md @@ -0,0 +1 @@ +# @masknet/typed-message \ No newline at end of file diff --git a/packages/typed-message/base/base.ts b/packages/typed-message/base/base.ts new file mode 100644 index 000000000000..5167aba19870 --- /dev/null +++ b/packages/typed-message/base/base.ts @@ -0,0 +1,42 @@ +export type Meta = ReadonlyMap +/** + * The basic structure that contains + */ +export interface TypedMessage { + /** + * The metadata this message contains. + * Not expected to display to normal users. + * + * It will be serialized to a JSON object. + */ + readonly meta?: ReadonlyMap + /** + * The type of this message. + * + * Custom message must starts with 'x-' (means extension). + */ + readonly type: string +} +export type SerializableTypedMessages = SerializableTypedMessage | NonSerializableWithAltTypedMessage +/** + * A TypedMessage which can be serialized safely into a JSON. + * If it containing a inner message, it must be serializable too. + */ +export interface SerializableTypedMessage extends TypedMessage { + readonly serializable: true + /** Since it is serializable, it must be version controlled. */ + readonly version: Version +} +/** + * A TypedMessage that is _not_ serializable but it provides a fallback which _is_ serializable. + */ +export interface NonSerializableWithAltTypedMessage extends TypedMessage { + readonly serializable: false + readonly alt: SerializableTypedMessages +} +/** + * A TypedMessage is _not_ serializable (not prepared or not intended to be). + */ +export interface NonSerializableTypedMessage extends TypedMessage { + readonly serializable: false +} diff --git a/packages/typed-message/base/binary-encode/decode.ts b/packages/typed-message/base/binary-encode/decode.ts new file mode 100644 index 000000000000..45fbac7f78a5 --- /dev/null +++ b/packages/typed-message/base/binary-encode/decode.ts @@ -0,0 +1,60 @@ +import { decode } from '@msgpack/msgpack' +import { Result } from 'ts-results' +import type { TypedMessage } from '../base' +import { makeTypedMessageText, makeTypedMessageTuple, makeTypedMessageUnknown } from '../core' +import { TypedMessageBinaryEncodingTypeEnum } from './type' + +const HEAD = '[@masknet/typed-message] ' +export function decodeTypedMessageFromDocument(bin: Uint8Array) { + return Result.wrap(() => { + const doc = decode(bin) + if (!Array.isArray(doc)) throw new Error(`${HEAD}Invalid document`) + const [docVer, message] = doc + if (typeof docVer !== 'number') throw new Error(`${HEAD}Invalid document`) + if (docVer !== 0) throw new Error(`${HEAD}Unknown document version`) + const result = decodeTypedMessage(message) + fixU8Array(result) + return result + }) +} +function decodeTypedMessage(tm: readonly unknown[]): TypedMessage { + const [type] = tm + if (typeof type !== 'string' && typeof type !== 'number') throw new TypeError(`${HEAD}Invalid TypedMessage`) + 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) + assertString(text) + return makeTypedMessageText(text, decodeMetadata(meta)) +} +function decodeTypedMessageTuple([, version, meta, items]: readonly unknown[]) { + assertNumber(version) + if (!Array.isArray(items)) throw new TypeError(`${HEAD}Invalid TypedMessageTuple`) + return makeTypedMessageTuple(items.map(decodeTypedMessage), decodeMetadata(meta)) +} +function decodeMetadata(meta: unknown): ReadonlyMap | undefined { + if (meta === null) return undefined + if (typeof meta !== 'object') throw new TypeError(`${HEAD}Invalid TypedMessage`) + if (Object.getPrototypeOf(meta) !== Object.prototype) throw new TypeError(`${HEAD}Invalid TypedMessage`) + return meta as any +} + +function assertNumber(x: unknown): asserts x is number { + if (typeof x !== 'number') throw new TypeError(`${HEAD}Invalid TypedMessage`) +} +function assertString(x: unknown): asserts x is string { + if (typeof x !== 'string') throw new TypeError(`${HEAD}Invalid TypedMessage`) +} +// Detach Uint8Array from it's underlying buffer +function fixU8Array(obj: unknown) { + // for Array and object + if (typeof obj === 'object' && obj !== null) { + for (const key in obj) { + const val = (obj as any)[key] + if (val instanceof Uint8Array) (obj as any)[key] = val.slice() + else fixU8Array(val) + } + } +} diff --git a/packages/typed-message/base/binary-encode/encode.ts b/packages/typed-message/base/binary-encode/encode.ts new file mode 100644 index 000000000000..81d4d65c5671 --- /dev/null +++ b/packages/typed-message/base/binary-encode/encode.ts @@ -0,0 +1,83 @@ +import { + isTypedMessageText, + isTypedMessageTupleSerializable, + TypedMessageText, + TypedMessageTupleSerializable, +} from '../core' +import type { SerializableTypedMessages, TypedMessage } from '../base' +import { encode } from '@msgpack/msgpack' +import { TypedMessageBinaryEncodingTypeEnum } from './type' + +const HEAD = '[@masknet/typed-message] ' +export function encodeTypedMessageToDocument(tm: SerializableTypedMessages) { + const doc = [0, encodeTypedMessage(tm)] + return encode(doc) +} +function encodeTypedMessage(tm: SerializableTypedMessages): any[] { + if (tm.serializable === false) { + if (tm.alt) return encodeTypedMessage(tm.alt) + throw new TypeError(`${HEAD}TypedMessage ${tm.type} does not support serialization.`) + } + if (isTypedMessageText(tm)) return encodeTypedMessageText(tm) + if (isTypedMessageTupleSerializable(tm)) return encodeTypedMessageTuple(tm) + throw new TypeError(`${HEAD}Unsupported TypedMessage ${tm.type} to be serialized.`) +} +function encodeTypedMessageText(tm: TypedMessageText): any[] { + // TODO: TextFormat + return [TypedMessageBinaryEncodingTypeEnum.Text, tm.version, encodeMeta(tm), tm.content] +} +function encodeTypedMessageTuple(tm: TypedMessageTupleSerializable): any[] { + return [TypedMessageBinaryEncodingTypeEnum.Tuple, tm.version, encodeMeta(tm), tm.items.map(encodeTypedMessage)] +} + +function encodeMeta(tm: TypedMessage) { + if (!tm.meta) return null + const record: Record = { __proto__: null } + for (const [key, val] of tm.meta) { + if (typeof key !== 'string') continue + if (typeof val === 'undefined') continue + try { + record[key] = collectValue(val) + } catch (err) { + console.warn(`${HEAD}key ${key} is dropped due to the error`, err) + } + } + return record +} +function collectValue(val: any): any { + try { + const type = typeof val + if (val === undefined) { + console.warn(`${HEAD}undefined converted to null.`) + return null + } + if (type === 'number' || val === null || type === 'boolean' || type === 'string') return val + + if (type === 'bigint' || type === 'function' || type === 'symbol') { + throw new TypeError(`${HEAD}Unsupported type ${type}`) + } + + if (val instanceof Uint8Array) return new Uint8Array(val) + if (val instanceof ArrayBuffer) { + throw new TypeError(`${HEAD}ArrayBuffer must be Uint8Array.`) + } + + if (Array.isArray(val)) return Array.from(val).map(collectValue) + + const proto = Object.getPrototypeOf(val) + if (proto !== Object.prototype && proto !== null) { + throw new TypeError(`${HEAD}unsupported value with a non trivial prototype.`) + } + + const result: Record = {} + for (const key in val) { + if (typeof key !== 'string') throw new TypeError(`${HEAD}Unsupported type symbol.`) + const v = val[key] + result[key] = collectValue(v) + } + return result + } catch (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/typed-message/base/deprecated-encode/index.ts b/packages/typed-message/base/deprecated-encode/index.ts new file mode 100644 index 000000000000..76a7e1ecb195 --- /dev/null +++ b/packages/typed-message/base/deprecated-encode/index.ts @@ -0,0 +1,36 @@ +import { None, Ok, Result } from 'ts-results' +import { makeTypedMessageText, TypedMessageText } from '../core' + +export function encodeTypedMessageV38Format(message: TypedMessageText) { + const encoder = new TextEncoder() + if (message.meta?.size) { + const rec = Object.fromEntries(message.meta.entries()) + return encoder.encode(`${JSON.stringify(rec)}\u{1F9E9}${message.content}`) + } + return encoder.encode(message.content) +} +export function decodeTypedMessageV38ToV40Format(raw: Uint8Array, version: -38 | -39 | -40) { + const decoder = new TextDecoder() + const text = Result.wrap(() => decoder.decode(raw)) + const { val, err } = text + if (err) return text + + if (version === -38) { + const maybeMetadata = (() => { + if (!val.includes('\u{1F9E9}')) return None + const [maybeJSON] = val.split('\u{1F9E9}') + return Result.wrap(() => JSON.parse(maybeJSON)) + .toOption() + .map((val) => { + if (typeof val !== 'object' || Array.isArray(val)) return new Map() + return new Map(Object.entries(val)) + }) + })() + return Ok( + maybeMetadata.some + ? makeTypedMessageText(val.replace(/.+\u{1F9E9}/u, ''), maybeMetadata.val) + : makeTypedMessageText(val), + ) + } + return Ok(makeTypedMessageText(val)) +} 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/typed-message/base/extension/anchor.ts b/packages/typed-message/base/extension/anchor.ts new file mode 100644 index 000000000000..73fb986d5c27 --- /dev/null +++ b/packages/typed-message/base/extension/anchor.ts @@ -0,0 +1,29 @@ +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' + readonly category: 'normal' | 'user' | 'cash' | 'hash' + readonly href: string + readonly content: string + readonly alt: TypedMessageText +} + +export const isTypedMessageAnchor = createIsType('x-anchor') + +export function makeTypedMessageAnchor( + category: TypedMessageAnchor['category'], + href: TypedMessageAnchor['href'], + content: TypedMessageAnchor['content'], +): TypedMessageAnchor { + return { + type: 'x-anchor', + serializable: false, + category, + href, + content, + alt: makeTypedMessageText(`[${content}](${href})`), + } +} 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..8f064ec919ea --- /dev/null +++ b/packages/typed-message/base/transformer/MaskPayload/index.ts @@ -0,0 +1,88 @@ +import { FlattenTypedMessage } from '..' +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..d0eec3db4836 --- /dev/null +++ b/packages/typed-message/base/utils/extract.ts @@ -0,0 +1,30 @@ +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/forEachChild' + +/** + * Get inner text from a TypedMessage + * @param message message + */ +export function extractTextFromTypedMessage(message: TypedMessage | null): Option { + if (!message) return None + const text: string[] = [] + forEachTypedMessageChild(message, function visitor(message) { + if (isTypedMessageText(message)) text.push(message.content) + else if (isTypedMessageAnchor(message)) text.push(message.content) + else 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)[] = [] + forEachTypedMessageChild(message, function visitor(message) { + if (isTypedMessageImage(message)) return image.push(message.image) + return 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..b873354169b6 --- /dev/null +++ b/packages/typed-message/base/utils/index.ts @@ -0,0 +1,38 @@ +export * from './metadata' +export * from './extract' +export * from './promise' + +import type { + TypedMessage, + SerializableTypedMessages, + SerializableTypedMessage, + NonSerializableWithAltTypedMessage, +} from '../base' +import { isTypedMessageTuple, TypedMessageTuple } from '../core' +import { eq } 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 + 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) +} diff --git a/packages/typed-message/base/utils/internal.ts b/packages/typed-message/base/utils/internal.ts new file mode 100644 index 000000000000..00513963ae3d --- /dev/null +++ b/packages/typed-message/base/utils/internal.ts @@ -0,0 +1,18 @@ +import type { SerializableTypedMessage, TypedMessage } from '../base' + +/** @internal */ +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 + } +} +/** @internal */ +export function composeSome(...fns: ((...args: Args) => boolean)[]) { + 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/typed-message/base/utils/metadata.ts b/packages/typed-message/base/utils/metadata.ts new file mode 100644 index 000000000000..e535e680c65c --- /dev/null +++ b/packages/typed-message/base/utils/metadata.ts @@ -0,0 +1,115 @@ +import { Result, Ok, Err, Some, Option, None } from 'ts-results' +import type { TypedMessage } from '../base' +import z_schema from 'z-schema' +import draft, { enableMapSet, Draft } from 'immer' +enableMapSet() + +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 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 + * @param jsonSchema JSON Schema to validate the metadata + * @example + * export const getFilePluginMetadata = createTypedMessageMetadataReader('plugin.meta.key', schema) + * getFilePluginMetadata(meta) + */ +export function createTypedMessageMetadataReader(key: string, jsonSchema?: object) { + if (jsonSchema) registerMetadataSchema(key, jsonSchema) + return (meta: TypedMessage['meta']) => readTypedMessageMetadataUntyped(meta, key) +} + +/** + * The raw parser of metadata reader + * @param meta Metadata object + * @param key Metadata key + * @param jsonSchema JSON Schema to validate the metadata + */ +export function readTypedMessageMetadataUntyped( + meta: TypedMessage['meta'], + key: string, + jsonSchema?: object, +): Result { + if (!meta) return Err.EMPTY + if (!meta.has(key)) return Err.EMPTY + if (metadataSchemaStore.has(key) && !jsonSchema) jsonSchema = metadataSchemaStore.get(key)! + const data = meta.get(key)! as T + if (!jsonSchema) console.warn('You should add a JSON Schema to verify the metadata in the TypedMessage') + else { + const match = isDataMatchJSONSchema(data, jsonSchema) + if (match.err) { + console.warn('The problematic metadata is dropped', data, 'errors:', match.val) + return Err.EMPTY + } + } + return Ok(data) +} + +export function isDataMatchJSONSchema(data: any, jsonSchema: object) { + const validator = new z_schema({}) + if (!validator.validate(data, jsonSchema)) return Err(validator.getLastErrors()) + return Ok.EMPTY +} + +/** + * Create a render of Metadata. + * @param metadataReader A metadata reader (can be return value of createTypedMessageMetadataReader) + */ +export function createRenderWithMetadata(metadataReader: (meta: TypedMessage['meta']) => Result) { + return ( + metadata: TypedMessage['meta'], + render: (data: T) => React.ReactElement | null, + ): React.ReactElement | null => { + const message = metadataReader(metadata) + if (message.ok) return render(message.val) + return null + } +} + +export function editMetadata( + metadata: TypedMessage['meta'], + edit: (meta: NonNullable>) => void, +): NonNullable { + return draft(metadata || new Map(), (e) => void edit(e)) +} +export function editTypedMessageMeta( + typedMessage: T, + edit: (meta: NonNullable>) => void, +): T { + const meta = editMetadata(typedMessage.meta, edit) + return { ...typedMessage, meta: meta.size === 0 ? undefined : meta } +} + +/** + * Render with metadata + * @param metadata Metadata + * @param key Metadata key + * @param render The render + * @param jsonSchema JSON Schema to validate the metadata + */ +export function renderWithMetadataUntyped( + metadata: TypedMessage['meta'], + key: string, + render: (data: unknown) => React.ReactNode, + jsonSchema?: object, +): React.ReactNode | null { + const message = readTypedMessageMetadataUntyped(metadata, key, jsonSchema) + if (message.ok) return render(message.val) + return null +} diff --git a/packages/typed-message/base/utils/parseLink.ts b/packages/typed-message/base/utils/parseLink.ts new file mode 100644 index 000000000000..7316a43bf953 --- /dev/null +++ b/packages/typed-message/base/utils/parseLink.ts @@ -0,0 +1,50 @@ +import _anchorme from 'anchorme' +import type { TypedMessageAnchor } from '..' +// 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/utils/promise.ts b/packages/typed-message/base/utils/promise.ts new file mode 100644 index 000000000000..11facaf34e35 --- /dev/null +++ b/packages/typed-message/base/utils/promise.ts @@ -0,0 +1,12 @@ +import { isTypedMessagePromise } from '../core' +import type { TypedMessage } from '../base' +import { forEachTypedMessageChild } from '../visitor/forEachChild' + +export function collectTypedMessagePromise(x: TypedMessage): Promise[] { + const result: Promise[] = [] + forEachTypedMessageChild(x, function visitor(x: TypedMessage) { + if (isTypedMessagePromise(x)) return result.push(x.promise) + return forEachTypedMessageChild(x, visitor) + }) + return result +} diff --git a/packages/typed-message/base/visitor/forEachChild.ts b/packages/typed-message/base/visitor/forEachChild.ts new file mode 100644 index 000000000000..de2d0d7c1145 --- /dev/null +++ b/packages/typed-message/base/visitor/forEachChild.ts @@ -0,0 +1,18 @@ +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) { + if (isTypedMessageTuple(node)) { + for (const each of node.items) visitor(each) + } 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..a677985107d3 --- /dev/null +++ b/packages/typed-message/base/visitor/index.ts @@ -0,0 +1 @@ +export * from './visitEachChild' 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..f7e30b2cfdf4 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Image.tsx @@ -0,0 +1,30 @@ +import { memo, useContext, useEffect, useState } from 'react' +import type { TypedMessageImage } from '../../../base' +import { useMetadataRender } from '../MetadataRender' +import { MessageRenderUIComponentsContext, ImageDefault } from '../utils/ComponentsContext' + +export const TypedMessageImageRenderer = memo(function TypedMessageImageRenderer(props: TypedMessageImage) { + const { Image = ImageDefault! } = useContext(MessageRenderUIComponentsContext) + 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 + if (typeof finalSrc !== 'string') return null + + return ( + <> + + {useMetadataRender(props)} + + ) +}) 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..bacafe8b89c7 --- /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 { TypedMessageRender } 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..ad8efc76560e --- /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 { RenderText } 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..a71a8333c575 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Core/Tuple.tsx @@ -0,0 +1,17 @@ +import { memo, useMemo } from 'react' +import type { TypedMessageTuple } from '../../../base' +import { hasCircular } from '../utils/circularDetect' +import { TypedMessageRender } from '../Entry' +import { useMetadataRender } from '../MetadataRender' + +export const TypedMessageTupleRenderer = memo(function TypedMessageTupleRenderer(props: TypedMessageTuple) { + if (useMemo(() => hasCircular(props.items), [props.items])) return null + return ( + <> + {props.items.map((message, index) => ( + + ))} + {useMetadataRender(props)} + + ) +}) 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..037d9debfd67 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Entry.tsx @@ -0,0 +1,26 @@ +import { useContext } from 'react' +import type { TypedMessage } from '../../base' +import { TypedMessageUnknownRenderer as TypedMessageUnknownRender } from './Core/Unknown' +import { RegistryContext } from './utils/RegistryContext' +export interface RenderProps { + message: TypedMessage +} +export function TypedMessageRender(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..ada6331e1bb5 --- /dev/null +++ b/packages/typed-message/dom/Renderer/Extension/Anchor.tsx @@ -0,0 +1,14 @@ +import { memo, useContext } from 'react' +import type { TypedMessageAnchor } from '../../../base' +import { LinkDefault, MessageRenderUIComponentsContext } from '../utils/ComponentsContext' + +export const TypedMessageAnchorRenderer = memo(function TypedMessageAnchorRenderer(props: TypedMessageAnchor) { + const { content, href, category } = props + const { Link = LinkDefault! } = useContext(MessageRenderUIComponentsContext) + // Not render meta of inline component. + return ( + + {content} + + ) +}) diff --git a/packages/typed-message/dom/Renderer/MetadataRender.tsx b/packages/typed-message/dom/Renderer/MetadataRender.tsx new file mode 100644 index 000000000000..61e79947349b --- /dev/null +++ b/packages/typed-message/dom/Renderer/MetadataRender.tsx @@ -0,0 +1,13 @@ +import { useContext } from 'react' +import { MessageRenderUIComponentsContext } from './utils/ComponentsContext' +import type { TypedMessage } from '../../base' + +export interface MetadataRenderProps { + metadata: TypedMessage['meta'] + message: TypedMessage +} +export function useMetadataRender(message: TypedMessage) { + const { Metadata } = useContext(MessageRenderUIComponentsContext) + 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/ComponentsContext.tsx b/packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx new file mode 100644 index 000000000000..a5f931e3c0d9 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx @@ -0,0 +1,40 @@ +import { createContext } from 'react' +import type { MetadataRenderProps } from '../MetadataRender' + +/** @internal */ +export const TextDefault: MessageRenderUIComponentsContext['Text'] = (props) => {props.children} +/** @internal */ +export const LinkDefault: MessageRenderUIComponentsContext['Link'] = (props) => ( + + {props.children} + +) +/** @internal */ +export const ImageDefault: MessageRenderUIComponentsContext['Image'] = (props) => + props.width === 0 ? null : + +/** @internal */ +export const MetadataDefault = () => null + +export interface MessageRenderUIComponentsContext { + Text?: React.ComponentType<{ children: string; fontSize?: number }> + Link?: React.ComponentType<{ + href?: string + category: 'normal' | 'hash' | 'cash' | 'user' + children: string + fontSize?: number + }> + Image?: React.ComponentType<{ + src: string + width?: number + height?: number + aspectRatio?: number + }> + Metadata?: React.ComponentType +} +export const MessageRenderUIComponentsContext = createContext({ + Text: TextDefault, + Link: LinkDefault, + Image: ImageDefault, + Metadata: MetadataDefault, +}) 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/TransformContext.ts b/packages/typed-message/dom/Renderer/utils/TransformContext.ts new file mode 100644 index 000000000000..7ebac43bf391 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/TransformContext.ts @@ -0,0 +1,15 @@ +import type { Transformer, TypedMessage } from '../../../base' +import { createContext, useContext, useMemo } from 'react' +import { emptyTransformationContext, TransformationContext } from '../../../base/transformer/context' +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..6829e2ed25ac --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -0,0 +1,39 @@ +import { memo, Fragment, createElement, useContext } from 'react' +import { MessageRenderUIComponentsContext, LinkDefault, TextDefault } from './ComponentsContext' +import { parseLink } from '../../../base/utils/parseLink' + +/** @internal */ +export interface RenderTextProps { + text: string +} + +/** @internal */ +export const RenderText = memo(function RenderText(props: RenderTextProps) { + const { Link = LinkDefault!, Text = TextDefault! } = useContext(MessageRenderUIComponentsContext) + return createElement(Fragment, {}, ...parseText(props.text, false, { Link, Text })) +}) + +function parseText( + string: string, + allowTextEnlarge: boolean, + components: Required>, +) { + const { Link, Text } = components + const fontSize = + 14 * + (allowTextEnlarge && Array.from(string).length < 45 + ? 1.5 + : allowTextEnlarge && Array.from(string).length < 85 + ? 1.2 + : 1) + 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..0f27ffc63a44 --- /dev/null +++ b/packages/typed-message/dom/index.ts @@ -0,0 +1,15 @@ +// Render +export { TypedMessageRender, type RenderProps } from './Renderer/Entry' +export { useMetadataRender, type MetadataRenderProps } from './Renderer/MetadataRender' + +// Render Registry +export { type RenderConfig, createTypedMessageRenderRegistry } from './Renderer/registry' +export { MessageRenderUIComponentsContext } from './Renderer/utils/ComponentsContext' +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/pnpm-lock.yaml b/pnpm-lock.yaml index e76a3ca94db8..e234851d9418 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1083,6 +1083,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 35a2de37afa5..bcf300e85029 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,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 From 2164d9224cfe627af2ca49a0b152a6e1de47efdd Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 21 Feb 2022 16:52:38 +0800 Subject: [PATCH 02/18] chore: migrate --- .eslintrc.json | 7 +- jest.config.ts | 1 + packages/.eslintrc.json | 7 +- packages/encryption/package.json | 1 + .../encryption/src/encryption/Decryption.ts | 9 +- .../src/encryption/DecryptionTypes.ts | 3 +- .../encryption/src/encryption/Encryption.ts | 2 +- packages/encryption/tsconfig.json | 2 +- packages/mask/package.json | 2 +- .../TypedMessageRender/Components/Text.tsx | 18 ++ .../shared-ui/TypedMessageRender/context.tsx | 36 +++ .../shared-ui/TypedMessageRender/registry.ts | 2 + .../TypedMessageRender/transformer.ts | 22 ++ packages/mask/shared-ui/tsconfig.json | 9 +- .../CompositionDialog/BadgeRenderer.tsx | 2 +- .../CompositionDialog/CompositionUI.tsx | 2 +- .../CompositionDialog/TypedMessageEditor.tsx | 2 +- .../AdditionalPostContent.tsx | 17 +- .../DecryptedPost/DecryptedPost.tsx | 9 +- .../DecryptedPost/DecryptedPostSuccess.tsx | 32 +- .../DecryptedPostMetadataRender.tsx | 25 ++ .../DisabledPluginSuggestion.tsx | 5 +- .../InjectedComponents/PostComments.tsx | 2 +- .../InjectedComponents/PostInspector.tsx | 8 +- .../InjectedComponents/PostReplacer.tsx | 39 ++- .../InjectedComponents/SetupGuide.tsx | 9 +- .../TypedMessageRenderer.tsx | 286 ------------------ .../shared/DebugMetadataInspector.tsx | 2 +- packages/mask/src/crypto/crypto-alpha-38.ts | 3 +- packages/mask/src/crypto/crypto-alpha-40.ts | 3 +- .../CryptoServices/decryptFrom.ts | 3 +- .../CryptoServices/encryptTo.ts | 11 +- .../plugins/ArtBlocks/SNSAdaptor/index.tsx | 4 +- .../plugins/Collectible/SNSAdaptor/index.tsx | 6 +- .../mask/src/plugins/Collectible/helpers.ts | 2 +- .../plugins/CryptoartAI/SNSAdaptor/index.tsx | 6 +- .../src/plugins/External/SNSAdaptor/index.tsx | 2 +- .../mask/src/plugins/External/messages.ts | 3 +- .../plugins/FindTruman/SNSAdaptor/index.tsx | 3 +- .../plugins/Furucombo/SNSAdaptor/index.tsx | 3 +- .../src/plugins/Gitcoin/SNSAdaptor/index.tsx | 3 +- .../plugins/GoodGhosting/SNSAdaptor/index.tsx | 3 +- .../src/plugins/ITO/SNSAdaptor/helpers.ts | 2 +- .../src/plugins/MaskBox/SNSAdaptor/index.tsx | 3 +- .../plugins/Polls/SNSAdaptor/PollsInPost.tsx | 2 +- packages/mask/src/plugins/Polls/helpers.ts | 2 +- .../plugins/PoolTogether/SNSAdaptor/index.tsx | 3 +- .../plugins/RedPacket/SNSAdaptor/helpers.ts | 2 +- .../src/plugins/Snapshot/SNSAdaptor/index.tsx | 3 +- .../mask/src/plugins/Trader/base-deferred.ts | 12 +- .../messages/TypedMessageCashTrending.tsx | 26 +- .../SNSAdaptor/UnlockProtocolInPost.tsx | 2 +- .../src/plugins/UnlockProtocol/helpers.ts | 2 +- .../src/plugins/dHEDGE/SNSAdaptor/index.tsx | 3 +- .../src/protocols/typed-message/debugger.tsx | 89 ------ .../mask/src/protocols/typed-message/index.ts | 2 - .../typed-message/render-registry.tsx | 31 -- .../mask/src/protocols/typed-message/types.ts | 7 - packages/mask/src/settings/settings.ts | 8 - .../facebook.com/automation/openComposeBox.ts | 3 +- .../facebook.com/collecting/posts.tsx | 7 +- .../instagram.com/collecting/posts.ts | 3 +- .../minds.com/automation/openComposeBox.ts | 3 +- .../minds.com/collecting/post.ts | 4 +- .../minds.com/utils/fetch.ts | 2 +- .../twitter.com/automation/openComposeBox.ts | 2 +- .../twitter.com/collecting/post.ts | 4 +- .../twitter.com/injection/PostDialogHint.tsx | 2 +- .../twitter.com/utils/fetch.ts | 2 +- .../defaults/inject/CommentBox.tsx | 2 +- packages/mask/src/social-network/types.ts | 2 +- .../utils/create-post-context.ts | 8 +- packages/mask/src/tsconfig.json | 2 + packages/mask/src/utils/debug/ui.ts | 4 +- .../JSON/DBRecord-JSON/PostRecord.ts | 2 +- packages/plugin-infra/package.json | 1 + packages/plugin-infra/src/PostContext.ts | 2 +- packages/plugin-infra/src/types.ts | 11 +- packages/plugin-infra/tsconfig.json | 3 +- .../CyberConnect/src/SNSAdaptor/index.tsx | 15 +- packages/plugins/CyberConnect/tsconfig.json | 3 +- packages/plugins/FileService/package.json | 1 + packages/plugins/FileService/src/helpers.ts | 2 +- packages/plugins/FileService/tsconfig.json | 1 + packages/shared-base/package.json | 1 + packages/shared-base/src/Messages/Mask.ts | 2 +- .../shared-base/src/TypedMessage/README.md | 3 - packages/shared-base/src/TypedMessage/base.ts | 41 --- .../src/TypedMessage/binary-encode/decode.ts | 59 ---- .../src/TypedMessage/binary-encode/encode.ts | 84 ----- .../src/TypedMessage/binary-encode/type.ts | 4 - packages/shared-base/src/TypedMessage/core.ts | 135 --------- .../TypedMessage/deprecated-encode/index.ts | 37 --- .../shared-base/src/TypedMessage/extension.ts | 31 -- .../shared-base/src/TypedMessage/index.ts | 13 - .../shared-base/src/TypedMessage/metadata.ts | 111 ------- .../src/TypedMessage/transforms/flatten.ts | 33 -- .../src/TypedMessage/transforms/promise.ts | 18 -- .../src/TypedMessage/utils/index.ts | 65 ---- .../src/TypedMessage/utils/internal.ts | 17 -- packages/shared-base/src/index.ts | 1 - packages/shared-base/tests/typed-message.ts | 3 +- packages/shared-base/tsconfig.json | 2 +- packages/shared-base/tsconfig.tests.json | 2 +- packages/typed-message/base/utils/extract.ts | 11 +- packages/typed-message/dom/Renderer/Entry.tsx | 2 + packages/web3-shared/base/tsconfig.json | 2 +- pnpm-lock.yaml | 12 +- 108 files changed, 323 insertions(+), 1272 deletions(-) create mode 100644 packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx create mode 100644 packages/mask/shared-ui/TypedMessageRender/context.tsx create mode 100644 packages/mask/shared-ui/TypedMessageRender/registry.ts create mode 100644 packages/mask/shared-ui/TypedMessageRender/transformer.ts create mode 100644 packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx delete mode 100644 packages/mask/src/components/InjectedComponents/TypedMessageRenderer.tsx delete mode 100644 packages/mask/src/protocols/typed-message/debugger.tsx delete mode 100644 packages/mask/src/protocols/typed-message/index.ts delete mode 100644 packages/mask/src/protocols/typed-message/render-registry.tsx delete mode 100644 packages/mask/src/protocols/typed-message/types.ts delete mode 100644 packages/shared-base/src/TypedMessage/README.md delete mode 100644 packages/shared-base/src/TypedMessage/base.ts delete mode 100644 packages/shared-base/src/TypedMessage/binary-encode/decode.ts delete mode 100644 packages/shared-base/src/TypedMessage/binary-encode/encode.ts delete mode 100644 packages/shared-base/src/TypedMessage/binary-encode/type.ts delete mode 100644 packages/shared-base/src/TypedMessage/core.ts delete mode 100644 packages/shared-base/src/TypedMessage/deprecated-encode/index.ts delete mode 100644 packages/shared-base/src/TypedMessage/extension.ts delete mode 100644 packages/shared-base/src/TypedMessage/index.ts delete mode 100644 packages/shared-base/src/TypedMessage/metadata.ts delete mode 100644 packages/shared-base/src/TypedMessage/transforms/flatten.ts delete mode 100644 packages/shared-base/src/TypedMessage/transforms/promise.ts delete mode 100644 packages/shared-base/src/TypedMessage/utils/index.ts delete mode 100644 packages/shared-base/src/TypedMessage/utils/internal.ts 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 3d48a608ba0d..7da8d9a0c2c3 100644 --- a/packages/encryption/tsconfig.json +++ b/packages/encryption/tsconfig.json @@ -7,5 +7,5 @@ "stripInternal": true }, "include": ["./src/**/*"], - "references": [{ "path": "../shared-base" }] + "references": [{ "path": "../shared-base" }, { "path": "../typed-message/base" }] } 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..22ba32c71615 --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -0,0 +1,18 @@ +import type { MessageRenderUIComponentsContext } from '@masknet/typed-message/dom' +import { Typography, Link } from '@mui/material' + +export const Text: MessageRenderUIComponentsContext['Text'] = (props) => ( + +) + +// TODO: provide SNS-aware #hast $link and @mention support +export const Anchor: MessageRenderUIComponentsContext['Link'] = (props) => ( + +) diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx new file mode 100644 index 000000000000..1d640f718498 --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -0,0 +1,36 @@ +import { + MessageRenderUIComponentsContext, + MetadataRenderProps, + RenderRegistryContext, + TransformerContext, +} from '@masknet/typed-message/dom' +import { TypedMessageRenderRegistry } from './registry' +import { TypedMessageTransformers } from './transformer' +import { useSubscription } from 'use-subscription' +import { emptyTransformationContext, TransformationContext } from '@masknet/typed-message' +import { useMemo } from 'react' +import { Text, Anchor as Link } from './Components/Text' + +export interface TypedMessageRenderContextProps extends React.PropsWithChildren<{}> { + context?: TransformationContext + metadataRender?: React.ComponentType +} + +export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) { + const registry = useSubscription(TypedMessageRenderRegistry.subscription) + const f = useSubscription(TypedMessageTransformers.subscription) + const val = useMemo(() => { + return [f, props.context || emptyTransformationContext] as const + }, [f, props.context]) + const Provider = useMemo((): MessageRenderUIComponentsContext => { + return { Text, Link, Metadata: props.metadataRender } + }, [props.metadataRender]) + + return ( + + + {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..b95a1876cc63 --- /dev/null +++ b/packages/mask/shared-ui/TypedMessageRender/transformer.ts @@ -0,0 +1,22 @@ +import { composeTransformers, FlattenTypedMessage, ParseLinkTransformer } from '@masknet/typed-message' + +export enum Order { + Flatten = 1000, + ParseLink = 900, + // MaskPayload = 800, + 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) diff --git a/packages/mask/shared-ui/tsconfig.json b/packages/mask/shared-ui/tsconfig.json index 2981983f40a4..9dc25225efe0 100644 --- a/packages/mask/shared-ui/tsconfig.json +++ b/packages/mask/shared-ui/tsconfig.json @@ -9,5 +9,12 @@ "resolveJsonModule": true }, "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" } + ] } 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..b8091cb631d9 100644 --- a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -1,8 +1,9 @@ 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 { 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' @@ -13,9 +14,7 @@ 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 +33,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 +75,9 @@ 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 681fc8c0e428..31f2e9ad82f1 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPost.tsx @@ -1,11 +1,6 @@ import { Fragment, useEffect, useMemo, useReducer } from 'react' -import { - delay, - makeTypedMessageTuple, - TypedMessageTuple, - type ProfileIdentifier, - type Payload, -} from '@masknet/shared-base' +import { delay, type ProfileIdentifier, type Payload } from '@masknet/shared-base' +import { makeTypedMessageTuple, TypedMessageTuple } from '@masknet/typed-message' 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..ce8fc8596917 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 { 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 @@ -77,8 +53,8 @@ export const DecryptPostSuccess = memo(function DecryptPostSuccess(props: Decryp return ( <> {shareMenu.ShareMenu} + x.DecryptedInspector, +) +export function 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/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..d6eeb59c9b67 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,17 +1,21 @@ import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -import { useValueRef } from '@masknet/shared' import { isTypedMessagePromise, isTypedMessageTuple, isWellKnownTypedMessages, makeTypedMessageTuple, -} from '@masknet/shared-base' + TransformationContext, + TypedMessage, +} from '@masknet/typed-message' +import { 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 { PluginRendererWithSuggestion } from './DecryptedPostMetadataRender' + const useStyles = makeStyles()({ root: { overflowWrap: 'break-word', @@ -27,7 +31,10 @@ export function PostReplacer(props: PostReplacerProps) { const { classes } = useStyles() const postMessage = usePostInfoDetails.rawMessage() const postPayload = usePostInfoDetails.containingMaskPayload() - const allPostReplacement = useValueRef(allPostReplacementSettings) + + const author = usePostInfoDetails.author() + const currentProfile = useCurrentIdentity()?.identifier + const url = usePostInfoDetails.url() const plugins = useActivatedPluginsSNSAdaptor(false) const processedPostMessage = useMemo( @@ -44,8 +51,6 @@ export function PostReplacer(props: PostReplacerProps) { [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 @@ -57,11 +62,29 @@ export function PostReplacer(props: PostReplacerProps) { else props.unzip?.() }, [shouldReplacePost]) + const initialTransformationContext = useMemo((): TransformationContext => { + return { + authorHint: author, + currentProfile, + postURL: url?.toString(), + } + }, [author, currentProfile, url]) + return shouldReplacePost ? ( - + + + !isTypedMessagePromise(x)))} /> ) : null } + +function Transformer(props: { message: TypedMessage }) { + const after = useTransformedValue(props.message) + return +} 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 d5293eddf7ff..601b1ea74c06 100644 --- a/packages/mask/src/crypto/crypto-alpha-40.ts +++ b/packages/mask/src/crypto/crypto-alpha-40.ts @@ -7,7 +7,8 @@ import { encodeText, encodeArrayBuffer, decodeArrayBuffer, decodeText, concatArr import { memoizePromise } from '../../utils-pure' 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 aad3bd56dacf..12ac3bd0bfc0 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 36c0f902d2b0..daf99e002234 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 { 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 171a280e2fa5..43f7a8121acb 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 bcdc72943447..d093e258dc70 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/base-deferred.ts b/packages/mask/src/plugins/Trader/base-deferred.ts index c7e987101372..8abfb14a0f2a 100644 --- a/packages/mask/src/plugins/Trader/base-deferred.ts +++ b/packages/mask/src/plugins/Trader/base-deferred.ts @@ -1,12 +1,12 @@ import type { Plugin } from '@masknet/plugin-infra' -import type { TypedMessage, TypedMessageTuple } from '@masknet/shared-base' +import { visitEachTypedMessageChild } from '@masknet/typed-message' 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 + typedMessageTransformer: function visitor(message, context) { + if (isCashTagMessage(message)) { + return makeTypedMessageCashTrending(message) + } + return visitEachTypedMessageChild(message, visitor, context) }, } diff --git a/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx b/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx index f3fdff25fda2..e6aa0e3def0a 100644 --- a/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ b/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx @@ -1,11 +1,10 @@ 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 { TypedMessageAnchor, TypedMessage, isTypedMessageAnchor } from '@masknet/typed-message' import { PluginTraderMessages, PluginTraderRPC } from '../messages' import { TagType } from '../types' -import { registerTypedMessageRenderer } from '../../../protocols/typed-message' +// TODO: when migrate, should have an API in the plugin infra for plugin to define render +import { TypedMessageRenderRegistry } from '../../../../shared-ui/TypedMessageRender/registry' export interface TypedMessageCashTrending extends Omit { readonly type: 'x-cash-trending' @@ -21,15 +20,13 @@ export function makeTypedMessageCashTrending(message: TypedMessageAnchor) { name: message.content.substr(1).toLowerCase(), } as TypedMessageCashTrending } - -registerTypedMessageRenderer('x-cash-trending', { - component: DefaultTypedMessageCashTrendingRenderer, - id: 'com.maskbook.trader.x-cash-trending', +TypedMessageRenderRegistry.registerTypedMessageRender('x-cash-trending', { + component: CashTrendingRenderer, + id: Symbol('com.mask.trader.x-cash-trending'), priority: 0, }) -function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProps) { - const chainId = useChainId() +function CashTrendingRenderer(props: TypedMessageCashTrending) { const [openTimer, setOpenTimer] = useState | null>(null) const onMouseOver = (ev: React.MouseEvent) => { // cache for async operations @@ -37,11 +34,10 @@ function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProp if (openTimer !== null) clearTimeout(openTimer) setOpenTimer( setTimeout(async () => { - if (props.message.category !== 'cash' && props.message.category !== 'hash') return - const { name, category } = props.message + if (props.category !== 'cash' && props.category !== 'hash') return + const { name, category } = props 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, @@ -60,8 +56,8 @@ function DefaultTypedMessageCashTrendingRenderer(props: TypedMessageRendererProp } return ( - - {props.message.content} + + {props.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 4783c9b38c0a..090eca64f8e6 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 @@ -2,7 +2,8 @@ import { LiveSelector } from '@dimensiondev/holoflows-kit' import { MaskMessages, CompositionRequest } from '../../../utils/messages' import { i18n } from '../../../../shared-ui/locales_legacy' import { untilDocumentReady } from '../../../utils/dom' -import { makeTypedMessageText, TypedMessage, delay } from '@masknet/shared-base' +import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' +import { delay } from '@masknet/shared-base' const nativeComposeButtonSelector = () => new LiveSelector() 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/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 5529f09ad44b..7f8341c185f2 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,4 +1,5 @@ -import { makeTypedMessageText, TypedMessage, delay } from '@masknet/shared-base' +import { makeTypedMessageText, TypedMessage } from '@masknet/typed-message' +import { delay } from '@masknet/shared-base' import { untilDocumentReady } from '../../../utils/dom' import { i18n } from '../../../../shared-ui/locales_legacy' import { MaskMessages, CompositionRequest } from '../../../utils/messages' 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 15a55a574202..b64de8dc39e4 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 '../../../../utils-pure' 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 bd4ff7fa6266..cca49c7f9bf3 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 @@ -6,14 +6,14 @@ import { postIdParser, postParser, postImagesParser } from '../utils/fetch' import { memoize } 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/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/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 4cb425b5f043..a59b9e63baa8 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -6,8 +6,8 @@ import type { ProfileIdentifier, ReadonlyIdentifierMap, ObservableWeakMap, - TypedMessage, } from '@masknet/shared-base' +import type { TypedMessage } from '@masknet/typed-message' import type { PaletteMode, Theme } from '@mui/material' import type { InjectedDialogClassKey, InjectedDialogProps } from '../components/shared/InjectedDialog' import type { Profile } from '../database' 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 989b4f1da134..e15d05d3a185 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 2ddf141cb8d2..f25b133bd2be 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 33f24ece0eec..c4039f574a1f 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 { Transformer, 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' @@ -149,7 +144,7 @@ export namespace Plugin.Shared { /** * A pure function that convert a TypedMessage into another one */ - typedMessageTransformer?: TypedMessageTransformer + typedMessageTransformer?: Transformer } export type TypedMessageTransformer = (message: TypedMessageTuple) => TypedMessageTuple /** The publisher of the plugin */ 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 f584728fc976..e0694de5ef50 100644 --- a/packages/plugins/FileService/package.json +++ b/packages/plugins/FileService/package.json @@ -11,6 +11,7 @@ "@masknet/shared": "workspace:*", "@masknet/shared-base": "workspace:*", "@masknet/theme": "workspace:*", + "@masknet/typed-message": "workspace:*", "@msgpack/msgpack": "^2.7.2", "arweave": "^1.10.3", "date-fns": "^2.28.0", diff --git a/packages/plugins/FileService/src/helpers.ts b/packages/plugins/FileService/src/helpers.ts index c64f928179cc..ca9cc3996831 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 type { FileInfo, FileInfoV1 } from './types' import schemaV1 from './schema-v1.json' diff --git a/packages/plugins/FileService/tsconfig.json b/packages/plugins/FileService/tsconfig.json index a7a6e510a964..0ed7a58a1f0f 100644 --- a/packages/plugins/FileService/tsconfig.json +++ b/packages/plugins/FileService/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../theme" }, { "path": "../../plugin-infra" }, { "path": "../../shared-base" }, + { "path": "../../typed-message/base" }, { "path": "../../shared" }, { "path": "../../icons" }, { "path": "../Wallet" } 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/base.ts b/packages/shared-base/src/TypedMessage/base.ts deleted file mode 100644 index 34adb8995404..000000000000 --- a/packages/shared-base/src/TypedMessage/base.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * The basic structure that contains - */ -export interface TypedMessage { - /** - * The metadata this message contains. - * Not expected to display to normal users. - * - * It will be serialized to a JSON object. - */ - readonly meta?: ReadonlyMap - /** - * The type of this message. - * - * Custom message must starts with 'x-' (means extension). - */ - readonly type: string -} -export type SerializableTypedMessages = SerializableTypedMessage | NonSerializableWithAltTypedMessage -/** - * A TypedMessage which can be serialized safely into a JSON. - * If it containing a inner message, it must be serializable too. - */ -export interface SerializableTypedMessage extends TypedMessage { - readonly serializable: true - /** Since it is serializable, it must be version controlled. */ - readonly version: Version -} -/** - * A TypedMessage that is _not_ serializable but it provides a fallback which _is_ serializable. - */ -export interface NonSerializableWithAltTypedMessage extends TypedMessage { - readonly serializable: false - readonly alt: SerializableTypedMessages -} -/** - * A TypedMessage is _not_ serializable (not prepared or not intended to be). - */ -export interface NonSerializableTypedMessage extends TypedMessage { - readonly serializable: false -} diff --git a/packages/shared-base/src/TypedMessage/binary-encode/decode.ts b/packages/shared-base/src/TypedMessage/binary-encode/decode.ts deleted file mode 100644 index 73bbd829abdb..000000000000 --- a/packages/shared-base/src/TypedMessage/binary-encode/decode.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { decode } from '@msgpack/msgpack' -import { Result } from 'ts-results' -import { makeTypedMessageText, makeTypedMessageTuple, makeTypedMessageUnknown, TypedMessage } from '..' -import { TypedMessageTypeEnum } from './type' - -const HEAD = '[@masknet/shared-base] TypedMessage: ' -export function decodeTypedMessageFromDocument(bin: Uint8Array) { - return Result.wrap(() => { - const doc = decode(bin) - if (!Array.isArray(doc)) throw new Error(`${HEAD}Invalid document`) - const [docVer, message] = doc - if (typeof docVer !== 'number') throw new Error(`${HEAD}Invalid document`) - if (docVer !== 0) throw new Error(`${HEAD}Unknown document version`) - const result = decodeTypedMessage(message) - fixU8Array(result) - return result - }) -} -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])) -} -function decodeTypedMessageText([, version, meta, text, format]: readonly unknown[]) { - assertNumber(version) - assertString(text) - return makeTypedMessageText(text, decodeMetadata(meta)) -} -function decodeTypedMessageTuple([, version, meta, items]: readonly unknown[]) { - assertNumber(version) - if (!Array.isArray(items)) throw new TypeError(`${HEAD}Invalid TypedMessageTuple`) - return makeTypedMessageTuple(items.map(decodeTypedMessage), decodeMetadata(meta)) -} -function decodeMetadata(meta: unknown): ReadonlyMap | undefined { - if (meta === null) return undefined - if (typeof meta !== 'object') throw new TypeError(`${HEAD}Invalid TypedMessage`) - if (Object.getPrototypeOf(meta) !== Object.prototype) throw new TypeError(`${HEAD}Invalid TypedMessage`) - return meta as any -} - -function assertNumber(x: unknown): asserts x is number { - if (typeof x !== 'number') throw new TypeError(`${HEAD}Invalid TypedMessage`) -} -function assertString(x: unknown): asserts x is string { - if (typeof x !== 'string') throw new TypeError(`${HEAD}Invalid TypedMessage`) -} -// Detach Uint8Array from it's underlying buffer -function fixU8Array(obj: unknown) { - // for Array and object - if (typeof obj === 'object' && obj !== null) { - for (const key in obj) { - const val = (obj as any)[key] - if (val instanceof Uint8Array) (obj as any)[key] = val.slice() - else fixU8Array(val) - } - } -} diff --git a/packages/shared-base/src/TypedMessage/binary-encode/encode.ts b/packages/shared-base/src/TypedMessage/binary-encode/encode.ts deleted file mode 100644 index 401e5dff5758..000000000000 --- a/packages/shared-base/src/TypedMessage/binary-encode/encode.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - isTypedMessageText, - isTypedMessageTupleSerializable, - SerializableTypedMessages, - TypedMessage, - TypedMessageText, - TypedMessageTupleSerializable, -} from '..' -import { encode } from '@msgpack/msgpack' -import { TypedMessageTypeEnum } from './type' - -const HEAD = '[@masknet/shared-base] TypedMessage: ' -export function encodeTypedMessageToDocument(tm: SerializableTypedMessages) { - const doc = [0, encodeTypedMessage(tm)] - return encode(doc) -} -function encodeTypedMessage(tm: SerializableTypedMessages): any[] { - if (tm.serializable === false) { - if (tm.alt) return encodeTypedMessage(tm.alt) - throw new TypeError(`${HEAD}TypedMessage ${tm.type} does not support serialization.`) - } - if (isTypedMessageText(tm)) return encodeTypedMessageText(tm) - if (isTypedMessageTupleSerializable(tm)) return encodeTypedMessageTuple(tm) - throw new TypeError(`${HEAD}Unsupported TypedMessage ${tm.type} to be serialized.`) -} -function encodeTypedMessageText(tm: TypedMessageText): any[] { - // TODO: TextFormat - return [TypedMessageTypeEnum.Text, tm.version, encodeMeta(tm), tm.content] -} -function encodeTypedMessageTuple(tm: TypedMessageTupleSerializable): any[] { - return [TypedMessageTypeEnum.Tuple, tm.version, encodeMeta(tm), tm.items.map(encodeTypedMessage)] -} - -function encodeMeta(tm: TypedMessage) { - if (!tm.meta) return null - const record: Record = { __proto__: null } - for (const [key, val] of tm.meta) { - if (typeof key !== 'string') continue - if (typeof val === 'undefined') continue - try { - record[key] = collectValue(val) - } catch (err) { - console.warn(`${HEAD}key ${key} is dropped due to the error`, err) - } - } - return record -} -function collectValue(val: any): any { - try { - const type = typeof val - if (val === undefined) { - console.warn(`${HEAD}undefined converted to null.`) - return null - } - if (type === 'number' || val === null || type === 'boolean' || type === 'string') return val - - if (type === 'bigint' || type === 'function' || type === 'symbol') { - throw new TypeError(`${HEAD}Unsupported type ${type}`) - } - - if (val instanceof Uint8Array) return new Uint8Array(val) - if (val instanceof ArrayBuffer) { - throw new TypeError(`${HEAD}ArrayBuffer must be Uint8Array.`) - } - - if (Array.isArray(val)) return Array.from(val).map(collectValue) - - const proto = Object.getPrototypeOf(val) - if (proto !== Object.prototype && proto !== null) { - throw new TypeError(`${HEAD}unsupported value with a non trivial prototype.`) - } - - const result: Record = {} - for (const key in val) { - if (typeof key !== 'string') throw new TypeError(`${HEAD}Unsupported type symbol.`) - const v = val[key] - result[key] = collectValue(v) - } - return result - } catch (err) { - console.warn('[@masknet/shared-base] TypedMessage: When converting value', val, ', an error occurred', err) - throw err - } -} 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/deprecated-encode/index.ts b/packages/shared-base/src/TypedMessage/deprecated-encode/index.ts deleted file mode 100644 index ff73a4da28f1..000000000000 --- a/packages/shared-base/src/TypedMessage/deprecated-encode/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { None, Ok, Result } from 'ts-results' -import type { TypedMessageText } from '..' -import { makeTypedMessageText } from '../core' - -export function encodeTypedMessageV38Format(message: TypedMessageText) { - const encoder = new TextEncoder() - if (message.meta?.size) { - const rec = Object.fromEntries(message.meta.entries()) - return encoder.encode(`${JSON.stringify(rec)}\u{1F9E9}${message.content}`) - } - return encoder.encode(message.content) -} -export function decodeTypedMessageV38ToV40Format(raw: Uint8Array, version: -38 | -39 | -40) { - const decoder = new TextDecoder() - const text = Result.wrap(() => decoder.decode(raw)) - const { val, err } = text - if (err) return text - - if (version === -38) { - const maybeMetadata = (() => { - if (!val.includes('\u{1F9E9}')) return None - const [maybeJSON] = val.split('\u{1F9E9}') - return Result.wrap(() => JSON.parse(maybeJSON)) - .toOption() - .map((val) => { - if (typeof val !== 'object' || Array.isArray(val)) return new Map() - return new Map(Object.entries(val)) - }) - })() - return Ok( - maybeMetadata.some - ? makeTypedMessageText(val.replace(/.+\u{1F9E9}/u, ''), maybeMetadata.val) - : makeTypedMessageText(val), - ) - } - return Ok(makeTypedMessageText(val)) -} diff --git a/packages/shared-base/src/TypedMessage/extension.ts b/packages/shared-base/src/TypedMessage/extension.ts deleted file mode 100644 index 1936116847c7..000000000000 --- a/packages/shared-base/src/TypedMessage/extension.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { NonSerializableWithAltTypedMessage, TypedMessage } from './base' -import { composeSome, createIsType } from './utils/internal' -import { makeTypedMessageText, TypedMessageText } from './core' -export type WellKnownExtensionTypedMessages = TypedMessageAnchor -/** It represents a single link */ -export interface TypedMessageAnchor extends NonSerializableWithAltTypedMessage { - readonly type: 'x-anchor' - readonly category: 'normal' | 'user' | 'cash' | 'hash' - readonly href: string - readonly content: string - readonly alt: TypedMessageText -} -export const isTypedMessageAnchor = createIsType('x-anchor') -export function makeTypedMessageAnchor( - category: TypedMessageAnchor['category'], - href: TypedMessageAnchor['href'], - content: TypedMessageAnchor['content'], -): TypedMessageAnchor { - return { - type: 'x-anchor', - serializable: false, - category, - href, - content, - alt: makeTypedMessageText(`[${content}](${href})`), - } -} -// -export const isWellKnownExtensionTypedMessages = composeSome(isTypedMessageAnchor) as ( - x: TypedMessage, -) => x is WellKnownExtensionTypedMessages 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/metadata.ts b/packages/shared-base/src/TypedMessage/metadata.ts deleted file mode 100644 index 731bcc89cd43..000000000000 --- a/packages/shared-base/src/TypedMessage/metadata.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Result, Ok, Err, Some, Option, None } from 'ts-results' -import type { TypedMessage } from './base' -import z_schema from 'z-schema' -import draft, { enableMapSet, Draft } from 'immer' -enableMapSet() - -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. - * @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 - * @param jsonSchema JSON Schema to validate the metadata - * @example - * export const getFilePluginMetadata = createTypedMessageMetadataReader('plugin.meta.key', schema) - * getFilePluginMetadata(meta) - */ -export function createTypedMessageMetadataReader(key: string, jsonSchema?: object) { - if (jsonSchema) registerMetadataSchema(key, jsonSchema) - return (meta: TypedMessage['meta']) => readTypedMessageMetadataUntyped(meta, key) -} -/** - * The raw parser of metadata reader - * @param meta Metadata object - * @param key Metadata key - * @param jsonSchema JSON Schema to validate the metadata - */ -export function readTypedMessageMetadataUntyped( - meta: TypedMessage['meta'], - key: string, - jsonSchema?: object, -): Result { - if (!meta) return Err.EMPTY - if (!meta.has(key)) return Err.EMPTY - if (metadataSchemaStore.has(key) && !jsonSchema) jsonSchema = metadataSchemaStore.get(key)! - const data = meta.get(key)! as T - if (!jsonSchema) console.warn('You should add a JSON Schema to verify the metadata in the TypedMessage') - else { - const match = isDataMatchJSONSchema(data, jsonSchema) - if (match.err) { - console.warn('The problematic metadata is dropped', data, 'errors:', match.val) - return Err.EMPTY - } - } - return Ok(data) -} - -export function isDataMatchJSONSchema(data: any, jsonSchema: object) { - const validator = new z_schema({}) - if (!validator.validate(data, jsonSchema)) return Err(validator.getLastErrors()) - return Ok.EMPTY -} - -/** - * Create a render of Metadata. - * @param metadataReader A metadata reader (can be return value of createTypedMessageMetadataReader) - */ -export function createRenderWithMetadata(metadataReader: (meta: TypedMessage['meta']) => Result) { - return ( - metadata: TypedMessage['meta'], - render: (data: T) => React.ReactElement | null, - ): React.ReactElement | null => { - const message = metadataReader(metadata) - if (message.ok) return render(message.val) - return null - } -} - -export function editMetadata( - metadata: TypedMessage['meta'], - edit: (meta: NonNullable>) => void, -): NonNullable { - return draft(metadata || new Map(), (e) => void edit(e)) -} -export function editTypedMessageMeta( - typedMessage: T, - edit: (meta: NonNullable>) => void, -): T { - const meta = editMetadata(typedMessage.meta, edit) - return { ...typedMessage, meta: meta.size === 0 ? undefined : meta } -} - -/** - * Render with metadata - * @param metadata Metadata - * @param key Metadata key - * @param render The render - * @param jsonSchema JSON Schema to validate the metadata - */ -export function renderWithMetadataUntyped( - metadata: TypedMessage['meta'], - key: string, - render: (data: unknown) => React.ReactNode, - jsonSchema?: object, -): React.ReactNode | null { - const message = readTypedMessageMetadataUntyped(metadata, key, jsonSchema) - if (message.ok) return render(message.val) - return null -} 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/TypedMessage/utils/internal.ts b/packages/shared-base/src/TypedMessage/utils/internal.ts deleted file mode 100644 index 69f16b807b3e..000000000000 --- a/packages/shared-base/src/TypedMessage/utils/internal.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { SerializableTypedMessage, TypedMessage } from '../base' - -/** @internal */ -export function createIsType(x: string, version?: number) { - return (y: TypedMessage): y is T => { - if (version !== undefined && (y as SerializableTypedMessage).version !== version) return false - return y.type === x - } -} -/** @internal */ -export function composeSome(...fns: ((...args: Args) => boolean)[]) { - return (...args: Args) => fns.some((f) => f(...args)) -} - -export function composeEvery(...fns: ((...args: Args) => boolean)[]) { - return (...args: Args) => fns.every((f) => f(...args)) -} 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/base/utils/extract.ts b/packages/typed-message/base/utils/extract.ts index d0eec3db4836..faaf025602c3 100644 --- a/packages/typed-message/base/utils/extract.ts +++ b/packages/typed-message/base/utils/extract.ts @@ -8,13 +8,18 @@ import { forEachTypedMessageChild } from '../visitor/forEachChild' * Get inner text from a TypedMessage * @param message message */ -export function extractTextFromTypedMessage(message: TypedMessage | null): Option { +export function extractTextFromTypedMessage( + message: TypedMessage | null, + options?: { linkAsText: boolean }, +): Option { if (!message) return None const text: string[] = [] forEachTypedMessageChild(message, function visitor(message) { if (isTypedMessageText(message)) text.push(message.content) - else if (isTypedMessageAnchor(message)) text.push(message.content) - else forEachTypedMessageChild(message, visitor) + else if (isTypedMessageAnchor(message)) { + text.push(message.content) + if (options?.linkAsText) text.push(`(${message.href})`) + } else forEachTypedMessageChild(message, visitor) }) if (text.length) return Some(text.join(' ')) return None diff --git a/packages/typed-message/dom/Renderer/Entry.tsx b/packages/typed-message/dom/Renderer/Entry.tsx index 037d9debfd67..0907857cb474 100644 --- a/packages/typed-message/dom/Renderer/Entry.tsx +++ b/packages/typed-message/dom/Renderer/Entry.tsx @@ -4,6 +4,8 @@ import { TypedMessageUnknownRenderer as TypedMessageUnknownRender } from './Core import { RegistryContext } from './utils/RegistryContext' export interface RenderProps { message: TypedMessage + /** @deprecated */ + allowTextEnlarge?: boolean } export function TypedMessageRender(props: RenderProps) { const { message } = props 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 e234851d9418..aeed1c47b137 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,10 +246,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: @@ -320,6 +322,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:* @@ -382,7 +385,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 @@ -456,6 +458,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 @@ -512,7 +515,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 @@ -585,6 +587,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 @@ -599,6 +602,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 @@ -687,6 +691,7 @@ importers: '@masknet/shared': workspace:* '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* + '@masknet/typed-message': workspace:* '@msgpack/msgpack': ^2.7.2 arweave: ^1.10.3 date-fns: ^2.28.0 @@ -701,6 +706,7 @@ importers: '@masknet/shared': link:../../shared '@masknet/shared-base': link:../../shared-base '@masknet/theme': link:../../theme + '@masknet/typed-message': link:../../typed-message '@msgpack/msgpack': 2.7.2 arweave: 1.10.13 date-fns: 2.28.0 @@ -986,6 +992,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 @@ -1007,6 +1014,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 From 782d8e9b133b9e816afa822f21c3d3d6c83e05b1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 21 Feb 2022 17:01:55 +0800 Subject: [PATCH 03/18] fix: type error --- .../shared-ui/TypedMessageRender/context.tsx | 21 +++------ .../InjectedComponents/PostReplacer.tsx | 47 +++---------------- 2 files changed, 13 insertions(+), 55 deletions(-) diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx index 1d640f718498..74ee3392645f 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -1,13 +1,7 @@ -import { - MessageRenderUIComponentsContext, - MetadataRenderProps, - RenderRegistryContext, - TransformerContext, -} from '@masknet/typed-message/dom' +import { MessageRenderUIComponentsContext, MetadataRenderProps, RegistryContext } from '@masknet/typed-message/dom' import { TypedMessageRenderRegistry } from './registry' -import { TypedMessageTransformers } from './transformer' import { useSubscription } from 'use-subscription' -import { emptyTransformationContext, TransformationContext } from '@masknet/typed-message' +import type { TransformationContext } from '@masknet/typed-message' import { useMemo } from 'react' import { Text, Anchor as Link } from './Components/Text' @@ -18,19 +12,16 @@ export interface TypedMessageRenderContextProps extends React.PropsWithChildren< export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) { const registry = useSubscription(TypedMessageRenderRegistry.subscription) - const f = useSubscription(TypedMessageTransformers.subscription) - const val = useMemo(() => { - return [f, props.context || emptyTransformationContext] as const - }, [f, props.context]) + // const transformerFunction = useSubscription(TypedMessageTransformers.subscription) const Provider = useMemo((): MessageRenderUIComponentsContext => { return { Text, Link, Metadata: props.metadataRender } }, [props.metadataRender]) return ( + // basic components provider: Text, Link, Image and Metadata - - {props.children} - + {/* Typed message render provider: a registry */} + {props.children} ) } diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index d6eeb59c9b67..dfbb9723bdf2 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,16 +1,7 @@ -import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -import { - isTypedMessagePromise, - isTypedMessageTuple, - isWellKnownTypedMessages, - makeTypedMessageTuple, - TransformationContext, - TypedMessage, -} from '@masknet/typed-message' +import type { TransformationContext, TypedMessage } from '@masknet/typed-message' import { TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' import { makeStyles } from '@masknet/theme' import { useEffect, useMemo } from 'react' -import { Result } from 'ts-results' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context' import { useCurrentIdentity } from '../DataSource/useActivatedUI' @@ -27,40 +18,19 @@ export interface PostReplacerProps { unzip?: () => void } -export function PostReplacer(props: PostReplacerProps) { +export function PostReplacer({ unzip, zip }: PostReplacerProps) { const { classes } = useStyles() const postMessage = usePostInfoDetails.rawMessage() - const postPayload = usePostInfoDetails.containingMaskPayload() const author = usePostInfoDetails.author() const currentProfile = useCurrentIdentity()?.identifier const url = usePostInfoDetails.url() - 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 posts which enhanced by plugins - processedPostMessage.items.some((x) => !isWellKnownTypedMessages(x)) || - // replace posts which encrypted by Mask - postPayload.ok - // zip/unzip original post useEffect(() => { - if (shouldReplacePost) props.zip?.() - else props.unzip?.() - }, [shouldReplacePost]) + zip?.() + return () => unzip?.() + }, []) const initialTransformationContext = useMemo((): TransformationContext => { return { @@ -70,18 +40,15 @@ export function PostReplacer(props: PostReplacerProps) { } }, [author, currentProfile, url]) - return shouldReplacePost ? ( + return ( - !isTypedMessagePromise(x)))} - /> - ) : null + ) } function Transformer(props: { message: TypedMessage }) { From 34db5f4d3617355c8b5d812c20f3cdfd6bb593f4 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 21 Feb 2022 17:08:50 +0800 Subject: [PATCH 04/18] fix: remove extra strings --- packages/mask/shared-ui/locales/en-US.json | 5 ----- packages/mask/shared-ui/locales/ja-JP.json | 2 -- packages/mask/shared-ui/locales/ko-KR.json | 5 ----- packages/mask/shared-ui/locales/qya-AA.json | 5 ----- packages/mask/shared-ui/locales/zh-CN.json | 5 ----- packages/mask/shared-ui/locales/zh-TW.json | 3 --- 6 files changed, 25 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 72c89e95f3e4..9f57df3a8c08 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.", "days": "Every {{days}} days", @@ -159,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..9af233b79a37 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}} 일마다", @@ -144,8 +141,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 fceda20e3e24..22857fd54c59 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", "days": "crwdns4107:0{{days}}crwdne4107:0", @@ -159,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 9ed9571c3d11..ef865d83df92 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}} 天", @@ -158,8 +155,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": "加速", From 0a1703054259ffb7b1d8d19a320bdcc8659c3a32 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Mon, 21 Feb 2022 18:02:14 +0800 Subject: [PATCH 05/18] fix: typo --- packages/mask/.webpack/config.ts | 4 ++-- packages/scripts/src/commands/new-package.ts | 2 +- packages/typed-message/README.md | 2 +- packages/typed-message/base/utils/index.ts | 1 - packages/typed-message/base/utils/promise.ts | 12 ------------ 5 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 packages/typed-message/base/utils/promise.ts diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index b8131855563c..a9d6b801d001 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -92,8 +92,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': join(__dirname, '../../packages/typed-message/base'), - '@masknet/typed-message/dom': join(__dirname, '../../packages/typed-message/dom'), + '@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/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/typed-message/README.md b/packages/typed-message/README.md index b168286af627..8862445e634d 100644 --- a/packages/typed-message/README.md +++ b/packages/typed-message/README.md @@ -1 +1 @@ -# @masknet/typed-message \ No newline at end of file +# @masknet/typed-message diff --git a/packages/typed-message/base/utils/index.ts b/packages/typed-message/base/utils/index.ts index b873354169b6..11d742385a4c 100644 --- a/packages/typed-message/base/utils/index.ts +++ b/packages/typed-message/base/utils/index.ts @@ -1,6 +1,5 @@ export * from './metadata' export * from './extract' -export * from './promise' import type { TypedMessage, diff --git a/packages/typed-message/base/utils/promise.ts b/packages/typed-message/base/utils/promise.ts deleted file mode 100644 index 11facaf34e35..000000000000 --- a/packages/typed-message/base/utils/promise.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { isTypedMessagePromise } from '../core' -import type { TypedMessage } from '../base' -import { forEachTypedMessageChild } from '../visitor/forEachChild' - -export function collectTypedMessagePromise(x: TypedMessage): Promise[] { - const result: Promise[] = [] - forEachTypedMessageChild(x, function visitor(x: TypedMessage) { - if (isTypedMessagePromise(x)) return result.push(x.promise) - return forEachTypedMessageChild(x, visitor) - }) - return result -} From 2c347459b9d84e7e2ecfbe141f7f098db00a99d1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 14:55:36 +0800 Subject: [PATCH 06/18] feat: split render fragments --- .../TypedMessageRender/Components/Text.tsx | 34 ++++++----- .../shared-ui/TypedMessageRender/context.tsx | 13 +++-- .../typed-message/dom/Renderer/Core/Image.tsx | 7 ++- .../typed-message/dom/Renderer/Core/Text.tsx | 4 +- .../dom/Renderer/Extension/Anchor.tsx | 13 +---- .../dom/Renderer/MetadataRender.tsx | 4 +- .../dom/Renderer/utils/ComponentsContext.tsx | 40 ------------- .../dom/Renderer/utils/RenderFragments.tsx | 58 +++++++++++++++++++ .../dom/Renderer/utils/renderText.tsx | 35 +++++++---- packages/typed-message/dom/index.ts | 6 +- 10 files changed, 127 insertions(+), 87 deletions(-) delete mode 100644 packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx create mode 100644 packages/typed-message/dom/Renderer/utils/RenderFragments.tsx diff --git a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx index 22ba32c71615..ecfc49a49ad3 100644 --- a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -1,18 +1,22 @@ -import type { MessageRenderUIComponentsContext } from '@masknet/typed-message/dom' +import { memo } from 'react' import { Typography, Link } from '@mui/material' +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' -export const Text: MessageRenderUIComponentsContext['Text'] = (props) => ( - -) +export const Text = memo(function Text(props: RenderFragmentsContextType.TextProps) { + return ( + + ) +}) -// TODO: provide SNS-aware #hast $link and @mention support -export const Anchor: MessageRenderUIComponentsContext['Link'] = (props) => ( - -) +export const Anchor = memo(function Anchor(props: RenderFragmentsContextType.LinkProps) { + return ( + + ) +}) diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx index 74ee3392645f..c47c35df00ec 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -1,4 +1,9 @@ -import { MessageRenderUIComponentsContext, MetadataRenderProps, RegistryContext } from '@masknet/typed-message/dom' +import { + RenderFragmentsContext, + type RenderFragmentsContextType, + MetadataRenderProps, + RegistryContext, +} from '@masknet/typed-message/dom' import { TypedMessageRenderRegistry } from './registry' import { useSubscription } from 'use-subscription' import type { TransformationContext } from '@masknet/typed-message' @@ -13,15 +18,15 @@ export interface TypedMessageRenderContextProps extends React.PropsWithChildren< export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) { const registry = useSubscription(TypedMessageRenderRegistry.subscription) // const transformerFunction = useSubscription(TypedMessageTransformers.subscription) - const Provider = useMemo((): MessageRenderUIComponentsContext => { + const Provider = useMemo((): RenderFragmentsContextType => { return { Text, Link, Metadata: props.metadataRender } }, [props.metadataRender]) return ( // basic components provider: Text, Link, Image and Metadata - + {/* Typed message render provider: a registry */} {props.children} - + ) } diff --git a/packages/typed-message/dom/Renderer/Core/Image.tsx b/packages/typed-message/dom/Renderer/Core/Image.tsx index f7e30b2cfdf4..441fcbf46749 100644 --- a/packages/typed-message/dom/Renderer/Core/Image.tsx +++ b/packages/typed-message/dom/Renderer/Core/Image.tsx @@ -1,10 +1,10 @@ import { memo, useContext, useEffect, useState } from 'react' import type { TypedMessageImage } from '../../../base' import { useMetadataRender } from '../MetadataRender' -import { MessageRenderUIComponentsContext, ImageDefault } from '../utils/ComponentsContext' +import { RenderFragmentsContext, DefaultRenderFragments } from '../utils/RenderFragments' export const TypedMessageImageRenderer = memo(function TypedMessageImageRenderer(props: TypedMessageImage) { - const { Image = ImageDefault! } = useContext(MessageRenderUIComponentsContext) + const { Image = DefaultRenderFragments.Image } = useContext(RenderFragmentsContext) const { image, width, height } = props const [blobSrc, setBlobSrc] = useState(null) @@ -19,12 +19,13 @@ export const TypedMessageImageRenderer = memo(function TypedMessageImageRenderer }, [image]) const finalSrc = blobSrc || image + const meta = useMetadataRender(props) if (typeof finalSrc !== 'string') return null return ( <> - {useMetadataRender(props)} + {meta} ) }) diff --git a/packages/typed-message/dom/Renderer/Core/Text.tsx b/packages/typed-message/dom/Renderer/Core/Text.tsx index ad8efc76560e..5694c585d15a 100644 --- a/packages/typed-message/dom/Renderer/Core/Text.tsx +++ b/packages/typed-message/dom/Renderer/Core/Text.tsx @@ -1,13 +1,13 @@ import { memo } from 'react' import type { TypedMessageText } from '../../../base' -import { RenderText } from '../utils/renderText' +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/Extension/Anchor.tsx b/packages/typed-message/dom/Renderer/Extension/Anchor.tsx index ada6331e1bb5..f07c3b70a6f2 100644 --- a/packages/typed-message/dom/Renderer/Extension/Anchor.tsx +++ b/packages/typed-message/dom/Renderer/Extension/Anchor.tsx @@ -1,14 +1,7 @@ -import { memo, useContext } from 'react' +import { memo } from 'react' import type { TypedMessageAnchor } from '../../../base' -import { LinkDefault, MessageRenderUIComponentsContext } from '../utils/ComponentsContext' +import { RenderLinkFragment } from '../utils/renderText' export const TypedMessageAnchorRenderer = memo(function TypedMessageAnchorRenderer(props: TypedMessageAnchor) { - const { content, href, category } = props - const { Link = LinkDefault! } = useContext(MessageRenderUIComponentsContext) - // Not render meta of inline component. - return ( - - {content} - - ) + return }) diff --git a/packages/typed-message/dom/Renderer/MetadataRender.tsx b/packages/typed-message/dom/Renderer/MetadataRender.tsx index 61e79947349b..cbbf057b537a 100644 --- a/packages/typed-message/dom/Renderer/MetadataRender.tsx +++ b/packages/typed-message/dom/Renderer/MetadataRender.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react' -import { MessageRenderUIComponentsContext } from './utils/ComponentsContext' +import { RenderFragmentsContext } from './utils/RenderFragments' import type { TypedMessage } from '../../base' export interface MetadataRenderProps { @@ -7,7 +7,7 @@ export interface MetadataRenderProps { message: TypedMessage } export function useMetadataRender(message: TypedMessage) { - const { Metadata } = useContext(MessageRenderUIComponentsContext) + const { Metadata } = useContext(RenderFragmentsContext) if (!Metadata || !message.meta) return null return } diff --git a/packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx b/packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx deleted file mode 100644 index a5f931e3c0d9..000000000000 --- a/packages/typed-message/dom/Renderer/utils/ComponentsContext.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createContext } from 'react' -import type { MetadataRenderProps } from '../MetadataRender' - -/** @internal */ -export const TextDefault: MessageRenderUIComponentsContext['Text'] = (props) => {props.children} -/** @internal */ -export const LinkDefault: MessageRenderUIComponentsContext['Link'] = (props) => ( - - {props.children} - -) -/** @internal */ -export const ImageDefault: MessageRenderUIComponentsContext['Image'] = (props) => - props.width === 0 ? null : - -/** @internal */ -export const MetadataDefault = () => null - -export interface MessageRenderUIComponentsContext { - Text?: React.ComponentType<{ children: string; fontSize?: number }> - Link?: React.ComponentType<{ - href?: string - category: 'normal' | 'hash' | 'cash' | 'user' - children: string - fontSize?: number - }> - Image?: React.ComponentType<{ - src: string - width?: number - height?: number - aspectRatio?: number - }> - Metadata?: React.ComponentType -} -export const MessageRenderUIComponentsContext = createContext({ - Text: TextDefault, - Link: LinkDefault, - Image: ImageDefault, - Metadata: MetadataDefault, -}) 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..6e6015887a24 --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx @@ -0,0 +1,58 @@ +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 { + 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 + fontSize?: number + } + export interface LinkProps { + children: string + href: string + fontSize?: number + } + export interface AtLinkProps { + children: string + fontSize?: number + } + export interface CashLinkProps { + children: string + fontSize?: number + } + export interface HashLinkProps { + children: string + fontSize?: number + } + 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/renderText.tsx b/packages/typed-message/dom/Renderer/utils/renderText.tsx index 6829e2ed25ac..27c1269761a0 100644 --- a/packages/typed-message/dom/Renderer/utils/renderText.tsx +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -1,6 +1,7 @@ import { memo, Fragment, createElement, useContext } from 'react' -import { MessageRenderUIComponentsContext, LinkDefault, TextDefault } from './ComponentsContext' +import { RenderFragmentsContext, RenderFragmentsContextType, DefaultRenderFragments } from './RenderFragments' import { parseLink } from '../../../base/utils/parseLink' +import type { TypedMessageAnchor } from '../../../base/extension' /** @internal */ export interface RenderTextProps { @@ -8,17 +9,31 @@ export interface RenderTextProps { } /** @internal */ -export const RenderText = memo(function RenderText(props: RenderTextProps) { - const { Link = LinkDefault!, Text = TextDefault! } = useContext(MessageRenderUIComponentsContext) - return createElement(Fragment, {}, ...parseText(props.text, false, { Link, Text })) +export const RenderTextFragment = memo(function RenderText(props: RenderTextProps) { + const { Text = DefaultRenderFragments.Text } = useContext(RenderFragmentsContext) + return createElement(Fragment, {}, ...parseText(props.text, false, Text)) }) -function parseText( - string: string, - allowTextEnlarge: boolean, - components: Required>, +/** @internal */ +export const RenderLinkFragment = memo(function RenderLink( + props: Pick & RenderFragmentsContextType.LinkProps, ) { - const { Link, Text } = components + const { children, href, category, fontSize } = 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, allowTextEnlarge: boolean, Text: NonNullable) { const fontSize = 14 * (allowTextEnlarge && Array.from(string).length < 45 @@ -33,7 +48,7 @@ function parseText( .map((x) => (x === '\n' ?
: )) } if (x.category === 'normal' && !x.content.match(/^https?:\/\//gi)) x.content = 'http://' + x.content - return + return }) return links } diff --git a/packages/typed-message/dom/index.ts b/packages/typed-message/dom/index.ts index 0f27ffc63a44..13abd5964e5d 100644 --- a/packages/typed-message/dom/index.ts +++ b/packages/typed-message/dom/index.ts @@ -4,7 +4,11 @@ export { useMetadataRender, type MetadataRenderProps } from './Renderer/Metadata // Render Registry export { type RenderConfig, createTypedMessageRenderRegistry } from './Renderer/registry' -export { MessageRenderUIComponentsContext } from './Renderer/utils/ComponentsContext' +export { + RenderFragmentsContext, + type RenderFragmentsContextType, + DefaultRenderFragments, +} from './Renderer/utils/RenderFragments' export { RegistryContext } from './Renderer/utils/RegistryContext' // Transformation From d9b8edcc3e77255fc4fa9fb75e9aa11d4f34c502 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 15:17:26 +0800 Subject: [PATCH 07/18] feat: sns-aware render fragement on twitter --- .../shared-ui/TypedMessageRender/context.tsx | 7 +++--- .../AdditionalPostContent.tsx | 4 ++- .../InjectedComponents/PostReplacer.tsx | 2 ++ .../customization/render-fragments.tsx | 25 +++++++++++++++++++ .../twitter.com/ui-provider.ts | 2 ++ packages/mask/src/social-network/types.ts | 2 ++ 6 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx index c47c35df00ec..20c99e30bb36 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -13,17 +13,18 @@ import { Text, Anchor as Link } from './Components/Text' 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 { Text, Link, Metadata: props.metadataRender } - }, [props.metadataRender]) + return { Text, Link, Metadata: props.metadataRender, ...props.renderFragments } + }, [props.metadataRender, props.renderFragments]) return ( - // basic components provider: Text, Link, Image and Metadata + // basic render fragments provider: Text, Link, Image and Metadata {/* Typed message render provider: a registry */} {props.children} diff --git a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx index b8091cb631d9..1affbedb99ad 100644 --- a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -9,6 +9,7 @@ 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', @@ -75,7 +76,8 @@ export const AdditionalContent = memo(function AdditionalContent(props: Addition
{header}
{message ? (
- +
diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index dfbb9723bdf2..df9ffa9c2dff 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -6,6 +6,7 @@ import { usePostInfoDetails } from '../DataSource/usePostInfo' import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context' import { useCurrentIdentity } from '../DataSource/useActivatedUI' import { PluginRendererWithSuggestion } from './DecryptedPostMetadataRender' +import { activatedSocialNetworkUI } from '../../social-network/ui' const useStyles = makeStyles()({ root: { @@ -43,6 +44,7 @@ export function PostReplacer({ unzip, zip }: PostReplacerProps) { return ( 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..8f70dd05eb16 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx @@ -0,0 +1,25 @@ +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' +import { memo } from 'react' +import { Link } from '@mui/material' +export const TwitterRenderFragments: RenderFragmentsContextType = { + AtLink: memo(function (props) { + const target = '/' + props.children.slice(1) + return ( + + ) + }), + HashLink: memo(function (props) { + const target = '/hashtag/' + props.children.slice(1) + return ( + + ) + }), +} 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 f69df46c0b9b..5f839d9ce12d 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 @@ -35,6 +35,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)` @@ -136,6 +137,7 @@ const twitterUI: SocialNetworkUI.Definition = { InjectedDialog: { classes: useInjectedDialogClassesOverwriteTwitter, }, + RenderFragments: TwitterRenderFragments, }, useTheme: useThemeTwitterVariant, i18nOverwrite: i18NOverwriteTwitter, diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index a59b9e63baa8..b68569bd8016 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -15,6 +15,7 @@ import type { PostInfo } from './PostInfo' import type { GrayscaleAlgorithm } from '@masknet/encryption' import type { createSNSAdaptorSpecializedPostContext } from './utils/create-post-context' import type { Subscription } from 'use-subscription' +import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' type ClassNameMap = { [P in ClassKey]: string } // Don't define values in namespaces @@ -271,6 +272,7 @@ export namespace SocialNetworkUI { } export interface ComponentOverwrite { InjectedDialog?: ComponentOverwriteConfig + RenderFragments?: RenderFragmentsContextType } export interface ComponentOverwriteConfig { classes?: () => { classes: Partial> } From 15d7582329c59f1e7e0091ec26458ecec1221852 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 16:22:50 +0800 Subject: [PATCH 08/18] fix: cash and hash tag --- .../TypedMessageRender/Components/Text.tsx | 30 +++++++-- .../shared-ui/TypedMessageRender/context.tsx | 2 +- packages/mask/shared-ui/tsconfig.json | 3 +- .../src/plugins/Trader/Dashboard/index.tsx | 2 - .../src/plugins/Trader/SNSAdaptor/cashTag.tsx | 23 +++++++ .../src/plugins/Trader/SNSAdaptor/index.tsx | 4 +- .../mask/src/plugins/Trader/Worker/index.ts | 2 - .../mask/src/plugins/Trader/base-deferred.ts | 12 ---- .../messages/TypedMessageCashTrending.tsx | 64 ------------------- .../customization/render-fragments.tsx | 15 ++++- packages/plugin-infra/src/types.ts | 16 +++-- 11 files changed, 77 insertions(+), 96 deletions(-) create mode 100644 packages/mask/src/plugins/Trader/SNSAdaptor/cashTag.tsx delete mode 100644 packages/mask/src/plugins/Trader/base-deferred.ts delete mode 100644 packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx diff --git a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx index ecfc49a49ad3..0e4cbcc4fe5c 100644 --- a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -1,6 +1,7 @@ -import { memo } from 'react' -import { Typography, Link } from '@mui/material' +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 Text = memo(function Text(props: RenderFragmentsContextType.TextProps) { return ( @@ -11,12 +12,33 @@ export const Text = memo(function Text(props: RenderFragmentsContextType.TextPro ) }) -export const Anchor = memo(function Anchor(props: RenderFragmentsContextType.LinkProps) { +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 index 20c99e30bb36..f2d3820b8a18 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -8,7 +8,7 @@ import { TypedMessageRenderRegistry } from './registry' import { useSubscription } from 'use-subscription' import type { TransformationContext } from '@masknet/typed-message' import { useMemo } from 'react' -import { Text, Anchor as Link } from './Components/Text' +import { Text, Link } from './Components/Text' export interface TypedMessageRenderContextProps extends React.PropsWithChildren<{}> { context?: TransformationContext diff --git a/packages/mask/shared-ui/tsconfig.json b/packages/mask/shared-ui/tsconfig.json index 9dc25225efe0..0af08eed08ac 100644 --- a/packages/mask/shared-ui/tsconfig.json +++ b/packages/mask/shared-ui/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../../shared/" }, { "path": "../../typed-message/base" }, { "path": "../../typed-message/dom" }, - { "path": "../../theme" } + { "path": "../../theme" }, + { "path": "../../plugin-infra" } ] } 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 8abfb14a0f2a..000000000000 --- a/packages/mask/src/plugins/Trader/base-deferred.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Plugin } from '@masknet/plugin-infra' -import { visitEachTypedMessageChild } from '@masknet/typed-message' -import { isCashTagMessage, makeTypedMessageCashTrending } from './messages/TypedMessageCashTrending' - -export const baseDeferred: Plugin.Shared.Utilities = { - typedMessageTransformer: function visitor(message, context) { - if (isCashTagMessage(message)) { - return makeTypedMessageCashTrending(message) - } - return visitEachTypedMessageChild(message, visitor, context) - }, -} 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 e6aa0e3def0a..000000000000 --- a/packages/mask/src/plugins/Trader/messages/TypedMessageCashTrending.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState } from 'react' -import { Link, Typography } from '@mui/material' -import { TypedMessageAnchor, TypedMessage, isTypedMessageAnchor } from '@masknet/typed-message' -import { PluginTraderMessages, PluginTraderRPC } from '../messages' -import { TagType } from '../types' -// TODO: when migrate, should have an API in the plugin infra for plugin to define render -import { TypedMessageRenderRegistry } from '../../../../shared-ui/TypedMessageRender/registry' - -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 -} -TypedMessageRenderRegistry.registerTypedMessageRender('x-cash-trending', { - component: CashTrendingRenderer, - id: Symbol('com.mask.trader.x-cash-trending'), - priority: 0, -}) - -function CashTrendingRenderer(props: TypedMessageCashTrending) { - 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.category !== 'cash' && props.category !== 'hash') return - const { name, category } = props - const type = category === 'cash' ? TagType.CASH : TagType.HASH - const dataProviders = await PluginTraderRPC.getAvailableDataProviders(type, name) - 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.content} - - - ) -} 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 index 8f70dd05eb16..57e2d95b526d 100644 --- 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 @@ -1,6 +1,7 @@ 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) @@ -13,9 +14,21 @@ export const TwitterRenderFragments: RenderFragmentsContextType = { ) }), HashLink: memo(function (props) { - const target = '/hashtag/' + props.children.slice(1) + const target = '/hashtag/' + encodeURIComponent(props.children.slice(1)) return ( + ) + }), + CashLink: memo(function (props) { + const target = `/search?q=${encodeURIComponent(props.children)}&src=cashtag_click` + return ( + } - export interface Utilities { - /** - * A pure function that convert a TypedMessage into another one - */ - typedMessageTransformer?: Transformer - } + export interface Utilities {} export type TypedMessageTransformer = (message: TypedMessageTuple) => TypedMessageTuple /** The publisher of the plugin */ export interface Publisher { @@ -254,6 +249,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 /** From ae56ad96da3c23a383fc8476ccb29c040716c1ac Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 17:03:43 +0800 Subject: [PATCH 09/18] feat: support fragment on facebook --- .../customization/render-fragments.tsx | 23 +++++++++++++++++++ .../facebook.com/ui-provider.ts | 2 ++ .../customization/render-fragments.tsx | 5 ++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 packages/mask/src/social-network-adaptor/facebook.com/customization/render-fragments.tsx 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..f8dc4bbbbac1 --- /dev/null +++ b/packages/mask/src/social-network-adaptor/facebook.com/customization/render-fragments.tsx @@ -0,0 +1,23 @@ +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)}` + 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/twitter.com/customization/render-fragments.tsx b/packages/mask/src/social-network-adaptor/twitter.com/customization/render-fragments.tsx index 57e2d95b526d..c8f4f776fb4d 100644 --- 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 @@ -14,10 +14,11 @@ export const TwitterRenderFragments: RenderFragmentsContextType = { ) }), HashLink: memo(function (props) { - const target = '/hashtag/' + encodeURIComponent(props.children.slice(1)) + const text = props.children.slice(1) + const target = `/hashtag/${encodeURIComponent(text)}?src=hashtag_click` return ( Date: Tue, 22 Feb 2022 18:13:02 +0800 Subject: [PATCH 10/18] fix: many regressions --- .../shared-ui/TypedMessageRender/context.tsx | 17 +++++++++--- .../TypedMessageRender/transformer.ts | 24 ++++++++++++++++- .../DecryptedPost/DecryptedPostSuccess.tsx | 2 +- .../DecryptedPostMetadataRender.tsx | 2 ++ .../InjectedComponents/PayloadReplacer.tsx | 26 ------------------- packages/typed-message/base/utils/extract.ts | 14 +++++++--- 6 files changed, 49 insertions(+), 36 deletions(-) delete mode 100644 packages/mask/src/components/InjectedComponents/PayloadReplacer.tsx diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx index f2d3820b8a18..08aeae13aaec 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -1,14 +1,17 @@ +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 type { TransformationContext } from '@masknet/typed-message' import { useMemo } from 'react' import { Text, Link } from './Components/Text' +import { TypedMessageTransformers } from './transformer' export interface TypedMessageRenderContextProps extends React.PropsWithChildren<{}> { context?: TransformationContext @@ -18,7 +21,7 @@ export interface TypedMessageRenderContextProps extends React.PropsWithChildren< export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) { const registry = useSubscription(TypedMessageRenderRegistry.subscription) - // const transformerFunction = useSubscription(TypedMessageTransformers.subscription) + const transformerFunction = useSubscription(TypedMessageTransformers.subscription) const Provider = useMemo((): RenderFragmentsContextType => { return { Text, Link, Metadata: props.metadataRender, ...props.renderFragments } }, [props.metadataRender, props.renderFragments]) @@ -26,8 +29,14 @@ export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) return ( // basic render fragments provider: Text, Link, Image and Metadata - {/* Typed message render provider: a registry */} - {props.children} + {/* transformer pipeline */} + + {/* transformation context */} + + {/* components provider */} + {props.children} + + ) } diff --git a/packages/mask/shared-ui/TypedMessageRender/transformer.ts b/packages/mask/shared-ui/TypedMessageRender/transformer.ts index b95a1876cc63..e1816712c018 100644 --- a/packages/mask/shared-ui/TypedMessageRender/transformer.ts +++ b/packages/mask/shared-ui/TypedMessageRender/transformer.ts @@ -1,9 +1,18 @@ -import { composeTransformers, FlattenTypedMessage, ParseLinkTransformer } from '@masknet/typed-message' +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, } /** @@ -20,3 +29,16 @@ 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/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx index ce8fc8596917..ac38282ac946 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx @@ -53,12 +53,12 @@ export const DecryptPostSuccess = memo(function DecryptPostSuccess(props: Decryp return ( <> {shareMenu.ShareMenu} - + ) }) diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx index e7c3a860b24b..46399339db92 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx @@ -6,10 +6,12 @@ import { useDisabledPluginSuggestionFromMeta, useDisabledPluginSuggestionFromPost, } from './DisabledPluginSuggestion' +import { MaskPostExtraPluginWrapper } from '../../plugins/MaskPluginWrapper' const PluginRenderer = createInjectHooksRenderer( useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, (x) => x.DecryptedInspector, + MaskPostExtraPluginWrapper, ) export function PluginRendererWithSuggestion(props: MetadataRenderProps) { const a = useDisabledPluginSuggestionFromMeta(props.metadata) 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/typed-message/base/utils/extract.ts b/packages/typed-message/base/utils/extract.ts index faaf025602c3..199b98e05747 100644 --- a/packages/typed-message/base/utils/extract.ts +++ b/packages/typed-message/base/utils/extract.ts @@ -13,23 +13,29 @@ export function extractTextFromTypedMessage( options?: { linkAsText: boolean }, ): Option { if (!message) return None + const text: string[] = [] - forEachTypedMessageChild(message, function visitor(message) { + 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)[] = [] - forEachTypedMessageChild(message, function visitor(message) { + function visitor(message: TypedMessage): number | void { if (isTypedMessageImage(message)) return image.push(message.image) return forEachTypedMessageChild(message, visitor) - }) + } + visitor(message) + forEachTypedMessageChild(message, visitor) return image } From c97cb1b6851cbec69c3eeedf92c6b0d465113db1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 18:16:00 +0800 Subject: [PATCH 11/18] chore: add allowTextEnlarge back --- packages/typed-message/dom/Renderer/Entry.tsx | 23 ++++++++++++------- .../dom/Renderer/utils/TextEnlargeContext.tsx | 3 +++ .../dom/Renderer/utils/renderText.tsx | 4 +++- 3 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx diff --git a/packages/typed-message/dom/Renderer/Entry.tsx b/packages/typed-message/dom/Renderer/Entry.tsx index 0907857cb474..52c57fb67dee 100644 --- a/packages/typed-message/dom/Renderer/Entry.tsx +++ b/packages/typed-message/dom/Renderer/Entry.tsx @@ -2,6 +2,7 @@ import { useContext } from 'react' import type { TypedMessage } from '../../base' import { TypedMessageUnknownRenderer as TypedMessageUnknownRender } from './Core/Unknown' import { RegistryContext } from './utils/RegistryContext' +import { TextEnlargeContext } from './utils/TextEnlargeContext' export interface RenderProps { message: TypedMessage /** @deprecated */ @@ -15,14 +16,20 @@ export function TypedMessageRender(props: RenderProps) { const Render = Registry(message.type)?.component || TypedMessageUnknownRender if (process.env.NODE_ENV === 'development') { return ( - { - if (ref) (ref as any).message = message - }}> - - + + { + if (ref) (ref as any).message = message + }}> + + + ) } - return + return ( + + + + ) } diff --git a/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx b/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx new file mode 100644 index 000000000000..cca656f672df --- /dev/null +++ b/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export const TextEnlargeContext = createContext(false) diff --git a/packages/typed-message/dom/Renderer/utils/renderText.tsx b/packages/typed-message/dom/Renderer/utils/renderText.tsx index 27c1269761a0..3862aecf588c 100644 --- a/packages/typed-message/dom/Renderer/utils/renderText.tsx +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -2,6 +2,7 @@ import { memo, Fragment, createElement, useContext } from 'react' import { RenderFragmentsContext, RenderFragmentsContextType, DefaultRenderFragments } from './RenderFragments' import { parseLink } from '../../../base/utils/parseLink' import type { TypedMessageAnchor } from '../../../base/extension' +import { TextEnlargeContext } from './TextEnlargeContext' /** @internal */ export interface RenderTextProps { @@ -11,7 +12,8 @@ export interface RenderTextProps { /** @internal */ export const RenderTextFragment = memo(function RenderText(props: RenderTextProps) { const { Text = DefaultRenderFragments.Text } = useContext(RenderFragmentsContext) - return createElement(Fragment, {}, ...parseText(props.text, false, Text)) + const allowTextEnlarge = useContext(TextEnlargeContext) + return createElement(Fragment, {}, ...parseText(props.text, allowTextEnlarge, Text)) }) /** @internal */ From 9ed5c67ab3a3cdb826c1f32af72861f622bf7e15 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 18:19:34 +0800 Subject: [PATCH 12/18] fix: react hooks rule --- packages/typed-message/dom/Renderer/Core/Tuple.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typed-message/dom/Renderer/Core/Tuple.tsx b/packages/typed-message/dom/Renderer/Core/Tuple.tsx index a71a8333c575..c99cb620d729 100644 --- a/packages/typed-message/dom/Renderer/Core/Tuple.tsx +++ b/packages/typed-message/dom/Renderer/Core/Tuple.tsx @@ -5,13 +5,14 @@ import { TypedMessageRender } 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) => ( ))} - {useMetadataRender(props)} + {meta} ) }) From 0245e5ae0c1d47e10f582d8109bd5d49a30118cc Mon Sep 17 00:00:00 2001 From: Jack Works Date: Tue, 22 Feb 2022 18:38:37 +0800 Subject: [PATCH 13/18] fix: text enlarge --- .../AdditionalPostContent.tsx | 14 +++++++---- .../DecryptedPost/DecryptedPostSuccess.tsx | 4 +-- .../DecryptedPostMetadataRender.tsx | 7 +++--- .../InjectedComponents/PostReplacer.tsx | 16 ++++++------ .../customization/render-fragments.tsx | 3 ++- .../customization/render-fragments.tsx | 6 +++-- packages/typed-message/dom/Renderer/Entry.tsx | 25 ++++++------------- packages/typed-message/dom/index.ts | 3 +++ 8 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx index 1affbedb99ad..0fd0bd639434 100644 --- a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -2,7 +2,7 @@ import { Typography, Card, Box, CircularProgress, CircularProgressProps } from ' import { makeStyles } from '@masknet/theme' import classNames from 'classnames' import { TypedMessage, makeTypedMessageText } from '@masknet/typed-message' -import { TypedMessageRender } from '@masknet/typed-message/dom' +import { TextEnlargeContext, 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' @@ -76,10 +76,14 @@ export const AdditionalContent = memo(function AdditionalContent(props: Addition
{header}
{message ? (
- - - + + + + +
) : null} diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx index ac38282ac946..ddb8df70f05e 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPost/DecryptedPostSuccess.tsx @@ -8,7 +8,7 @@ import type { Profile } from '../../../database' import type { TypedMessage } from '@masknet/typed-message' import type { ProfileIdentifier } from '@masknet/shared-base' import { wrapAuthorDifferentMessage } from './authorDifferentMessage' -import { PluginRendererWithSuggestion } from '../DecryptedPostMetadataRender' +import { DecryptedUI_PluginRendererWithSuggestion } from '../DecryptedPostMetadataRender' export interface DecryptPostSuccessProps extends withClasses { data: { content: TypedMessage } @@ -58,7 +58,7 @@ export const DecryptPostSuccess = memo(function DecryptPostSuccess(props: Decryp title={t('decrypted_postbox_title')} message={content} /> - + ) }) diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx index 46399339db92..2b191c1ad1a3 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx @@ -8,20 +8,21 @@ import { } from './DisabledPluginSuggestion' import { MaskPostExtraPluginWrapper } from '../../plugins/MaskPluginWrapper' -const PluginRenderer = createInjectHooksRenderer( +const Decrypted = createInjectHooksRenderer( useActivatedPluginsSNSAdaptor.visibility.useNotMinimalMode, (x) => x.DecryptedInspector, MaskPostExtraPluginWrapper, ) -export function PluginRendererWithSuggestion(props: MetadataRenderProps) { +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))) + console.log(props) return ( <> - + ) } diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index df9ffa9c2dff..97efdf7ea0f4 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,11 +1,10 @@ import type { TransformationContext, TypedMessage } from '@masknet/typed-message' -import { TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' +import { TextEnlargeContext, TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' import { makeStyles } from '@masknet/theme' import { useEffect, useMemo } from 'react' import { usePostInfoDetails } from '../DataSource/usePostInfo' import { TypedMessageRenderContext } from '../../../shared-ui/TypedMessageRender/context' import { useCurrentIdentity } from '../DataSource/useActivatedUI' -import { PluginRendererWithSuggestion } from './DecryptedPostMetadataRender' import { activatedSocialNetworkUI } from '../../social-network/ui' const useStyles = makeStyles()({ @@ -43,12 +42,13 @@ export function PostReplacer({ unzip, zip }: PostReplacerProps) { return ( - - - + + + + + ) } 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 index f8dc4bbbbac1..15075fd2c608 100644 --- 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 @@ -6,9 +6,10 @@ import { useTagEnhancer } from '../../../../shared-ui/TypedMessageRender/Compone 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 ( - { - if (ref) (ref as any).message = message - }}> - - - + { + if (ref) (ref as any).message = message + }}> + + ) } - return ( - - - - ) + return } diff --git a/packages/typed-message/dom/index.ts b/packages/typed-message/dom/index.ts index 13abd5964e5d..5e434f3451d4 100644 --- a/packages/typed-message/dom/index.ts +++ b/packages/typed-message/dom/index.ts @@ -2,6 +2,9 @@ export { TypedMessageRender, type RenderProps } from './Renderer/Entry' export { useMetadataRender, type MetadataRenderProps } from './Renderer/MetadataRender' +// Render behavior +export { TextEnlargeContext } from './Renderer/utils/TextEnlargeContext' + // Render Registry export { type RenderConfig, createTypedMessageRenderRegistry } from './Renderer/registry' export { From ada7fddad109c30e4290ea774addba3e07ffb5f7 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 23 Feb 2022 00:17:11 +0800 Subject: [PATCH 14/18] chore: reply review --- packages/typed-message/base/transformer/MaskPayload/index.ts | 2 +- packages/typed-message/base/utils/extract.ts | 2 +- packages/typed-message/base/utils/parseLink.ts | 2 +- packages/typed-message/base/visitor/index.ts | 1 + packages/typed-message/dom/Renderer/utils/TransformContext.ts | 3 +-- packages/typed-message/dom/Renderer/utils/renderText.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/typed-message/base/transformer/MaskPayload/index.ts b/packages/typed-message/base/transformer/MaskPayload/index.ts index 8f064ec919ea..7117f5671f73 100644 --- a/packages/typed-message/base/transformer/MaskPayload/index.ts +++ b/packages/typed-message/base/transformer/MaskPayload/index.ts @@ -1,4 +1,4 @@ -import { FlattenTypedMessage } from '..' +import { FlattenTypedMessage } from '../Flatten' import type { TypedMessage } from '../../base' import { isTypedMessageImage, isTypedMessageText, isTypedMessageTuple, TypedMessageImage } from '../../core' import { isTypedMessageAnchor, isTypedMessageMaskPayload } from '../../extension' diff --git a/packages/typed-message/base/utils/extract.ts b/packages/typed-message/base/utils/extract.ts index 199b98e05747..2196e755f7fd 100644 --- a/packages/typed-message/base/utils/extract.ts +++ b/packages/typed-message/base/utils/extract.ts @@ -2,7 +2,7 @@ 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/forEachChild' +import { forEachTypedMessageChild } from '../visitor' /** * Get inner text from a TypedMessage diff --git a/packages/typed-message/base/utils/parseLink.ts b/packages/typed-message/base/utils/parseLink.ts index 7316a43bf953..7d4e1333ef43 100644 --- a/packages/typed-message/base/utils/parseLink.ts +++ b/packages/typed-message/base/utils/parseLink.ts @@ -1,5 +1,5 @@ import _anchorme from 'anchorme' -import type { TypedMessageAnchor } from '..' +import type { TypedMessageAnchor } from '../extension' // ESM/CJS compat const anchorme = ((_anchorme as any).default || _anchorme) as typeof _anchorme diff --git a/packages/typed-message/base/visitor/index.ts b/packages/typed-message/base/visitor/index.ts index a677985107d3..c5fdbce1dec9 100644 --- a/packages/typed-message/base/visitor/index.ts +++ b/packages/typed-message/base/visitor/index.ts @@ -1 +1,2 @@ export * from './visitEachChild' +export * from './forEachChild' diff --git a/packages/typed-message/dom/Renderer/utils/TransformContext.ts b/packages/typed-message/dom/Renderer/utils/TransformContext.ts index 7ebac43bf391..144d8d5d4299 100644 --- a/packages/typed-message/dom/Renderer/utils/TransformContext.ts +++ b/packages/typed-message/dom/Renderer/utils/TransformContext.ts @@ -1,6 +1,5 @@ -import type { Transformer, TypedMessage } from '../../../base' +import { Transformer, TypedMessage, emptyTransformationContext, TransformationContext } from '../../../base' import { createContext, useContext, useMemo } from 'react' -import { emptyTransformationContext, TransformationContext } from '../../../base/transformer/context' export const TransformerProvider = createContext((x) => x) export const TransformationContextProvider = createContext(emptyTransformationContext) diff --git a/packages/typed-message/dom/Renderer/utils/renderText.tsx b/packages/typed-message/dom/Renderer/utils/renderText.tsx index 3862aecf588c..5f825649fdab 100644 --- a/packages/typed-message/dom/Renderer/utils/renderText.tsx +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -1,7 +1,7 @@ import { memo, Fragment, createElement, useContext } from 'react' import { RenderFragmentsContext, RenderFragmentsContextType, DefaultRenderFragments } from './RenderFragments' +import type { TypedMessageAnchor } from '../../../base' import { parseLink } from '../../../base/utils/parseLink' -import type { TypedMessageAnchor } from '../../../base/extension' import { TextEnlargeContext } from './TextEnlargeContext' /** @internal */ From 150912ee3700a06c6bcb3468423e4202ee7fbfc6 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 23 Feb 2022 12:47:28 +0800 Subject: [PATCH 15/18] fix: review bug --- .../TypedMessageRender/Components/Text.tsx | 17 ++--------- .../shared-ui/TypedMessageRender/context.tsx | 4 +-- .../AdditionalPostContent.tsx | 6 ++-- .../InjectedComponents/PostReplacer.tsx | 6 ++-- .../customization/render-fragments.tsx | 9 +----- .../customization/render-fragments.tsx | 26 ++--------------- .../dom/Renderer/Core/Promise.tsx | 6 ++-- .../typed-message/dom/Renderer/Core/Tuple.tsx | 4 +-- packages/typed-message/dom/Renderer/Entry.tsx | 19 ++++++++++++ .../dom/Renderer/utils/RenderFragments.tsx | 9 ++---- .../dom/Renderer/utils/TextEnlargeContext.tsx | 3 -- .../dom/Renderer/utils/TextResizerContext.tsx | 28 ++++++++++++++++++ .../dom/Renderer/utils/renderText.tsx | 29 ++++++------------- packages/typed-message/dom/index.ts | 4 +-- 14 files changed, 80 insertions(+), 90 deletions(-) delete mode 100644 packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx create mode 100644 packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx diff --git a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx index 0e4cbcc4fe5c..4fc473590c89 100644 --- a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -3,23 +3,12 @@ import { Typography, Link as MaterialLink } from '@mui/material' import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' -export const Text = memo(function Text(props: RenderFragmentsContextType.TextProps) { - return ( - - ) +export const Container = memo(function Container(props) { + return {props.children} }) export const Link = memo(function Anchor(props: RenderFragmentsContextType.LinkProps) { - return ( - - ) + return }) export function useTagEnhancer(kind: 'hash' | 'cash', content: string) { diff --git a/packages/mask/shared-ui/TypedMessageRender/context.tsx b/packages/mask/shared-ui/TypedMessageRender/context.tsx index 08aeae13aaec..06cd69a1fb7b 100644 --- a/packages/mask/shared-ui/TypedMessageRender/context.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/context.tsx @@ -10,7 +10,7 @@ import { import { TypedMessageRenderRegistry } from './registry' import { useSubscription } from 'use-subscription' import { useMemo } from 'react' -import { Text, Link } from './Components/Text' +import { Container, Link } from './Components/Text' import { TypedMessageTransformers } from './transformer' export interface TypedMessageRenderContextProps extends React.PropsWithChildren<{}> { @@ -23,7 +23,7 @@ export function TypedMessageRenderContext(props: TypedMessageRenderContextProps) const registry = useSubscription(TypedMessageRenderRegistry.subscription) const transformerFunction = useSubscription(TypedMessageTransformers.subscription) const Provider = useMemo((): RenderFragmentsContextType => { - return { Text, Link, Metadata: props.metadataRender, ...props.renderFragments } + return { Container, Link, Metadata: props.metadataRender, ...props.renderFragments } }, [props.metadataRender, props.renderFragments]) return ( diff --git a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx index 0fd0bd639434..426cbf2eb80b 100644 --- a/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx +++ b/packages/mask/src/components/InjectedComponents/AdditionalPostContent.tsx @@ -2,7 +2,7 @@ import { Typography, Card, Box, CircularProgress, CircularProgressProps } from ' import { makeStyles } from '@masknet/theme' import classNames from 'classnames' import { TypedMessage, makeTypedMessageText } from '@masknet/typed-message' -import { TextEnlargeContext, TypedMessageRender } from '@masknet/typed-message/dom' +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' @@ -76,14 +76,14 @@ export const AdditionalContent = memo(function AdditionalContent(props: Addition
{header}
{message ? (
- + - +
) : null} diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index 97efdf7ea0f4..179a46d0b2c1 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,5 +1,5 @@ import type { TransformationContext, TypedMessage } from '@masknet/typed-message' -import { TextEnlargeContext, TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' +import { TextResizeContext, TypedMessageRender, useTransformedValue } from '@masknet/typed-message/dom' import { makeStyles } from '@masknet/theme' import { useEffect, useMemo } from 'react' import { usePostInfoDetails } from '../DataSource/usePostInfo' @@ -42,13 +42,13 @@ export function PostReplacer({ unzip, zip }: PostReplacerProps) { return ( - + - + ) } 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 index 15075fd2c608..c20a0e2b5133 100644 --- 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 @@ -7,14 +7,7 @@ function Hash(props: RenderFragmentsContextType.HashLinkProps | RenderFragmentsC const text = props.children.slice(1) const target = `/hashtag/${encodeURIComponent(text)}` const { hasMatch, ...events } = useTagEnhancer('hash', text) - return ( - - ) + return } export const FacebookRenderFragments: RenderFragmentsContextType = { // AtLink: not supported 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 index 7be8b1c6f671..ecfff4710ec9 100644 --- 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 @@ -5,37 +5,17 @@ import { useTagEnhancer } from '../../../../shared-ui/TypedMessageRender/Compone export const TwitterRenderFragments: RenderFragmentsContextType = { AtLink: memo(function (props) { const target = '/' + props.children.slice(1) - return ( - - ) + 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 ( - - ) + 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 ( - - ) + return }), } diff --git a/packages/typed-message/dom/Renderer/Core/Promise.tsx b/packages/typed-message/dom/Renderer/Core/Promise.tsx index bacafe8b89c7..dc18a89e7eb3 100644 --- a/packages/typed-message/dom/Renderer/Core/Promise.tsx +++ b/packages/typed-message/dom/Renderer/Core/Promise.tsx @@ -1,6 +1,6 @@ import { memo, Suspense, useEffect, useState } from 'react' import type { TypedMessagePromise } from '../../../base' -import { TypedMessageRender } from '../Entry' +import { TypedMessageRenderInline } from '../Entry' import { useTransformedValue } from '../utils/TransformContext' export const TypedMessagePromiseRenderer = memo(function TypedMessagePromiseRenderer(props: TypedMessagePromise) { const { promise, alt } = props @@ -12,9 +12,9 @@ export const TypedMessagePromiseRenderer = memo(function TypedMessagePromiseRend }, [promise, _]) const transformedValue = useTransformedValue(promise.value) - if (transformedValue) return + if (transformedValue) return return ( - : null}> + : null}> ) diff --git a/packages/typed-message/dom/Renderer/Core/Tuple.tsx b/packages/typed-message/dom/Renderer/Core/Tuple.tsx index c99cb620d729..5f84d22298e6 100644 --- a/packages/typed-message/dom/Renderer/Core/Tuple.tsx +++ b/packages/typed-message/dom/Renderer/Core/Tuple.tsx @@ -1,7 +1,7 @@ import { memo, useMemo } from 'react' import type { TypedMessageTuple } from '../../../base' import { hasCircular } from '../utils/circularDetect' -import { TypedMessageRender } from '../Entry' +import { TypedMessageRenderInline } from '../Entry' import { useMetadataRender } from '../MetadataRender' export const TypedMessageTupleRenderer = memo(function TypedMessageTupleRenderer(props: TypedMessageTuple) { @@ -10,7 +10,7 @@ export const TypedMessageTupleRenderer = memo(function TypedMessageTupleRenderer return ( <> {props.items.map((message, index) => ( - + ))} {meta} diff --git a/packages/typed-message/dom/Renderer/Entry.tsx b/packages/typed-message/dom/Renderer/Entry.tsx index 037d9debfd67..722a3a707a84 100644 --- a/packages/typed-message/dom/Renderer/Entry.tsx +++ b/packages/typed-message/dom/Renderer/Entry.tsx @@ -2,11 +2,30 @@ 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 diff --git a/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx b/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx index 6e6015887a24..7641a3393c7b 100644 --- a/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx +++ b/packages/typed-message/dom/Renderer/utils/RenderFragments.tsx @@ -2,7 +2,7 @@ import { createContext, memo } from 'react' import type { MetadataRenderProps } from '../MetadataRender' export const DefaultRenderFragments = { - Text: memo((props: RenderFragmentsContextType.TextProps) => {props.children}), + Text: memo((props: RenderFragmentsContextType.TextProps) => <>{props.children}), Link: memo((props: RenderFragmentsContextType.LinkProps) => ( {props.children} @@ -13,8 +13,8 @@ export const DefaultRenderFragments = { ), Metadata: memo(() => null), } - export interface RenderFragmentsContextType { + Container?: React.ComponentType<{}> Text?: React.ComponentType Image?: React.ComponentType Metadata?: React.ComponentType @@ -29,24 +29,19 @@ export interface RenderFragmentsContextType { export namespace RenderFragmentsContextType { export interface TextProps { children: string - fontSize?: number } export interface LinkProps { children: string href: string - fontSize?: number } export interface AtLinkProps { children: string - fontSize?: number } export interface CashLinkProps { children: string - fontSize?: number } export interface HashLinkProps { children: string - fontSize?: number } export interface ImageProps { src: string diff --git a/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx b/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx deleted file mode 100644 index cca656f672df..000000000000 --- a/packages/typed-message/dom/Renderer/utils/TextEnlargeContext.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react' - -export const TextEnlargeContext = createContext(false) 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..f87b011f2d0b --- /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 = 14 + if (length < 45) fontSize *= 1.5 + else if (length < 80) fontSize *= 1.2 + + element.style.fontSize = `${fontSize}px` + } + 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/renderText.tsx b/packages/typed-message/dom/Renderer/utils/renderText.tsx index 5f825649fdab..92d59c50429b 100644 --- a/packages/typed-message/dom/Renderer/utils/renderText.tsx +++ b/packages/typed-message/dom/Renderer/utils/renderText.tsx @@ -2,7 +2,6 @@ import { memo, Fragment, createElement, useContext } from 'react' import { RenderFragmentsContext, RenderFragmentsContextType, DefaultRenderFragments } from './RenderFragments' import type { TypedMessageAnchor } from '../../../base' import { parseLink } from '../../../base/utils/parseLink' -import { TextEnlargeContext } from './TextEnlargeContext' /** @internal */ export interface RenderTextProps { @@ -12,15 +11,14 @@ export interface RenderTextProps { /** @internal */ export const RenderTextFragment = memo(function RenderText(props: RenderTextProps) { const { Text = DefaultRenderFragments.Text } = useContext(RenderFragmentsContext) - const allowTextEnlarge = useContext(TextEnlargeContext) - return createElement(Fragment, {}, ...parseText(props.text, allowTextEnlarge, Text)) + return createElement(Fragment, {}, ...parseText(props.text, Text)) }) /** @internal */ export const RenderLinkFragment = memo(function RenderLink( props: Pick & RenderFragmentsContextType.LinkProps, ) { - const { children, href, category, fontSize } = props + const { children, href, category } = props const context = useContext(RenderFragmentsContext) const { Text = DefaultRenderFragments.Text, @@ -29,28 +27,19 @@ export const RenderLinkFragment = memo(function RenderLink( CashLink = Text, HashLink = Text, } = context - if (category === 'cash') return - if (category === 'hash') return - if (category === 'user') return - return + if (category === 'cash') return + if (category === 'hash') return + if (category === 'user') return + return }) -function parseText(string: string, allowTextEnlarge: boolean, Text: NonNullable) { - const fontSize = - 14 * - (allowTextEnlarge && Array.from(string).length < 45 - ? 1.5 - : allowTextEnlarge && Array.from(string).length < 85 - ? 1.2 - : 1) +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' ?
: )) + 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 }) return links } diff --git a/packages/typed-message/dom/index.ts b/packages/typed-message/dom/index.ts index 5e434f3451d4..6e86b70a8684 100644 --- a/packages/typed-message/dom/index.ts +++ b/packages/typed-message/dom/index.ts @@ -1,9 +1,9 @@ // Render -export { TypedMessageRender, type RenderProps } from './Renderer/Entry' +export { TypedMessageRender, TypedMessageRenderInline, type RenderProps } from './Renderer/Entry' export { useMetadataRender, type MetadataRenderProps } from './Renderer/MetadataRender' // Render behavior -export { TextEnlargeContext } from './Renderer/utils/TextEnlargeContext' +export { TextResizeContext } from './Renderer/utils/TextResizerContext' // Render Registry export { type RenderConfig, createTypedMessageRenderRegistry } from './Renderer/registry' From eb55a138b94b81710c062d1cade5a2a5bb2e4534 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 23 Feb 2022 13:44:43 +0800 Subject: [PATCH 16/18] fix: review bug --- packages/mask/shared-ui/locales/en-US.json | 1 - packages/mask/shared-ui/locales/ko-KR.json | 1 - packages/mask/shared-ui/locales/qya-AA.json | 1 - packages/mask/shared-ui/locales/zh-CN.json | 1 - .../InjectedComponents/DecryptedPostMetadataRender.tsx | 1 - 5 files changed, 5 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index bd1ed3a9121f..e95460f0573b 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -127,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", diff --git a/packages/mask/shared-ui/locales/ko-KR.json b/packages/mask/shared-ui/locales/ko-KR.json index 9af233b79a37..4e05eb84a169 100644 --- a/packages/mask/shared-ui/locales/ko-KR.json +++ b/packages/mask/shared-ui/locales/ko-KR.json @@ -111,7 +111,6 @@ "post_dialog__title": "Mask: 편집", "post_dialog_enable_paste_auto": "자동 붙이기 지원", "post_modal_hint__button": "암호화된 메시지를 편집하기", - "post_substitute_label": "Mask", "hide": "숨기기", "reset": "초기화", "editor": "에디터", diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index 7ef93e0cc4fc..d0063b83397e 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -126,7 +126,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", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index 182f0abc1434..ef29bb1cb64c 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -125,7 +125,6 @@ "post_dialog__title": "Mask:编辑", "post_dialog_enable_paste_auto": "启用自动粘贴", "post_modal_hint__button": "编辑加密贴文", - "post_substitute_label": "Mask", "hide": "隐藏", "reset": "重置", "editor": "编辑器", diff --git a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx index 2b191c1ad1a3..66a5914878b1 100644 --- a/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx +++ b/packages/mask/src/components/InjectedComponents/DecryptedPostMetadataRender.tsx @@ -18,7 +18,6 @@ export function DecryptedUI_PluginRendererWithSuggestion(props: MetadataRenderPr const b = useDisabledPluginSuggestionFromPost(extractTextFromTypedMessage(props.message), []) const suggest = Array.from(new Set(a.concat(b))) - console.log(props) return ( <> From 5e3e00686959849d0a73db2ca8937f6ce317d3f1 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 24 Feb 2022 12:44:42 +0800 Subject: [PATCH 17/18] fix: bug --- .../TypedMessageRender/Components/Text.tsx | 6 +- .../InjectedComponents/PostReplacer.tsx | 55 +++++++++++++++---- packages/typed-message/base/utils/index.ts | 12 +--- .../base/visitor/forEachChild.ts | 8 ++- .../dom/Renderer/utils/TextResizerContext.tsx | 8 +-- 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx index 4fc473590c89..10c4d5091c1e 100644 --- a/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx +++ b/packages/mask/shared-ui/TypedMessageRender/Components/Text.tsx @@ -4,7 +4,11 @@ import type { RenderFragmentsContextType } from '@masknet/typed-message/dom' import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra' export const Container = memo(function Container(props) { - return {props.children} + return ( + + {props.children} + + ) }) export const Link = memo(function Anchor(props: RenderFragmentsContextType.LinkProps) { diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index 179a46d0b2c1..00b12bb59e98 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -1,4 +1,12 @@ -import type { TransformationContext, TypedMessage } from '@masknet/typed-message' +import { + 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' @@ -18,7 +26,7 @@ export interface PostReplacerProps { unzip?: () => void } -export function PostReplacer({ unzip, zip }: PostReplacerProps) { +export function PostReplacer(props: PostReplacerProps) { const { classes } = useStyles() const postMessage = usePostInfoDetails.rawMessage() @@ -26,12 +34,6 @@ export function PostReplacer({ unzip, zip }: PostReplacerProps) { const currentProfile = useCurrentIdentity()?.identifier const url = usePostInfoDetails.url() - // zip/unzip original post - useEffect(() => { - zip?.() - return () => unzip?.() - }, []) - const initialTransformationContext = useMemo((): TransformationContext => { return { authorHint: author, @@ -46,14 +48,43 @@ export function PostReplacer({ unzip, zip }: PostReplacerProps) { - +
) } -function Transformer(props: { message: TypedMessage }) { - const after = useTransformedValue(props.message) - return +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 + forEachTypedMessageChild(message, function visitor(node) { + if (isTypedMessageAnchor(node)) { + if (node.category === 'cash' || node.category === 'hash') { + result = true + return 'stop' + } + } else forEachTypedMessageChild(node, visitor) + return + }) + return result } diff --git a/packages/typed-message/base/utils/index.ts b/packages/typed-message/base/utils/index.ts index 11d742385a4c..0e491e29c3c0 100644 --- a/packages/typed-message/base/utils/index.ts +++ b/packages/typed-message/base/utils/index.ts @@ -7,8 +7,7 @@ import type { SerializableTypedMessage, NonSerializableWithAltTypedMessage, } from '../base' -import { isTypedMessageTuple, TypedMessageTuple } from '../core' -import { eq } from 'lodash-unified' +import { isEqual } from 'lodash-unified' export function isNonSerializableTypedMessageWithAlt(x: TypedMessage): x is NonSerializableWithAltTypedMessage { const y = x as NonSerializableWithAltTypedMessage @@ -27,11 +26,6 @@ export function isSerializableTypedMessage(x: TypedMessage): x is SerializableTy 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) + // perform a deep equal + return isEqual(message1, message2) } diff --git a/packages/typed-message/base/visitor/forEachChild.ts b/packages/typed-message/base/visitor/forEachChild.ts index de2d0d7c1145..db644388bb25 100644 --- a/packages/typed-message/base/visitor/forEachChild.ts +++ b/packages/typed-message/base/visitor/forEachChild.ts @@ -3,9 +3,13 @@ import { isNonSerializableTypedMessageWithAlt } from '../utils' import type { TypedMessage } from '../base' import { isTypedMessageMaskPayload } from '../extension' -export function forEachTypedMessageChild(node: TypedMessage, visitor: (x: TypedMessage) => void) { +export function forEachTypedMessageChild(node: TypedMessage, visitor: (x: TypedMessage) => void | 'stop') { + let stop: void | 'stop' if (isTypedMessageTuple(node)) { - for (const each of node.items) visitor(each) + 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) diff --git a/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx b/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx index f87b011f2d0b..aaa260cbd244 100644 --- a/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx +++ b/packages/typed-message/dom/Renderer/utils/TextResizerContext.tsx @@ -12,11 +12,11 @@ export function useTextResize(shouldEnable: boolean) { const updateFontSize = () => { const length = Array.from(element.innerText).length - let fontSize = 14 - if (length < 45) fontSize *= 1.5 - else if (length < 80) fontSize *= 1.2 + let fontSize = 1 + if (length < 45) fontSize = 1.5 + else if (length < 80) fontSize = 1.2 - element.style.fontSize = `${fontSize}px` + element.style.fontSize = `${fontSize}rem` } updateFontSize() From 6f275faaacce0e6f60d206974e9a3c57b9e1c98a Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 24 Feb 2022 13:12:39 +0800 Subject: [PATCH 18/18] fix: bug --- .../src/components/InjectedComponents/PostReplacer.tsx | 7 ++++--- packages/typed-message/base/utils/extract.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx index 00b12bb59e98..0308ca2a2fb6 100644 --- a/packages/mask/src/components/InjectedComponents/PostReplacer.tsx +++ b/packages/mask/src/components/InjectedComponents/PostReplacer.tsx @@ -77,14 +77,15 @@ function Transformer({ message, unzip, zip }: { message: TypedMessage } & PostRe } function hasCashOrHashTag(message: TypedMessage): boolean { let result = false - forEachTypedMessageChild(message, function visitor(node) { + function visitor(node: TypedMessage): 'stop' | void { if (isTypedMessageAnchor(node)) { if (node.category === 'cash' || node.category === 'hash') { result = true return 'stop' } } else forEachTypedMessageChild(node, visitor) - return - }) + } + visitor(message) + forEachTypedMessageChild(message, visitor) return result } diff --git a/packages/typed-message/base/utils/extract.ts b/packages/typed-message/base/utils/extract.ts index 2196e755f7fd..de84bef18d48 100644 --- a/packages/typed-message/base/utils/extract.ts +++ b/packages/typed-message/base/utils/extract.ts @@ -31,8 +31,8 @@ export function extractImageFromTypedMessage(message: TypedMessage | null): (str if (!message) return [] const image: (string | Blob)[] = [] - function visitor(message: TypedMessage): number | void { - if (isTypedMessageImage(message)) return image.push(message.image) + function visitor(message: TypedMessage): void { + if (isTypedMessageImage(message)) return void image.push(message.image) return forEachTypedMessageChild(message, visitor) } visitor(message)