Display more information in chat replies (#10950)
This commit is contained in:
@@ -44,12 +44,13 @@ import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
|||||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, native, platform, useTheme, utils} from '#/alf'
|
import {atoms as a, native, platform, tokens, useTheme, utils} from '#/alf'
|
||||||
import {isOnlyEmoji} from '#/alf/typography'
|
import {isOnlyEmoji} from '#/alf/typography'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||||
import {useMessageDialogs} from '#/components/dms/MessageOverlays'
|
import {useMessageDialogs} from '#/components/dms/MessageOverlays'
|
||||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
||||||
|
import {useReplyPreviewText} from '#/components/dms/replyPreview'
|
||||||
import {ArrowCornerDownRight_Stroke2_Corner3_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
|
import {ArrowCornerDownRight_Stroke2_Corner3_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
@@ -834,6 +835,7 @@ function ReplyQuote({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
const getReplyPreviewText = useReplyPreviewText()
|
||||||
|
|
||||||
const senderProfile = useMaybeProfileShadow(
|
const senderProfile = useMaybeProfileShadow(
|
||||||
relatedProfiles.get(replyTo.sender.did),
|
relatedProfiles.get(replyTo.sender.did),
|
||||||
@@ -857,22 +859,15 @@ function ReplyQuote({
|
|||||||
let text: string
|
let text: string
|
||||||
let subtle = false
|
let subtle = false
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
text = l`Blocked message hidden`
|
text = l({
|
||||||
|
message: '(blocked message hidden)',
|
||||||
|
comment: 'A reply summary in chat',
|
||||||
|
})
|
||||||
subtle = true
|
subtle = true
|
||||||
} else if (ChatBskyConvoDefs.isMessageView(replyTo)) {
|
} else if (ChatBskyConvoDefs.isMessageView(replyTo)) {
|
||||||
text = replyTo.text
|
;({text, subtle} = getReplyPreviewText(replyTo))
|
||||||
if (!text.trim()) {
|
|
||||||
subtle = true
|
|
||||||
if (ChatBskyEmbedJoinLink.isView(replyTo.embed)) {
|
|
||||||
text = l`(chat invite link)`
|
|
||||||
} else if (AppBskyEmbedRecord.isView(replyTo.embed)) {
|
|
||||||
text = l`(contains embedded content)`
|
|
||||||
} else {
|
|
||||||
text = l`No text`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
text = l`Deleted message`
|
text = l({message: '(deleted message)', comment: 'A reply summary in chat'})
|
||||||
subtle = true
|
subtle = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,6 +883,8 @@ function ReplyQuote({
|
|||||||
a.mb_xs,
|
a.mb_xs,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.p_sm,
|
a.p_sm,
|
||||||
|
// The padding above is a little loose, so we tighten it up here.
|
||||||
|
{paddingTop: tokens.space.sm - 2},
|
||||||
a.flex_col,
|
a.flex_col,
|
||||||
a.align_start,
|
a.align_start,
|
||||||
a.border,
|
a.border,
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
AppBskyEmbedExternal,
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
type ChatBskyConvoDefs,
|
||||||
|
ChatBskyEmbedJoinLink,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {BSKY_APP_HOST, toShortUrl} from '#/lib/strings/url-helpers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the embed of a quoted message that has no text of its own, so the
|
||||||
|
* reply preview can show what was shared instead of a generic placeholder. For
|
||||||
|
* a link card we surface the URI directly; otherwise we classify the quoted
|
||||||
|
* record (post/feed/list/etc.) so the caller can render a translated label.
|
||||||
|
*/
|
||||||
|
type ReplyEmbedSummary =
|
||||||
|
| {type: 'external'; uri: string}
|
||||||
|
| {type: 'post'}
|
||||||
|
| {type: 'unknown'}
|
||||||
|
|
||||||
|
function summarizeReplyEmbed(
|
||||||
|
embed: ChatBskyConvoDefs.MessageView['embed'],
|
||||||
|
): ReplyEmbedSummary {
|
||||||
|
if (!AppBskyEmbedRecord.isView(embed)) return {type: 'unknown'}
|
||||||
|
const {record} = embed
|
||||||
|
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||||
|
const inner = record.embeds?.[0]
|
||||||
|
if (AppBskyEmbedExternal.isView(inner)) {
|
||||||
|
return {type: 'external', uri: inner.external.uri}
|
||||||
|
}
|
||||||
|
return {type: 'post'}
|
||||||
|
}
|
||||||
|
return {type: 'unknown'}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a formatter that computes the preview text for a message being quoted
|
||||||
|
* in a reply, shared between the staged-reply composer and the sent reply
|
||||||
|
* bubble so the two stay in sync. When the message has its own text we use it
|
||||||
|
* verbatim; otherwise we summarize the embed.
|
||||||
|
*
|
||||||
|
* `subtle` indicates the text is a placeholder (not real message content), so
|
||||||
|
* callers can render it in a muted/italic style.
|
||||||
|
*/
|
||||||
|
export function useReplyPreviewText(): (
|
||||||
|
message: ChatBskyConvoDefs.MessageView,
|
||||||
|
) => {text: string; subtle: boolean} {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
return (message: ChatBskyConvoDefs.MessageView) => {
|
||||||
|
const text = message.text
|
||||||
|
if (text.trim()) {
|
||||||
|
return {text, subtle: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChatBskyEmbedJoinLink.isView(message.embed)) {
|
||||||
|
const {joinLinkPreview} = message.embed
|
||||||
|
if (ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) {
|
||||||
|
return {
|
||||||
|
text: `${BSKY_APP_HOST}/chat/${joinLinkPreview.code}`,
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ChatBskyGroupDefs.isDisabledJoinLinkPreviewView(joinLinkPreview)) {
|
||||||
|
return {
|
||||||
|
text: l({
|
||||||
|
message: '(disabled chat invite link)',
|
||||||
|
comment: 'A reply summary in chat',
|
||||||
|
}),
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ChatBskyGroupDefs.isInvalidJoinLinkPreviewView(joinLinkPreview)) {
|
||||||
|
return {
|
||||||
|
text: l({
|
||||||
|
message: '(invalid chat invite link)',
|
||||||
|
comment: 'A reply summary in chat',
|
||||||
|
}),
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: l({
|
||||||
|
message: '(chat invite link)',
|
||||||
|
comment: 'A reply summary in chat',
|
||||||
|
}),
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = summarizeReplyEmbed(message.embed)
|
||||||
|
switch (summary.type) {
|
||||||
|
case 'external':
|
||||||
|
return {text: toShortUrl(summary.uri), subtle: true}
|
||||||
|
case 'post':
|
||||||
|
return {
|
||||||
|
text: l({
|
||||||
|
message: '(quoted post)',
|
||||||
|
comment: 'A reply summary in chat',
|
||||||
|
}),
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
text: l({message: '(no text)', comment: 'A reply summary in chat'}),
|
||||||
|
subtle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {LayoutAnimation, View} from 'react-native'
|
import {LayoutAnimation, View} from 'react-native'
|
||||||
import {AppBskyEmbedRecord, ChatBskyEmbedJoinLink} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
@@ -8,6 +8,7 @@ import {useConvoActive} from '#/state/messages/convo'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
||||||
|
import {useReplyPreviewText} from '#/components/dms/replyPreview'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -16,15 +17,27 @@ import {Text} from '#/components/Typography'
|
|||||||
* being replied to, with a button to cancel the reply.
|
* being replied to, with a button to cancel the reply.
|
||||||
*/
|
*/
|
||||||
export function MessageInputReply() {
|
export function MessageInputReply() {
|
||||||
const t = useTheme()
|
const {replyTo} = useMessageReplies()
|
||||||
const {t: l} = useLingui()
|
|
||||||
const convo = useConvoActive()
|
|
||||||
const {replyTo, clearReply} = useMessageReplies()
|
|
||||||
|
|
||||||
if (!replyTo) {
|
if (!replyTo) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <MessageInputReplyInner replyTo={replyTo} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageInputReplyInner({
|
||||||
|
replyTo,
|
||||||
|
}: {
|
||||||
|
replyTo: ChatBskyConvoDefs.MessageView
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const convo = useConvoActive()
|
||||||
|
const {clearReply} = useMessageReplies()
|
||||||
|
const getReplyPreviewText = useReplyPreviewText()
|
||||||
|
const {text, subtle} = getReplyPreviewText(replyTo)
|
||||||
|
|
||||||
const onRemove = () => {
|
const onRemove = () => {
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
clearReply()
|
clearReply()
|
||||||
@@ -35,19 +48,6 @@ export function MessageInputReply() {
|
|||||||
? createSanitizedDisplayName(senderProfile, false)
|
? createSanitizedDisplayName(senderProfile, false)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let text = replyTo.text
|
|
||||||
let subtle = false
|
|
||||||
if (!text.trim()) {
|
|
||||||
subtle = true
|
|
||||||
if (ChatBskyEmbedJoinLink.isView(replyTo.embed)) {
|
|
||||||
text = l`(chat invite link)`
|
|
||||||
} else if (AppBskyEmbedRecord.isView(replyTo.embed)) {
|
|
||||||
text = l`(contains embedded content)`
|
|
||||||
} else {
|
|
||||||
text = l`No text`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -82,7 +82,7 @@ export function MessageInputReply() {
|
|||||||
<Button
|
<Button
|
||||||
label={l`Cancel reply`}
|
label={l`Cancel reply`}
|
||||||
onPress={onRemove}
|
onPress={onRemove}
|
||||||
style={[a.px_2xs]}
|
style={[a.px_2xs, {transform: [{translateX: 2}]}]}
|
||||||
hitSlop={HITSLOP_20}>
|
hitSlop={HITSLOP_20}>
|
||||||
<XIcon size="xs" style={t.atoms.text_contrast_high} />
|
<XIcon size="xs" style={t.atoms.text_contrast_high} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user