From 195d2f7d2bd193108938901c3f757a9e89080b63 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 3 Oct 2022 16:02:03 -0500 Subject: Implement mentions rendering --- src/state/lib/api.ts | 25 ++++++++ src/view/com/modals/ComposePost.tsx | 2 +- src/view/com/post-thread/PostThreadItem.tsx | 37 ++++++++--- src/view/com/post/Post.tsx | 16 +++-- src/view/com/posts/FeedItem.tsx | 18 ++++-- src/view/com/util/RichText.tsx | 95 +++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 src/view/com/util/RichText.tsx (limited to 'src') diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index feed41c41..db7e2ab21 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -6,9 +6,15 @@ // import {ReactNativeStore} from './auth' import AdxApi from '../../third-party/api' import {ServiceClient} from '../../third-party/api/src/index' +import { + TextSlice, + Entity as Entities, +} from '../../third-party/api/src/types/todo/social/post' import {AdxUri} from '../../third-party/uri' import {RootStoreModel} from '../models/root-store' +type Entity = Entities[0] + export function doPolyfill() { AdxApi.xrpc.fetch = fetchHandler } @@ -32,11 +38,13 @@ export async function post( } } } + const entities = extractEntities(text) return await store.api.todo.social.post.create( {did: store.me.did || ''}, { text, reply, + entities, createdAt: new Date().toISOString(), }, ) @@ -196,3 +204,20 @@ async function iterateAll( } } while (res.records.length === 100) }*/ + +function extractEntities(text: string): Entity[] | undefined { + let match + let ents: Entity[] = [] + const re = /(^|\s)@([a-zA-Z0-9\.-]+)(\b)/g + while ((match = re.exec(text))) { + ents.push({ + type: 'mention', + value: match[2], + index: [ + match.index + 1, // skip the (^|\s) but include the '@' + match.index + 2 + match[2].length, + ], + }) + } + return ents.length > 0 ? ents : undefined +} diff --git a/src/view/com/modals/ComposePost.tsx b/src/view/com/modals/ComposePost.tsx index 6acb01005..e1e7cac5f 100644 --- a/src/view/com/modals/ComposePost.tsx +++ b/src/view/com/modals/ComposePost.tsx @@ -79,7 +79,7 @@ export function Component({replyTo}: {replyTo?: string}) { const textDecorated = useMemo(() => { return (text || '').split(/(\s)/g).map((item, i) => { - if (/@[a-zA-Z0-9]+/g.test(item)) { + if (/^@[a-zA-Z0-9\.-]+$/g.test(item)) { return ( {item} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index daba54b5a..d28017e44 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,13 +1,14 @@ import React, {useMemo} from 'react' import {observer} from 'mobx-react-lite' import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native' -import Svg, {Line, Circle} from 'react-native-svg' +import Svg, {Line} from 'react-native-svg' import {AdxUri} from '../../../third-party/uri' import * as PostType from '../../../third-party/api/src/types/todo/social/post' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' import {ComposePostModel} from '../../../state/models/shell' import {Link} from '../util/Link' +import {RichText} from '../util/RichText' import {PostDropdownBtn} from '../util/DropdownBtn' import {s, colors} from '../../lib/styles' import {ago, pluralize} from '../../lib/strings' @@ -144,9 +145,14 @@ export const PostThreadItem = observer(function PostThreadItem({ - - {record.text} - + + + {item._isHighlightedPost && hasEngagement ? ( {item.repostCount ? ( @@ -266,9 +272,13 @@ export const PostThreadItem = observer(function PostThreadItem({ /> - - {record.text} - + + + @@ -325,16 +335,23 @@ const styles = StyleSheet.create({ paddingRight: 5, }, postText: { - paddingBottom: 8, fontFamily: 'Helvetica Neue', }, + postTextContainer: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + paddingBottom: 8, + }, postTextLarge: { - paddingLeft: 4, - paddingBottom: 20, fontSize: 24, lineHeight: 32, fontWeight: '300', }, + postTextLargeContainer: { + paddingLeft: 4, + paddingBottom: 20, + }, expandedInfo: { flexDirection: 'row', padding: 10, diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 92113b50b..4cd35659f 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -15,6 +15,7 @@ import {PostThreadViewModel} from '../../../state/models/post-thread-view' import {ComposePostModel} from '../../../state/models/shell' import {Link} from '../util/Link' import {UserInfoText} from '../util/UserInfoText' +import {RichText} from '../util/RichText' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' import {ago} from '../../lib/strings' @@ -115,9 +116,13 @@ export const Post = observer(function Post({uri}: {uri: string}) { )} - - {record.text} - + + + )} - - {record.text} - + + + +}) { + if (!entities?.length) { + return {text} + } + if (!style) style = [] + else if (!Array.isArray(style)) style = [style] + entities.sort(sortByIndex) + const segments = Array.from(toSegments(text, entities)) + const els = [] + let key = 0 + for (const segment of segments) { + if (typeof segment === 'string') { + els.push( + + {segment} + , + ) + } else { + els.push( + + + {segment.text} + + , + ) + } + key++ + } + return <>{els} +} + +function sortByIndex(a: Entity, b: Entity) { + return a.index[0] - b.index[0] +} + +function* toSegments(text: string, entities: Entity[]) { + let cursor = 0 + let i = 0 + do { + let currEnt = entities[i] + if (cursor < currEnt.index[0]) { + yield text.slice(cursor, currEnt.index[0]) + } else { + i++ + continue + } + if (currEnt.index[0] < currEnt.index[1]) { + let subtext = text.slice(currEnt.index[0], currEnt.index[1]) + if ( + !subtext.trim() || + stripUsername(subtext) !== stripUsername(currEnt.value) + ) { + // dont yield links to empty strings or strings that don't match the entity value + yield subtext + } else { + yield { + entity: currEnt, + text: subtext, + } + } + } + cursor = currEnt.index[1] + i++ + } while (i < entities.length) + if (cursor < text.length) { + yield text.slice(cursor, text.length) + } +} + +function stripUsername(v: string): string { + return v.trim().replace('@', '') +} -- cgit 1.4.1