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