Create logged-out view for group chat invites (#10598)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {BskyAgent, type ModerationOpts} from '@atproto/api'
|
||||
import {AtpAgent, type ModerationOpts} from '@atproto/api'
|
||||
|
||||
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
|
||||
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||
@@ -43,7 +43,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
...moderationPrefs,
|
||||
labelers: moderationPrefs.labelers.length
|
||||
? moderationPrefs.labelers
|
||||
: BskyAgent.appLabelers.map(did => ({
|
||||
: AtpAgent.appLabelers.map(did => ({
|
||||
did,
|
||||
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
|
||||
})),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {AtpAgent} from '@atproto/api'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries/index'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const joinLinkPreviewQueryKeyRoot = 'join-link-preview'
|
||||
|
||||
export const createJoinLinkPreviewQueryKey = (args: {
|
||||
codes: string[]
|
||||
hasSession: boolean
|
||||
}) =>
|
||||
createQueryKey(joinLinkPreviewQueryKeyRoot, args, {
|
||||
persistedVersion: 1,
|
||||
})
|
||||
|
||||
export function useJoinLinkPreviewsQuery({
|
||||
codes,
|
||||
hasSession,
|
||||
}: {
|
||||
codes?: string[]
|
||||
hasSession: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}),
|
||||
queryFn: async () => {
|
||||
if (!codes) throw new Error('No invite code')
|
||||
try {
|
||||
const previewAgent = new AtpAgent({service: CHAT_SERVICE})
|
||||
const res = hasSession
|
||||
? await agent.chat.bsky.group.getJoinLinkPreviews(
|
||||
{codes},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
: await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes})
|
||||
return res.data
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch join link preview', {safeMessage: error})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
enabled: codes != null && codes.length > 0,
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePrefetchJoinLinkPreviews() {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => {
|
||||
return queryClient.prefetchQuery({
|
||||
queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}),
|
||||
queryFn: async () => {
|
||||
const previewAgent = new AtpAgent({service: CHAT_SERVICE})
|
||||
const res = hasSession
|
||||
? await agent.chat.bsky.group.getJoinLinkPreviews(
|
||||
{codes},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
: await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes})
|
||||
return res.data
|
||||
},
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {type ChatBskyGroupRequestJoin} from '@atproto/api'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
export function useRequestJoinGroupChat({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
} = {}) {
|
||||
const agent = useAgent()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({code}: {code: string}) => {
|
||||
if (!hasSession) throw new Error('Must be logged in to join')
|
||||
if (!code) throw new Error('No invite code')
|
||||
|
||||
const res = await agent.chat.bsky.group.requestJoin(
|
||||
{code},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
return res.data
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: error => {
|
||||
logger.error('Failed to join group chat', {safeMessage: error})
|
||||
onError?.(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
export function useWithdrawJoinGroupChatRequest({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
} = {}) {
|
||||
const agent = useAgent()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({convoId}: {convoId: string}) => {
|
||||
if (!hasSession)
|
||||
throw new Error('Must be logged in to withdraw a join request')
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
|
||||
const res = await agent.chat.bsky.group.withdrawJoinRequest(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
return res.data
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: error => {
|
||||
logger.error('Failed to withdraw join request', {safeMessage: error})
|
||||
onError?.(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {createContext, useContext, useState} from 'react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
|
||||
type StarterPackLanding = {
|
||||
type: 'starterpack'
|
||||
uri: string
|
||||
isClip?: boolean
|
||||
}
|
||||
|
||||
type GroupChatJoinRequestLanding = {
|
||||
type: 'groupchat'
|
||||
uri: string
|
||||
code: string
|
||||
}
|
||||
|
||||
type LandingType = StarterPackLanding | GroupChatJoinRequestLanding | undefined
|
||||
|
||||
type SetContext = (v: LandingType) => void
|
||||
|
||||
const stateContext = createContext<LandingType>(undefined)
|
||||
stateContext.displayName = 'ActiveLandingStateContext'
|
||||
const setContext = createContext<SetContext>((_: LandingType) => {})
|
||||
setContext.displayName = 'ActiveLandingSetContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [state, setState] = useState<LandingType>()
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<setContext.Provider value={setState}>{children}</setContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Core hooks
|
||||
export const useActiveLanding = () => useContext(stateContext)
|
||||
export const useSetActiveLanding = () => useContext(setContext)
|
||||
|
||||
// Filtered hooks for convenience
|
||||
export const useActiveStarterPack = () => {
|
||||
const landing = useActiveLanding()
|
||||
return landing?.type === 'starterpack' ? landing : undefined
|
||||
}
|
||||
|
||||
export const useSetActiveStarterPack = () => {
|
||||
const setLanding = useSetActiveLanding()
|
||||
const currentLanding = useActiveLanding()
|
||||
return (pack: {uri: string; isClip?: boolean} | undefined) => {
|
||||
if (!pack) {
|
||||
setLanding(undefined)
|
||||
} else {
|
||||
if (currentLanding && currentLanding.type !== 'starterpack') {
|
||||
logger.debug(
|
||||
`[landing] Replacing ${currentLanding.type} landing with starterpack`,
|
||||
)
|
||||
}
|
||||
setLanding({type: 'starterpack', ...pack})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useActiveGroupChatJoinRequest = () => {
|
||||
const landing = useActiveLanding()
|
||||
return landing?.type === 'groupchat' ? landing : undefined
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {createContext, useContext, useMemo, useState} from 'react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {useActiveStarterPack} from '#/state/shell/starter-pack'
|
||||
import {useActiveLanding} from '#/state/shell/landing'
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
type State = {
|
||||
@@ -26,7 +26,12 @@ type Controls = {
|
||||
/**
|
||||
* The did of the account to populate the login form with.
|
||||
*/
|
||||
requestedAccount?: (string & {}) | 'none' | 'new' | 'starterpack'
|
||||
requestedAccount?:
|
||||
| (string & {})
|
||||
| 'none'
|
||||
| 'new'
|
||||
| 'starterpack'
|
||||
| 'groupchat'
|
||||
}) => void
|
||||
/**
|
||||
* Clears the requested account so that next time the logged out view is
|
||||
@@ -48,17 +53,34 @@ const ControlsContext = createContext<Controls>({
|
||||
})
|
||||
ControlsContext.displayName = 'LoggedOutControlsContext'
|
||||
|
||||
function getRequestedAccountFromLanding(
|
||||
landing: ReturnType<typeof useActiveLanding>,
|
||||
hasSession: boolean,
|
||||
): string | undefined {
|
||||
if (hasSession || !landing) return undefined
|
||||
|
||||
switch (landing.type) {
|
||||
case 'starterpack':
|
||||
return IS_WEB ? 'starterpack' : 'new'
|
||||
case 'groupchat':
|
||||
return 'groupchat'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const activeStarterPack = useActiveStarterPack()
|
||||
const activeLanding = useActiveLanding()
|
||||
const {hasSession} = useSession()
|
||||
const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
|
||||
|
||||
const requestedAccount = getRequestedAccountFromLanding(
|
||||
activeLanding,
|
||||
hasSession,
|
||||
)
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
showLoggedOut: shouldShowStarterPack,
|
||||
requestedAccountSwitchTo: shouldShowStarterPack
|
||||
? IS_WEB
|
||||
? 'starterpack'
|
||||
: 'new'
|
||||
: undefined,
|
||||
showLoggedOut: Boolean(requestedAccount),
|
||||
requestedAccountSwitchTo: requestedAccount,
|
||||
})
|
||||
|
||||
const controls = useMemo<Controls>(
|
||||
|
||||
Reference in New Issue
Block a user