[Chat] Fix assumption that every chat has an owner (#10373)

This commit is contained in:
Samuel Newman
2026-05-06 14:11:07 +01:00
committed by GitHub
parent 2aa544fade
commit a7ed3ee3cc
12 changed files with 163 additions and 90 deletions
+3
View File
@@ -86,6 +86,9 @@ stats.json
# VSCode # VSCode
.vscode .vscode
# Zed
.zed
# gitignore and github actions # gitignore and github actions
!.gitignore !.gitignore
!.github !.github
@@ -1,5 +1,9 @@
import {ScrollView, View} from 'react-native' import {ScrollView, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api' import {
type ChatBskyActorDefs,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -66,8 +70,8 @@ export function RecentChats({
if (!convo) return null if (!convo) return null
if ( if (
(convo.kind === 'direct' && !convo.primaryMember ||
convo.primaryMember.handle === 'missing.invalid') || convo.primaryMember.handle === 'missing.invalid' ||
convo.view.muted convo.view.muted
) { ) {
return null return null
@@ -77,6 +81,7 @@ export function RecentChats({
<RecentChatItem <RecentChatItem
key={convo.view.id} key={convo.view.id}
convo={convo} convo={convo}
primaryMember={convo.primaryMember}
onPress={() => onSelectChat(convo.view.id)} onPress={() => onSelectChat(convo.view.id)}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
/> />
@@ -103,15 +108,17 @@ function RecentChatItem({
onPress, onPress,
moderationOpts, moderationOpts,
convo, convo,
primaryMember,
}: { }: {
onPress: () => void onPress: () => void
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
convo: ConvoWithDetails convo: ConvoWithDetails
primaryMember: ChatBskyActorDefs.ProfileViewBasic
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const primaryProfile = useProfileShadow(convo.primaryMember) const primaryProfile = useProfileShadow(primaryMember)
const moderation = moderateProfile(primaryProfile, moderationOpts) const moderation = moderateProfile(primaryProfile, moderationOpts)
const name = const name =
@@ -479,14 +479,15 @@ function ExistingChatCard({
const {t: l} = useLingui() const {t: l} = useLingui()
const enabled = const enabled =
convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true
const moderation = moderateProfile(convo.primaryMember, moderationOpts)
const name = const name =
convo.kind === 'group' convo.kind === 'group'
? convo.details.name ? convo.details.name
: createSanitizedDisplayName( : createSanitizedDisplayName(
convo.primaryMember, convo.primaryMember,
true, true,
moderation.ui('displayName'), moderateProfile(convo.primaryMember, moderationOpts).ui(
'displayName',
),
) )
const handleOnPress = useCallback(() => { const handleOnPress = useCallback(() => {
+8 -1
View File
@@ -15,6 +15,7 @@ import {
Unlock_Stroke2_Corner2_Rounded as UnlockIcon, Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
} from '#/components/icons/Lock' } from '#/components/icons/Lock'
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
export type SystemMessageInfo = { export type SystemMessageInfo = {
message: MessageDescriptor message: MessageDescriptor
@@ -68,7 +69,13 @@ export function getSystemMessageInfo(
} else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) { } else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) {
return {Icon: UnlockIcon, message: msg`Chat unlocked`} return {Icon: UnlockIcon, message: msg`Chat unlocked`}
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) { } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) {
return {Icon: LockIcon, message: msg`Chat locked permanently`} const name = getReferredDisplayName(data.lockedBy, relatedProfiles)
return {
Icon: XIcon,
message: name
? msg`${name} ended the group chat`
: msg`This group chat was ended`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) { } else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
return { return {
Icon: PencilIcon, Icon: PencilIcon,
+1 -6
View File
@@ -69,7 +69,7 @@ export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
| { | {
kind: 'group' kind: 'group'
details: $Typed<ChatBskyConvoDefs.GroupConvo> details: $Typed<ChatBskyConvoDefs.GroupConvo>
primaryMember: GroupConvoMember // the owner primaryMember?: GroupConvoMember // the owner - may have left, thus optional
members: Array<GroupConvoMember> members: Array<GroupConvoMember>
} }
| { | {
@@ -117,11 +117,6 @@ export function parseConvoView(
} }
} }
if (!owner) {
logger.warn('No owner found in group convo')
return null
}
return { return {
view: convoView, view: convoView,
kind: 'group', kind: 'group',
@@ -75,7 +75,7 @@ export function Member({
const displayName = isDeletedAccount const displayName = isDeletedAccount
? l`Deleted Account` ? l`Deleted Account`
: createSanitizedDisplayName(profile, true, moderation.ui('displayName')) : createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
const isProfileOwner = profile.did === convo.primaryMember.did const isProfileOwner = profile.did === convo.primaryMember?.did
const isSelf = currentAccount?.did === profile.did const isSelf = currentAccount?.did === profile.did
let statusBadge: React.ReactNode | null = null let statusBadge: React.ReactNode | null = null
if (isSelf) { if (isSelf) {
@@ -1,6 +1,10 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' import {
ChatBskyActorDefs,
ChatBskyConvoDefs,
ModerationOpts,
} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -14,6 +18,7 @@ import {
import {logger} from '#/logger' import {logger} from '#/logger'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
import {ConvoStatus} from '#/state/messages/convo/types' import {ConvoStatus} from '#/state/messages/convo/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name' import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members' import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
@@ -99,6 +104,7 @@ function SettingsInner() {
const {t: l} = useLingui() const {t: l} = useLingui()
const convoState = useConvo() const convoState = useConvo()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const moderationOpts = useModerationOpts()
if (convoState.status === ConvoStatus.Error) { if (convoState.status === ConvoStatus.Error) {
return ( return (
@@ -111,7 +117,7 @@ function SettingsInner() {
) )
} }
if (!isConvoActive(convoState)) { if (!isConvoActive(convoState) || !moderationOpts) {
return ( return (
<View style={[a.flex_1, a.align_center, a.justify_center]}> <View style={[a.flex_1, a.align_center, a.justify_center]}>
<Loader size="xl" /> <Loader size="xl" />
@@ -135,7 +141,9 @@ function SettingsInner() {
) )
} }
return <GroupSettings convo={convoState.convo} /> return (
<GroupSettings convo={convoState.convo} moderationOpts={moderationOpts} />
)
} }
function keyExtractor(item: Item) { function keyExtractor(item: Item) {
@@ -157,8 +165,10 @@ function isGroupMember(
function GroupSettings({ function GroupSettings({
convo, convo,
moderationOpts,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
moderationOpts: ModerationOpts
}) { }) {
const initialNumToRender = useInitialNumToRender({minItemHeight: 68}) const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
const bottomBarOffset = useBottomBarOffset() const bottomBarOffset = useBottomBarOffset()
@@ -166,7 +176,7 @@ function GroupSettings({
const {currentAccount} = useSession() const {currentAccount} = useSession()
const primaryMember = convo.primaryMember const primaryMember = convo.primaryMember
const isOwner = primaryMember.did === currentAccount?.did const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {data: memberListData = [], isPending} = useListConvoMembersQuery({ const {data: memberListData = [], isPending} = useListConvoMembersQuery({
convoId: convo.view.id, convoId: convo.view.id,
@@ -209,8 +219,8 @@ function GroupSettings({
...memberListData ...memberListData
.filter(isGroupMember) .filter(isGroupMember)
.sort((a, b) => { .sort((a, b) => {
const aIsOwner = a.did === primaryMember.did const aIsOwner = a.did === primaryMember?.did
const bIsOwner = b.did === primaryMember.did const bIsOwner = b.did === primaryMember?.did
const aIsSelf = a.did === currentAccount?.did const aIsSelf = a.did === currentAccount?.did
const bIsSelf = b.did === currentAccount?.did const bIsSelf = b.did === currentAccount?.did
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
@@ -223,7 +233,7 @@ function GroupSettings({
key: profile.did, key: profile.did,
profile, profile,
status: status:
primaryMember.did === profile.did primaryMember?.did === profile.did
? 'owner' ? 'owner'
: invites.includes(profile.did) : invites.includes(profile.did)
? 'invited' ? 'invited'
@@ -271,7 +281,13 @@ function GroupSettings({
desktopFixedHeight desktopFixedHeight
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
ListHeaderComponent={<SettingsHeader convo={convo} isOwner={isOwner} />} ListHeaderComponent={
<SettingsHeader
convo={convo}
isOwner={isOwner}
moderationOpts={moderationOpts}
/>
}
renderItem={renderItem} renderItem={renderItem}
sideBorders={false} sideBorders={false}
windowSize={11} windowSize={11}
@@ -282,9 +298,11 @@ function GroupSettings({
function SettingsHeader({ function SettingsHeader({
convo, convo,
isOwner, isOwner,
moderationOpts,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
isOwner: boolean isOwner: boolean
moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
const {i18n, t: l} = useLingui() const {i18n, t: l} = useLingui()
@@ -517,11 +535,15 @@ function SettingsHeader({
onChangeText={setNewGroupName} onChangeText={setNewGroupName}
onConfirm={handleEditName} onConfirm={handleEditName}
/> />
<InviteLinkDialog {convo.primaryMember && (
convo={convo} <InviteLinkDialog
control={inviteLinkDialog} convo={convo}
isOwner={isOwner} owner={convo.primaryMember}
/> control={inviteLinkDialog}
isOwner={isOwner}
moderationOpts={moderationOpts}
/>
)}
<LockChatPrompt control={lockChatPrompt} onConfirm={handleConfirmLock} /> <LockChatPrompt control={lockChatPrompt} onConfirm={handleConfirmLock} />
<LeaveChatPrompt <LeaveChatPrompt
control={leaveChatPrompt} control={leaveChatPrompt}
@@ -14,7 +14,11 @@ import {useHaptics} from '#/lib/haptics'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {decrementBadgeCount} from '#/lib/notifications/notifications' import {decrementBadgeCount} from '#/lib/notifications/notifications'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import {
type Shadow,
useMaybeProfileShadow,
useProfileShadow,
} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
precacheConvoQuery, precacheConvoQuery,
@@ -120,7 +124,7 @@ function DirectChatItem({
return ( return (
<BaseChatItem <BaseChatItem
convo={convo.view} convo={convo}
avatar={ avatar={
<PreviewableUserAvatar <PreviewableUserAvatar
profile={profile} profile={profile}
@@ -167,10 +171,11 @@ function GroupChatItem({
children?: React.ReactNode children?: React.ReactNode
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
const groupOwner = useProfileShadow(convo.primaryMember) const groupOwner = useMaybeProfileShadow(convo.primaryMember)
const moderation = useMemo( const moderation = useMemo(
() => moderateProfile(groupOwner, moderationOpts), () =>
groupOwner ? moderateProfile(groupOwner, moderationOpts) : undefined,
[groupOwner, moderationOpts], [groupOwner, moderationOpts],
) )
@@ -178,7 +183,7 @@ function GroupChatItem({
return ( return (
<BaseChatItem <BaseChatItem
convo={convo.view} convo={convo}
avatar={<AvatarBubbles profiles={convo.members} size={52} />} avatar={<AvatarBubbles profiles={convo.members} size={52} />}
title={chatName} title={chatName}
accessibilityHint={l`Go to the group chat named "${chatName}"`} accessibilityHint={l`Go to the group chat named "${chatName}"`}
@@ -208,15 +213,15 @@ function BaseChatItem({
postAlerts, postAlerts,
children, children,
}: { }: {
convo: ChatBskyConvoDefs.ConvoView convo: ConvoWithDetails
avatar: React.ReactNode avatar: React.ReactNode
title: string title: string
subtitle?: string subtitle?: string
accessibilityHint: string accessibilityHint: string
isDeletedAccount: boolean isDeletedAccount: boolean
isBlockedAccount: boolean isBlockedAccount: boolean
primaryProfile: Shadow<bsky.profile.AnyProfileView> primaryProfile?: Shadow<bsky.profile.AnyProfileView>
primaryProfileModeration: ModerationDecision primaryProfileModeration?: ModerationDecision
showMenu?: boolean showMenu?: boolean
showProfileBadges: boolean showProfileBadges: boolean
postAlerts?: React.ReactNode postAlerts?: React.ReactNode
@@ -233,9 +238,16 @@ function BaseChatItem({
const playHaptic = useHaptics() const playHaptic = useHaptics()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount const hasUnread =
convo.view.unreadCount > 0 &&
!isDeletedAccount &&
!(
convo.kind === 'group' &&
convo.details.lockStatus === 'locked-permanently'
)
const blockInfo = useMemo(() => { const blockInfo = useMemo(() => {
if (!primaryProfileModeration) return {listBlocks: [], userBlock: undefined}
const modui = primaryProfileModeration.ui('profileView') const modui = primaryProfileModeration.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking') const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list') const listBlocks = blocks.filter(alert => alert.source.type === 'list')
@@ -246,7 +258,11 @@ function BaseChatItem({
} }
}, [primaryProfileModeration]) }, [primaryProfileModeration])
const isDimStyle = convo.muted || isBlockedAccount || isDeletedAccount const isDimStyle =
convo.view.muted ||
isBlockedAccount ||
isDeletedAccount ||
(convo.kind === 'group' && convo.details.lockStatus !== 'unlocked')
const {lastMessage, lastMessageSentAt, latestReportableMessage} = const {lastMessage, lastMessageSentAt, latestReportableMessage} =
useMemo(() => { useMemo(() => {
@@ -257,8 +273,8 @@ function BaseChatItem({
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
// Deleted message // Deleted message
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { if (ChatBskyConvoDefs.isDeletedMessageView(convo.view.lastMessage)) {
lastMessageSentAt = convo.lastMessage.sentAt lastMessageSentAt = convo.view.lastMessage.sentAt
lastMessage = isDeletedAccount lastMessage = isDeletedAccount
? l`Conversation deleted` ? l`Conversation deleted`
@@ -266,9 +282,9 @@ function BaseChatItem({
} }
// Message // Message
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { if (ChatBskyConvoDefs.isMessageView(convo.view.lastMessage)) {
const info = getMessageInfo({ const info = getMessageInfo({
convo, convo: convo.view,
currentAccountDid: currentAccount?.did, currentAccountDid: currentAccount?.did,
i18n, i18n,
}) })
@@ -280,9 +296,9 @@ function BaseChatItem({
} }
// Reaction // Reaction
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) { if (ChatBskyConvoDefs.isMessageAndReactionView(convo.view.lastReaction)) {
const info = getReactionInfo({ const info = getReactionInfo({
convo, convo: convo.view,
currentAccountDid: currentAccount?.did, currentAccountDid: currentAccount?.did,
i18n, i18n,
}) })
@@ -297,14 +313,14 @@ function BaseChatItem({
} }
// System message // System message
if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) { if (ChatBskyConvoDefs.isSystemMessageView(convo.view.lastMessage)) {
const info = getSystemMessageInfo( const info = getSystemMessageInfo(
convo.lastMessage.data, convo.view.lastMessage.data,
new Map(convo.members.map(m => [m.did, m])), new Map(convo.view.members.map(m => [m.did, m])),
) )
if (info) { if (info) {
lastMessage = i18n._(info.message) lastMessage = i18n._(info.message)
lastMessageSentAt = convo.lastMessage.sentAt lastMessageSentAt = convo.view.lastMessage.sentAt
} }
} }
@@ -332,11 +348,11 @@ function BaseChatItem({
const onPress = useCallback( const onPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
for (const member of convo.members) { for (const member of convo.view.members) {
unstableCacheProfileView(queryClient, member) unstableCacheProfileView(queryClient, member)
} }
precacheConvoQuery(queryClient, convo) precacheConvoQuery(queryClient, convo.view)
void decrementBadgeCount(convo.unreadCount) void decrementBadgeCount(convo.view.unreadCount)
if (isDeletedAccount) { if (isDeletedAccount) {
e.preventDefault() e.preventDefault()
menuControl.open() menuControl.open()
@@ -359,7 +375,7 @@ function BaseChatItem({
icon: EnvelopeOpen, icon: EnvelopeOpen,
action: () => { action: () => {
markAsRead({ markAsRead({
convoId: convo.id, convoId: convo.view.id,
}) })
}, },
} }
@@ -402,7 +418,7 @@ function BaseChatItem({
</View> </View>
<Link <Link
to={`/messages/${convo.id}`} to={`/messages/${convo.view.id}`}
label={title} label={title}
accessibilityHint={accessibilityHint} accessibilityHint={accessibilityHint}
accessibilityActions={ accessibilityActions={
@@ -419,7 +435,7 @@ function BaseChatItem({
] ]
: undefined : undefined
} }
onPressIn={() => precacheConvoQuery(queryClient, convo)} onPressIn={() => precacheConvoQuery(queryClient, convo.view)}
onPress={onPress} onPress={onPress}
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined} onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
onAccessibilityAction={showMenu ? onLongPress : undefined}> onAccessibilityAction={showMenu ? onLongPress : undefined}>
@@ -455,7 +471,7 @@ function BaseChatItem({
</Text> </Text>
</View> </View>
{showProfileBadges && ( {showProfileBadges && primaryProfile && (
<ProfileBadges <ProfileBadges
profile={primaryProfile} profile={primaryProfile}
size="md" size="md"
@@ -480,7 +496,7 @@ function BaseChatItem({
</TimeElapsed> </TimeElapsed>
</View> </View>
)} )}
{(convo.muted || isBlockedAccount) && ( {(convo.view.muted || isBlockedAccount) && (
<Text <Text
style={[ style={[
a.text_sm, a.text_sm,
@@ -550,13 +566,14 @@ function BaseChatItem({
<ChatListItemPortal.Outlet /> <ChatListItemPortal.Outlet />
{showMenu && ( {/* TODO: Allow showing menu for groups where the owner has left! */}
{showMenu && primaryProfile && (
<ConvoMenu <ConvoMenu
convo={convo} convo={convo.view}
profile={primaryProfile} profile={primaryProfile}
control={menuControl} control={menuControl}
currentScreen="list" currentScreen="list"
showMarkAsRead={convo.unreadCount > 0} showMarkAsRead={convo.view.unreadCount > 0}
hideTrigger={IS_NATIVE} hideTrigger={IS_NATIVE}
blockInfo={blockInfo} blockInfo={blockInfo}
style={[ style={[
@@ -576,7 +593,7 @@ function BaseChatItem({
<LeaveConvoPrompt <LeaveConvoPrompt
control={leaveConvoControl} control={leaveConvoControl}
convoId={convo.id} convoId={convo.view.id}
currentScreen="list" currentScreen="list"
/> />
</View> </View>
@@ -1,5 +1,6 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
@@ -17,7 +18,10 @@ import {
StackedButton, StackedButton,
} from '#/components/Button' } from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {type ConvoWithDetails} from '#/components/dms/util' import {
type ConvoWithDetails,
type GroupConvoMember,
} from '#/components/dms/util'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
@@ -39,16 +43,24 @@ enum Step {
export function InviteLinkDialog({ export function InviteLinkDialog({
convo, convo,
control, control,
owner,
isOwner, isOwner,
moderationOpts,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
owner: GroupConvoMember
isOwner: boolean isOwner: boolean
moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l, i18n} = useLingui() const {t: l, i18n} = useLingui()
const ownerName = createSanitizedDisplayName(convo.primaryMember) const ownerName = createSanitizedDisplayName(
owner,
false,
moderateProfile(owner, moderationOpts).ui('displayName'),
)
const {joinLink} = convo.details const {joinLink} = convo.details
const enabledStatus = joinLink?.enabledStatus const enabledStatus = joinLink?.enabledStatus
@@ -2,6 +2,7 @@ import {View} from 'react-native'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members' import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -23,13 +24,14 @@ export function MessagesListInfoPanel({
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const convoId = convo.view.id
const addMembersControl = Dialog.useDialogControl() const addMembersControl = Dialog.useDialogControl()
const inviteLinkControl = Dialog.useDialogControl() const inviteLinkControl = Dialog.useDialogControl()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const convoId = convo.view.id
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, { const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
onSuccess: () => { onSuccess: () => {
addMembersControl.close() addMembersControl.close()
@@ -46,9 +48,9 @@ export function MessagesListInfoPanel({
// (isOwner && groupConvo) || // (isOwner && groupConvo) ||
// (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled') // (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled')
const isOwner = convo?.primaryMember.did === currentAccount?.did const isOwner = convo.primaryMember?.did === currentAccount?.did
const members = (convo?.members ?? []).filter( const members = (convo.members ?? []).filter(
profile => profile.did !== currentAccount?.did, profile => profile.did !== currentAccount?.did,
) )
@@ -81,7 +83,7 @@ export function MessagesListInfoPanel({
return ( return (
<> <>
<View style={[a.align_center, a.justify_center]}> <View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={convo?.members} /> <AvatarBubbles animate={true} profiles={convo.members} />
{convo.details.name ? ( {convo.details.name ? (
<Text style={[a.text_2xl, a.font_bold, a.mt_lg, t.atoms.text]}> <Text style={[a.text_2xl, a.font_bold, a.mt_lg, t.atoms.text]}>
{convo.details.name} {convo.details.name}
@@ -139,11 +141,15 @@ export function MessagesListInfoPanel({
</View> </View>
) : null} ) : null}
</View> </View>
<InviteLinkDialog {convo.primaryMember && moderationOpts && (
isOwner={isOwner} <InviteLinkDialog
convo={convo} convo={convo}
control={inviteLinkControl} owner={convo.primaryMember}
/> moderationOpts={moderationOpts}
isOwner={isOwner}
control={inviteLinkControl}
/>
)}
<Dialog.Outer <Dialog.Outer
control={addMembersControl} control={addMembersControl}
testID="addChatMembersDialog" testID="addChatMembersDialog"
@@ -25,19 +25,22 @@ export function RequestListItem({
return null return null
} }
const isDeletedAccount = convo.primaryMember.handle === 'missing.invalid' const isDeletedAccount =
!convo.primaryMember || convo.primaryMember.handle === 'missing.invalid'
return ( return (
<View style={[a.relative, a.flex_1]}> <View style={[a.relative, a.flex_1]}>
<ChatListItem convo={convo.view} showMenu={false}> <ChatListItem convo={convo.view} showMenu={false}>
<View style={[a.pt_xs, a.pb_2xs]}> {convo.primaryMember && (
<KnownFollowers <View style={[a.pt_xs, a.pb_2xs]}>
profile={convo.primaryMember} <KnownFollowers
moderationOpts={moderationOpts} profile={convo.primaryMember}
minimal moderationOpts={moderationOpts}
showIfEmpty minimal
/> showIfEmpty
</View> />
</View>
)}
{/* spacer, since you can't nest pressables */} {/* spacer, since you can't nest pressables */}
<View style={[a.pt_md, a.pb_xs, a.w_full, {opacity: 0}]} aria-hidden> <View style={[a.pt_md, a.pb_xs, a.w_full, {opacity: 0}]} aria-hidden>
{/* Placeholder text so that it responds to the font height */} {/* Placeholder text so that it responds to the font height */}
@@ -60,7 +63,7 @@ export function RequestListItem({
paddingLeft: tokens.space.lg + 52 + tokens.space.md, paddingLeft: tokens.space.lg + 52 + tokens.space.md,
}, },
]}> ]}>
{!isDeletedAccount ? ( {convo.primaryMember && !isDeletedAccount ? (
<> <>
<AcceptChatButton convo={convo.view} currentScreen="list" /> <AcceptChatButton convo={convo.view} currentScreen="list" />
<RejectMenu <RejectMenu
@@ -18,6 +18,7 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {parseConvoView} from '#/components/dms/util'
import {useLeftConvos} from './leave-conversation' import {useLeftConvos} from './leave-conversation'
export const RQKEY_ROOT = 'convo-list' export const RQKEY_ROOT = 'convo-list'
@@ -453,19 +454,18 @@ function calculateCount(
return ( return (
convos convos
.filter(convo => convo.id !== currentConvoId) .filter(convo => convo.id !== currentConvoId)
.reduce((acc, convo) => { .reduce((acc, convoView) => {
const otherMember = convo.members.find( const convo = parseConvoView(convoView, currentAccountDid)
member => member.did !== currentAccountDid,
)
if (!otherMember || !moderationOpts) return acc if (!convo || !moderationOpts) return acc
const moderation = moderateProfile(otherMember, moderationOpts)
const shouldIgnore = const shouldIgnore =
convo.muted || convo.view.muted ||
moderation.blocked || !convo.primaryMember ||
otherMember.handle === 'missing.invalid' moderateProfile(convo.primaryMember, moderationOpts).blocked ||
const unreadCount = !shouldIgnore && convo.unreadCount > 0 ? 1 : 0 convo.primaryMember.handle === 'missing.invalid' ||
(convo.kind === 'group' && convo.details.lockStatus !== 'unlocked')
const unreadCount = !shouldIgnore && convo.view.unreadCount > 0 ? 1 : 0
return acc + unreadCount return acc + unreadCount
}, 0) ?? 0 }, 0) ?? 0