import {useState} from 'react'
import {View} from 'react-native'
import {
ChatBskyActorDefs,
ChatBskyConvoDefs,
type ModerationOpts,
} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
type NavigationProp,
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
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 {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
import {useListJoinRequestsQuery} from '#/state/queries/messages/list-join-requests'
import {useLockConvo} from '#/state/queries/messages/lock-conversation'
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
import {useSession} from '#/state/session'
import {List} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {AfterReportConversationDialog} from '#/components/dms/AfterReportConversationDialog'
import {ReportConversationDialog} from '#/components/dms/ReportConversationDialog'
import {
type ConvoWithDetails,
type GroupConvoMember,
} from '#/components/dms/util'
import {Error} from '#/components/Error'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
import {
Bell2_Stroke2_Corner0_Rounded as BellIcon,
Bell2Off_Stroke2_Corner0_Rounded as BellOffIcon,
} from '#/components/icons/Bell2'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {InviteLinkDialog} from '../components/InviteLinkDialog'
import {AddMembersLink} from './AddMembersLink'
import {Member, MemberPlaceholder} from './Member'
import {MembersAndRequests} from './MembersAndRequests'
import {EditNamePrompt, LeaveChatPrompt, LockChatPrompt} from './prompts'
type Item =
| {type: 'MEMBERS_AND_REQUESTS'; key: string}
| {type: 'ADD_MEMBERS_LINK'; key: string}
| {
type: 'CHAT_MEMBER'
key: string
profile: GroupConvoMember
status: 'owner' | 'standard'
}
| {
type: 'CHAT_MEMBER_PLACEHOLDER'
key: string
}
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'MessagesConversationSettings'
>
export function MessagesConversationSettingsScreen({route}: Props) {
const convoId = route.params.conversation
return (
Group chat settings
)
}
function SettingsInner() {
const {t: l} = useLingui()
const convoState = useConvo()
const navigation = useNavigation()
const moderationOpts = useModerationOpts()
if (convoState.status === ConvoStatus.Error) {
return (
convoState.error.retry()}
sideBorders={false}
/>
)
}
if (!convoState.convo || !moderationOpts) {
return (
)
}
if (convoState.convo.kind !== 'group') {
return (
{
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.replace('Messages', {animation: 'pop'})
}
}}
/>
)
}
return (
)
}
function keyExtractor(item: Item) {
return item.key
}
function isGroupMember(
member: ChatBskyActorDefs.ProfileViewBasic,
): member is GroupConvoMember {
// Kind is missing when the account has been deleted.
return (
member.kind === undefined ||
bsky.dangerousIsType(
member.kind,
ChatBskyActorDefs.isGroupConvoMember,
)
)
}
function GroupSettings({
convo,
moderationOpts,
isReady,
}: {
convo: Extract
moderationOpts: ModerationOpts
isReady: boolean
}) {
const [isPTRing, setIsPTRing] = useState(false)
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
const bottomBarOffset = useBottomBarOffset()
const {currentAccount} = useSession()
const primaryMember = convo.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {data: memberListData = [], refetch} = useListConvoMembersQuery({
convoId: convo.view.id,
placeholderData: convo.members,
})
const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
useListJoinRequestsQuery({
convoId: convo.view.id,
enabled: isOwner,
})
const requestCount =
joinRequestsData?.pages.reduce(
(sum, page) => sum + page.requests.length,
0,
) ?? 0
const groupMembers = memberListData.filter(isGroupMember).sort((a, b) => {
const aIsOwner = a.did === primaryMember?.did
const bIsOwner = b.did === primaryMember?.did
const aIsSelf = a.did === currentAccount?.did
const bIsSelf = b.did === currentAccount?.did
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1
return 0
})
const items: Item[] = [
{
type: 'MEMBERS_AND_REQUESTS',
key: 'members-and-requests',
},
...(isOwner && convo.details.lockStatus === 'unlocked'
? [{type: 'ADD_MEMBERS_LINK', key: 'add-members-link'} as const]
: []),
]
items.push(
...groupMembers.map(
(profile): Item => ({
type: 'CHAT_MEMBER',
key: profile.did,
profile,
status: primaryMember?.did === profile.did ? 'owner' : 'standard',
}),
),
)
const placeholderCount = Math.max(
0,
convo.details.memberCount - groupMembers.length,
)
for (let i = 0; i < placeholderCount; i++) {
items.push({
type: 'CHAT_MEMBER_PLACEHOLDER',
key: `chat-member-placeholder-${i}`,
})
}
function renderItem({item}: {item: Item}) {
switch (item.type) {
case 'MEMBERS_AND_REQUESTS':
return (
)
case 'ADD_MEMBERS_LINK':
return
case 'CHAT_MEMBER':
return (
)
case 'CHAT_MEMBER_PLACEHOLDER':
return
default:
return null
}
}
const onRefresh = async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh group chat members', {message: err})
}
setIsPTRing(false)
}
return (
}
renderItem={renderItem}
sideBorders={false}
windowSize={11}
refreshing={isPTRing}
onRefresh={() => void onRefresh()}
/>
)
}
function SettingsHeader({
convo,
isOwner,
moderationOpts,
isReady,
}: {
convo: Extract
isOwner: boolean
moderationOpts: ModerationOpts
isReady: boolean
}) {
const t = useTheme()
const {i18n, t: l} = useLingui()
const navigation = useNavigation()
const groupName = convo.details.name
const [newGroupName, setNewGroupName] = useState(groupName)
const lockStatus = convo.details.lockStatus
const {joinLink} = convo.details
const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled'
const reportSubjectDid = convo.primaryMember?.did
const {mutate: editGroupName, isPending: isEditingName} =
useEditGroupChatName(convo.view.id, {
onSuccess: () => {
Toast.show(l({message: 'Group chat name updated', context: 'toast'}))
},
onError: e => {
setNewGroupName(groupName)
logger.error('Failed to edit group chat name', {message: e})
Toast.show(l`Failed to edit group chat name`, {type: 'error'})
},
})
const {mutate: muteConvo, isPending: isMuting} = useMuteConvo(convo.view.id, {
onSuccess: data => {
if (data.convo.muted) {
Toast.show(l({message: 'Group chat muted', context: 'toast'}))
} else {
Toast.show(l({message: 'Group chat unmuted', context: 'toast'}))
}
},
onError: e => {
logger.error('Failed to mute group chat', {message: e})
Toast.show(l`Failed to mute group chat`, {type: 'error'})
},
})
const {mutate: leaveConvo, isPending: isLeaving} = useLeaveConvo(
convo.view.id,
{
onSuccess: () => {
navigation.replace('Messages', {animation: 'pop'})
},
onError: e => {
logger.error('Failed to leave group chat', {message: e})
Toast.show(
l({message: 'Failed to leave group chat', context: 'toast'}),
{type: 'error'},
)
},
},
)
const {mutate: lockConvo, isPending: isLocking} = useLockConvo(
convo.view.id,
{
onSuccess: data => {
if (!ChatBskyConvoDefs.isGroupConvo(data.convo.kind)) return
if (data.convo.kind.lockStatus === 'locked') {
Toast.show(l({message: 'Group chat locked', context: 'toast'}))
} else {
Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
}
},
onError: (e, {lock}) => {
if (lock) {
logger.error('Failed to lock group chat', {message: e})
Toast.show(l`Failed to lock group chat`, {type: 'error'})
} else {
logger.error('Failed to unlock group chat', {message: e})
Toast.show(l`Failed to unlock group chat`, {type: 'error'})
}
},
},
)
const inviteLinkDialog = Dialog.useDialogControl()
const editNamePrompt = Prompt.usePromptControl()
const lockChatPrompt = Prompt.usePromptControl()
const leaveChatPrompt = Prompt.usePromptControl()
const reportControl = Prompt.usePromptControl()
const deleteControl = Prompt.usePromptControl()
const createdAt = new Date(convo.details.createdAt)
const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently'
return (
<>
{groupName}
Created{' '}
{i18n.date(createdAt, {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
muteConvo({mute: !convo.view.muted})}
/>
{isOwner ? (
{
setNewGroupName(groupName)
editNamePrompt.open()
}}
/>
) : null}
{isJoinLinkEnabled ? (
) : null}
{canLockGroupChat ? (
lockConvo({lock: false})
: lockChatPrompt.open
}
/>
) : null}
{!isOwner && reportSubjectDid ? (
) : null}
{!isOwner ? (
) : null}
editGroupName({name: newGroupName})}
/>
{convo.primaryMember && (
)}
lockConvo({lock: true})}
/>
{reportSubjectDid ? (
<>
>
) : null}
>
)
}
function SettingsButton({
color = 'secondary',
disabled,
icon,
label,
text,
onPress,
}: {
color?: ButtonColor
disabled?: boolean
icon: React.ComponentType
label: string
text: string
onPress: () => void
}) {
const t = useTheme()
return (
{text}
)
}