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

zio/stable
Samuel Newman 2024-03-15 12:11:34 +00:00
commit f71ec52517
23 changed files with 225 additions and 111 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.72.0", "version": "1.73.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View File

@ -1,5 +1,47 @@
export type Events = { export type LogEvents = {
init: { init: {
initMs: number initMs: number
} }
'feed:endReached': {
feedType: string
itemCount: number
}
'post:create': {
imageCount: number
isReply: boolean
hasLink: boolean
hasQuote: boolean
langs: string
logContext: 'Composer'
}
'post:like': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unlike': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unrepost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'profile:follow': {
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
| 'ProfileCard'
| 'ProfileHeader'
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
}
'profile:unfollow': {
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
| 'ProfileCard'
| 'ProfileHeader'
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
}
} }

View File

@ -6,7 +6,9 @@ import {
} from 'statsig-react-native-expo' } from 'statsig-react-native-expo'
import {useSession} from '../../state/session' import {useSession} from '../../state/session'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {Events} from './events' import {LogEvents} from './events'
export type {LogEvents}
const statsigOptions = { const statsigOptions = {
environment: { environment: {
@ -31,9 +33,9 @@ export function attachRouteToLogEvents(
getCurrentRouteName = getRouteName getCurrentRouteName = getRouteName
} }
export function logEvent<E extends keyof Events>( export function logEvent<E extends keyof LogEvents>(
eventName: E & string, eventName: E & string,
rawMetadata?: Events[E] & FlatJSONRecord, rawMetadata: LogEvents[E] & FlatJSONRecord,
) { ) {
const fullMetadata = { const fullMetadata = {
...rawMetadata, ...rawMetadata,

View File

@ -5,6 +5,7 @@ import {Shadow} from '#/state/cache/types'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {updatePostShadow} from '#/state/cache/post-shadow' import {updatePostShadow} from '#/state/cache/post-shadow'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
export const RQKEY = (postUri: string) => ['post', postUri] export const RQKEY = (postUri: string) => ['post', postUri]
@ -58,12 +59,14 @@ export function useGetPost() {
export function usePostLikeMutationQueue( export function usePostLikeMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>, post: Shadow<AppBskyFeedDefs.PostView>,
logContext: LogEvents['post:like']['logContext'] &
LogEvents['post:unlike']['logContext'],
) { ) {
const postUri = post.uri const postUri = post.uri
const postCid = post.cid const postCid = post.cid
const initialLikeUri = post.viewer?.like const initialLikeUri = post.viewer?.like
const likeMutation = usePostLikeMutation() const likeMutation = usePostLikeMutation(logContext)
const unlikeMutation = usePostUnlikeMutation() const unlikeMutation = usePostUnlikeMutation(logContext)
const queueToggle = useToggleMutationQueue({ const queueToggle = useToggleMutationQueue({
initialState: initialLikeUri, initialState: initialLikeUri,
@ -111,22 +114,30 @@ export function usePostLikeMutationQueue(
return [queueLike, queueUnlike] return [queueLike, queueUnlike]
} }
function usePostLikeMutation() { function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the like {uri: string}, // responds with the uri of the like
Error, Error,
{uri: string; cid: string} // the post's uri and cid {uri: string; cid: string} // the post's uri and cid
>({ >({
mutationFn: post => getAgent().like(post.uri, post.cid), mutationFn: post => {
logEvent('post:like', {logContext})
return getAgent().like(post.uri, post.cid)
},
onSuccess() { onSuccess() {
track('Post:Like') track('Post:Like')
}, },
}) })
} }
function usePostUnlikeMutation() { function usePostUnlikeMutation(
logContext: LogEvents['post:unlike']['logContext'],
) {
return useMutation<void, Error, {postUri: string; likeUri: string}>({ return useMutation<void, Error, {postUri: string; likeUri: string}>({
mutationFn: ({likeUri}) => getAgent().deleteLike(likeUri), mutationFn: ({likeUri}) => {
logEvent('post:unlike', {logContext})
return getAgent().deleteLike(likeUri)
},
onSuccess() { onSuccess() {
track('Post:Unlike') track('Post:Unlike')
}, },
@ -135,12 +146,14 @@ function usePostUnlikeMutation() {
export function usePostRepostMutationQueue( export function usePostRepostMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>, post: Shadow<AppBskyFeedDefs.PostView>,
logContext: LogEvents['post:repost']['logContext'] &
LogEvents['post:unrepost']['logContext'],
) { ) {
const postUri = post.uri const postUri = post.uri
const postCid = post.cid const postCid = post.cid
const initialRepostUri = post.viewer?.repost const initialRepostUri = post.viewer?.repost
const repostMutation = usePostRepostMutation() const repostMutation = usePostRepostMutation(logContext)
const unrepostMutation = usePostUnrepostMutation() const unrepostMutation = usePostUnrepostMutation(logContext)
const queueToggle = useToggleMutationQueue({ const queueToggle = useToggleMutationQueue({
initialState: initialRepostUri, initialState: initialRepostUri,
@ -188,22 +201,32 @@ export function usePostRepostMutationQueue(
return [queueRepost, queueUnrepost] return [queueRepost, queueUnrepost]
} }
function usePostRepostMutation() { function usePostRepostMutation(
logContext: LogEvents['post:repost']['logContext'],
) {
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the repost {uri: string}, // responds with the uri of the repost
Error, Error,
{uri: string; cid: string} // the post's uri and cid {uri: string; cid: string} // the post's uri and cid
>({ >({
mutationFn: post => getAgent().repost(post.uri, post.cid), mutationFn: post => {
logEvent('post:repost', {logContext})
return getAgent().repost(post.uri, post.cid)
},
onSuccess() { onSuccess() {
track('Post:Repost') track('Post:Repost')
}, },
}) })
} }
function usePostUnrepostMutation() { function usePostUnrepostMutation(
logContext: LogEvents['post:unrepost']['logContext'],
) {
return useMutation<void, Error, {postUri: string; repostUri: string}>({ return useMutation<void, Error, {postUri: string; repostUri: string}>({
mutationFn: ({repostUri}) => getAgent().deleteRepost(repostUri), mutationFn: ({repostUri}) => {
logEvent('post:unrepost', {logContext})
return getAgent().deleteRepost(repostUri)
},
onSuccess() { onSuccess() {
track('Post:Unrepost') track('Post:Unrepost')
}, },

View File

@ -26,6 +26,7 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts' import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {ThreadNode} from './post-thread' import {ThreadNode} from './post-thread'
export const RQKEY = (did: string) => ['profile', did] export const RQKEY = (did: string) => ['profile', did]
@ -186,11 +187,13 @@ export function useProfileUpdateMutation() {
export function useProfileFollowMutationQueue( export function useProfileFollowMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>, profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext'],
) { ) {
const did = profile.did const did = profile.did
const initialFollowingUri = profile.viewer?.following const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation() const followMutation = useProfileFollowMutation(logContext)
const unfollowMutation = useProfileUnfollowMutation() const unfollowMutation = useProfileUnfollowMutation(logContext)
const queueToggle = useToggleMutationQueue({ const queueToggle = useToggleMutationQueue({
initialState: initialFollowingUri, initialState: initialFollowingUri,
@ -237,9 +240,12 @@ export function useProfileFollowMutationQueue(
return [queueFollow, queueUnfollow] return [queueFollow, queueUnfollow]
} }
function useProfileFollowMutation() { function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
) {
return useMutation<{uri: string; cid: string}, Error, {did: string}>({ return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => { mutationFn: async ({did}) => {
logEvent('profile:follow', {logContext})
return await getAgent().follow(did) return await getAgent().follow(did)
}, },
onSuccess(data, variables) { onSuccess(data, variables) {
@ -248,9 +254,12 @@ function useProfileFollowMutation() {
}) })
} }
function useProfileUnfollowMutation() { function useProfileUnfollowMutation(
logContext: LogEvents['profile:unfollow']['logContext'],
) {
return useMutation<void, Error, {did: string; followUri: string}>({ return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => { mutationFn: async ({followUri}) => {
logEvent('profile:unfollow', {logContext})
track('Profile:Unfollow', {username: followUri}) track('Profile:Unfollow', {username: followUri})
return await getAgent().deleteFollow(followUri) return await getAgent().deleteFollow(followUri)
}, },

View File

@ -56,7 +56,7 @@ export function RecommendedFollowsItem({
) )
} }
export function ProfileCard({ function ProfileCard({
profile, profile,
onFollowStateChange, onFollowStateChange,
moderation, moderation,
@ -72,7 +72,10 @@ export function ProfileCard({
const pal = usePalette('default') const pal = usePalette('default')
const [addingMoreSuggestions, setAddingMoreSuggestions] = const [addingMoreSuggestions, setAddingMoreSuggestions] =
React.useState(false) React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'RecommendedFollowsItem',
)
const onToggleFollow = React.useCallback(async () => { const onToggleFollow = React.useCallback(async () => {
try { try {

View File

@ -65,6 +65,7 @@ import {logger} from '#/logger'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {useDialogStateControlContext} from 'state/dialogs' import {useDialogStateControlContext} from 'state/dialogs'
import {logEvent} from '#/lib/statsig/statsig'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -255,6 +256,16 @@ export const ComposePost = observer(function ComposePost({
setIsProcessing(false) setIsProcessing(false)
return return
} finally { } finally {
if (postUri) {
logEvent('post:create', {
imageCount: gallery.size,
isReply: replyTo != null,
hasLink: extLink != null,
hasQuote: quote != null,
langs: langPrefs.postLanguage,
logContext: 'Composer',
})
}
track('Create Post', { track('Create Post', {
imageCount: gallery.size, imageCount: gallery.size,
}) })

View File

@ -182,7 +182,6 @@ let FeedItem = ({
testID={`feedItem-by-${item.notification.author.handle}`} testID={`feedItem-by-${item.notification.author.handle}`}
style={[ style={[
styles.outer, styles.outer,
pal.view,
pal.border, pal.border,
item.notification.isRead item.notification.isRead
? undefined ? undefined

View File

@ -1,42 +0,0 @@
// FixedTouchableHighlight.tsx
import React, {ComponentProps, useRef} from 'react'
import {GestureResponderEvent, TouchableHighlight} from 'react-native'
type Position = {pageX: number; pageY: number}
export default function FixedTouchableHighlight({
onPress,
onPressIn,
...props
}: ComponentProps<typeof TouchableHighlight>) {
const _touchActivatePositionRef = useRef<Position | null>(null)
function _onPressIn(e: GestureResponderEvent) {
const {pageX, pageY} = e.nativeEvent
_touchActivatePositionRef.current = {
pageX,
pageY,
}
onPressIn?.(e)
}
function _onPress(e: GestureResponderEvent) {
const {pageX, pageY} = e.nativeEvent
const absX = Math.abs(_touchActivatePositionRef.current?.pageX! - pageX)
const absY = Math.abs(_touchActivatePositionRef.current?.pageY! - pageY)
const dragged = absX > 2 || absY > 2
if (!dragged) {
onPress?.(e)
}
}
return (
<TouchableHighlight onPressIn={_onPressIn} onPress={_onPress} {...props}>
{props.children}
</TouchableHighlight>
)
}

View File

@ -42,7 +42,10 @@ function PostThreadFollowBtnLoaded({
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> = const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
useProfileShadow(profileUnshadowed) useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'PostThreadItem',
)
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
const isFollowing = !!profile.viewer?.following const isFollowing = !!profile.viewer?.following

View File

@ -407,6 +407,7 @@ let PostThreadItemLoaded = ({
record={record} record={record}
richText={richText} richText={richText}
onPressReply={onPressReply} onPressReply={onPressReply}
logContext="PostThreadItem"
/> />
</View> </View>
</View> </View>
@ -431,7 +432,6 @@ let PostThreadItemLoaded = ({
<PostHider <PostHider
testID={`postThreadItem-by-${post.author.handle}`} testID={`postThreadItem-by-${post.author.handle}`}
href={postHref} href={postHref}
style={[pal.view]}
moderation={moderation.content} moderation={moderation.content}
iconSize={isThreadedChild ? 26 : 38} iconSize={isThreadedChild ? 26 : 38}
iconStyles={ iconStyles={
@ -560,6 +560,7 @@ let PostThreadItemLoaded = ({
record={record} record={record}
richText={richText} richText={richText}
onPressReply={onPressReply} onPressReply={onPressReply}
logContext="PostThreadItem"
/> />
</View> </View>
</View> </View>
@ -620,7 +621,6 @@ function PostOuterWrapper({
return ( return (
<View <View
style={[ style={[
pal.view,
pal.border, pal.border,
styles.cursor, styles.cursor,
{ {
@ -648,7 +648,6 @@ function PostOuterWrapper({
<View <View
style={[ style={[
styles.outer, styles.outer,
pal.view,
pal.border, pal.border,
showParentReplyLine && hasPrecedingItem && styles.noTopBorder, showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
styles.cursor, styles.cursor,

View File

@ -133,7 +133,7 @@ function PostInner({
}, [setLimitLines]) }, [setLimitLines])
return ( return (
<Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}> <Link href={itemHref} style={[styles.outer, pal.border, style]}>
{showReplyLine && <View style={styles.replyLine} />} {showReplyLine && <View style={styles.replyLine} />}
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
@ -220,6 +220,7 @@ function PostInner({
record={record} record={record}
richText={richText} richText={richText}
onPressReply={onPressReply} onPressReply={onPressReply}
logContext="Post"
/> />
</View> </View>
</View> </View>

View File

@ -33,6 +33,7 @@ import {useLingui} from '@lingui/react'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home' import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {logEvent} from '#/lib/statsig/statsig'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -223,16 +224,29 @@ let Feed = ({
setIsPTRing(false) setIsPTRing(false)
}, [refetch, track, setIsPTRing, onHasNew]) }, [refetch, track, setIsPTRing, onHasNew])
const feedType = feed.split('|')[0]
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
logEvent('feed:endReached', {
feedType: feedType,
itemCount: feedItems.length,
})
track('Feed:onEndReached') track('Feed:onEndReached')
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more posts', {message: err}) logger.error('Failed to load more posts', {message: err})
} }
}, [isFetching, hasNextPage, isError, fetchNextPage, track]) }, [
isFetching,
hasNextPage,
isError,
fetchNextPage,
track,
feedType,
feedItems.length,
])
const onPressTryAgain = React.useCallback(() => { const onPressTryAgain = React.useCallback(() => {
refetch() refetch()

View File

@ -144,7 +144,6 @@ let FeedItemInner = ({
const outerStyles = [ const outerStyles = [
styles.outer, styles.outer,
pal.view,
{ {
borderColor: pal.colors.border, borderColor: pal.colors.border,
paddingBottom: paddingBottom:
@ -310,6 +309,7 @@ let FeedItemInner = ({
showAppealLabelItem={ showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost post.author.did === currentAccount?.did && isModeratedPost
} }
logContext="FeedItem"
/> />
</View> </View>
</View> </View>

View File

@ -78,11 +78,7 @@ function ViewFullThread({slice}: {slice: FeedPostSlice}) {
}, [slice.rootUri]) }, [slice.rootUri])
return ( return (
<Link <Link style={[styles.viewFullThread]} href={itemHref} asAnchor noFeedback>
style={[pal.view, styles.viewFullThread]}
href={itemHref}
asAnchor
noFeedback>
<View style={styles.viewFullThreadDots}> <View style={styles.viewFullThreadDots}>
<Svg width="4" height="40"> <Svg width="4" height="40">
<Line <Line

View File

@ -13,13 +13,18 @@ export function FollowButton({
followedType = 'default', followedType = 'default',
profile, profile,
labelStyle, labelStyle,
logContext,
}: { }: {
unfollowedType?: ButtonType unfollowedType?: ButtonType
followedType?: ButtonType followedType?: ButtonType
profile: Shadow<AppBskyActorDefs.ProfileViewBasic> profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
labelStyle?: StyleProp<TextStyle> labelStyle?: StyleProp<TextStyle>
logContext: 'ProfileCard'
}) { }) {
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
logContext,
)
const {_} = useLingui() const {_} = useLingui()
const onPressFollow = async () => { const onPressFollow = async () => {

View File

@ -230,7 +230,9 @@ export function ProfileCardWithFollowBtn({
renderButton={ renderButton={
isMe isMe
? undefined ? undefined
: profileShadow => <FollowButton profile={profileShadow} /> : profileShadow => (
<FollowButton profile={profileShadow} logContext="ProfileCard" />
)
} }
/> />
) )

View File

@ -103,7 +103,10 @@ let ProfileHeader = ({
const invalidHandle = isInvalidHandle(profile.handle) const invalidHandle = isInvalidHandle(profile.handle)
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'ProfileHeader',
)
const [__, queueUnblock] = useProfileBlockMutationQueue(profile) const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
const unblockPromptControl = Prompt.usePromptControl() const unblockPromptControl = Prompt.usePromptControl()
const moderation = useMemo( const moderation = useMemo(

View File

@ -170,7 +170,10 @@ function SuggestedFollow({
const pal = usePalette('default') const pal = usePalette('default')
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'ProfileHeaderSuggestedFollows',
)
const onPressFollow = React.useCallback(async () => { const onPressFollow = React.useCallback(async () => {
try { try {

View File

@ -52,9 +52,17 @@ let ProfileMenu = ({
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const [, queueUnfollow] = useProfileFollowMutationQueue(profile) const [, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'ProfileMenu',
)
const blockPromptControl = Prompt.usePromptControl() const blockPromptControl = Prompt.usePromptControl()
const loggedOutWarningPromptControl = Prompt.usePromptControl()
const showLoggedOutWarning = React.useMemo(() => {
return !!profile.labels?.find(label => label.val === '!no-unauthenticated')
}, [profile.labels])
const invalidateProfileQuery = React.useCallback(() => { const invalidateProfileQuery = React.useCallback(() => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@ -189,7 +197,13 @@ let ProfileMenu = ({
<Menu.Item <Menu.Item
testID="profileHeaderDropdownShareBtn" testID="profileHeaderDropdownShareBtn"
label={_(msg`Share`)} label={_(msg`Share`)}
onPress={onPressShare}> onPress={() => {
if (showLoggedOutWarning) {
loggedOutWarningPromptControl.open()
} else {
onPressShare()
}
}}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share</Trans> <Trans>Share</Trans>
</Menu.ItemText> </Menu.ItemText>
@ -307,6 +321,16 @@ let ProfileMenu = ({
} }
confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'} confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'}
/> />
<Prompt.Basic
control={loggedOutWarningPromptControl}
title={_(msg`Note about sharing`)}
description={_(
msg`This profile is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
)}
onConfirm={onPressShare}
confirmButtonCta={_(msg`Share anyway`)}
/>
</EventStopper> </EventStopper>
) )
} }

View File

@ -8,7 +8,6 @@ import {
View, View,
ViewStyle, ViewStyle,
Pressable, Pressable,
TouchableWithoutFeedback,
TouchableOpacity, TouchableOpacity,
} from 'react-native' } from 'react-native'
import {useLinkProps, StackActions} from '@react-navigation/native' import {useLinkProps, StackActions} from '@react-navigation/native'
@ -23,7 +22,6 @@ import {
import {isAndroid, isWeb} from 'platform/detection' import {isAndroid, isWeb} from 'platform/detection'
import {sanitizeUrl} from '@braintree/sanitize-url' import {sanitizeUrl} from '@braintree/sanitize-url'
import {PressableWithHover} from './PressableWithHover' import {PressableWithHover} from './PressableWithHover'
import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser' import {useOpenLink} from '#/state/preferences/in-app-browser'
import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
@ -31,6 +29,7 @@ import {
DebouncedNavigationProp, DebouncedNavigationProp,
useNavigationDeduped, useNavigationDeduped,
} from 'lib/hooks/useNavigationDeduped' } from 'lib/hooks/useNavigationDeduped'
import {useTheme} from '#/alf'
type Event = type Event =
| React.MouseEvent<HTMLAnchorElement, MouseEvent> | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@ -63,6 +62,7 @@ export const Link = memo(function Link({
navigationAction, navigationAction,
...props ...props
}: Props) { }: Props) {
const t = useTheme()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const navigation = useNavigationDeduped() const navigation = useNavigationDeduped()
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
@ -85,37 +85,23 @@ export const Link = memo(function Link({
) )
if (noFeedback) { if (noFeedback) {
if (isAndroid) {
// workaround for Android not working well with left/right swipe gestures and TouchableWithoutFeedback
// https://github.com/callstack/react-native-pager-view/issues/424
return (
<FixedTouchableHighlight
testID={testID}
onPress={onPress}
// @ts-ignore web only -prf
href={asAnchor ? sanitizeUrl(href) : undefined}
accessible={accessible}
accessibilityRole="link"
{...props}>
<View style={style}>
{children ? children : <Text>{title || 'link'}</Text>}
</View>
</FixedTouchableHighlight>
)
}
return ( return (
<WebAuxClickWrapper> <WebAuxClickWrapper>
<TouchableWithoutFeedback <Pressable
testID={testID} testID={testID}
onPress={onPress} onPress={onPress}
accessible={accessible} accessible={accessible}
accessibilityRole="link" accessibilityRole="link"
{...props}> {...props}
android_ripple={{
color: t.atoms.bg_contrast_25.backgroundColor,
}}
unstable_pressDelay={isAndroid ? 90 : undefined}>
{/* @ts-ignore web only -prf */} {/* @ts-ignore web only -prf */}
<View style={style} href={anchorHref}> <View style={style} href={anchorHref}>
{children ? children : <Text>{title || 'link'}</Text>} {children ? children : <Text>{title || 'link'}</Text>}
</View> </View>
</TouchableWithoutFeedback> </Pressable>
</WebAuxClickWrapper> </WebAuxClickWrapper>
) )
} }

View File

@ -85,11 +85,13 @@ let PostDropdownBtn = ({
const {mutedWordsDialogControl} = useGlobalDialogsControlContext() const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const deletePromptControl = useDialogControl() const deletePromptControl = useDialogControl()
const hidePromptControl = useDialogControl() const hidePromptControl = useDialogControl()
const loggedOutWarningPromptControl = useDialogControl()
const rootUri = record.reply?.root?.uri || postUri const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri) const isThreadMuted = mutedThreads.includes(rootUri)
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
const isAuthor = postAuthor.did === currentAccount?.did const isAuthor = postAuthor.did === currentAccount?.did
const href = React.useMemo(() => { const href = React.useMemo(() => {
const urip = new AtUri(postUri) const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey) return makeProfileLink(postAuthor, 'post', urip.rkey)
@ -167,6 +169,17 @@ let PostDropdownBtn = ({
hidePost({uri: postUri}) hidePost({uri: postUri})
}, [postUri, hidePost]) }, [postUri, hidePost])
const shouldShowLoggedOutWarning = React.useMemo(() => {
return !!postAuthor.labels?.find(
label => label.val === '!no-unauthenticated',
)
}, [postAuthor])
const onSharePost = React.useCallback(() => {
const url = toShareUrl(href)
shareUrl(url)
}, [href])
return ( return (
<EventStopper onKeyDown={false}> <EventStopper onKeyDown={false}>
<Menu.Root> <Menu.Root>
@ -217,8 +230,11 @@ let PostDropdownBtn = ({
testID="postDropdownShareBtn" testID="postDropdownShareBtn"
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
onPress={() => { onPress={() => {
const url = toShareUrl(href) if (shouldShowLoggedOutWarning) {
shareUrl(url) loggedOutWarningPromptControl.open()
} else {
onSharePost()
}
}}> }}>
<Menu.ItemText> <Menu.ItemText>
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
@ -342,6 +358,16 @@ let PostDropdownBtn = ({
onConfirm={onHidePost} onConfirm={onHidePost}
confirmButtonCta={_(msg`Hide`)} confirmButtonCta={_(msg`Hide`)}
/> />
<Prompt.Basic
control={loggedOutWarningPromptControl}
title={_(msg`Note about sharing`)}
description={_(
msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
)}
onConfirm={onSharePost}
confirmButtonCta={_(msg`Share anyway`)}
/>
</EventStopper> </EventStopper>
) )
} }

View File

@ -44,6 +44,7 @@ let PostCtrls = ({
showAppealLabelItem, showAppealLabelItem,
style, style,
onPressReply, onPressReply,
logContext,
}: { }: {
big?: boolean big?: boolean
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
@ -52,13 +53,17 @@ let PostCtrls = ({
showAppealLabelItem?: boolean showAppealLabelItem?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
onPressReply: () => void onPressReply: () => void
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}): React.ReactNode => { }): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post) const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post) const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
post,
logContext,
)
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
const defaultCtrlColor = React.useMemo( const defaultCtrlColor = React.useMemo(