forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useEffect, useMemo, useState} from 'react'
2import {Keyboard, type StyleProp, type ViewStyle} from 'react-native'
3import {type AnimatedStyle} from 'react-native-reanimated'
4import {type AppBskyFeedPostgate} from '@atproto/api'
5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7import deepEqual from 'fast-deep-equal'
8
9import {isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
12import {createPostgateRecord} from '#/state/queries/postgate/util'
13import {usePreferencesQuery} from '#/state/queries/preferences'
14import {
15 type ThreadgateAllowUISetting,
16 threadgateAllowUISettingToAllowRecordValue,
17 threadgateRecordToAllowUISetting,
18} from '#/state/queries/threadgate'
19import {Button, ButtonIcon, ButtonText} from '#/components/Button'
20import * as Dialog from '#/components/Dialog'
21import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
22import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
23import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
24import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
25import * as Tooltip from '#/components/Tooltip'
26import {Text} from '#/components/Typography'
27import {useAnalytics} from '#/analytics'
28import {IS_NATIVE} from '#/env'
29import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
30
31export function ThreadgateBtn({
32 postgate,
33 onChangePostgate,
34 threadgateAllowUISettings,
35 onChangeThreadgateAllowUISettings,
36}: {
37 postgate: AppBskyFeedPostgate.Record
38 onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
39
40 threadgateAllowUISettings: ThreadgateAllowUISetting[]
41 onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
42
43 style?: StyleProp<AnimatedStyle<ViewStyle>>
44}) {
45 const {_} = useLingui()
46 const ax = useAnalytics()
47 const control = Dialog.useDialogControl()
48 const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged()
49 const [showTooltip, setShowTooltip] = useState(false)
50 const [tooltipWasShown] = useState(!threadgateNudged)
51
52 useEffect(() => {
53 if (!threadgateNudged) {
54 const timeout = setTimeout(() => {
55 setShowTooltip(true)
56 }, 1000)
57 return () => clearTimeout(timeout)
58 }
59 }, [threadgateNudged])
60
61 const onDismissTooltip = (visible: boolean) => {
62 if (visible) return
63 setThreadgateNudged(true)
64 setShowTooltip(false)
65 }
66
67 const {data: preferences} = usePreferencesQuery()
68 const [persist, setPersist] = useState(false)
69
70 const onPress = () => {
71 ax.metric('composer:threadgate:open', {
72 nudged: tooltipWasShown,
73 })
74
75 if (IS_NATIVE && Keyboard.isVisible()) {
76 Keyboard.dismiss()
77 }
78
79 setShowTooltip(false)
80 setThreadgateNudged(true)
81
82 control.open()
83 }
84
85 const prefThreadgateAllowUISettings = threadgateRecordToAllowUISetting({
86 $type: 'app.bsky.feed.threadgate',
87 post: '',
88 createdAt: new Date().toISOString(),
89 allow: preferences?.postInteractionSettings.threadgateAllowRules,
90 })
91 const prefPostgate = createPostgateRecord({
92 post: '',
93 embeddingRules:
94 preferences?.postInteractionSettings?.postgateEmbeddingRules || [],
95 })
96
97 const isDirty = useMemo(() => {
98 const everybody = [{type: 'everybody'}]
99 return (
100 !deepEqual(
101 threadgateAllowUISettings,
102 prefThreadgateAllowUISettings ?? everybody,
103 ) ||
104 !deepEqual(postgate.embeddingRules, prefPostgate?.embeddingRules ?? [])
105 )
106 }, [
107 prefThreadgateAllowUISettings,
108 prefPostgate,
109 threadgateAllowUISettings,
110 postgate,
111 ])
112
113 const {mutate: persistChanges, isPending: isSaving} =
114 usePostInteractionSettingsMutation({
115 onError: err => {
116 if (!isNetworkError(err)) {
117 logger.error('Failed to persist threadgate settings', {
118 safeMessage: err,
119 })
120 }
121 },
122 onSettled: () => {
123 control.close(() => {
124 setPersist(false)
125 })
126 },
127 })
128
129 const anyoneCanReply =
130 threadgateAllowUISettings.length === 1 &&
131 threadgateAllowUISettings[0].type === 'everybody'
132 const anyoneCanQuote =
133 !postgate.embeddingRules || postgate.embeddingRules.length === 0
134 const anyoneCanInteract = anyoneCanReply && anyoneCanQuote
135 const label = anyoneCanInteract
136 ? _(msg`Anyone can interact`)
137 : _(msg`Interaction limited`)
138
139 return (
140 <>
141 <Tooltip.Outer
142 visible={showTooltip}
143 onVisibleChange={onDismissTooltip}
144 position="top">
145 <Tooltip.Target>
146 <Button
147 color={showTooltip ? 'primary_subtle' : 'secondary'}
148 size="small"
149 testID="openReplyGateButton"
150 onPress={onPress}
151 label={label}
152 accessibilityHint={_(
153 msg`Opens a dialog to choose who can interact with this post`,
154 )}>
155 <ButtonIcon icon={anyoneCanInteract ? EarthIcon : GroupIcon} />
156 <ButtonText numberOfLines={1}>{label}</ButtonText>
157 <ButtonIcon icon={TinyChevronIcon} size="2xs" />
158 </Button>
159 </Tooltip.Target>
160 <Tooltip.TextBubble>
161 <Text>
162 <Trans>Psst! You can edit who can interact with this post.</Trans>
163 </Text>
164 </Tooltip.TextBubble>
165 </Tooltip.Outer>
166
167 <PostInteractionSettingsControlledDialog
168 control={control}
169 onSave={() => {
170 if (persist) {
171 persistChanges({
172 threadgateAllowRules: threadgateAllowUISettingToAllowRecordValue(
173 threadgateAllowUISettings,
174 ),
175 postgateEmbeddingRules: postgate.embeddingRules ?? [],
176 })
177 } else {
178 control.close()
179 }
180 }}
181 isSaving={isSaving}
182 postgate={postgate}
183 onChangePostgate={onChangePostgate}
184 threadgateAllowUISettings={threadgateAllowUISettings}
185 onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
186 isDirty={isDirty}
187 persist={persist}
188 onChangePersist={setPersist}
189 />
190 </>
191 )
192}