[Threadgate] Tweak threadgate buttons (#9173)

* tweak in-post threadgate button

* tweak composer threadgate button

* reduce date length slightly

* pressed styles

* make date length depend on breakpoint

* add chevron to label btn

* add tiny chevron, special-case button icon width

* [Threadgate] Add hint (#9350)

* get tooltip working on web

* add compatibility layer for working in iOS sheets

* add timeout to profile tooltip now that it appears instantly

* rm debug code

* Update ThreadgateBtn.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* remeasure when keyboard changes

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* fix types

* [Threadgate] Refresh dialog (#9342)

* wip new ui

* update threadgate dialog with new designs

* restore nobody option

* relayout android sheet when ratio changes

* fix ratio changing case in bottom sheet

* timebox reached, use setTimeout

* update panel styles

* missing imports

* extract out Panel

* tweak layout animation

* fix icon color

* use same color mechamism for icon as text

* restore the header

* refreshed toggle styles (#9343)

* [Threadgate] Persist settings (#9341)

* add persist toggle to threadgate dialog

* move state back down

* sort out spacing

* wire up query

* @surfdude29 tweaks

* use tiny chevron in WhoCanReply

* wait for prefetch before opening

* move Panel into the Toggle namespace

* default -> pref

* use medium date length

* rm hover state from web selects, fix border radius

* fix key issue in Selects

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
Samuel Newman
2025-11-14 16:21:37 +02:00
committed by GitHub
parent b56ee74cdb
commit b422765aed
27 changed files with 1057 additions and 468 deletions
@@ -1,17 +1,31 @@
import {useEffect, useMemo, useState} from 'react'
import {Keyboard, type StyleProp, type ViewStyle} from 'react-native'
import {type AnimatedStyle} from 'react-native-reanimated'
import {type AppBskyFeedPostgate} from '@atproto/api'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import deepEqual from 'lodash.isequal'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate'
import {native} from '#/alf'
import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
import {createPostgateRecord} from '#/state/queries/postgate/util'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {
type ThreadgateAllowUISetting,
threadgateAllowUISettingToAllowRecordValue,
threadgateRecordToAllowUISetting,
} from '#/state/queries/threadgate'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
import * as Tooltip from '#/components/Tooltip'
import {Text} from '#/components/Typography'
import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
export function ThreadgateBtn({
postgate,
@@ -29,15 +43,82 @@ export function ThreadgateBtn({
}) {
const {_} = useLingui()
const control = Dialog.useDialogControl()
const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged()
const [showTooltip, setShowTooltip] = useState(false)
useEffect(() => {
if (!threadgateNudged) {
const timeout = setTimeout(() => {
setShowTooltip(true)
}, 1000)
return () => clearTimeout(timeout)
}
}, [threadgateNudged])
const onDismissTooltip = (visible: boolean) => {
if (visible) return
setThreadgateNudged(true)
setShowTooltip(false)
}
const {data: preferences} = usePreferencesQuery()
const [persist, setPersist] = useState(false)
const onPress = () => {
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
setShowTooltip(false)
setThreadgateNudged(true)
control.open()
}
const prefThreadgateAllowUISettings = threadgateRecordToAllowUISetting({
$type: 'app.bsky.feed.threadgate',
post: '',
createdAt: new Date().toISOString(),
allow: preferences?.postInteractionSettings.threadgateAllowRules,
})
const prefPostgate = createPostgateRecord({
post: '',
embeddingRules:
preferences?.postInteractionSettings?.postgateEmbeddingRules || [],
})
const isDirty = useMemo(() => {
const everybody = [{type: 'everybody'}]
return (
!deepEqual(
threadgateAllowUISettings,
prefThreadgateAllowUISettings ?? everybody,
) ||
!deepEqual(postgate.embeddingRules, prefPostgate?.embeddingRules ?? [])
)
}, [
prefThreadgateAllowUISettings,
prefPostgate,
threadgateAllowUISettings,
postgate,
])
const {mutate: persistChanges, isPending: isSaving} =
usePostInteractionSettingsMutation({
onError: err => {
if (!isNetworkError(err)) {
logger.error('Failed to persist threadgate settings', {
safeMessage: err,
})
}
},
onSettled: () => {
control.close(() => {
setPersist(false)
})
},
})
const anyoneCanReply =
threadgateAllowUISettings.length === 1 &&
threadgateAllowUISettings[0].type === 'everybody'
@@ -50,34 +131,54 @@ export function ThreadgateBtn({
return (
<>
<Button
variant="solid"
color="secondary"
size="small"
testID="openReplyGateButton"
onPress={onPress}
label={label}
accessibilityHint={_(
msg`Opens a dialog to choose who can reply to this thread`,
)}
style={[
native({
paddingHorizontal: 8,
paddingVertical: 6,
}),
]}>
<ButtonIcon icon={anyoneCanInteract ? Earth : Group} />
<ButtonText numberOfLines={1}>{label}</ButtonText>
</Button>
<Tooltip.Outer
visible={showTooltip}
onVisibleChange={onDismissTooltip}
position="top">
<Tooltip.Target>
<Button
color={showTooltip ? 'primary_subtle' : 'secondary'}
size="small"
testID="openReplyGateButton"
onPress={onPress}
label={label}
accessibilityHint={_(
msg`Opens a dialog to choose who can interact with this post`,
)}>
<ButtonIcon icon={anyoneCanInteract ? EarthIcon : GroupIcon} />
<ButtonText numberOfLines={1}>{label}</ButtonText>
<ButtonIcon icon={TinyChevronIcon} size="2xs" />
</Button>
</Tooltip.Target>
<Tooltip.TextBubble>
<Text>
<Trans>Psst! You can edit who can interact with this post.</Trans>
</Text>
</Tooltip.TextBubble>
</Tooltip.Outer>
<PostInteractionSettingsControlledDialog
control={control}
onSave={() => {
control.close()
if (persist) {
persistChanges({
threadgateAllowRules: threadgateAllowUISettingToAllowRecordValue(
threadgateAllowUISettings,
),
postgateEmbeddingRules: postgate.embeddingRules ?? [],
})
} else {
control.close()
}
}}
isSaving={isSaving}
postgate={postgate}
onChangePostgate={onChangePostgate}
threadgateAllowUISettings={threadgateAllowUISettings}
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
isDirty={isDirty}
persist={persist}
onChangePersist={setPersist}
/>
</>
)