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]) }