Create screen for managing chat requests (#10635)
This commit is contained in:
@@ -339,6 +339,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/messages/inbox", server.WebGeneric)
|
e.GET("/messages/inbox", server.WebGeneric)
|
||||||
e.GET("/messages/:conversation", server.WebGeneric)
|
e.GET("/messages/:conversation", server.WebGeneric)
|
||||||
e.GET("/messages/:conversation/settings", server.WebGeneric)
|
e.GET("/messages/:conversation/settings", server.WebGeneric)
|
||||||
|
e.GET("/messages/:conversation/requests", server.WebGeneric)
|
||||||
|
|
||||||
// profile endpoints; only first populates info
|
// profile endpoints; only first populates info
|
||||||
e.GET("/profile/:handleOrDID", server.WebProfile)
|
e.GET("/profile/:handleOrDID", server.WebProfile)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import {MessagesScreen} from '#/screens/Messages/ChatList'
|
|||||||
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
||||||
import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings'
|
import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings'
|
||||||
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
|
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
|
||||||
|
import {MessagesJoinRequestsScreen} from '#/screens/Messages/JoinRequests'
|
||||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
||||||
@@ -488,6 +489,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => MessagesConversationSettingsScreen}
|
getComponent={() => MessagesConversationSettingsScreen}
|
||||||
options={{title: title(msg`Group chat settings`), requireAuth: true}}
|
options={{title: title(msg`Group chat settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="MessagesJoinRequests"
|
||||||
|
getComponent={() => MessagesJoinRequestsScreen}
|
||||||
|
options={{title: title(msg`Requests to join`), requireAuth: true}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="MessagesSettings"
|
name="MessagesSettings"
|
||||||
getComponent={() => MessagesSettingsScreen}
|
getComponent={() => MessagesSettingsScreen}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export type CommonNavigatorParams = {
|
|||||||
Topic: {topic: string}
|
Topic: {topic: string}
|
||||||
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
||||||
MessagesConversationSettings: {conversation: string}
|
MessagesConversationSettings: {conversation: string}
|
||||||
|
MessagesJoinRequests: {conversation: string}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
MessagesInbox: undefined
|
MessagesInbox: undefined
|
||||||
NotificationsActivityList: {posts: string}
|
NotificationsActivityList: {posts: string}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
|||||||
MessagesInbox: '/messages/inbox',
|
MessagesInbox: '/messages/inbox',
|
||||||
MessagesConversation: '/messages/:conversation',
|
MessagesConversation: '/messages/:conversation',
|
||||||
MessagesConversationSettings: '/messages/:conversation/settings',
|
MessagesConversationSettings: '/messages/:conversation/settings',
|
||||||
|
MessagesJoinRequests: '/messages/:conversation/requests',
|
||||||
// starter packs
|
// starter packs
|
||||||
Start: '/start/:name/:rkey',
|
Start: '/start/:name/:rkey',
|
||||||
StarterPackEdit: '/starter-pack/edit/:rkey',
|
StarterPackEdit: '/starter-pack/edit/:rkey',
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {plural} from '@lingui/core/macro'
|
import {plural} from '@lingui/core/macro'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||||
|
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function MembersAndRequests({
|
export function MembersAndRequests({
|
||||||
memberCount,
|
convo,
|
||||||
memberLimit,
|
|
||||||
requestCount,
|
requestCount,
|
||||||
hasMoreRequests,
|
hasMoreRequests,
|
||||||
isOwner,
|
isOwner,
|
||||||
}: {
|
}: {
|
||||||
memberCount: number
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
memberLimit: number
|
|
||||||
requestCount: number
|
requestCount: number
|
||||||
hasMoreRequests: boolean
|
hasMoreRequests: boolean
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
const memberCount = convo.details.memberCount
|
||||||
|
const memberLimit = convo.details.memberLimit
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.justify_between, a.px_xl, a.pt_xl, a.pb_sm]}>
|
<View style={[a.flex_row, a.justify_between, a.px_xl, a.pt_xl, a.pb_sm]}>
|
||||||
@@ -42,8 +47,11 @@ export function MembersAndRequests({
|
|||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={l`View incoming group chat requests`}
|
label={l`View incoming group chat requests`}
|
||||||
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
||||||
// TODO Need to implement this. -dsb
|
{...createStaticClick(() => {
|
||||||
to="#">
|
navigation.navigate('MessagesJoinRequests', {
|
||||||
|
conversation: convo.view.id,
|
||||||
|
})
|
||||||
|
})}>
|
||||||
{hasMoreRequests
|
{hasMoreRequests
|
||||||
? l({
|
? l({
|
||||||
message: `${requestCount}+ requests`,
|
message: `${requestCount}+ requests`,
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ function GroupSettings({
|
|||||||
case 'MEMBERS_AND_REQUESTS':
|
case 'MEMBERS_AND_REQUESTS':
|
||||||
return (
|
return (
|
||||||
<MembersAndRequests
|
<MembersAndRequests
|
||||||
memberCount={convo.details.memberCount}
|
convo={convo}
|
||||||
memberLimit={convo.details.memberLimit}
|
|
||||||
requestCount={requestCount}
|
requestCount={requestCount}
|
||||||
hasMoreRequests={!!hasMoreRequests}
|
hasMoreRequests={!!hasMoreRequests}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
|||||||
@@ -0,0 +1,491 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {
|
||||||
|
ChatBskyGroupApproveJoinRequest,
|
||||||
|
type ChatBskyGroupListJoinRequests,
|
||||||
|
ChatBskyGroupRejectJoinRequest,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
import {type InfiniteData, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
|
import {isNetworkError} from '#/lib/hooks/useCleanError'
|
||||||
|
import {
|
||||||
|
type CommonNavigatorParams,
|
||||||
|
type NativeStackScreenProps,
|
||||||
|
type NavigationProp,
|
||||||
|
} from '#/lib/routes/types'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {ConvoProvider, useConvo} from '#/state/messages/convo'
|
||||||
|
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useJoinRequestMutation} from '#/state/queries/messages/join-requests'
|
||||||
|
import {
|
||||||
|
createListJoinRequestsQueryKey,
|
||||||
|
useListJoinRequestsQuery,
|
||||||
|
} from '#/state/queries/messages/list-join-requests'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {List} from '#/view/com/util/List'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||||
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||||
|
import {Error} from '#/components/Error'
|
||||||
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||||
|
import {KnownFollowers} from '#/components/KnownFollowers'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
|
import * as Toast from '#/components/Toast'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {InviteLinkDialog} from './components/InviteLinkDialog'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'MessagesJoinRequests'
|
||||||
|
>
|
||||||
|
|
||||||
|
export function MessagesJoinRequestsScreen(props: Props) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const aaCopy = useAgeAssuranceCopy()
|
||||||
|
return (
|
||||||
|
<AgeRestrictedScreen
|
||||||
|
screenTitle={l`Requests to join`}
|
||||||
|
infoText={aaCopy.chatsInfoText}>
|
||||||
|
<MessagesJoinRequestsScreenInner {...props} />
|
||||||
|
</AgeRestrictedScreen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessagesJoinRequestsScreenInner({route}: Props) {
|
||||||
|
const convoId = route.params.conversation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen>
|
||||||
|
<ConvoProvider key={convoId} convoId={convoId}>
|
||||||
|
<JoinRequestsInner />
|
||||||
|
</ConvoProvider>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function JoinRequestsInner() {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const convoState = useConvo()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
if (convoState.status === ConvoStatus.Error) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Error
|
||||||
|
title={l`Something went wrong`}
|
||||||
|
message={l`We couldn’t load this conversation’s join requests`}
|
||||||
|
onRetry={() => convoState.error.retry()}
|
||||||
|
sideBorders={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!convoState.convo) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convoState.convo.kind !== 'group') {
|
||||||
|
return (
|
||||||
|
<Error
|
||||||
|
title={l`Wrong kind of conversation`}
|
||||||
|
message={l`This screen is only available for group conversations.`}
|
||||||
|
onGoBack={() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.replace('Messages', {animation: 'pop'})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <JoinRequestsList convo={convoState.convo} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function JoinRequestsList({
|
||||||
|
convo,
|
||||||
|
}: {
|
||||||
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const bottomBarOffset = useBottomBarOffset()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const inviteLinkControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
const getRemainingRequestCount = () => {
|
||||||
|
const data = queryClient.getQueryData<
|
||||||
|
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||||
|
>(createListJoinRequestsQueryKey({convoId: convo.view.id}))
|
||||||
|
return data?.pages.reduce((sum, page) => sum + page.requests.length, 0) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
const [footerHeight, setFooterHeight] = useState(0)
|
||||||
|
|
||||||
|
const owner = convo.primaryMember
|
||||||
|
const isOwner = !!owner && owner.did === currentAccount?.did
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: joinRequestsData,
|
||||||
|
isPending,
|
||||||
|
isError,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
refetch,
|
||||||
|
} = useListJoinRequestsQuery({
|
||||||
|
convoId: convo.view.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const items =
|
||||||
|
joinRequestsData?.pages.flatMap(page =>
|
||||||
|
page.requests.map(request => request.requestedBy),
|
||||||
|
) ?? []
|
||||||
|
const requestCount =
|
||||||
|
joinRequestsData?.pages.reduce(
|
||||||
|
(sum, page) => sum + page.requests.length,
|
||||||
|
0,
|
||||||
|
) ?? 0
|
||||||
|
|
||||||
|
const {mutate: approveJoinRequest, isPending: isApprovePending} =
|
||||||
|
useJoinRequestMutation('approve', convo.view.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.show(l`Request approved.`)
|
||||||
|
if (getRemainingRequestCount() < 1) {
|
||||||
|
navigation.replace('MessagesConversationSettings', {
|
||||||
|
conversation: convo.view.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
let errorMessage = l`Failed to accept join request`
|
||||||
|
if (isNetworkError(error)) {
|
||||||
|
errorMessage = l`A network error occurred. Please check your internet connection.`
|
||||||
|
} else if (
|
||||||
|
error instanceof ChatBskyGroupApproveJoinRequest.InvalidConvoError
|
||||||
|
) {
|
||||||
|
errorMessage = l`Conversation not found.`
|
||||||
|
} else if (
|
||||||
|
error instanceof ChatBskyGroupApproveJoinRequest.InsufficientRoleError
|
||||||
|
) {
|
||||||
|
errorMessage = l`Only admins can accept join requests.`
|
||||||
|
} else if (
|
||||||
|
error instanceof
|
||||||
|
ChatBskyGroupApproveJoinRequest.MemberLimitReachedError
|
||||||
|
) {
|
||||||
|
errorMessage = l`The member limit has been reached.`
|
||||||
|
}
|
||||||
|
Toast.show(errorMessage, {type: 'error'})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {mutate: rejectJoinRequest, isPending: isRejectPending} =
|
||||||
|
useJoinRequestMutation('reject', convo.view.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.show(l`Request ignored.`)
|
||||||
|
if (getRemainingRequestCount() < 1) {
|
||||||
|
navigation.replace('MessagesConversationSettings', {
|
||||||
|
conversation: convo.view.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
let errorMessage = l`Failed to ignore join request`
|
||||||
|
if (isNetworkError(error)) {
|
||||||
|
errorMessage = l`A network error occurred. Please check your internet connection.`
|
||||||
|
} else if (
|
||||||
|
error instanceof ChatBskyGroupRejectJoinRequest.InvalidConvoError
|
||||||
|
) {
|
||||||
|
errorMessage = l`Conversation not found.`
|
||||||
|
} else if (
|
||||||
|
error instanceof ChatBskyGroupRejectJoinRequest.InsufficientRoleError
|
||||||
|
) {
|
||||||
|
errorMessage = l`Only admins can ignore join requests.`
|
||||||
|
}
|
||||||
|
Toast.show(errorMessage, {type: 'error'})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMutating = isApprovePending || isRejectPending
|
||||||
|
|
||||||
|
const renderItem = ({item}: {item: bsky.profile.AnyProfileView}) => {
|
||||||
|
if (!moderationOpts) return null
|
||||||
|
return (
|
||||||
|
<View style={[a.relative, a.flex_1, a.p_lg]}>
|
||||||
|
<View style={[a.flex_row, a.align_start, a.gap_md]}>
|
||||||
|
<ProfileCard.Link profile={item}>
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={item}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
size={44}
|
||||||
|
disabledPreview
|
||||||
|
/>
|
||||||
|
</ProfileCard.Link>
|
||||||
|
<View>
|
||||||
|
<ProfileCard.Name profile={item} moderationOpts={moderationOpts} />
|
||||||
|
<ProfileCard.Handle profile={item} />
|
||||||
|
<View style={[a.mt_xs]}>
|
||||||
|
<KnownFollowers
|
||||||
|
profile={item}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
minimal
|
||||||
|
showIfEmpty
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm, a.mt_md]}>
|
||||||
|
<AcceptButton
|
||||||
|
disabled={isMutating}
|
||||||
|
onPress={() => approveJoinRequest({member: item.did})}
|
||||||
|
/>
|
||||||
|
<RejectButton
|
||||||
|
disabled={isMutating}
|
||||||
|
onPress={() => rejectJoinRequest({member: item.did})}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const footer = (
|
||||||
|
<View
|
||||||
|
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.left_0,
|
||||||
|
a.right_0,
|
||||||
|
{bottom: 0},
|
||||||
|
a.px_xl,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.bg,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{
|
||||||
|
paddingTop: a.py_lg.paddingTop,
|
||||||
|
paddingBottom: a.py_lg.paddingBottom + bottomBarOffset,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={l`Edit invite link`}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
onPress={() => inviteLinkControl.open()}
|
||||||
|
style={[a.w_full]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans context="button">Edit invite link</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const onEndReached = async () => {
|
||||||
|
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||||
|
try {
|
||||||
|
await fetchNextPage()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load more join requests', {message: err})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
|
try {
|
||||||
|
await refetch()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to refresh group chat requests', {message: err})
|
||||||
|
}
|
||||||
|
setIsPTRing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header count={requestCount} hasMoreRequests={hasNextPage} />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.p_lg,
|
||||||
|
]}>
|
||||||
|
<ErrorIcon size="3xl" fill={t.atoms.text_contrast_high.color} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_center,
|
||||||
|
a.px_lg,
|
||||||
|
a.text_md,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
<Trans>Unable to fetch join requests.</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
label={l`Press to retry`}
|
||||||
|
onPress={() => void onRefresh()}
|
||||||
|
disabled={isPTRing}
|
||||||
|
size="large"
|
||||||
|
style={[a.mt_md]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={isPTRing ? Loader : RetryIcon} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showFooter = isOwner
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header count={requestCount} hasMoreRequests={hasNextPage} />
|
||||||
|
<List
|
||||||
|
data={items}
|
||||||
|
keyExtractor={(item: bsky.profile.AnyProfileView) => item.did}
|
||||||
|
renderItem={renderItem}
|
||||||
|
ListEmptyComponent={
|
||||||
|
isPending ? (
|
||||||
|
<View
|
||||||
|
style={[a.flex_1, a.align_center, a.justify_center, a.py_4xl]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
contentContainerStyle={
|
||||||
|
showFooter ? {paddingBottom: footerHeight} : undefined
|
||||||
|
}
|
||||||
|
scrollIndicatorInsets={showFooter ? {bottom: footerHeight} : undefined}
|
||||||
|
refreshing={isPTRing}
|
||||||
|
onEndReached={() => void onEndReached()}
|
||||||
|
onRefresh={() => void onRefresh()}
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
|
sideBorders={false}
|
||||||
|
desktopFixedHeight
|
||||||
|
/>
|
||||||
|
{showFooter ? footer : null}
|
||||||
|
{owner && moderationOpts && (
|
||||||
|
<InviteLinkDialog
|
||||||
|
convo={convo}
|
||||||
|
control={inviteLinkControl}
|
||||||
|
owner={owner}
|
||||||
|
isOwner={isOwner}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({
|
||||||
|
count,
|
||||||
|
hasMoreRequests,
|
||||||
|
}: {
|
||||||
|
count?: number
|
||||||
|
hasMoreRequests?: boolean
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
return (
|
||||||
|
<Layout.Header.Outer>
|
||||||
|
<Layout.Header.BackButton />
|
||||||
|
<Layout.Header.Content>
|
||||||
|
<Layout.Header.TitleText>
|
||||||
|
{count === undefined ? (
|
||||||
|
<Trans>Requests to join</Trans>
|
||||||
|
) : hasMoreRequests ? (
|
||||||
|
l({
|
||||||
|
message: `${count}+ requests to join`,
|
||||||
|
comment:
|
||||||
|
'Displayed when there are more requests to join a group chat than have been loaded',
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Plural
|
||||||
|
value={count}
|
||||||
|
zero="No requests to join"
|
||||||
|
one="# request to join"
|
||||||
|
other="# requests to join"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Layout.Header.TitleText>
|
||||||
|
</Layout.Header.Content>
|
||||||
|
<Layout.Header.Slot />
|
||||||
|
</Layout.Header.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AcceptButton({
|
||||||
|
disabled,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
disabled?: boolean
|
||||||
|
onPress: () => void
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={l`Accept join request`}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
disabled={disabled}
|
||||||
|
onPress={onPress}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans comment="Accept a request to join a chat" context="button">
|
||||||
|
Accept
|
||||||
|
</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RejectButton({
|
||||||
|
disabled,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
disabled?: boolean
|
||||||
|
onPress: () => void
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={l`Ignore join request`}
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
disabled={disabled}
|
||||||
|
onPress={onPress}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans comment="Ignore a request to join a chat" context="button">
|
||||||
|
Ignore
|
||||||
|
</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
type ChatBskyActorDefs,
|
||||||
|
type ChatBskyGroupApproveJoinRequest,
|
||||||
|
type ChatBskyGroupListJoinRequests,
|
||||||
|
type ChatBskyGroupRejectJoinRequest,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
type InfiniteData,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||||
|
import {createListJoinRequestsQueryKey} from './list-join-requests'
|
||||||
|
|
||||||
|
type JoinRequestAction = 'approve' | 'reject'
|
||||||
|
|
||||||
|
type JoinRequestOutput<A extends JoinRequestAction> = A extends 'approve'
|
||||||
|
? ChatBskyGroupApproveJoinRequest.OutputSchema
|
||||||
|
: ChatBskyGroupRejectJoinRequest.OutputSchema
|
||||||
|
|
||||||
|
export function useJoinRequestMutation<A extends JoinRequestAction>(
|
||||||
|
action: A,
|
||||||
|
convoId: string | undefined,
|
||||||
|
{
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess?: (data: JoinRequestOutput<A>) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({member}: {member: string}) => {
|
||||||
|
if (!convoId) throw new Error('No convoId provided')
|
||||||
|
const endpoint =
|
||||||
|
action === 'approve'
|
||||||
|
? agent.chat.bsky.group.approveJoinRequest
|
||||||
|
: agent.chat.bsky.group.rejectJoinRequest
|
||||||
|
const {data} = await endpoint(
|
||||||
|
{convoId, member},
|
||||||
|
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||||
|
)
|
||||||
|
return data as JoinRequestOutput<A>
|
||||||
|
},
|
||||||
|
onMutate: ({member}) => {
|
||||||
|
if (!convoId) return
|
||||||
|
|
||||||
|
const requestsKey = createListJoinRequestsQueryKey({convoId})
|
||||||
|
const prevRequests =
|
||||||
|
queryClient.getQueryData<
|
||||||
|
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||||
|
>(requestsKey)
|
||||||
|
|
||||||
|
const requestedByProfile = prevRequests?.pages
|
||||||
|
.flatMap(page => page.requests)
|
||||||
|
.find(request => request.requestedBy.did === member)?.requestedBy
|
||||||
|
|
||||||
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||||
|
>(requestsKey, prev => {
|
||||||
|
if (!prev?.pages) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
pages: prev.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
requests: page.requests.filter(
|
||||||
|
request => request.requestedBy.did !== member,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let prevMembers: ChatBskyActorDefs.ProfileViewBasic[] | undefined
|
||||||
|
if (action === 'approve' && requestedByProfile) {
|
||||||
|
const membersKey = listConvoMembersQueryKey(convoId)
|
||||||
|
prevMembers =
|
||||||
|
queryClient.getQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||||
|
membersKey,
|
||||||
|
)
|
||||||
|
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||||
|
membersKey,
|
||||||
|
prev => {
|
||||||
|
if (!prev) return prev
|
||||||
|
if (prev.some(m => m.did === member)) return prev
|
||||||
|
return [...prev, requestedByProfile]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {prevRequests, prevMembers}
|
||||||
|
},
|
||||||
|
onSuccess: data => {
|
||||||
|
if (convoId) {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: createListJoinRequestsQueryKey({convoId}),
|
||||||
|
})
|
||||||
|
if (action === 'approve') {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: listConvoMembersQueryKey(convoId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSuccess?.(data)
|
||||||
|
},
|
||||||
|
onError: (error, _variables, context) => {
|
||||||
|
logger.error(error)
|
||||||
|
if (convoId && context?.prevRequests) {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
createListJoinRequestsQueryKey({convoId}),
|
||||||
|
context.prevRequests,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (convoId && action === 'approve' && context?.prevMembers) {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
listConvoMembersQueryKey(convoId),
|
||||||
|
context.prevMembers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onError?.(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user