diff --git a/__tests__/lib/strings/rich-text-sanitize.ts b/__tests__/lib/strings/rich-text-sanitize.ts new file mode 100644 index 0000000000..d0bbae5e8f --- /dev/null +++ b/__tests__/lib/strings/rich-text-sanitize.ts @@ -0,0 +1,123 @@ +import {AppBskyFeedPost} from '@atproto/api' +type Entity = AppBskyFeedPost.Entity +import {RichText} from '../../../src/lib/strings/rich-text' +import {removeExcessNewlines} from '../../../src/lib/strings/rich-text-sanitize' + +describe('removeExcessNewlines', () => { + it('removes more than two consecutive new lines', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with spaces', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n \n \n \n \n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n \n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('returns original string if there are no consecutive new lines', () => { + const input = new RichText('test\n\ntest\n\ntest\n\ntest\n\ntest') + const output = removeExcessNewlines(input) + expect(output.text).toEqual(input.text) + }) + + it('returns original string if there are no new lines', () => { + const input = new RichText('test test test test test') + const output = removeExcessNewlines(input) + expect(output.text).toEqual(input.text) + }) + + it('returns empty string if input is empty', () => { + const input = new RichText('') + const output = removeExcessNewlines(input) + expect(output.text).toEqual('') + }) + + it('works with different types of new line characters', () => { + const input = new RichText( + 'test\r\ntest\n\rtest\rtest\n\n\n\ntest\n\r \n \n \n \n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\r\ntest\n\rtest\rtest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width space', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200B\u200B\n\n\n\ntest\n \u200B\u200B \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width non-joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200C\u200C\n\n\n\ntest\n \u200C\u200C \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with zero width joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u200D\u200D\n\n\n\ntest\n \u200D\u200D \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with soft hyphen', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u00AD\u00AD\n\n\n\ntest\n \u00AD\u00AD \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) + + it('removes more than two consecutive new lines with word joiner', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\u2060\u2060\n\n\n\ntest\n \u2060\u2060 \n\n\n\ntest\n\n\n\n\n\n\ntest', + ) + const output = removeExcessNewlines(input) + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + }) +}) + +describe('removeExcessNewlines w/entities', () => { + it('preserves entities as expected', () => { + const input = new RichText( + 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', + [ + {index: {start: 0, end: 13}, type: '', value: ''}, + {index: {start: 13, end: 24}, type: '', value: ''}, + {index: {start: 9, end: 15}, type: '', value: ''}, + {index: {start: 4, end: 9}, type: '', value: ''}, + ], + ) + const output = removeExcessNewlines(input) + expect(entToStr(input.text, input.entities?.[0])).toEqual( + 'test\n\n\n\n\ntest', + ) + expect(entToStr(input.text, input.entities?.[1])).toEqual( + '\n\n\n\n\n\n\ntest', + ) + expect(entToStr(input.text, input.entities?.[2])).toEqual('test\n\n') + expect(entToStr(input.text, input.entities?.[3])).toEqual('\n\n\n\n\n') + expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') + expect(entToStr(output.text, output.entities?.[0])).toEqual('test\n\ntest') + expect(entToStr(output.text, output.entities?.[1])).toEqual('test') + expect(entToStr(output.text, output.entities?.[2])).toEqual('test') + expect(output.entities?.[3]).toEqual(undefined) + }) +}) + +function entToStr(str: string, ent?: Entity) { + if (!ent) { + return '' + } + return str.slice(ent.index.start, ent.index.end) +} diff --git a/__tests__/lib/strings/rich-text.ts b/__tests__/lib/strings/rich-text.ts new file mode 100644 index 0000000000..e52ac6cec1 --- /dev/null +++ b/__tests__/lib/strings/rich-text.ts @@ -0,0 +1,123 @@ +import {RichText} from '../../../src/lib/strings/rich-text' + +describe('richText.insert', () => { + const input = new RichText('hello world', [ + {index: {start: 2, end: 7}, type: '', value: ''}, + ]) + + it('correctly adjusts entities (scenario A - before)', () => { + const output = input.clone().insert(0, 'test') + expect(output.text).toEqual('testhello world') + expect(output.entities?.[0].index.start).toEqual(6) + expect(output.entities?.[0].index.end).toEqual(11) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) + + it('correctly adjusts entities (scenario B - inner)', () => { + const output = input.clone().insert(4, 'test') + expect(output.text).toEqual('helltesto world') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(11) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('lltesto w') + }) + + it('correctly adjusts entities (scenario C - after)', () => { + const output = input.clone().insert(8, 'test') + expect(output.text).toEqual('hello wotestrld') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(7) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) +}) + +describe('richText.delete', () => { + const input = new RichText('hello world', [ + {index: {start: 2, end: 7}, type: '', value: ''}, + ]) + + it('correctly adjusts entities (scenario A - entirely outer)', () => { + const output = input.clone().delete(0, 9) + expect(output.text).toEqual('ld') + expect(output.entities?.length).toEqual(0) + }) + + it('correctly adjusts entities (scenario B - entirely after)', () => { + const output = input.clone().delete(7, 11) + expect(output.text).toEqual('hello w') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(7) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) + + it('correctly adjusts entities (scenario C - partially after)', () => { + const output = input.clone().delete(4, 11) + expect(output.text).toEqual('hell') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(4) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('ll') + }) + + it('correctly adjusts entities (scenario D - entirely inner)', () => { + const output = input.clone().delete(3, 5) + expect(output.text).toEqual('hel world') + expect(output.entities?.[0].index.start).toEqual(2) + expect(output.entities?.[0].index.end).toEqual(5) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('l w') + }) + + it('correctly adjusts entities (scenario E - partially before)', () => { + const output = input.clone().delete(1, 5) + expect(output.text).toEqual('h world') + expect(output.entities?.[0].index.start).toEqual(1) + expect(output.entities?.[0].index.end).toEqual(3) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual(' w') + }) + + it('correctly adjusts entities (scenario F - entirely before)', () => { + const output = input.clone().delete(0, 2) + expect(output.text).toEqual('llo world') + expect(output.entities?.[0].index.start).toEqual(0) + expect(output.entities?.[0].index.end).toEqual(5) + expect( + output.text.slice( + output.entities?.[0].index.start, + output.entities?.[0].index.end, + ), + ).toEqual('llo w') + }) +}) diff --git a/package.json b/package.json index bf4b3e6cd5..344f26edc1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "email-validator": "^2.0.4", "he": "^1.2.0", "lodash.chunk": "^4.2.0", + "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", "lodash.omit": "^4.5.0", "lodash.shuffle": "^4.2.0", @@ -91,6 +92,7 @@ "@types/he": "^1.1.2", "@types/jest": "^26.0.23", "@types/lodash.chunk": "^4.2.7", + "@types/lodash.clonedeep": "^4.5.7", "@types/lodash.isequal": "^4.5.6", "@types/lodash.omit": "^4.5.7", "@types/lodash.shuffle": "^4.2.7", diff --git a/src/lib/strings/rich-text-sanitize.ts b/src/lib/strings/rich-text-sanitize.ts new file mode 100644 index 0000000000..0b5895707a --- /dev/null +++ b/src/lib/strings/rich-text-sanitize.ts @@ -0,0 +1,32 @@ +import {RichText} from './rich-text' + +const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/ +const REPLACEMENT_STR = '\n\n' + +export function removeExcessNewlines(richText: RichText): RichText { + return clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR) +} + +// TODO: check on whether this works correctly with multi-byte codepoints +export function clean( + richText: RichText, + targetRegexp: RegExp, + replacementString: string, +): RichText { + richText = richText.clone() + + let match = richText.text.match(targetRegexp) + while (match && typeof match.index !== 'undefined') { + const oldText = richText.text + const removeStartIndex = match.index + const removeEndIndex = removeStartIndex + match[0].length + richText.delete(removeStartIndex, removeEndIndex) + if (richText.text === oldText) { + break // sanity check + } + richText.insert(removeStartIndex, replacementString) + match = richText.text.match(targetRegexp) + } + + return richText +} diff --git a/src/lib/strings/rich-text.ts b/src/lib/strings/rich-text.ts new file mode 100644 index 0000000000..1df2144e06 --- /dev/null +++ b/src/lib/strings/rich-text.ts @@ -0,0 +1,216 @@ +/* += Rich Text Manipulation + +When we sanitize rich text, we have to update the entity indices as the +text is modified. This can be modeled as inserts() and deletes() of the +rich text string. The possible scenarios are outlined below, along with +their expected behaviors. + +NOTE: Slices are start inclusive, end exclusive + +== richTextInsert() + +Target string: + + 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o r l d // string value + ^-------^ // target slice {start: 2, end: 7} + +Scenarios: + +A: ^ // insert "test" at 0 +B: ^ // insert "test" at 4 +C: ^ // insert "test" at 8 + +A = before -> move both by num added +B = inner -> move end by num added +C = after -> noop + +Results: + +A: 0 1 2 3 4 5 6 7 8 910 // string indices + t e s t h e l l o w // string value + ^-------^ // target slice {start: 6, end: 11} + +B: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l t e s t o w // string value + ^---------------^ // target slice {start: 2, end: 11} + +C: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o t e s // string value + ^-------^ // target slice {start: 2, end: 7} + +== richTextDelete() + +Target string: + + 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w o r l d // string value + ^-------^ // target slice {start: 2, end: 7} + +Scenarios: + +A: ^---------------^ // remove slice {start: 0, end: 9} +B: ^-----^ // remove slice {start: 7, end: 11} +C: ^-----------^ // remove slice {start: 4, end: 11} +D: ^-^ // remove slice {start: 3, end: 5} +E: ^-----^ // remove slice {start: 1, end: 5} +F: ^-^ // remove slice {start: 0, end: 2} + +A = entirely outer -> delete slice +B = entirely after -> noop +C = partially after -> move end to remove-start +D = entirely inner -> move end by num removed +E = partially before -> move start to remove-start index, move end by num removed +F = entirely before -> move both by num removed + +Results: + +A: 0 1 2 3 4 5 6 7 8 910 // string indices + l d // string value + // target slice (deleted) + +B: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l o w // string value + ^-------^ // target slice {start: 2, end: 7} + +C: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l l // string value + ^-^ // target slice {start: 2, end: 4} + +D: 0 1 2 3 4 5 6 7 8 910 // string indices + h e l w o r l d // string value + ^---^ // target slice {start: 2, end: 5} + +E: 0 1 2 3 4 5 6 7 8 910 // string indices + h w o r l d // string value + ^-^ // target slice {start: 1, end: 3} + +F: 0 1 2 3 4 5 6 7 8 910 // string indices + l l o w o r l d // string value + ^-------^ // target slice {start: 0, end: 5} + */ + +import cloneDeep from 'lodash.clonedeep' +import {AppBskyFeedPost} from '@atproto/api' +import {removeExcessNewlines} from './rich-text-sanitize' + +export type Entity = AppBskyFeedPost.Entity +export interface RichTextOpts { + cleanNewlines?: boolean +} + +export class RichText { + constructor( + public text: string, + public entities?: Entity[], + opts?: RichTextOpts, + ) { + if (opts?.cleanNewlines) { + removeExcessNewlines(this).copyInto(this) + } + } + + clone() { + return new RichText(this.text, cloneDeep(this.entities)) + } + + copyInto(target: RichText) { + target.text = this.text + target.entities = cloneDeep(this.entities) + } + + insert(insertIndex: number, insertText: string) { + this.text = + this.text.slice(0, insertIndex) + + insertText + + this.text.slice(insertIndex) + + if (!this.entities?.length) { + return this + } + + const numCharsAdded = insertText.length + for (const ent of this.entities) { + // see comment at top of file for labels of each scenario + // scenario A (before) + if (insertIndex <= ent.index.start) { + // move both by num added + ent.index.start += numCharsAdded + ent.index.end += numCharsAdded + } + // scenario B (inner) + else if (insertIndex >= ent.index.start && insertIndex < ent.index.end) { + // move end by num added + ent.index.end += numCharsAdded + } + // scenario C (after) + // noop + } + return this + } + + delete(removeStartIndex: number, removeEndIndex: number) { + this.text = + this.text.slice(0, removeStartIndex) + this.text.slice(removeEndIndex) + + if (!this.entities?.length) { + return this + } + + const numCharsRemoved = removeEndIndex - removeStartIndex + for (const ent of this.entities) { + // see comment at top of file for labels of each scenario + // scenario A (entirely outer) + if ( + removeStartIndex <= ent.index.start && + removeEndIndex >= ent.index.end + ) { + // delete slice (will get removed in final pass) + ent.index.start = 0 + ent.index.end = 0 + } + // scenario B (entirely after) + else if (removeStartIndex > ent.index.end) { + // noop + } + // scenario C (partially after) + else if ( + removeStartIndex > ent.index.start && + removeStartIndex <= ent.index.end && + removeEndIndex > ent.index.end + ) { + // move end to remove start + ent.index.end = removeStartIndex + } + // scenario D (entirely inner) + else if ( + removeStartIndex >= ent.index.start && + removeEndIndex <= ent.index.end + ) { + // move end by num removed + ent.index.end -= numCharsRemoved + } + // scenario E (partially before) + else if ( + removeStartIndex < ent.index.start && + removeEndIndex >= ent.index.start && + removeEndIndex <= ent.index.end + ) { + // move start to remove-start index, move end by num removed + ent.index.start = removeStartIndex + ent.index.end -= numCharsRemoved + } + // scenario F (entirely before) + else if (removeEndIndex < ent.index.start) { + // move both by num removed + ent.index.start -= numCharsRemoved + ent.index.end -= numCharsRemoved + } + } + + // filter out any entities that were made irrelevant + this.entities = this.entities.filter(ent => ent.index.start < ent.index.end) + return this + } +} diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index 22367c3c10..26fd0703a9 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -6,6 +6,7 @@ import {extractEntities} from '../../lib/strings' import {isNetworkError} from '../../lib/errors' import {LinkMeta} from '../../lib/link-meta' import {Image} from '../../lib/images' +import {RichText} from './../../lib/strings/rich-text' const TIMEOUT = 10e3 // 10s @@ -35,7 +36,7 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) { export async function post( store: RootStoreModel, - text: string, + rawText: string, replyTo?: string, extLink?: ExternalEmbedDraft, images?: string[], @@ -44,6 +45,9 @@ export async function post( ) { let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined let reply + const text = new RichText(rawText, undefined, { + cleanNewlines: true, + }).text.trim() onStateChange?.('Processing...') const entities = extractEntities(text, knownHandles) diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 08dc024909..91db88733c 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -14,6 +14,7 @@ import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' import * as apilib from '../lib/api' import {cleanError} from '../../lib/strings' +import {RichText} from '../../lib/strings/rich-text' const PAGE_SIZE = 30 @@ -39,6 +40,7 @@ export class FeedItemModel { reply?: FeedViewPost['reply'] replyParent?: FeedItemModel reason?: FeedViewPost['reason'] + richText?: RichText constructor( public rootStore: RootStoreModel, @@ -51,6 +53,11 @@ export class FeedItemModel { const valid = AppBskyFeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record + this.richText = new RichText( + this.postRecord.text, + this.postRecord.entities, + {cleanNewlines: true}, + ) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index 5c89082518..6b1e7ab9bb 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -7,6 +7,7 @@ import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' import * as apilib from '../lib/api' import {cleanError} from '../../lib/strings' +import {RichText} from '../../lib/strings/rich-text' function* reactKeyGenerator(): Generator { let counter = 0 @@ -27,6 +28,7 @@ export class PostThreadViewPostModel { postRecord?: FeedPost.Record parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[] + richText?: RichText constructor( public rootStore: RootStoreModel, @@ -39,6 +41,11 @@ export class PostThreadViewPostModel { const valid = FeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record + this.richText = new RichText( + this.postRecord.text, + this.postRecord.entities, + {cleanNewlines: true}, + ) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts index dc0b1fbc63..4540cad371 100644 --- a/src/state/models/profile-view.ts +++ b/src/state/models/profile-view.ts @@ -4,14 +4,13 @@ import { AppBskyActorGetProfile as GetProfile, AppBskyActorProfile as Profile, AppBskySystemDeclRef, - AppBskyFeedPost, } from '@atproto/api' type DeclRef = AppBskySystemDeclRef.Main -type Entity = AppBskyFeedPost.Entity import {extractEntities} from '../../lib/strings' import {RootStoreModel} from './root-store' import * as apilib from '../lib/api' import {cleanError} from '../../lib/strings' +import {RichText} from '../../lib/strings/rich-text' export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' @@ -51,7 +50,7 @@ export class ProfileViewModel { viewer = new ProfileViewViewerModel() // added data - descriptionEntities?: Entity[] + descriptionRichText?: RichText constructor( public rootStore: RootStoreModel, @@ -230,6 +229,10 @@ export class ProfileViewModel { Object.assign(this.viewer, res.data.viewer) this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following) } - this.descriptionEntities = extractEntities(this.description || '') + this.descriptionRichText = new RichText( + this.description || '', + extractEntities(this.description || ''), + {cleanNewlines: true}, + ) } } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index a9ab08e6ea..0974683ba5 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -177,7 +177,7 @@ export const PostThreadItem = observer(function PostThreadItem({ - {record.text ? ( + {item.richText?.text ? ( @@ -301,12 +300,11 @@ export const PostThreadItem = observer(function PostThreadItem({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index c9404e556f..3411b683ec 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -180,12 +180,11 @@ export const Post = observer(function Post({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 3c69553210..c7a362158d 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -194,12 +194,11 @@ export const FeedItem = observer(function ({ This post is by a muted account. - ) : record.text ? ( + ) : item.richText?.text ? ( diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index e235359c0f..a7d3b33cea 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -256,12 +256,11 @@ export const ProfileHeader = observer(function ProfileHeader({ - {view.description ? ( + {view.descriptionRichText ? ( ) : undefined} {view.viewer.muted ? ( diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index f04f4566de..f53c437e1e 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -4,27 +4,22 @@ import {TextLink} from '../Link' import {Text} from './Text' import {lh} from '../../../lib/styles' import {toShortUrl} from '../../../../lib/strings' +import { + RichText as RichTextObj, + Entity, +} from '../../../../lib/strings/rich-text' import {useTheme, TypographyVariant} from '../../../lib/ThemeContext' import {usePalette} from '../../../lib/hooks/usePalette' -type TextSlice = {start: number; end: number} -type Entity = { - index: TextSlice - type: string - value: string -} - export function RichText({ type = 'md', - text, - entities, + richText, lineHeight = 1.2, style, numberOfLines, }: { type?: TypographyVariant - text: string - entities?: Entity[] + richText?: RichTextObj lineHeight?: number style?: StyleProp numberOfLines?: number @@ -32,6 +27,12 @@ export function RichText({ const theme = useTheme() const pal = usePalette('default') const lineHeightStyle = lh(theme, type, lineHeight) + + if (!richText) { + return null + } + + const {text, entities} = richText if (!entities?.length) { if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { style = { diff --git a/yarn.lock b/yarn.lock index 19cbbc0d0e..9235d5992a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2824,6 +2824,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.clonedeep@^4.5.7": + version "4.5.7" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197" + integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw== + dependencies: + "@types/lodash" "*" + "@types/lodash.isequal@^4.5.6": version "4.5.6" resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b" @@ -8937,6 +8944,11 @@ lodash.chunk@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"