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