Avoid leaking previous messages via replies (#11017)

This commit is contained in:
DS Boyce
2026-06-30 10:35:12 -07:00
committed by GitHub
parent cc893028e3
commit 28c44a11ef
2 changed files with 92 additions and 60 deletions
+87 -58
View File
@@ -74,16 +74,17 @@ const BORDER_RADIUS = 20
const SQUARED_BORDER_RADIUS = 4
const DISPLAY_NAME_INSET = 20
function messageIsReply(
message:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null,
): boolean {
export type MessageItemNeighbor =
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
function messageIsReply(message: MessageItemNeighbor): boolean {
return (
ChatBskyConvoDefs.isMessageView(message) &&
(ChatBskyConvoDefs.isMessageView(message.replyTo) ||
ChatBskyConvoDefs.isDeletedMessageView(message.replyTo))
ChatBskyConvoDefs.isDeletedMessageView(message.replyTo) ||
ChatBskyConvoDefs.isMessageBeforeUserJoinedGroupView(message.replyTo))
)
}
@@ -96,10 +97,7 @@ function isWithinClusterBoundary({
}: {
isPending: boolean
message: ChatBskyConvoDefs.MessageView
adjacentMessage:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
adjacentMessage: MessageItemNeighbor
isFromSameSender: boolean
direction: 'prev' | 'next'
}): boolean {
@@ -135,14 +133,8 @@ let MessageItem = ({
}: {
item: ConvoItem & {type: 'message' | 'pending-message'}
isGroupChat?: boolean
prevMessage:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
nextMessage:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
prevMessage: MessageItemNeighbor
nextMessage: MessageItemNeighbor
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
}): React.ReactNode => {
const t = useTheme()
@@ -157,14 +149,22 @@ let MessageItem = ({
const {openReactions} = useMessageDialogs()
const {scrollToMessage, highlightedMessage} = useMessageReplies()
// `replyTo` comes back hydrated as the referenced message (or a deleted-
// message tombstone). Narrow away the open-union fallback so we only render
// shapes we understand.
// `replyTo` comes back hydrated as the referenced message, a deleted-message
// tombstone, or a before-joined placeholder. Narrow away the open-union
// fallback so we only render shapes we understand.
const replyTo =
ChatBskyConvoDefs.isMessageView(message.replyTo) ||
ChatBskyConvoDefs.isDeletedMessageView(message.replyTo)
ChatBskyConvoDefs.isDeletedMessageView(message.replyTo) ||
ChatBskyConvoDefs.isMessageBeforeUserJoinedGroupView(message.replyTo)
? message.replyTo
: undefined
const replyToMessageId =
replyTo && !ChatBskyConvoDefs.isMessageBeforeUserJoinedGroupView(replyTo)
? replyTo.id
: undefined
const onPressReplyTo = replyToMessageId
? () => scrollToMessage(replyToMessageId)
: undefined
const isPending = item.type === 'pending-message'
@@ -468,7 +468,7 @@ let MessageItem = ({
isGroupChat={isGroupChat}
replierDisplayName={displayName}
relatedProfiles={relatedProfiles}
onPress={() => scrollToMessage(replyTo.id)}
onPress={onPressReplyTo}
/>
) : displayName && showDisplayName ? (
<Text
@@ -541,7 +541,7 @@ let MessageItem = ({
replyTo={replyTo}
isFromSelf={isFromSelf}
relatedProfiles={relatedProfiles}
onPress={() => scrollToMessage(replyTo.id)}
onPress={onPressReplyTo}
/>
) : null}
<RichText
@@ -751,28 +751,56 @@ function ReplyCaption({
relatedProfiles,
onPress,
}: {
replyTo: ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView
replyTo:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| ChatBskyConvoDefs.MessageBeforeUserJoinedGroupView
isFromSelf: boolean
isGroupChat: boolean
replierDisplayName: string | null
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
onPress: () => void
onPress?: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const {currentAccount} = useSession()
const originalSenderIsSelf = replyTo.sender.did === currentAccount?.did
const originalProfile = relatedProfiles.get(replyTo.sender.did)
const originalName = originalSenderIsSelf
? null
: originalProfile
? createSanitizedDisplayName(originalProfile)
: null
let caption: string = ''
if (
ChatBskyConvoDefs.isMessageView(replyTo) ||
ChatBskyConvoDefs.isDeletedMessageView(replyTo)
) {
const originalSenderIsSelf = replyTo.sender.did === currentAccount?.did
const originalProfile = relatedProfiles.get(replyTo.sender.did)
const originalName = originalSenderIsSelf
? null
: originalProfile
? createSanitizedDisplayName(originalProfile)
: null
caption = isFromSelf
? originalSenderIsSelf
? l`You replied to yourself`
: originalName
? l`You replied to ${originalName}`
: l`You replied`
: originalSenderIsSelf
? l`${replierDisplayName} replied to you`
: originalName
? l`${replierDisplayName} replied to ${originalName}`
: l`${replierDisplayName} replied`
} else {
caption = l`Someone replied`
}
return (
<Button
label={l`Scroll to the message this is replying to`}
label={
onPress
? l`Scroll to the message this is replying to`
: l`A reply to a message sent before you joined`
}
disabled={!onPress}
onPress={onPress}
style={[
a.w_full,
@@ -796,23 +824,7 @@ function ReplyCaption({
style={[a.text_xs, a.flex_shrink, t.atoms.text_contrast_medium]}
numberOfLines={1}
emoji>
{isFromSelf ? (
originalSenderIsSelf ? (
<Trans>You replied to yourself</Trans>
) : originalName ? (
<Trans>You replied to {originalName}</Trans>
) : (
<Trans>You replied</Trans>
)
) : originalSenderIsSelf ? (
<Trans>{replierDisplayName} replied to you</Trans>
) : originalName ? (
<Trans>
{replierDisplayName} replied to {originalName}
</Trans>
) : (
<Trans>{replierDisplayName} replied</Trans>
)}
{caption}
</Text>
</Button>
)
@@ -828,17 +840,25 @@ function ReplyQuote({
relatedProfiles,
onPress,
}: {
replyTo: ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView
replyTo:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| ChatBskyConvoDefs.MessageBeforeUserJoinedGroupView
isFromSelf: boolean
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
onPress: () => void
onPress?: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const getReplyPreviewText = useReplyPreviewText()
const senderDid =
ChatBskyConvoDefs.isMessageView(replyTo) ||
ChatBskyConvoDefs.isDeletedMessageView(replyTo)
? replyTo.sender.did
: undefined
const senderProfile = useMaybeProfileShadow(
relatedProfiles.get(replyTo.sender.did),
senderDid ? relatedProfiles.get(senderDid) : undefined,
)
// Hide the quoted content if we block, or are blocked by, the original
// sender - mirroring how the message bubble itself is hidden.
@@ -866,6 +886,12 @@ function ReplyQuote({
subtle = true
} else if (ChatBskyConvoDefs.isMessageView(replyTo)) {
;({text, subtle} = getReplyPreviewText(replyTo))
} else if (ChatBskyConvoDefs.isMessageBeforeUserJoinedGroupView(replyTo)) {
text = l({
message: `(message sent before you joined)`,
comment: 'A reply summary in chat',
})
subtle = true
} else {
text = l({message: '(deleted message)', comment: 'A reply summary in chat'})
subtle = true
@@ -874,10 +900,13 @@ function ReplyQuote({
return (
<Button
label={
senderName
? l`Replied-to message from ${senderName}, tap to scroll to it`
: l`Replied-to message, tap to scroll to it`
!onPress
? l`Replied-to message was sent before you joined`
: senderName
? l`Replied-to message from ${senderName}, tap to scroll to it`
: l`Replied-to message, tap to scroll to it`
}
disabled={!onPress}
onPress={onPress}
style={[
a.mb_xs,
@@ -60,7 +60,10 @@ import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
import {MessageListError} from '#/screens/Messages/components/MessageListError'
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
import {DateDivider} from '#/components/dms/DateDivider'
import {MessageItem} from '#/components/dms/MessageItem'
import {
MessageItem,
type MessageItemNeighbor,
} from '#/components/dms/MessageItem'
import {MessageOverlays} from '#/components/dms/MessageOverlays'
import {MessageRepliesProvider} from '#/components/dms/MessageReplies'
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
@@ -104,7 +107,7 @@ function keyExtractor(item: RenderItem) {
function getNeighborMessage(
items: RenderItem[],
index: number,
): ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView | null {
): MessageItemNeighbor {
const neighbor = items[index]
if (!neighbor) return null
if (