[Chat] Hide blocked accounts' reactions in chats (#10889)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,11 @@ import {DateDivider} from './DateDivider'
|
||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||
import {MessageItemInviteEmbed} from './MessageItemInviteEmbed'
|
||||
import {groupReactions} from './ReactionsDialog'
|
||||
import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util'
|
||||
import {
|
||||
CLUSTERED_MESSAGE_THRESHOLD_MS,
|
||||
filterBlockedReactions,
|
||||
MESSAGE_GAP_THRESHOLD_MS,
|
||||
} from './util'
|
||||
|
||||
const AVATAR_SIZE = 28
|
||||
const CLUSTERED_MESSAGE_GAP = 2
|
||||
@@ -168,9 +172,15 @@ let MessageItem = ({
|
||||
const isInMiddleOfCluster =
|
||||
isInCluster && !isFirstInCluster && !isLastInCluster
|
||||
|
||||
const hasReactions = message.reactions && message.reactions.length > 0
|
||||
const visibleReactions = useMemo(
|
||||
() => filterBlockedReactions(message.reactions, relatedProfiles),
|
||||
[message.reactions, relatedProfiles],
|
||||
)
|
||||
|
||||
const hasReactions = visibleReactions.length > 0
|
||||
const prevHasReactions =
|
||||
prevIsMessage && prevMessage.reactions && prevMessage.reactions.length > 0
|
||||
prevIsMessage &&
|
||||
filterBlockedReactions(prevMessage.reactions, relatedProfiles).length > 0
|
||||
const isNextEmojiOnly = nextIsMessage && isOnlyEmoji(nextMessage.text)
|
||||
const isPrevEmojiOnly = prevIsMessage && isOnlyEmoji(prevMessage.text)
|
||||
const squaredBottomCorner =
|
||||
@@ -240,11 +250,11 @@ let MessageItem = ({
|
||||
)
|
||||
|
||||
const groupedReactions = useMemo(
|
||||
() => groupReactions(message.reactions),
|
||||
[message.reactions],
|
||||
() => groupReactions(visibleReactions),
|
||||
[visibleReactions],
|
||||
)
|
||||
|
||||
const reactions = useMemo(() => message.reactions ?? [], [message.reactions])
|
||||
const reactions = visibleReactions
|
||||
|
||||
const hasSelfReacted = reactions.some(
|
||||
r => r.sender.did === currentAccount?.did,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {filterBlockedReactions} from '#/components/dms/util'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
@@ -52,10 +53,13 @@ export function ReactionsDialog({
|
||||
|
||||
const [selected, setSelected] = useState('all')
|
||||
|
||||
const reactions = message.reactions
|
||||
const reactions = useMemo(
|
||||
() => filterBlockedReactions(message.reactions, relatedProfiles),
|
||||
[message.reactions, relatedProfiles],
|
||||
)
|
||||
const groupedReactions = useMemo(() => groupReactions(reactions), [reactions])
|
||||
|
||||
const filteredReactions = reactions?.filter(
|
||||
const filteredReactions = reactions.filter(
|
||||
r => selected === 'all' || r.value === selected,
|
||||
)
|
||||
|
||||
@@ -69,7 +73,7 @@ export function ReactionsDialog({
|
||||
<ReactionTabs
|
||||
groupedReactions={groupedReactions}
|
||||
selected={selected}
|
||||
totalReactions={reactions?.length ?? 0}
|
||||
totalReactions={reactions.length}
|
||||
onFilter={setSelected}
|
||||
/>
|
||||
<Dialog.Close />
|
||||
@@ -96,7 +100,7 @@ export function ReactionsDialog({
|
||||
header={IS_WEB ? header : null}
|
||||
style={[web({maxWidth: 400})]}>
|
||||
{filteredReactions
|
||||
?.sort((a, b) => {
|
||||
.sort((a, b) => {
|
||||
if (a.sender.did === currentAccount?.did) return -1
|
||||
if (b.sender.did === currentAccount?.did) return 1
|
||||
return 0
|
||||
@@ -113,7 +117,7 @@ export function ReactionsDialog({
|
||||
message={message}
|
||||
profile={sender}
|
||||
reaction={reaction}
|
||||
allReactions={reactions ?? []}
|
||||
allReactions={reactions}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
type ChatBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ChatBskyEmbedJoinLink,
|
||||
} from '@atproto/api'
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
toBskyAppUrl,
|
||||
toShortUrl,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export type UserMessageInfo = {
|
||||
message: string | null
|
||||
@@ -21,13 +23,39 @@ export type UserMessageInfo = {
|
||||
isBlockedMessage: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether the given did is blocked (in either direction) within a
|
||||
* convo. Prefers the passed-in shadowed `primaryProfile` so optimistic blocks
|
||||
* reflect immediately, before the convo list refetches - the raw `members`
|
||||
* fetched with the convo are invisible to the profile shadow cache. Group
|
||||
* members other than the owner fall back to the raw (potentially stale) member.
|
||||
*/
|
||||
export function isDidBlockedInConvo({
|
||||
did,
|
||||
members,
|
||||
primaryProfile,
|
||||
}: {
|
||||
did: string | undefined
|
||||
members: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
primaryProfile?: bsky.profile.AnyProfileView
|
||||
}): boolean {
|
||||
if (!did) return false
|
||||
if (primaryProfile && primaryProfile.did === did) {
|
||||
return isBlockedOrBlocking(primaryProfile)
|
||||
}
|
||||
const member = members.find(m => m.did === did)
|
||||
return member ? isBlockedOrBlocking(member) : false
|
||||
}
|
||||
|
||||
export function getMessageInfo({
|
||||
convo,
|
||||
currentAccountDid,
|
||||
primaryProfile,
|
||||
i18n,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
currentAccountDid: string | undefined
|
||||
primaryProfile?: bsky.profile.AnyProfileView
|
||||
i18n: I18n
|
||||
}): UserMessageInfo | null {
|
||||
if (!ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||
@@ -42,7 +70,11 @@ export function getMessageInfo({
|
||||
const isGroup = ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
||||
|
||||
const reportableMessage = isFromMe ? undefined : lastMessage
|
||||
const isBlockedMessage = sender ? isBlockedOrBlocking(sender) : false
|
||||
const isBlockedMessage = isDidBlockedInConvo({
|
||||
did: senderDid,
|
||||
members: convo.members,
|
||||
primaryProfile,
|
||||
})
|
||||
|
||||
const prefix = (message: string) => {
|
||||
if (isFromMe) {
|
||||
|
||||
@@ -3,19 +3,24 @@ import {type I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {isDidBlockedInConvo} from '#/components/dms/getMessageInfo'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export type UserReactionInfo = {
|
||||
message: string
|
||||
createdAt: string
|
||||
isBlocked: boolean
|
||||
}
|
||||
|
||||
export function getReactionInfo({
|
||||
convo,
|
||||
currentAccountDid,
|
||||
primaryProfile,
|
||||
i18n,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
currentAccountDid: string | undefined
|
||||
primaryProfile?: bsky.profile.AnyProfileView
|
||||
i18n: I18n
|
||||
}): UserReactionInfo | null {
|
||||
if (!ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
|
||||
@@ -28,6 +33,21 @@ export function getReactionInfo({
|
||||
const sender = convo.members.find(m => m.did === senderDid)
|
||||
const name = sender ? createSanitizedDisplayName(sender) : null
|
||||
|
||||
// Hide the preview when either the reactor or the author of the reacted-to
|
||||
// message is blocked - otherwise a blocked reactor's name or a blocked
|
||||
// sender's message text would leak into the chat list.
|
||||
const isBlocked =
|
||||
isDidBlockedInConvo({
|
||||
did: senderDid,
|
||||
members: convo.members,
|
||||
primaryProfile,
|
||||
}) ||
|
||||
isDidBlockedInConvo({
|
||||
did: reactedTo.sender?.did,
|
||||
members: convo.members,
|
||||
primaryProfile,
|
||||
})
|
||||
|
||||
const lastMessageText = reactedTo.text
|
||||
const fallbackMessage = i18n._(
|
||||
msg({
|
||||
@@ -50,5 +70,6 @@ export function getReactionInfo({
|
||||
return {
|
||||
message,
|
||||
createdAt: reaction.createdAt,
|
||||
isBlocked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
|
||||
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
||||
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/profile-shadow'
|
||||
import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types'
|
||||
@@ -86,6 +87,25 @@ export function hasAlreadyReacted(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops reactions from accounts the viewer is blocking or blocked by, so a
|
||||
* blocked reactor's identity is never surfaced via the reaction pills or the
|
||||
* reactions dialog. `relatedProfiles` is shadow-synced by the convo agent, so
|
||||
* this reflects optimistic blocks. Reactions whose sender isn't in
|
||||
* `relatedProfiles` are kept - we can't determine their block status, and they
|
||||
* already render anonymously ("Someone reacted").
|
||||
*/
|
||||
export function filterBlockedReactions(
|
||||
reactions: ChatBskyConvoDefs.ReactionView[] | undefined,
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
|
||||
): ChatBskyConvoDefs.ReactionView[] {
|
||||
if (!reactions) return []
|
||||
return reactions.filter(reaction => {
|
||||
const profile = relatedProfiles.get(reaction.sender.did)
|
||||
return !profile || !isBlockedOrBlocking(profile)
|
||||
})
|
||||
}
|
||||
|
||||
export function hasReachedReactionLimit(
|
||||
message: ChatBskyConvoDefs.MessageView,
|
||||
myDid: string | undefined,
|
||||
|
||||
@@ -327,6 +327,7 @@ function BaseChatItem({
|
||||
const info = getMessageInfo({
|
||||
convo: convo.view,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
primaryProfile,
|
||||
i18n,
|
||||
})
|
||||
if (info) {
|
||||
@@ -342,10 +343,12 @@ function BaseChatItem({
|
||||
const info = getReactionInfo({
|
||||
convo: convo.view,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
primaryProfile,
|
||||
i18n,
|
||||
})
|
||||
if (
|
||||
info &&
|
||||
!info.isBlocked &&
|
||||
(!lastMessageSentAt ||
|
||||
new Date(lastMessageSentAt) < new Date(info.createdAt))
|
||||
) {
|
||||
@@ -379,7 +382,7 @@ function BaseChatItem({
|
||||
LastMessageIcon,
|
||||
lastMessageSentAt,
|
||||
}
|
||||
}, [l, convo, currentAccount?.did, isDeletedAccount, i18n])
|
||||
}, [l, convo, currentAccount?.did, isDeletedAccount, primaryProfile, i18n])
|
||||
|
||||
const [showActions, setShowActions] = useState(false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user