import React from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import {AppBskyFeedDefs, AppBskyFeedPostgate, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import isEqual from 'lodash.isequal' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {useMyListsQuery} from '#/state/queries/my-lists' import { createPostgateQueryKey, getPostgateRecord, usePostgateQuery, useWritePostgateMutation, } from '#/state/queries/postgate' import { createPostgateRecord, embeddingRules, } from '#/state/queries/postgate/util' import { createThreadgateViewQueryKey, getThreadgateView, ThreadgateAllowUISetting, threadgateViewToAllowUISetting, useSetThreadgateAllowMutation, useThreadgateViewQuery, } from '#/state/queries/threadgate' import {useAgent, useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' import * as Toggle from '#/components/forms/Toggle' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' export type PostInteractionSettingsFormProps = { onSave: () => void isSaving?: boolean postgate: AppBskyFeedPostgate.Record onChangePostgate: (v: AppBskyFeedPostgate.Record) => void threadgateAllowUISettings: ThreadgateAllowUISetting[] onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void replySettingsDisabled?: boolean } export function PostInteractionSettingsControlledDialog({ control, ...rest }: PostInteractionSettingsFormProps & { control: Dialog.DialogControlProps }) { 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 } export function PostInteractionSettingsDialog( props: PostInteractionSettingsDialogProps, ) { return ( ) } export function PostInteractionSettingsDialogControlledInner( props: PostInteractionSettingsDialogProps, ) { const {_} = useLingui() const {currentAccount} = useSession() const [isSaving, setIsSaving] = React.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] = React.useState() const [editedAllowUISettings, setEditedAllowUISettings] = React.useState() const isLoading = isLoadingThreadgate || isLoadingPostgate const threadgateView = threadgateViewLoaded || props.initialThreadgateView const isThreadgateOwnedByViewer = React.useMemo(() => { return currentAccount?.did === new AtUri(props.rootPostUri).host }, [props.rootPostUri, currentAccount?.did]) const postgateValue = React.useMemo(() => { return ( editedPostgate || postgate || createPostgateRecord({post: props.postUri}) ) }, [postgate, editedPostgate, props.postUri]) const allowUIValue = React.useMemo(() => { return ( editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView) ) }, [threadgateView, editedAllowUISettings]) const onSave = React.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) { logger.error(`Failed to save post interaction settings`, { context: 'PostInteractionSettingsDialogControlledInner', safeMessage: e.message, }) Toast.show( _( msg`There was an issue. Please check your internet connection and try again.`, ), 'xmark', ) } finally { setIsSaving(false) } }, [ _, props.postUri, props.rootPostUri, props.control, editedPostgate, editedAllowUISettings, setIsSaving, writePostgateRecord, setThreadgateAllow, isThreadgateOwnedByViewer, ]) return ( {isLoading ? ( ) : ( )} ) } export function PostInteractionSettingsForm({ onSave, isSaving, postgate, onChangePostgate, threadgateAllowUISettings, onChangeThreadgateAllowUISettings, replySettingsDisabled, }: PostInteractionSettingsFormProps) { const t = useTheme() const {_} = useLingui() const control = Dialog.useDialogContext() const {data: lists} = useMyListsQuery('curate') const [quotesEnabled, setQuotesEnabled] = React.useState( !( postgate.embeddingRules && postgate.embeddingRules.find( v => v.$type === embeddingRules.disableRule.$type, ) ), ) const onPressAudience = (setting: ThreadgateAllowUISetting) => { // remove boolean values let newSelected: ThreadgateAllowUISetting[] = threadgateAllowUISettings.filter( v => v.type !== 'nobody' && v.type !== 'everybody', ) // toggle const i = newSelected.findIndex(v => isEqual(v, setting)) if (i === -1) { newSelected.push(setting) } else { newSelected.splice(i, 1) } onChangeThreadgateAllowUISettings(newSelected) } const onChangeQuotesEnabled = React.useCallback( (enabled: boolean) => { setQuotesEnabled(enabled) onChangePostgate( createPostgateRecord({ ...postgate, embeddingRules: enabled ? [] : [embeddingRules.disableRule], }), ) }, [setQuotesEnabled, postgate, onChangePostgate], ) const noOneCanReply = !!threadgateAllowUISettings.find( v => v.type === 'nobody', ) return ( Post interaction settings Customize who can interact with this post. Quote settings {quotesEnabled ? ( Quote posts enabled ) : ( Quote posts disabled )} {replySettingsDisabled && ( Reply settings are chosen by the author of the thread )} Reply settings Allow replies from: v.type === 'everybody') } onPress={() => onChangeThreadgateAllowUISettings([{type: 'everybody'}]) } style={{flex: 1}} disabled={replySettingsDisabled} /> onChangeThreadgateAllowUISettings([{type: 'nobody'}]) } style={{flex: 1}} disabled={replySettingsDisabled} /> {!noOneCanReply && ( <> Or combine these options: v.type === 'mention', ) } onPress={() => onPressAudience({type: 'mention'})} disabled={replySettingsDisabled} /> v.type === 'following', ) } onPress={() => onPressAudience({type: 'following'})} disabled={replySettingsDisabled} /> {lists && lists.length > 0 ? lists.map(list => ( v.type === 'list' && v.list === list.uri, ) } onPress={() => onPressAudience({type: 'list', list: list.uri}) } disabled={replySettingsDisabled} /> )) : // No loading states to avoid jumps for the common case (no lists) null} )} ) } function Selectable({ label, isSelected, onPress, style, disabled, }: { label: string isSelected: boolean onPress: () => void style?: StyleProp disabled?: boolean }) { const t = useTheme() return ( ) } export function usePrefetchPostInteractionSettings({ postUri, rootPostUri, }: { postUri: string rootPostUri: string }) { const queryClient = useQueryClient() const agent = useAgent() return React.useCallback(async () => { try { await Promise.all([ queryClient.prefetchQuery({ queryKey: createPostgateQueryKey(postUri), queryFn: () => getPostgateRecord({agent, postUri}), staleTime: STALE.SECONDS.THIRTY, }), queryClient.prefetchQuery({ queryKey: createThreadgateViewQueryKey(rootPostUri), queryFn: () => getThreadgateView({agent, postUri: rootPostUri}), staleTime: STALE.SECONDS.THIRTY, }), ]) } catch (e: any) { logger.error(`Failed to prefetch post interaction settings`, { safeMessage: e.message, }) } }, [queryClient, agent, postUri, rootPostUri]) }