diff options
Diffstat (limited to 'src/components/WhoCanReply.tsx')
-rw-r--r-- | src/components/WhoCanReply.tsx | 293 |
1 files changed, 127 insertions, 166 deletions
diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index 1ffb4da39..ab6ef8293 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -1,39 +1,34 @@ import React from 'react' -import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' +import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native' import { AppBskyFeedDefs, - AppBskyFeedGetPostThread, + AppBskyFeedPost, AppBskyGraphDefs, AtUri, - BskyAgent, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' -import {createThreadgate} from '#/lib/api' -import {until} from '#/lib/async/until' import {HITSLOP_10} from '#/lib/constants' import {makeListLink, makeProfileLink} from '#/lib/routes/links' -import {logger} from '#/logger' import {isNative} from '#/platform/detection' -import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread' import { - ThreadgateSetting, - threadgateViewToSettings, + ThreadgateAllowUISetting, + threadgateViewToAllowUISetting, } from '#/state/queries/threadgate' -import {useAgent} from '#/state/session' -import * as Toast from 'view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog' +import { + PostInteractionSettingsDialog, + usePrefetchPostInteractionSettings, +} from '#/components/dialogs/PostInteractionSettingsDialog' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' +import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' -import {TextLink} from '../view/com/util/Link' -import {ThreadgateEditorDialog} from './dialogs/ThreadgateEditor' import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil' interface WhoCanReplyProps { @@ -47,31 +42,34 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { const t = useTheme() const infoDialogControl = useDialogControl() const editDialogControl = useDialogControl() - const agent = useAgent() - const queryClient = useQueryClient() - const settings = React.useMemo( - () => threadgateViewToSettings(post.threadgate), - [post], - ) - const isRootPost = !('reply' in post.record) + /* + * `WhoCanReply` is only used for root posts atm, in case this changes + * unexpectedly, we should check to make sure it's for sure the root URI. + */ + const rootUri = + AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root + ? post.record.reply.root.uri + : post.uri + const settings = React.useMemo(() => { + return threadgateViewToAllowUISetting(post.threadgate) + }, [post.threadgate]) - if (!isRootPost) { - return null - } - if (!settings.length && !isThreadAuthor) { - return null - } + const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ + postUri: post.uri, + rootPostUri: rootUri, + }) - const isEverybody = settings.length === 0 - const isNobody = !!settings.find(gate => gate.type === 'nobody') - const description = isEverybody + const anyoneCanReply = + settings.length === 1 && settings[0].type === 'everybody' + const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody' + const description = anyoneCanReply ? _(msg`Everybody can reply`) - : isNobody + : noOneCanReply ? _(msg`Replies disabled`) : _(msg`Some people can reply`) - const onPressEdit = () => { + const onPressOpen = () => { if (isNative && Keyboard.isVisible()) { Keyboard.dismiss() } @@ -82,52 +80,23 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { } } - const onEditConfirm = async (newSettings: ThreadgateSetting[]) => { - if (JSON.stringify(settings) === JSON.stringify(newSettings)) { - return - } - try { - if (newSettings.length) { - await createThreadgate(agent, post.uri, newSettings) - } else { - await agent.api.com.atproto.repo.deleteRecord({ - repo: agent.session!.did, - collection: 'app.bsky.feed.threadgate', - rkey: new AtUri(post.uri).rkey, - }) - } - await whenAppViewReady(agent, post.uri, res => { - const thread = res.data.thread - if (AppBskyFeedDefs.isThreadViewPost(thread)) { - const fetchedSettings = threadgateViewToSettings( - thread.post.threadgate, - ) - return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) - } - return false - }) - Toast.show(_(msg`Thread settings updated`)) - queryClient.invalidateQueries({ - queryKey: [POST_THREAD_RQKEY_ROOT], - }) - } catch (err) { - Toast.show( - _( - msg`There was an issue. Please check your internet connection and try again.`, - ), - 'xmark', - ) - logger.error('Failed to edit threadgate', {message: err}) - } - } - return ( <> <Button label={ isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`) } - onPress={isThreadAuthor ? onPressEdit : infoDialogControl.open} + onPress={onPressOpen} + {...(isThreadAuthor + ? Platform.select({ + web: { + onHoverIn: prefetchPostInteractionSettings, + }, + native: { + onPressIn: prefetchPostInteractionSettings, + }, + }) + : {})} hitSlop={HITSLOP_10}> {({hovered}) => ( <View style={[a.flex_row, a.align_center, a.gap_xs, style]}> @@ -145,22 +114,27 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { ]}> {description} </Text> + {isThreadAuthor && ( <PencilLine width={12} fill={t.palette.primary_500} /> )} </View> )} </Button> - <WhoCanReplyDialog - control={infoDialogControl} - post={post} - settings={settings} - /> - {isThreadAuthor && ( - <ThreadgateEditorDialog + + {isThreadAuthor ? ( + <PostInteractionSettingsDialog + postUri={post.uri} + rootPostUri={rootUri} control={editDialogControl} - threadgate={settings} - onConfirm={onEditConfirm} + initialThreadgateView={post.threadgate} + /> + ) : ( + <WhoCanReplyDialog + control={infoDialogControl} + post={post} + settings={settings} + embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} /> )} </> @@ -174,7 +148,7 @@ function Icon({ }: { color: string width?: number - settings: ThreadgateSetting[] + settings: ThreadgateAllowUISetting[] }) { const isEverybody = settings.length === 0 const isNobody = !!settings.find(gate => gate.type === 'nobody') @@ -186,79 +160,84 @@ function WhoCanReplyDialog({ control, post, settings, + embeddingDisabled, }: { control: Dialog.DialogControlProps post: AppBskyFeedDefs.PostView - settings: ThreadgateSetting[] + settings: ThreadgateAllowUISetting[] + embeddingDisabled: boolean }) { + const {_} = useLingui() return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <WhoCanReplyDialogInner post={post} settings={settings} /> + <Dialog.ScrollableInner + label={_(msg`Dialog: adjust who can interact with this post`)} + style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}> + <View style={[a.gap_sm]}> + <Text style={[a.font_bold, a.text_xl, a.pb_sm]}> + <Trans>Who can interact with this post?</Trans> + </Text> + <Rules + post={post} + settings={settings} + embeddingDisabled={embeddingDisabled} + /> + </View> + </Dialog.ScrollableInner> </Dialog.Outer> ) } -function WhoCanReplyDialogInner({ - post, - settings, -}: { - post: AppBskyFeedDefs.PostView - settings: ThreadgateSetting[] -}) { - const {_} = useLingui() - return ( - <Dialog.ScrollableInner - label={_(msg`Who can reply dialog`)} - style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}> - <View style={[a.gap_sm]}> - <Text style={[a.font_bold, a.text_xl]}> - <Trans>Who can reply?</Trans> - </Text> - <Rules post={post} settings={settings} /> - </View> - </Dialog.ScrollableInner> - ) -} - function Rules({ post, settings, + embeddingDisabled, }: { post: AppBskyFeedDefs.PostView - settings: ThreadgateSetting[] + settings: ThreadgateAllowUISetting[] + embeddingDisabled: boolean }) { const t = useTheme() + return ( - <Text - style={[ - a.text_md, - a.leading_tight, - a.flex_wrap, - t.atoms.text_contrast_medium, - ]}> - {!settings.length ? ( - <Trans>Everybody can reply</Trans> - ) : settings[0].type === 'nobody' ? ( - <Trans>Replies to this thread are disabled</Trans> - ) : ( - <Trans> - Only{' '} - {settings.map((rule, i) => ( - <> - <Rule - key={`rule-${i}`} - rule={rule} - post={post} - lists={post.threadgate!.lists} - /> - <Separator key={`sep-${i}`} i={i} length={settings.length} /> - </> - ))}{' '} - can reply - </Trans> + <> + <Text + style={[ + a.text_sm, + a.leading_snug, + a.flex_wrap, + t.atoms.text_contrast_medium, + ]}> + {settings[0].type === 'everybody' ? ( + <Trans>Everybody can reply to this post.</Trans> + ) : settings[0].type === 'nobody' ? ( + <Trans>Replies to this post are disabled.</Trans> + ) : ( + <Trans> + Only{' '} + {settings.map((rule, i) => ( + <React.Fragment key={`rule-${i}`}> + <Rule rule={rule} post={post} lists={post.threadgate!.lists} /> + <Separator i={i} length={settings.length} /> + </React.Fragment> + ))}{' '} + can reply. + </Trans> + )}{' '} + </Text> + {embeddingDisabled && ( + <Text + style={[ + a.text_sm, + a.leading_snug, + a.flex_wrap, + t.atoms.text_contrast_medium, + ]}> + <Trans>No one but the author can quote this post.</Trans> + </Text> )} - </Text> + </> ) } @@ -267,11 +246,10 @@ function Rule({ post, lists, }: { - rule: ThreadgateSetting + rule: ThreadgateAllowUISetting post: AppBskyFeedDefs.PostView lists: AppBskyGraphDefs.ListViewBasic[] | undefined }) { - const t = useTheme() if (rule.type === 'mention') { return <Trans>mentioned users</Trans> } @@ -279,12 +257,12 @@ function Rule({ return ( <Trans> users followed by{' '} - <TextLink - type="sm" - href={makeProfileLink(post.author)} - text={`@${post.author.handle}`} - style={{color: t.palette.primary_500}} - /> + <InlineLinkText + label={`@${post.author.handle}`} + to={makeProfileLink(post.author)} + style={[a.text_sm, a.leading_snug]}> + @{post.author.handle} + </InlineLinkText> </Trans> ) } @@ -294,12 +272,12 @@ function Rule({ const listUrip = new AtUri(list.uri) return ( <Trans> - <TextLink - type="sm" - href={makeListLink(listUrip.hostname, listUrip.rkey)} - text={list.name} - style={{color: t.palette.primary_500}} - />{' '} + <InlineLinkText + label={list.name} + to={makeListLink(listUrip.hostname, listUrip.rkey)} + style={[a.text_sm, a.leading_snug]}> + {list.name} + </InlineLinkText>{' '} members </Trans> ) @@ -320,20 +298,3 @@ function Separator({i, length}: {i: number; length: number}) { } return <>, </> } - -async function whenAppViewReady( - agent: BskyAgent, - uri: string, - fn: (res: AppBskyFeedGetPostThread.Response) => boolean, -) { - await until( - 5, // 5 tries - 1e3, // 1s delay between tries - fn, - () => - agent.app.bsky.feed.getPostThread({ - uri, - depth: 0, - }), - ) -} |