Group Clops Feature Branch (#10360)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-04-24 13:38:50 -05:00
committed by GitHub
parent e832791367
commit cdb8d4bfb8
63 changed files with 3517 additions and 1844 deletions
+7 -1
View File
@@ -4,23 +4,29 @@ import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf'
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
import type * as bsky from '#/types/bsky'
export function ActionsWrapper({
message,
isFromSelf,
senderProfile,
children,
onTap,
}: {
message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean
isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode
onTap?: () => void
}) {
const {t: l} = useLingui()
return (
<MessageContextMenu message={message} onTap={onTap}>
<MessageContextMenu
message={message}
senderProfile={senderProfile}
onTap={onTap}>
{trigger =>
// will always be true, since this file is platform split
trigger.IS_NATIVE && (
+4 -1
View File
@@ -10,6 +10,7 @@ import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import * as Toast from '#/components/Toast'
import type * as bsky from '#/types/bsky'
import {EmojiReactionPicker} from './EmojiReactionPicker'
import {hasReachedReactionLimit} from './util'
@@ -17,12 +18,14 @@ export function ActionsWrapper({
message,
hasReactions,
isFromSelf,
senderProfile,
children,
onTap,
}: {
message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean
isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode
onTap?: () => void
}) {
@@ -114,7 +117,7 @@ export function ActionsWrapper({
)
}}
</EmojiReactionPicker>
<MessageContextMenu message={message}>
<MessageContextMenu message={message} senderProfile={senderProfile}>
{({props, state, IS_NATIVE, control}) => {
// always false, file is platform split
if (IS_NATIVE) return null
+14 -5
View File
@@ -98,11 +98,16 @@ function reducer(state: State, action: Action): State {
}
export function AddMembersFlow({
members,
title,
onAddMembers,
}: {
members: string[]
title: string
onAddMembers: (dids: string[]) => void
onAddMembers: (
dids: string[],
profiles: bsky.profile.AnyProfileView[],
) => void
}) {
const t = useTheme()
const {t: l} = useLingui()
@@ -154,7 +159,11 @@ export function AddMembersFlow({
} else if (searchText.length) {
if (results?.length) {
for (const profile of results) {
if (profile.did === currentAccount?.did) continue
if (
profile.did === currentAccount?.did ||
members.includes(profile.did)
)
continue
_items.push({
type: 'profile',
key: profile.did,
@@ -202,7 +211,7 @@ export function AddMembersFlow({
}
return _items
}, [isError, searchText, l, results, currentAccount?.did, follows])
}, [isError, searchText, l, results, currentAccount?.did, members, follows])
if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: l`No results`})
@@ -213,8 +222,8 @@ export function AddMembersFlow({
}, [control])
const handlePressAdd = useCallback(() => {
onAddMembers(groupChatDids)
}, [groupChatDids, onAddMembers])
onAddMembers(groupChatDids, groupChatProfiles)
}, [groupChatDids, groupChatProfiles, onAddMembers])
const renderItems = useCallback(
({item}: {item: Item}) => {
+1 -1
View File
@@ -4,7 +4,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {subDays} from 'date-fns'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '../Typography'
import {Text} from '#/components/Typography'
import {localDateString} from './util'
const timeFormatter = new Intl.DateTimeFormat(undefined, {
+6 -5
View File
@@ -25,15 +25,18 @@ import {usePromptControl} from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
import {EmojiReactionPicker} from './EmojiReactionPicker'
import {hasReachedReactionLimit} from './util'
export let MessageContextMenu = ({
message,
senderProfile,
children,
onTap,
}: {
message: ChatBskyConvoDefs.MessageView
senderProfile?: bsky.profile.AnyProfileView
children: TriggerProps['children']
onTap?: () => void
}): React.ReactNode => {
@@ -110,9 +113,7 @@ export let MessageContextMenu = ({
[l, convo, message, currentAccount?.did],
)
const sender = convo.convo.members.find(
member => member.did === message.sender.did,
)
const sender = senderProfile
return (
<>
@@ -183,7 +184,7 @@ export let MessageContextMenu = ({
control={reportControl}
subject={{
view: 'message',
convoId: convo.convo.id,
convoId: convo.convo.view.id,
message,
}}
onAfterSubmit={() => {
@@ -197,7 +198,7 @@ export let MessageContextMenu = ({
control={blockOrDeleteControl}
currentScreen="conversation"
params={{
convoId: convo.convo.id,
convoId: convo.convo.view.id,
message,
}}
/>
+19 -15
View File
@@ -30,7 +30,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links'
import {useConvoActive} from '#/state/messages/convo'
import {type ConvoItem} from '#/state/messages/convo/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
@@ -43,7 +42,6 @@ import {InlineLinkText, Link} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {DateDivider} from './DateDivider'
import {useDateDividerToggle} from './DateDividerToggle'
import {MessageItemEmbed} from './MessageItemEmbed'
@@ -93,19 +91,18 @@ function isWithinClusterBoundary({
let MessageItem = ({
item,
isGroupChat = false,
profile,
}: {
item: ConvoItem & {type: 'message' | 'pending-message'}
isGroupChat?: boolean
profile?: bsky.profile.AnyProfileView
}): React.ReactNode => {
const t = useTheme()
const {currentAccount} = useSession()
const {t: l} = useLingui()
const {convo} = useConvoActive()
const moderationOpts = useModerationOpts()
const queryClient = useQueryClient()
const profile = item.relatedProfiles.get(item.message.sender.did)
const reactionsControl = useDialogControl()
const reactionTapRef = useRef(false)
@@ -277,9 +274,7 @@ let MessageItem = ({
return l`You reacted ${reaction.value}`
} else {
const senderDid = reaction.sender.did
const memberSender = convo.members.find(
member => member.did === senderDid,
)
const memberSender = item.relatedProfiles.get(senderDid)
if (memberSender) {
return l`${createSanitizedDisplayName(memberSender)} reacted ${reaction.value}`
}
@@ -290,7 +285,13 @@ let MessageItem = ({
one: '# person',
other: '# people',
})} reacted ${groupedReactions.map(g => g.value).join(' ')}`
}, [reactions, groupedReactions, currentAccount?.did, convo.members, l])
}, [
reactions,
groupedReactions,
currentAccount?.did,
item.relatedProfiles,
l,
])
const appliedReactions = (
<LayoutAnimationConfig skipEntering skipExiting>
@@ -375,7 +376,7 @@ let MessageItem = ({
) : null}
<ReactionsDialog
control={reactionsControl}
members={convo.members}
relatedProfiles={item.relatedProfiles}
message={message}
reactions={message.reactions}
groupedReactions={groupedReactions}
@@ -391,11 +392,13 @@ let MessageItem = ({
return (
<>
{(hasLargeGapFromPrev || isDateDividerToggled) && (
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
<DateDivider date={message.sentAt} />
</Animated.View>
)}
<LayoutAnimationConfig skipExiting skipEntering>
{(hasLargeGapFromPrev || isDateDividerToggled) && (
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
<DateDivider date={message.sentAt} />
</Animated.View>
)}
</LayoutAnimationConfig>
<View style={[messageInset, effectiveFirstInCluster && a.mt_md]}>
<View style={[a.relative]}>
{showAvatar ? (
@@ -434,6 +437,7 @@ let MessageItem = ({
hasReactions={hasReactions}
isFromSelf={isFromSelf}
message={message}
senderProfile={profile}
onTap={() => {
if (reactionTapRef.current) return
if (!hasLargeGapFromPrev) {
+4 -1
View File
@@ -161,11 +161,13 @@ function GroupHeaderReady({
})
}
const lockStatus = convo.details.lockStatus
return (
<Wrapper
heading={
<>
<AvatarBubbles size="small" profiles={convo.members} />
<AvatarBubbles size={40} profiles={convo.members} />
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
{convo.details.name}
</Text>
@@ -175,6 +177,7 @@ function GroupHeaderReady({
settings={
<Button
label={l`Open group chat settings`}
disabled={lockStatus === 'locked-permanently'}
size="small"
color="secondary"
shape="round"
+4 -4
View File
@@ -7,7 +7,7 @@ import {
View,
} from 'react-native'
import Animated from 'react-native-reanimated'
import {type ChatBskyConvoDefs} from '@atproto/api'
import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants'
@@ -33,13 +33,13 @@ type Reaction = {
export function ReactionsDialog({
control,
members,
relatedProfiles,
message,
reactions,
groupedReactions,
}: {
control: Dialog.DialogControlProps
members: bsky.profile.AnyProfileView[]
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
message: ChatBskyConvoDefs.MessageView
reactions?: ChatBskyConvoDefs.ReactionView[]
groupedReactions?: Reaction[]
@@ -100,7 +100,7 @@ export function ReactionsDialog({
return 0
})
.map(reaction => {
const sender = members.find(m => m.did === reaction.sender.did)
const sender = relatedProfiles.get(reaction.sender.did)
if (!sender) return null
return (
<ReactionRow
+3 -3
View File
@@ -23,15 +23,15 @@ export type SystemMessageInfo = {
function getReferredDisplayName(
user: ChatBskyConvoDefs.SystemMessageReferredUser,
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
): string | null {
const profile = relatedProfiles.find(p => p.did === user.did)
const profile = relatedProfiles.get(user.did)
return profile ? createSanitizedDisplayName(profile) : null
}
export function getSystemMessageInfo(
data: ChatBskyConvoDefs.SystemMessageView['data'],
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
): SystemMessageInfo | null {
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
const name = getReferredDisplayName(data.member, relatedProfiles)
+2 -2
View File
@@ -68,13 +68,13 @@ export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
| {
kind: 'group'
details: ChatBskyConvoDefs.GroupConvo
details: $Typed<ChatBskyConvoDefs.GroupConvo>
primaryMember: GroupConvoMember // the owner
members: Array<GroupConvoMember>
}
| {
kind: 'direct'
details: ChatBskyConvoDefs.DirectConvo
details: $Typed<ChatBskyConvoDefs.DirectConvo>
primaryMember: DirectConvoMember // the other user
members: Array<DirectConvoMember>
}