Bluesky app fork with some witchin' additions 💫

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

authored by samuel.fm and committed by

GitHub ab85b509 65faee4f

+89 -9
+1
package.json
··· 156 156 "expo-location": "~19.0.8", 157 157 "expo-media-library": "~18.2.1", 158 158 "expo-notifications": "~0.32.14", 159 + "expo-privacy-sensitive": "^0.1.0", 159 160 "expo-screen-orientation": "~9.0.8", 160 161 "expo-sharing": "~14.0.8", 161 162 "expo-sms": "^14.0.7",
+65
src/screens/PostThread/components/GrowthHack.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {PrivacySensitive} from 'expo-privacy-sensitive' 4 + 5 + import {useAppState} from '#/lib/hooks/useAppState' 6 + import {isIOS} from '#/platform/detection' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {sizes as iconSizes} from '#/components/icons/common' 9 + import {Mark as Logo} from '#/components/icons/Logo' 10 + 11 + const ICON_SIZE = 'xl' as const 12 + 13 + export function GrowthHack({ 14 + children, 15 + align = 'right', 16 + }: { 17 + children: React.ReactNode 18 + align?: 'left' | 'right' 19 + }) { 20 + const t = useTheme() 21 + 22 + // the button has a variable width and is absolutely positioned, so we need to manually 23 + // set the minimum width of the underlying button 24 + const [width, setWidth] = useState<number | undefined>(undefined) 25 + 26 + const appState = useAppState() 27 + 28 + if (!isIOS || appState !== 'active') return children 29 + 30 + return ( 31 + <View 32 + style={[ 33 + a.relative, 34 + a.justify_center, 35 + align === 'right' ? a.align_end : a.align_start, 36 + width === undefined ? {opacity: 0} : {minWidth: width}, 37 + ]}> 38 + <PrivacySensitive 39 + style={[ 40 + a.absolute, 41 + a.z_10, 42 + a.flex_col, 43 + align === 'right' 44 + ? [a.right_0, a.align_end] 45 + : [a.left_0, a.align_start], 46 + // when finding the size of the button, we need the containing 47 + // element to have a concrete size otherwise the text will 48 + // collapse to 0 width. so set it to a really big number 49 + // and hide the entire thing (see above) 50 + width === undefined && {width: 10000}, 51 + ]}> 52 + <View 53 + onLayout={evt => setWidth(evt.nativeEvent.layout.width)} 54 + style={[ 55 + t.atoms.bg, 56 + // make sure it covers the icon! the won't always be a button 57 + {minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]}, 58 + ]}> 59 + {children} 60 + </View> 61 + </PrivacySensitive> 62 + <Logo size={ICON_SIZE} /> 63 + </View> 64 + ) 65 + }
+1 -1
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 381 381 </View> 382 382 </Link> 383 383 {showFollowButton && ( 384 - <View collapsable={false}> 384 + <View collapsable={false} style={[a.self_center]}> 385 385 <ThreadItemAnchorFollowButton did={post.author.did} /> 386 386 </View> 387 387 )}
+17 -8
src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
··· 5 5 import {useNavigation} from '@react-navigation/native' 6 6 7 7 import {logger} from '#/logger' 8 + import {isIOS} from '#/platform/detection' 8 9 import {useProfileShadow} from '#/state/cache/profile-shadow' 9 10 import { 10 11 useProfileFollowMutationQueue, ··· 14 15 import * as Toast from '#/view/com/util/Toast' 15 16 import {atoms as a, useBreakpoints} from '#/alf' 16 17 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17 - import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 18 - import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 18 + import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 19 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 20 + import {GrowthHack} from './GrowthHack' 19 21 20 22 export function ThreadItemAnchorFollowButton({did}: {did: string}) { 23 + if (isIOS) { 24 + return ( 25 + <GrowthHack> 26 + <ThreadItemAnchorFollowButtonInner did={did} /> 27 + </GrowthHack> 28 + ) 29 + } 30 + 31 + return <ThreadItemAnchorFollowButtonInner did={did} /> 32 + } 33 + 34 + export function ThreadItemAnchorFollowButtonInner({did}: {did: string}) { 21 35 const {data: profile, isLoading} = useProfileQuery({did}) 22 36 23 37 // We will never hit this - the profile will always be cached or loaded above ··· 113 127 label={_(msg`Follow ${profile.handle}`)} 114 128 onPress={onPress} 115 129 size="small" 116 - variant="solid" 117 130 color={isFollowing ? 'secondary' : 'secondary_inverted'} 118 131 style={[a.rounded_full]}> 119 132 {gtMobile && ( 120 - <ButtonIcon 121 - icon={isFollowing ? Check : Plus} 122 - position="left" 123 - size="sm" 124 - /> 133 + <ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" /> 125 134 )} 126 135 <ButtonText> 127 136 {!isFollowing ? (
+5
yarn.lock
··· 11372 11372 expo-application "~7.0.8" 11373 11373 expo-constants "~18.0.11" 11374 11374 11375 + expo-privacy-sensitive@^0.1.0: 11376 + version "0.1.0" 11377 + resolved "https://registry.yarnpkg.com/expo-privacy-sensitive/-/expo-privacy-sensitive-0.1.0.tgz#2177d7a3cb8ed352df94c5806d012dfb7b48bc84" 11378 + integrity sha512-N0xa8yz+u7HvGY5CqZeo5cwtTOyFQxOxxt15jeW1eAjLKZAcNrtrDGGJP18TX2eh5TfJ3I6OtmECJg9Q8+Yorw== 11379 + 11375 11380 expo-pwa@0.0.127: 11376 11381 version "0.0.127" 11377 11382 resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.127.tgz#b8d2fd28efff408a24e0f2539bfb47e09f8e4ebe"