diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 2a20373a..4c75f47a 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -3,6 +3,7 @@ import psl from 'psl' import TLDs from 'tlds' import {BSKY_SERVICE} from 'lib/constants' +import {isInvalidHandle} from 'lib/strings/handles' export const BSKY_APP_HOST = 'https://bsky.app' const BSKY_TRUSTED_HOSTS = [ @@ -83,6 +84,10 @@ export function toShareUrl(url: string): string { return url } +export function toBskyAppUrl(url: string): string { + return new URL(url, BSKY_APP_HOST).toString() +} + export function isBskyAppUrl(url: string): boolean { return url.startsWith('https://bsky.app/') } @@ -183,6 +188,22 @@ export function feedUriToHref(url: string): string { } } +export function postUriToRelativePath( + uri: string, + options?: {handle?: string}, +): string | undefined { + try { + const {hostname, rkey} = new AtUri(uri) + const handleOrDid = + options?.handle && !isInvalidHandle(options.handle) + ? options.handle + : hostname + return `/profile/${handleOrDid}/post/${rkey}` + } catch { + return undefined + } +} + /** * Checks if the label in the post text matches the host of the link facet. * diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index e6f657b4..f72515ac 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -312,25 +312,19 @@ export function MessagesList({ }) if (postLinkFacet) { - // remove the post link from the text - rt.delete( - postLinkFacet.index.byteStart, - postLinkFacet.index.byteEnd, - ) + const isAtStart = postLinkFacet.index.byteStart === 0 + const isAtEnd = + postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength - // re-trim the text, now that we've removed the post link - // - // if the post link is at the start of the text, we don't want to leave a leading space - // so trim on both sides - if (postLinkFacet.index.byteStart === 0) { - rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) - } else { - // otherwise just trim the end - rt = new RichText( - {text: rt.text.trimEnd()}, - {cleanNewlines: true}, + // remove the post link from the text + if (isAtStart || isAtEnd) { + rt.delete( + postLinkFacet.index.byteStart, + postLinkFacet.index.byteEnd, ) } + + rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) } } } catch (error) { diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index d5658249..9f880836 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useState} from 'react' import {GestureResponderEvent, View} from 'react-native' import { AppBskyActorDefs, + AppBskyEmbedRecord, ChatBskyConvoDefs, moderateProfile, ModerationOpts, @@ -9,6 +10,11 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import { + postUriToRelativePath, + toBskyAppUrl, + toShortUrl, +} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -95,21 +101,64 @@ function ChatListItemReady({ const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount - let lastMessage = _(msg`No messages yet`) - let lastMessageSentAt: string | null = null - if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { - if (convo.lastMessage.sender?.did === currentAccount?.did) { - lastMessage = _(msg`You: ${convo.lastMessage.text}`) - } else { - lastMessage = convo.lastMessage.text + const {lastMessage, lastMessageSentAt} = React.useMemo(() => { + let lastMessage = _(msg`No messages yet`) + let lastMessageSentAt: string | null = null + + if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { + const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did + + if (convo.lastMessage.text) { + if (isFromMe) { + lastMessage = _(msg`You: ${convo.lastMessage.text}`) + } else { + lastMessage = convo.lastMessage.text + } + } else if (convo.lastMessage.embed) { + const defaultEmbeddedContentMessage = _( + msg`(contains embedded content)`, + ) + + if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) { + const embed = convo.lastMessage.embed + + if (AppBskyEmbedRecord.isViewRecord(embed.record)) { + const record = embed.record + const path = postUriToRelativePath(record.uri, { + handle: record.author.handle, + }) + const href = path ? toBskyAppUrl(path) : undefined + const short = href + ? toShortUrl(href) + : defaultEmbeddedContentMessage + if (isFromMe) { + lastMessage = _(msg`You: ${short}`) + } else { + lastMessage = short + } + } + } else { + if (isFromMe) { + lastMessage = _(msg`You: ${defaultEmbeddedContentMessage}`) + } else { + lastMessage = defaultEmbeddedContentMessage + } + } + } + + lastMessageSentAt = convo.lastMessage.sentAt } - lastMessageSentAt = convo.lastMessage.sentAt - } - if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { - lastMessage = isDeletedAccount - ? _(msg`Conversation deleted`) - : _(msg`Message deleted`) - } + if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { + lastMessage = isDeletedAccount + ? _(msg`Conversation deleted`) + : _(msg`Message deleted`) + } + + return { + lastMessage, + lastMessageSentAt, + } + }, [_, convo.lastMessage, currentAccount?.did, isDeletedAccount]) const [showActions, setShowActions] = useState(false)