Watermark posts in screenshots (#9637)

* watermark posts in screenshots

* disable when app is backgrounded

* fix alignment

* show watermark even when there isn't a button
This commit is contained in:
Samuel Newman
2026-01-15 05:03:13 +02:00
committed by GitHub
parent 65faee4f08
commit ab85b509c0
5 changed files with 89 additions and 9 deletions
@@ -0,0 +1,65 @@
import {useState} from 'react'
import {View} from 'react-native'
import {PrivacySensitive} from 'expo-privacy-sensitive'
import {useAppState} from '#/lib/hooks/useAppState'
import {isIOS} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {sizes as iconSizes} from '#/components/icons/common'
import {Mark as Logo} from '#/components/icons/Logo'
const ICON_SIZE = 'xl' as const
export function GrowthHack({
children,
align = 'right',
}: {
children: React.ReactNode
align?: 'left' | 'right'
}) {
const t = useTheme()
// the button has a variable width and is absolutely positioned, so we need to manually
// set the minimum width of the underlying button
const [width, setWidth] = useState<number | undefined>(undefined)
const appState = useAppState()
if (!isIOS || appState !== 'active') return children
return (
<View
style={[
a.relative,
a.justify_center,
align === 'right' ? a.align_end : a.align_start,
width === undefined ? {opacity: 0} : {minWidth: width},
]}>
<PrivacySensitive
style={[
a.absolute,
a.z_10,
a.flex_col,
align === 'right'
? [a.right_0, a.align_end]
: [a.left_0, a.align_start],
// when finding the size of the button, we need the containing
// element to have a concrete size otherwise the text will
// collapse to 0 width. so set it to a really big number
// and hide the entire thing (see above)
width === undefined && {width: 10000},
]}>
<View
onLayout={evt => setWidth(evt.nativeEvent.layout.width)}
style={[
t.atoms.bg,
// make sure it covers the icon! the won't always be a button
{minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]},
]}>
{children}
</View>
</PrivacySensitive>
<Logo size={ICON_SIZE} />
</View>
)
}
@@ -381,7 +381,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</View>
</Link>
{showFollowButton && (
<View collapsable={false}>
<View collapsable={false} style={[a.self_center]}>
<ThreadItemAnchorFollowButton did={post.author.did} />
</View>
)}
@@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
@@ -14,10 +15,23 @@ import {useRequireAuth} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {GrowthHack} from './GrowthHack'
export function ThreadItemAnchorFollowButton({did}: {did: string}) {
if (isIOS) {
return (
<GrowthHack>
<ThreadItemAnchorFollowButtonInner did={did} />
</GrowthHack>
)
}
return <ThreadItemAnchorFollowButtonInner did={did} />
}
export function ThreadItemAnchorFollowButtonInner({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
// We will never hit this - the profile will always be cached or loaded above
@@ -113,15 +127,10 @@ function PostThreadFollowBtnLoaded({
label={_(msg`Follow ${profile.handle}`)}
onPress={onPress}
size="small"
variant="solid"
color={isFollowing ? 'secondary' : 'secondary_inverted'}
style={[a.rounded_full]}>
{gtMobile && (
<ButtonIcon
icon={isFollowing ? Check : Plus}
position="left"
size="sm"
/>
<ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" />
)}
<ButtonText>
{!isFollowing ? (