Bluesky app fork with some witchin' additions 馃挮
at main 192 lines 6.3 kB view raw
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}