Bluesky app fork with some witchin' additions 馃挮
at main 190 lines 6.2 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 '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}