diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index feed41c418..db7e2ab21d 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 6acb01005a..e1e7cac5f1 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 daba54b5a3..d28017e447 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 92113b50b0..4cd35659f8 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('@', '') +} diff --git a/todos.txt b/todos.txt index 82b90a6b4b..a9dcf99d8e 100644 --- a/todos.txt +++ b/todos.txt @@ -3,6 +3,8 @@ Paul's todo list - General - Update to RN 0.70 - Go through every button and make sure it does what it's supposed to + - Cache some profile/userinfo lookups + - Cursor behaviors on all views - Onboarding flow - * - Private beta @@ -10,6 +12,7 @@ Paul's todo list - Firehose - Composer - Update the view after creating a post + - Mentions - Profile - Disable badges for now - Disable editing avi or banner