Bluesky app fork with some witchin' additions 💫

Merge remote-tracking branch 'origin/main' into samuel/alf-login

+225 -111
+1 -1
package.json
··· 1 1 { 2 2 "name": "bsky.app", 3 - "version": "1.72.0", 3 + "version": "1.73.0", 4 4 "private": true, 5 5 "engines": { 6 6 "node": ">=18"
+43 -1
src/lib/statsig/events.ts
··· 1 - export type Events = { 1 + export type LogEvents = { 2 2 init: { 3 3 initMs: number 4 + } 5 + 'feed:endReached': { 6 + feedType: string 7 + itemCount: number 8 + } 9 + 'post:create': { 10 + imageCount: number 11 + isReply: boolean 12 + hasLink: boolean 13 + hasQuote: boolean 14 + langs: string 15 + logContext: 'Composer' 16 + } 17 + 'post:like': { 18 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 19 + } 20 + 'post:repost': { 21 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 22 + } 23 + 'post:unlike': { 24 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 25 + } 26 + 'post:unrepost': { 27 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 28 + } 29 + 'profile:follow': { 30 + logContext: 31 + | 'RecommendedFollowsItem' 32 + | 'PostThreadItem' 33 + | 'ProfileCard' 34 + | 'ProfileHeader' 35 + | 'ProfileHeaderSuggestedFollows' 36 + | 'ProfileMenu' 37 + } 38 + 'profile:unfollow': { 39 + logContext: 40 + | 'RecommendedFollowsItem' 41 + | 'PostThreadItem' 42 + | 'ProfileCard' 43 + | 'ProfileHeader' 44 + | 'ProfileHeaderSuggestedFollows' 45 + | 'ProfileMenu' 4 46 } 5 47 }
+5 -3
src/lib/statsig/statsig.tsx
··· 6 6 } from 'statsig-react-native-expo' 7 7 import {useSession} from '../../state/session' 8 8 import {sha256} from 'js-sha256' 9 - import {Events} from './events' 9 + import {LogEvents} from './events' 10 + 11 + export type {LogEvents} 10 12 11 13 const statsigOptions = { 12 14 environment: { ··· 31 33 getCurrentRouteName = getRouteName 32 34 } 33 35 34 - export function logEvent<E extends keyof Events>( 36 + export function logEvent<E extends keyof LogEvents>( 35 37 eventName: E & string, 36 - rawMetadata?: Events[E] & FlatJSONRecord, 38 + rawMetadata: LogEvents[E] & FlatJSONRecord, 37 39 ) { 38 40 const fullMetadata = { 39 41 ...rawMetadata,
+35 -12
src/state/queries/post.ts
··· 5 5 import {getAgent} from '#/state/session' 6 6 import {updatePostShadow} from '#/state/cache/post-shadow' 7 7 import {track} from '#/lib/analytics/analytics' 8 + import {logEvent, LogEvents} from '#/lib/statsig/statsig' 8 9 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' 9 10 10 11 export const RQKEY = (postUri: string) => ['post', postUri] ··· 58 59 59 60 export function usePostLikeMutationQueue( 60 61 post: Shadow<AppBskyFeedDefs.PostView>, 62 + logContext: LogEvents['post:like']['logContext'] & 63 + LogEvents['post:unlike']['logContext'], 61 64 ) { 62 65 const postUri = post.uri 63 66 const postCid = post.cid 64 67 const initialLikeUri = post.viewer?.like 65 - const likeMutation = usePostLikeMutation() 66 - const unlikeMutation = usePostUnlikeMutation() 68 + const likeMutation = usePostLikeMutation(logContext) 69 + const unlikeMutation = usePostUnlikeMutation(logContext) 67 70 68 71 const queueToggle = useToggleMutationQueue({ 69 72 initialState: initialLikeUri, ··· 111 114 return [queueLike, queueUnlike] 112 115 } 113 116 114 - function usePostLikeMutation() { 117 + function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) { 115 118 return useMutation< 116 119 {uri: string}, // responds with the uri of the like 117 120 Error, 118 121 {uri: string; cid: string} // the post's uri and cid 119 122 >({ 120 - mutationFn: post => getAgent().like(post.uri, post.cid), 123 + mutationFn: post => { 124 + logEvent('post:like', {logContext}) 125 + return getAgent().like(post.uri, post.cid) 126 + }, 121 127 onSuccess() { 122 128 track('Post:Like') 123 129 }, 124 130 }) 125 131 } 126 132 127 - function usePostUnlikeMutation() { 133 + function usePostUnlikeMutation( 134 + logContext: LogEvents['post:unlike']['logContext'], 135 + ) { 128 136 return useMutation<void, Error, {postUri: string; likeUri: string}>({ 129 - mutationFn: ({likeUri}) => getAgent().deleteLike(likeUri), 137 + mutationFn: ({likeUri}) => { 138 + logEvent('post:unlike', {logContext}) 139 + return getAgent().deleteLike(likeUri) 140 + }, 130 141 onSuccess() { 131 142 track('Post:Unlike') 132 143 }, ··· 135 146 136 147 export function usePostRepostMutationQueue( 137 148 post: Shadow<AppBskyFeedDefs.PostView>, 149 + logContext: LogEvents['post:repost']['logContext'] & 150 + LogEvents['post:unrepost']['logContext'], 138 151 ) { 139 152 const postUri = post.uri 140 153 const postCid = post.cid 141 154 const initialRepostUri = post.viewer?.repost 142 - const repostMutation = usePostRepostMutation() 143 - const unrepostMutation = usePostUnrepostMutation() 155 + const repostMutation = usePostRepostMutation(logContext) 156 + const unrepostMutation = usePostUnrepostMutation(logContext) 144 157 145 158 const queueToggle = useToggleMutationQueue({ 146 159 initialState: initialRepostUri, ··· 188 201 return [queueRepost, queueUnrepost] 189 202 } 190 203 191 - function usePostRepostMutation() { 204 + function usePostRepostMutation( 205 + logContext: LogEvents['post:repost']['logContext'], 206 + ) { 192 207 return useMutation< 193 208 {uri: string}, // responds with the uri of the repost 194 209 Error, 195 210 {uri: string; cid: string} // the post's uri and cid 196 211 >({ 197 - mutationFn: post => getAgent().repost(post.uri, post.cid), 212 + mutationFn: post => { 213 + logEvent('post:repost', {logContext}) 214 + return getAgent().repost(post.uri, post.cid) 215 + }, 198 216 onSuccess() { 199 217 track('Post:Repost') 200 218 }, 201 219 }) 202 220 } 203 221 204 - function usePostUnrepostMutation() { 222 + function usePostUnrepostMutation( 223 + logContext: LogEvents['post:unrepost']['logContext'], 224 + ) { 205 225 return useMutation<void, Error, {postUri: string; repostUri: string}>({ 206 - mutationFn: ({repostUri}) => getAgent().deleteRepost(repostUri), 226 + mutationFn: ({repostUri}) => { 227 + logEvent('post:unrepost', {logContext}) 228 + return getAgent().deleteRepost(repostUri) 229 + }, 207 230 onSuccess() { 208 231 track('Post:Unrepost') 209 232 },
+13 -4
src/state/queries/profile.ts
··· 26 26 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts' 27 27 import {STALE} from '#/state/queries' 28 28 import {track} from '#/lib/analytics/analytics' 29 + import {logEvent, LogEvents} from '#/lib/statsig/statsig' 29 30 import {ThreadNode} from './post-thread' 30 31 31 32 export const RQKEY = (did: string) => ['profile', did] ··· 186 187 187 188 export function useProfileFollowMutationQueue( 188 189 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>, 190 + logContext: LogEvents['profile:follow']['logContext'] & 191 + LogEvents['profile:unfollow']['logContext'], 189 192 ) { 190 193 const did = profile.did 191 194 const initialFollowingUri = profile.viewer?.following 192 - const followMutation = useProfileFollowMutation() 193 - const unfollowMutation = useProfileUnfollowMutation() 195 + const followMutation = useProfileFollowMutation(logContext) 196 + const unfollowMutation = useProfileUnfollowMutation(logContext) 194 197 195 198 const queueToggle = useToggleMutationQueue({ 196 199 initialState: initialFollowingUri, ··· 237 240 return [queueFollow, queueUnfollow] 238 241 } 239 242 240 - function useProfileFollowMutation() { 243 + function useProfileFollowMutation( 244 + logContext: LogEvents['profile:follow']['logContext'], 245 + ) { 241 246 return useMutation<{uri: string; cid: string}, Error, {did: string}>({ 242 247 mutationFn: async ({did}) => { 248 + logEvent('profile:follow', {logContext}) 243 249 return await getAgent().follow(did) 244 250 }, 245 251 onSuccess(data, variables) { ··· 248 254 }) 249 255 } 250 256 251 - function useProfileUnfollowMutation() { 257 + function useProfileUnfollowMutation( 258 + logContext: LogEvents['profile:unfollow']['logContext'], 259 + ) { 252 260 return useMutation<void, Error, {did: string; followUri: string}>({ 253 261 mutationFn: async ({followUri}) => { 262 + logEvent('profile:unfollow', {logContext}) 254 263 track('Profile:Unfollow', {username: followUri}) 255 264 return await getAgent().deleteFollow(followUri) 256 265 },
+5 -2
src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
··· 56 56 ) 57 57 } 58 58 59 - export function ProfileCard({ 59 + function ProfileCard({ 60 60 profile, 61 61 onFollowStateChange, 62 62 moderation, ··· 72 72 const pal = usePalette('default') 73 73 const [addingMoreSuggestions, setAddingMoreSuggestions] = 74 74 React.useState(false) 75 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 75 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 76 + profile, 77 + 'RecommendedFollowsItem', 78 + ) 76 79 77 80 const onToggleFollow = React.useCallback(async () => { 78 81 try {
+11
src/view/com/composer/Composer.tsx
··· 65 65 import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' 66 66 import * as Prompt from '#/components/Prompt' 67 67 import {useDialogStateControlContext} from 'state/dialogs' 68 + import {logEvent} from '#/lib/statsig/statsig' 68 69 69 70 type Props = ComposerOpts 70 71 export const ComposePost = observer(function ComposePost({ ··· 255 256 setIsProcessing(false) 256 257 return 257 258 } finally { 259 + if (postUri) { 260 + logEvent('post:create', { 261 + imageCount: gallery.size, 262 + isReply: replyTo != null, 263 + hasLink: extLink != null, 264 + hasQuote: quote != null, 265 + langs: langPrefs.postLanguage, 266 + logContext: 'Composer', 267 + }) 268 + } 258 269 track('Create Post', { 259 270 imageCount: gallery.size, 260 271 })
-1
src/view/com/notifications/FeedItem.tsx
··· 182 182 testID={`feedItem-by-${item.notification.author.handle}`} 183 183 style={[ 184 184 styles.outer, 185 - pal.view, 186 185 pal.border, 187 186 item.notification.isRead 188 187 ? undefined
-42
src/view/com/pager/FixedTouchableHighlight.tsx
··· 1 - // FixedTouchableHighlight.tsx 2 - import React, {ComponentProps, useRef} from 'react' 3 - import {GestureResponderEvent, TouchableHighlight} from 'react-native' 4 - 5 - type Position = {pageX: number; pageY: number} 6 - 7 - export default function FixedTouchableHighlight({ 8 - onPress, 9 - onPressIn, 10 - ...props 11 - }: ComponentProps<typeof TouchableHighlight>) { 12 - const _touchActivatePositionRef = useRef<Position | null>(null) 13 - 14 - function _onPressIn(e: GestureResponderEvent) { 15 - const {pageX, pageY} = e.nativeEvent 16 - 17 - _touchActivatePositionRef.current = { 18 - pageX, 19 - pageY, 20 - } 21 - 22 - onPressIn?.(e) 23 - } 24 - 25 - function _onPress(e: GestureResponderEvent) { 26 - const {pageX, pageY} = e.nativeEvent 27 - 28 - const absX = Math.abs(_touchActivatePositionRef.current?.pageX! - pageX) 29 - const absY = Math.abs(_touchActivatePositionRef.current?.pageY! - pageY) 30 - 31 - const dragged = absX > 2 || absY > 2 32 - if (!dragged) { 33 - onPress?.(e) 34 - } 35 - } 36 - 37 - return ( 38 - <TouchableHighlight onPressIn={_onPressIn} onPress={_onPress} {...props}> 39 - {props.children} 40 - </TouchableHighlight> 41 - ) 42 - }
+4 -1
src/view/com/post-thread/PostThreadFollowBtn.tsx
··· 42 42 const {isTabletOrDesktop} = useWebMediaQueries() 43 43 const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> = 44 44 useProfileShadow(profileUnshadowed) 45 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 45 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 46 + profile, 47 + 'PostThreadItem', 48 + ) 46 49 const requireAuth = useRequireAuth() 47 50 48 51 const isFollowing = !!profile.viewer?.following
+2 -3
src/view/com/post-thread/PostThreadItem.tsx
··· 407 407 record={record} 408 408 richText={richText} 409 409 onPressReply={onPressReply} 410 + logContext="PostThreadItem" 410 411 /> 411 412 </View> 412 413 </View> ··· 431 432 <PostHider 432 433 testID={`postThreadItem-by-${post.author.handle}`} 433 434 href={postHref} 434 - style={[pal.view]} 435 435 moderation={moderation.content} 436 436 iconSize={isThreadedChild ? 26 : 38} 437 437 iconStyles={ ··· 560 560 record={record} 561 561 richText={richText} 562 562 onPressReply={onPressReply} 563 + logContext="PostThreadItem" 563 564 /> 564 565 </View> 565 566 </View> ··· 620 621 return ( 621 622 <View 622 623 style={[ 623 - pal.view, 624 624 pal.border, 625 625 styles.cursor, 626 626 { ··· 648 648 <View 649 649 style={[ 650 650 styles.outer, 651 - pal.view, 652 651 pal.border, 653 652 showParentReplyLine && hasPrecedingItem && styles.noTopBorder, 654 653 styles.cursor,
+2 -1
src/view/com/post/Post.tsx
··· 133 133 }, [setLimitLines]) 134 134 135 135 return ( 136 - <Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}> 136 + <Link href={itemHref} style={[styles.outer, pal.border, style]}> 137 137 {showReplyLine && <View style={styles.replyLine} />} 138 138 <View style={styles.layout}> 139 139 <View style={styles.layoutAvi}> ··· 220 220 record={record} 221 221 richText={richText} 222 222 onPressReply={onPressReply} 223 + logContext="Post" 223 224 /> 224 225 </View> 225 226 </View>
+15 -1
src/view/com/posts/Feed.tsx
··· 33 33 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' 34 34 import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home' 35 35 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 36 + import {logEvent} from '#/lib/statsig/statsig' 36 37 37 38 const LOADING_ITEM = {_reactKey: '__loading__'} 38 39 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 223 224 setIsPTRing(false) 224 225 }, [refetch, track, setIsPTRing, onHasNew]) 225 226 227 + const feedType = feed.split('|')[0] 226 228 const onEndReached = React.useCallback(async () => { 227 229 if (isFetching || !hasNextPage || isError) return 228 230 231 + logEvent('feed:endReached', { 232 + feedType: feedType, 233 + itemCount: feedItems.length, 234 + }) 229 235 track('Feed:onEndReached') 230 236 try { 231 237 await fetchNextPage() 232 238 } catch (err) { 233 239 logger.error('Failed to load more posts', {message: err}) 234 240 } 235 - }, [isFetching, hasNextPage, isError, fetchNextPage, track]) 241 + }, [ 242 + isFetching, 243 + hasNextPage, 244 + isError, 245 + fetchNextPage, 246 + track, 247 + feedType, 248 + feedItems.length, 249 + ]) 236 250 237 251 const onPressTryAgain = React.useCallback(() => { 238 252 refetch()
+1 -1
src/view/com/posts/FeedItem.tsx
··· 144 144 145 145 const outerStyles = [ 146 146 styles.outer, 147 - pal.view, 148 147 { 149 148 borderColor: pal.colors.border, 150 149 paddingBottom: ··· 310 309 showAppealLabelItem={ 311 310 post.author.did === currentAccount?.did && isModeratedPost 312 311 } 312 + logContext="FeedItem" 313 313 /> 314 314 </View> 315 315 </View>
+1 -5
src/view/com/posts/FeedSlice.tsx
··· 78 78 }, [slice.rootUri]) 79 79 80 80 return ( 81 - <Link 82 - style={[pal.view, styles.viewFullThread]} 83 - href={itemHref} 84 - asAnchor 85 - noFeedback> 81 + <Link style={[styles.viewFullThread]} href={itemHref} asAnchor noFeedback> 86 82 <View style={styles.viewFullThreadDots}> 87 83 <Svg width="4" height="40"> 88 84 <Line
+6 -1
src/view/com/profile/FollowButton.tsx
··· 13 13 followedType = 'default', 14 14 profile, 15 15 labelStyle, 16 + logContext, 16 17 }: { 17 18 unfollowedType?: ButtonType 18 19 followedType?: ButtonType 19 20 profile: Shadow<AppBskyActorDefs.ProfileViewBasic> 20 21 labelStyle?: StyleProp<TextStyle> 22 + logContext: 'ProfileCard' 21 23 }) { 22 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 24 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 25 + profile, 26 + logContext, 27 + ) 23 28 const {_} = useLingui() 24 29 25 30 const onPressFollow = async () => {
+3 -1
src/view/com/profile/ProfileCard.tsx
··· 230 230 renderButton={ 231 231 isMe 232 232 ? undefined 233 - : profileShadow => <FollowButton profile={profileShadow} /> 233 + : profileShadow => ( 234 + <FollowButton profile={profileShadow} logContext="ProfileCard" /> 235 + ) 234 236 } 235 237 /> 236 238 )
+4 -1
src/view/com/profile/ProfileHeader.tsx
··· 103 103 const invalidHandle = isInvalidHandle(profile.handle) 104 104 const {isDesktop} = useWebMediaQueries() 105 105 const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) 106 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 106 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 107 + profile, 108 + 'ProfileHeader', 109 + ) 107 110 const [__, queueUnblock] = useProfileBlockMutationQueue(profile) 108 111 const unblockPromptControl = Prompt.usePromptControl() 109 112 const moderation = useMemo(
+4 -1
src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
··· 170 170 const pal = usePalette('default') 171 171 const moderationOpts = useModerationOpts() 172 172 const profile = useProfileShadow(profileUnshadowed) 173 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 173 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 174 + profile, 175 + 'ProfileHeaderSuggestedFollows', 176 + ) 174 177 175 178 const onPressFollow = React.useCallback(async () => { 176 179 try {
+26 -2
src/view/com/profile/ProfileMenu.tsx
··· 52 52 53 53 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 54 54 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 55 - const [, queueUnfollow] = useProfileFollowMutationQueue(profile) 55 + const [, queueUnfollow] = useProfileFollowMutationQueue( 56 + profile, 57 + 'ProfileMenu', 58 + ) 56 59 57 60 const blockPromptControl = Prompt.usePromptControl() 61 + const loggedOutWarningPromptControl = Prompt.usePromptControl() 62 + 63 + const showLoggedOutWarning = React.useMemo(() => { 64 + return !!profile.labels?.find(label => label.val === '!no-unauthenticated') 65 + }, [profile.labels]) 58 66 59 67 const invalidateProfileQuery = React.useCallback(() => { 60 68 queryClient.invalidateQueries({ ··· 189 197 <Menu.Item 190 198 testID="profileHeaderDropdownShareBtn" 191 199 label={_(msg`Share`)} 192 - onPress={onPressShare}> 200 + onPress={() => { 201 + if (showLoggedOutWarning) { 202 + loggedOutWarningPromptControl.open() 203 + } else { 204 + onPressShare() 205 + } 206 + }}> 193 207 <Menu.ItemText> 194 208 <Trans>Share</Trans> 195 209 </Menu.ItemText> ··· 306 320 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 307 321 } 308 322 confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'} 323 + /> 324 + 325 + <Prompt.Basic 326 + control={loggedOutWarningPromptControl} 327 + title={_(msg`Note about sharing`)} 328 + description={_( 329 + msg`This profile is only visible to logged-in users. It won't be visible to people who aren't logged in.`, 330 + )} 331 + onConfirm={onPressShare} 332 + confirmButtonCta={_(msg`Share anyway`)} 309 333 /> 310 334 </EventStopper> 311 335 )
+9 -23
src/view/com/util/Link.tsx
··· 8 8 View, 9 9 ViewStyle, 10 10 Pressable, 11 - TouchableWithoutFeedback, 12 11 TouchableOpacity, 13 12 } from 'react-native' 14 13 import {useLinkProps, StackActions} from '@react-navigation/native' ··· 23 22 import {isAndroid, isWeb} from 'platform/detection' 24 23 import {sanitizeUrl} from '@braintree/sanitize-url' 25 24 import {PressableWithHover} from './PressableWithHover' 26 - import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' 27 25 import {useModalControls} from '#/state/modals' 28 26 import {useOpenLink} from '#/state/preferences/in-app-browser' 29 27 import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' ··· 31 29 DebouncedNavigationProp, 32 30 useNavigationDeduped, 33 31 } from 'lib/hooks/useNavigationDeduped' 32 + import {useTheme} from '#/alf' 34 33 35 34 type Event = 36 35 | React.MouseEvent<HTMLAnchorElement, MouseEvent> ··· 63 62 navigationAction, 64 63 ...props 65 64 }: Props) { 65 + const t = useTheme() 66 66 const {closeModal} = useModalControls() 67 67 const navigation = useNavigationDeduped() 68 68 const anchorHref = asAnchor ? sanitizeUrl(href) : undefined ··· 85 85 ) 86 86 87 87 if (noFeedback) { 88 - if (isAndroid) { 89 - // workaround for Android not working well with left/right swipe gestures and TouchableWithoutFeedback 90 - // https://github.com/callstack/react-native-pager-view/issues/424 91 - return ( 92 - <FixedTouchableHighlight 93 - testID={testID} 94 - onPress={onPress} 95 - // @ts-ignore web only -prf 96 - href={asAnchor ? sanitizeUrl(href) : undefined} 97 - accessible={accessible} 98 - accessibilityRole="link" 99 - {...props}> 100 - <View style={style}> 101 - {children ? children : <Text>{title || 'link'}</Text>} 102 - </View> 103 - </FixedTouchableHighlight> 104 - ) 105 - } 106 88 return ( 107 89 <WebAuxClickWrapper> 108 - <TouchableWithoutFeedback 90 + <Pressable 109 91 testID={testID} 110 92 onPress={onPress} 111 93 accessible={accessible} 112 94 accessibilityRole="link" 113 - {...props}> 95 + {...props} 96 + android_ripple={{ 97 + color: t.atoms.bg_contrast_25.backgroundColor, 98 + }} 99 + unstable_pressDelay={isAndroid ? 90 : undefined}> 114 100 {/* @ts-ignore web only -prf */} 115 101 <View style={style} href={anchorHref}> 116 102 {children ? children : <Text>{title || 'link'}</Text>} 117 103 </View> 118 - </TouchableWithoutFeedback> 104 + </Pressable> 119 105 </WebAuxClickWrapper> 120 106 ) 121 107 }
+28 -2
src/view/com/util/forms/PostDropdownBtn.tsx
··· 85 85 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 86 86 const deletePromptControl = useDialogControl() 87 87 const hidePromptControl = useDialogControl() 88 + const loggedOutWarningPromptControl = useDialogControl() 88 89 89 90 const rootUri = record.reply?.root?.uri || postUri 90 91 const isThreadMuted = mutedThreads.includes(rootUri) 91 92 const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) 92 93 const isAuthor = postAuthor.did === currentAccount?.did 94 + 93 95 const href = React.useMemo(() => { 94 96 const urip = new AtUri(postUri) 95 97 return makeProfileLink(postAuthor, 'post', urip.rkey) ··· 167 169 hidePost({uri: postUri}) 168 170 }, [postUri, hidePost]) 169 171 172 + const shouldShowLoggedOutWarning = React.useMemo(() => { 173 + return !!postAuthor.labels?.find( 174 + label => label.val === '!no-unauthenticated', 175 + ) 176 + }, [postAuthor]) 177 + 178 + const onSharePost = React.useCallback(() => { 179 + const url = toShareUrl(href) 180 + shareUrl(url) 181 + }, [href]) 182 + 170 183 return ( 171 184 <EventStopper onKeyDown={false}> 172 185 <Menu.Root> ··· 217 230 testID="postDropdownShareBtn" 218 231 label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} 219 232 onPress={() => { 220 - const url = toShareUrl(href) 221 - shareUrl(url) 233 + if (shouldShowLoggedOutWarning) { 234 + loggedOutWarningPromptControl.open() 235 + } else { 236 + onSharePost() 237 + } 222 238 }}> 223 239 <Menu.ItemText> 224 240 {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} ··· 341 357 description={_(msg`This post will be hidden from feeds.`)} 342 358 onConfirm={onHidePost} 343 359 confirmButtonCta={_(msg`Hide`)} 360 + /> 361 + 362 + <Prompt.Basic 363 + control={loggedOutWarningPromptControl} 364 + title={_(msg`Note about sharing`)} 365 + description={_( 366 + msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`, 367 + )} 368 + onConfirm={onSharePost} 369 + confirmButtonCta={_(msg`Share anyway`)} 344 370 /> 345 371 </EventStopper> 346 372 )
+7 -2
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 44 44 showAppealLabelItem, 45 45 style, 46 46 onPressReply, 47 + logContext, 47 48 }: { 48 49 big?: boolean 49 50 post: Shadow<AppBskyFeedDefs.PostView> ··· 52 53 showAppealLabelItem?: boolean 53 54 style?: StyleProp<ViewStyle> 54 55 onPressReply: () => void 56 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 55 57 }): React.ReactNode => { 56 58 const theme = useTheme() 57 59 const {_} = useLingui() 58 60 const {openComposer} = useComposerControls() 59 61 const {closeModal} = useModalControls() 60 - const [queueLike, queueUnlike] = usePostLikeMutationQueue(post) 61 - const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post) 62 + const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) 63 + const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( 64 + post, 65 + logContext, 66 + ) 62 67 const requireAuth = useRequireAuth() 63 68 64 69 const defaultCtrlColor = React.useMemo(