import {useCallback, useMemo, useState} from 'react'
import {LayoutAnimation, Text as NestedText, View} from 'react-native'
import {
type AppBskyFeedDefs,
type AppBskyFeedPostgate,
AtUri,
} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useHaptics} from '#/lib/haptics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {useGetPost} from '#/state/queries/post'
import {
createPostgateQueryKey,
getPostgateRecord,
usePostgateQuery,
useWritePostgateMutation,
} from '#/state/queries/postgate'
import {
createPostgateRecord,
embeddingRules,
} from '#/state/queries/postgate/util'
import {
createThreadgateViewQueryKey,
type ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation,
useThreadgateViewQuery,
} from '#/state/queries/threadgate'
import {
PostThreadContextProvider,
usePostThreadContext,
} from '#/state/queries/usePostThread'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
export type PostInteractionSettingsFormProps = {
canSave?: boolean
onSave: () => void
isSaving?: boolean
isDirty?: boolean
persist?: boolean
onChangePersist?: (v: boolean) => void
postgate: AppBskyFeedPostgate.Record
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
threadgateAllowUISettings: ThreadgateAllowUISetting[]
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
replySettingsDisabled?: boolean
}
/**
* Threadgate settings dialog. Used in the composer.
*/
export function PostInteractionSettingsControlledDialog({
control,
...rest
}: PostInteractionSettingsFormProps & {
control: Dialog.DialogControlProps
}) {
const ax = useAnalytics()
const onClose = useNonReactiveCallback(() => {
ax.metric('composer:threadgate:save', {
hasChanged: !!rest.isDirty,
persist: !!rest.persist,
replyOptions:
rest.threadgateAllowUISettings?.map(gate => gate.type)?.join(',') ?? '',
quotesEnabled: !rest.postgate?.embeddingRules?.find(
v => v.$type === embeddingRules.disableRule.$type,
),
})
})
return (
)
}
function DialogInner(props: Omit) {
const {_} = useLingui()
return (
)
}
export type PostInteractionSettingsDialogProps = {
control: Dialog.DialogControlProps
/**
* URI of the post to edit the interaction settings for. Could be a root post
* or could be a reply.
*/
postUri: string
/**
* The URI of the root post in the thread. Used to determine if the viewer
* owns the threadgate record and can therefore edit it.
*/
rootPostUri: string
/**
* Optional initial {@link AppBskyFeedDefs.ThreadgateView} to use if we
* happen to have one before opening the settings dialog.
*/
initialThreadgateView?: AppBskyFeedDefs.ThreadgateView
}
/**
* Threadgate settings dialog. Used in the thread.
*/
export function PostInteractionSettingsDialog(
props: PostInteractionSettingsDialogProps,
) {
const postThreadContext = usePostThreadContext()
return (
)
}
export function PostInteractionSettingsDialogControlledInner(
props: PostInteractionSettingsDialogProps,
) {
const ax = useAnalytics()
const {_} = useLingui()
const {currentAccount} = useSession()
const [isSaving, setIsSaving] = useState(false)
const {data: threadgateViewLoaded, isLoading: isLoadingThreadgate} =
useThreadgateViewQuery({postUri: props.rootPostUri})
const {data: postgate, isLoading: isLoadingPostgate} = usePostgateQuery({
postUri: props.postUri,
})
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation()
const [editedPostgate, setEditedPostgate] =
useState()
const [editedAllowUISettings, setEditedAllowUISettings] =
useState()
const isLoading = isLoadingThreadgate || isLoadingPostgate
const threadgateView = threadgateViewLoaded || props.initialThreadgateView
const isThreadgateOwnedByViewer = useMemo(() => {
return currentAccount?.did === new AtUri(props.rootPostUri).host
}, [props.rootPostUri, currentAccount?.did])
const postgateValue = useMemo(() => {
return (
editedPostgate || postgate || createPostgateRecord({post: props.postUri})
)
}, [postgate, editedPostgate, props.postUri])
const allowUIValue = useMemo(() => {
return (
editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView)
)
}, [threadgateView, editedAllowUISettings])
const onSave = useCallback(async () => {
if (!editedPostgate && !editedAllowUISettings) {
props.control.close()
return
}
setIsSaving(true)
try {
const requests = []
if (editedPostgate) {
requests.push(
writePostgateRecord({
postUri: props.postUri,
postgate: editedPostgate,
}),
)
}
if (editedAllowUISettings && isThreadgateOwnedByViewer) {
requests.push(
setThreadgateAllow({
postUri: props.rootPostUri,
allow: editedAllowUISettings,
}),
)
}
await Promise.all(requests)
props.control.close()
} catch (e: any) {
ax.logger.error(`Failed to save post interaction settings`, {
source: 'PostInteractionSettingsDialogControlledInner',
safeMessage: e.message,
})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
'xmark',
)
} finally {
setIsSaving(false)
}
}, [
_,
ax,
props.postUri,
props.rootPostUri,
props.control,
editedPostgate,
editedAllowUISettings,
setIsSaving,
writePostgateRecord,
setThreadgateAllow,
isThreadgateOwnedByViewer,
])
return (
{isLoading ? (
Loading post interaction settings...
) : (
<>
>
)}
)
}
export function PostInteractionSettingsForm({
canSave = true,
onSave,
isSaving,
postgate,
onChangePostgate,
threadgateAllowUISettings,
onChangeThreadgateAllowUISettings,
replySettingsDisabled,
isDirty,
persist,
onChangePersist,
}: PostInteractionSettingsFormProps) {
const t = useTheme()
const {_} = useLingui()
const playHaptic = useHaptics()
const [showLists, setShowLists] = useState(false)
const {
data: lists,
isPending: isListsPending,
isError: isListsError,
} = useMyListsQuery('curate')
const [quotesEnabled, setQuotesEnabled] = useState(
!(
postgate.embeddingRules &&
postgate.embeddingRules.find(
v => v.$type === embeddingRules.disableRule.$type,
)
),
)
const onChangeQuotesEnabled = useCallback(
(enabled: boolean) => {
setQuotesEnabled(enabled)
onChangePostgate(
createPostgateRecord({
...postgate,
embeddingRules: enabled ? [] : [embeddingRules.disableRule],
}),
)
},
[setQuotesEnabled, postgate, onChangePostgate],
)
const noOneCanReply = !!threadgateAllowUISettings.find(
v => v.type === 'nobody',
)
const everyoneCanReply = !!threadgateAllowUISettings.find(
v => v.type === 'everybody',
)
const numberOfListsSelected = threadgateAllowUISettings.filter(
v => v.type === 'list',
).length
const toggleGroupValues = useMemo(() => {
const values: string[] = []
for (const setting of threadgateAllowUISettings) {
switch (setting.type) {
case 'everybody':
case 'nobody':
// no granularity, early return with nothing
return []
case 'followers':
values.push('followers')
break
case 'following':
values.push('following')
break
case 'mention':
values.push('mention')
break
case 'list':
values.push(`list:${setting.list}`)
break
default:
break
}
}
return values
}, [threadgateAllowUISettings])
const toggleGroupOnChange = (values: string[]) => {
const settings: ThreadgateAllowUISetting[] = []
if (values.length === 0) {
settings.push({type: 'everybody'})
} else {
for (const value of values) {
if (value.startsWith('list:')) {
const listId = value.slice('list:'.length)
settings.push({type: 'list', list: listId})
} else {
settings.push({type: value as 'followers' | 'following' | 'mention'})
}
}
}
onChangeThreadgateAllowUISettings(settings)
}
return (
{replySettingsDisabled && (
Reply settings are chosen by the author of the thread
)}
Who can reply
{
if (val.includes('everyone')) {
onChangeThreadgateAllowUISettings([{type: 'everybody'}])
} else if (val.includes('nobody')) {
onChangeThreadgateAllowUISettings([{type: 'nobody'}])
} else {
onChangeThreadgateAllowUISettings([{type: 'mention'}])
}
}}>
{({selected}) => (
Anyone
)}
{({selected}) => (
Nobody
)}
{({selected}) => (
Your followers
)}
{({selected}) => (
People you follow
)}
{({selected}) => (
People you mention
)}
{showLists &&
(isListsPending ? (
Loading lists...
) : isListsError ? (
An error occurred while loading your lists :/
) : lists.length === 0 ? (
You don't have any lists yet.
) : (
lists.map((list, i) => (
{({selected}) => (
{list.name}
)}
))
))}
{({selected}) => (
Allow quote posts
)}
{typeof persist !== 'undefined' && (
{isDirty ? (
onChangePersist?.(!persist)}>
Save these options for next time
) : (
These are your default settings
)}
)}
)
}
function Header() {
return (
Post interaction settings
)
}
export function usePrefetchPostInteractionSettings({
postUri,
rootPostUri,
}: {
postUri: string
rootPostUri: string
}) {
const ax = useAnalytics()
const queryClient = useQueryClient()
const agent = useAgent()
const getPost = useGetPost()
return useCallback(async () => {
try {
await Promise.all([
queryClient.prefetchQuery({
queryKey: createPostgateQueryKey(postUri),
queryFn: () =>
getPostgateRecord({agent, postUri}).then(res => res ?? null),
staleTime: STALE.SECONDS.THIRTY,
}),
queryClient.prefetchQuery({
queryKey: createThreadgateViewQueryKey(rootPostUri),
queryFn: async () => {
const post = await getPost({uri: rootPostUri})
return post.threadgate ?? null
},
staleTime: STALE.SECONDS.THIRTY,
}),
])
} catch (e: any) {
ax.logger.error(`Failed to prefetch post interaction settings`, {
safeMessage: e.message,
})
}
}, [ax, queryClient, agent, postUri, rootPostUri, getPost])
}