From becc708c610015c510edeac87394b3f77ac4ed06 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 9 May 2024 21:08:56 +0100 Subject: [PATCH] =?UTF-8?q?[=F0=9F=90=B4]=20Rich=20text=20in=20messages=20?= =?UTF-8?q?(#3926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add facets to message * richtext messages * undo richtexttag changes * whoops, don't redetect facets * dont set color directly * shorten links and filter invalid facets * fix link shortening * pass in underline style --- src/alf/atoms.ts | 18 +++++++++++ src/components/RichText.tsx | 30 ++++++++++++------- src/components/dms/MessageItem.tsx | 20 +++++++++---- src/lib/strings/rich-text-manip.ts | 1 + .../Messages/Conversation/MessagesList.tsx | 26 ++++++++++++++-- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 45ab72ca..3e5ddf04 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -840,4 +840,22 @@ export const atoms = { mr_auto: { marginRight: 'auto', }, + /* + * Pointer events + */ + pointer_events_none: { + pointerEvents: 'none', + }, + pointer_events_auto: { + pointerEvents: 'auto', + }, + /* + * Text decoration + */ + underline: { + textDecorationLine: 'underline', + }, + strike_through: { + textDecorationLine: 'line-through', + }, } as const diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 0d49e713..ed69c199 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {TextStyle} from 'react-native' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -26,6 +27,7 @@ export function RichText({ enableTags = false, authorHandle, onLinkPress, + interactiveStyle, }: TextStyleProp & Pick & { value: RichTextAPI | string @@ -35,13 +37,22 @@ export function RichText({ enableTags?: boolean authorHandle?: string onLinkPress?: LinkProps['onPress'] + interactiveStyle?: TextStyle }) { const richText = React.useMemo( () => value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), [value], ) - const styles = [a.leading_snug, flatten(style)] + + const flattenedStyle = flatten(style) + const plainStyles = [a.leading_snug, flattenedStyle] + const interactiveStyles = [ + a.leading_snug, + a.pointer_events_auto, + flatten(interactiveStyle), + flattenedStyle, + ] const {text, facets} = richText @@ -67,7 +78,7 @@ export function RichText({ @@ -93,7 +104,7 @@ export function RichText({ @@ -110,7 +121,7 @@ export function RichText({ selectable={selectable} key={key} to={link.uri} - style={[...styles, {pointerEvents: 'auto'}]} + style={interactiveStyles} // @ts-ignore TODO dataSet={WORD_WRAP} shareOnLongPress @@ -130,7 +141,7 @@ export function RichText({ key={key} text={segment.text} tag={tag.tag} - style={styles} + style={interactiveStyles} selectable={selectable} authorHandle={authorHandle} />, @@ -145,7 +156,7 @@ export function RichText({ @@ -219,19 +230,16 @@ function RichTextTag({ onFocus={onFocus} onBlur={onBlur} style={[ - style, - { - pointerEvents: 'auto', - color: t.palette.primary_500, - }, web({ cursor: 'pointer', }), + {color: t.palette.primary_500}, (hovered || focused || pressed) && { ...web({outline: 0}), textDecorationLine: 'underline', textDecorationColor: t.palette.primary_500, }, + style, ]}> {text} diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index f8f5197c..e9128c5a 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useMemo, useRef} from 'react' import {LayoutAnimation, StyleProp, TextStyle, View} from 'react-native' +import {RichText as RichTextAPI} from '@atproto/api' import {ChatBskyConvoDefs} from '@atproto-labs/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -9,8 +10,9 @@ import {TimeElapsed} from 'view/com/util/TimeElapsed' import {atoms as a, useTheme} from '#/alf' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {Text} from '#/components/Typography' +import {RichText} from '../RichText' -export let MessageItem = ({ +let MessageItem = ({ item, next, pending, @@ -65,6 +67,10 @@ export let MessageItem = ({ const pendingColor = t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800 + const rt = useMemo(() => { + return new RichTextAPI({text: item.text, facets: item.facets}) + }, [item.text, item.facets]) + return ( @@ -87,15 +93,17 @@ export let MessageItem = ({ ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, ]}> - - {item.text} - + ]} + interactiveStyle={a.underline} + enableTags + /> ) } - MessageItem = React.memo(MessageItem) +export {MessageItem} let MessageItemMetadata = ({ message, diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts index d9cd8c07..508e0772 100644 --- a/src/lib/strings/rich-text-manip.ts +++ b/src/lib/strings/rich-text-manip.ts @@ -1,4 +1,5 @@ import {RichText, UnicodeString} from '@atproto/api' + import {toShortUrl} from './url-helpers' export function shortenLinks(rt: RichText): RichText { diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 1b07f887..0b8ab524 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -7,12 +7,15 @@ import { import {runOnJS, useSharedValue} from 'react-native-reanimated' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {shortenLinks} from '#/lib/strings/rich-text-manip' import {isIOS} from '#/platform/detection' import {useConvo} from '#/state/messages/convo' import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' +import {useAgent} from '#/state/session' import {ScrollProvider} from 'lib/ScrollContext' import {isWeb} from 'platform/detection' import {List} from 'view/com/util/List' @@ -87,6 +90,7 @@ function onScrollToIndexFailed() { export function MessagesList() { const convo = useConvo() + const {getAgent} = useAgent() const flatListRef = useRef(null) // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items @@ -159,14 +163,30 @@ export function MessagesList() { }, [convo, hasInitiallyScrolled]) const onSendMessage = useCallback( - (text: string) => { + async (text: string) => { + let rt = new RichText({text}, {cleanNewlines: true}) + await rt.detectFacets(getAgent()) + rt = shortenLinks(rt) + + // filter out any mention facets that didn't map to a user + rt.facets = rt.facets?.filter(facet => { + const mention = facet.features.find(feature => + AppBskyRichtextFacet.isMention(feature), + ) + if (mention && !mention.did) { + return false + } + return true + }) + if (convo.status === ConvoStatus.Ready) { convo.sendMessage({ - text, + text: rt.text, + facets: rt.facets, }) } }, - [convo], + [convo, getAgent], ) const onScroll = React.useCallback(