Merge remote-tracking branch 'upstream/main' into Improve-notification-localization

This commit is contained in:
Minseo Lee
2024-08-09 09:12:11 +09:00
32 changed files with 348 additions and 496 deletions
+12 -2
View File
@@ -133,16 +133,26 @@ function useExperimentalSuggestedUsersQuery() {
const {currentAccount} = useSession()
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
const dids = React.useMemo(() => {
const {likes, follows, seen} = userActionSnapshot
const {likes, follows, followSuggestions, seen} = userActionSnapshot
const likeDids = likes
.map(l => new AtUri(l))
.map(uri => uri.host)
.filter(did => !follows.includes(did))
let suggestedDids: string[] = []
if (followSuggestions.length > 0) {
suggestedDids = [
// It's ok if these will pick the same item (weighed by its frequency)
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
]
}
const seenDids = seen
.sort(sortSeenPosts)
.map(l => new AtUri(l.uri))
.map(uri => uri.host)
return [...new Set([...likeDids, ...seenDids])].filter(
return [...new Set([...suggestedDids, ...likeDids, ...seenDids])].filter(
did => did !== currentAccount?.did,
)
}, [userActionSnapshot, currentAccount])
+2 -20
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -40,7 +40,7 @@ export function ContentHider({
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={[styles.outer, style]}>
<View testID={testID} style={style}>
{children}
</View>
)
@@ -163,21 +163,3 @@ export function ContentHider({
</View>
)
}
const styles = StyleSheet.create({
outer: {},
cover: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 8,
marginTop: 4,
paddingVertical: 14,
paddingLeft: 14,
paddingRight: 18,
},
showBtn: {
marginLeft: 'auto',
alignSelf: 'center',
},
})
+2 -12
View File
@@ -5,7 +5,7 @@ import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {logEvent, useGate} from 'lib/statsig/statsig'
import {logEvent} from 'lib/statsig/statsig'
import {devicePlatform, isAndroid, isNative} from 'platform/detection'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
@@ -86,7 +86,6 @@ export function useNotificationsRegistration() {
}
export function useRequestNotificationsPermission() {
const gate = useGate()
const {currentAccount} = useSession()
const agent = useAgent()
@@ -102,16 +101,7 @@ export function useRequestNotificationsPermission() {
) {
return
}
if (
context === 'StartOnboarding' &&
gate('request_notifications_permission_after_onboarding_v2')
) {
return
}
if (
context === 'AfterOnboarding' &&
!gate('request_notifications_permission_after_onboarding_v2')
) {
if (context === 'AfterOnboarding') {
return
}
if (context === 'Home' && !currentAccount) {
+4
View File
@@ -159,6 +159,7 @@ export type LogEvents = {
| 'AvatarButton'
| 'StarterPackProfilesList'
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
}
'profile:unfollow': {
logContext:
@@ -173,6 +174,7 @@ export type LogEvents = {
| 'AvatarButton'
| 'StarterPackProfilesList'
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
}
'chat:create': {
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
@@ -211,6 +213,8 @@ export type LogEvents = {
'feed:interstitial:profileCard:press': {}
'feed:interstitial:feedCard:press': {}
'profile:header:suggestedFollowsCard:press': {}
'debug:followingPrefs': {
followingShowRepliesFromPref: 'all' | 'following' | 'off'
followingRepliesMinLikePref: number
-8
View File
@@ -1,18 +1,10 @@
export type Gate =
// Keep this alphabetic please.
| 'debug_show_feedcontext'
| 'explore_page_profile_card_social_proof'
| 'native_pwi_disabled'
| 'new_user_guided_tour'
| 'new_user_progress_guide'
| 'onboarding_minimum_interests'
| 'request_notifications_permission_after_onboarding_v2'
| 'session_withproxy_fix'
| 'show_avi_follow_button'
| 'show_follow_back_label_v2'
| 'suggested_feeds_interstitial'
| 'suggested_follows_interstitial'
| 'ungroup_follow_backs'
| 'video_debug'
| 'videos'
| 'small_avi_thumb'
@@ -157,7 +157,7 @@ let ProfileHeaderStandard = ({
hideBackButton={hideBackButton}
isPlaceholderProfile={isPlaceholderProfile}>
<View
style={[a.px_lg, a.pt_md, a.pb_sm]}
style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
<View
style={[
-3
View File
@@ -26,7 +26,6 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {useAgent} from '#/state/session'
import {useModerationOpts} from '../../preferences/moderation-opts'
import {STALE} from '..'
@@ -59,7 +58,6 @@ export function useNotificationFeedQuery(opts?: {
const moderationOpts = useModerationOpts()
const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false
const gate = useGate()
// false: force showing all notifications
// undefined: let the server decide
@@ -88,7 +86,6 @@ export function useNotificationFeedQuery(opts?: {
queryClient,
moderationOpts,
fetchAdditionalData: true,
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
priority,
})
page = fetchedPage
+1 -4
View File
@@ -8,7 +8,6 @@ import {useQueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import {resetBadgeCount} from 'lib/notifications/notifications'
@@ -48,7 +47,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const agent = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const gate = useGate()
const [numUnread, setNumUnread] = React.useState('')
@@ -151,7 +149,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// only fetch subjects when the page is going to be used
// in the notifications query, otherwise skip it
fetchAdditionalData: !!invalidate,
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
})
const unreadCount = countUnread(page)
const unreadCountStr =
@@ -192,7 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
},
}
}, [setNumUnread, queryClient, moderationOpts, agent, gate])
}, [setNumUnread, queryClient, moderationOpts, agent])
checkUnreadRef.current = api.checkUnread
return (
+2 -7
View File
@@ -30,7 +30,6 @@ export async function fetchPage({
queryClient,
moderationOpts,
fetchAdditionalData,
shouldUngroupFollowBacks,
}: {
agent: BskyAgent
cursor: string | undefined
@@ -38,7 +37,6 @@ export async function fetchPage({
queryClient: QueryClient
moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean
shouldUngroupFollowBacks?: () => boolean
priority?: boolean
}): Promise<{
page: FeedPage
@@ -58,7 +56,7 @@ export async function fetchPage({
)
// group notifications which are essentially similar (follows, likes on a post)
let notifsGrouped = groupNotifications(notifs, {shouldUngroupFollowBacks})
let notifsGrouped = groupNotifications(notifs)
// we fetch subjects of notifications (usually posts) now instead of lazily
// in the UI to avoid relayouts
@@ -117,7 +115,6 @@ export function shouldFilterNotif(
export function groupNotifications(
notifs: AppBskyNotificationListNotifications.Notification[],
options?: {shouldUngroupFollowBacks?: () => boolean},
): FeedNotification[] {
const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) {
@@ -137,9 +134,7 @@ export function groupNotifications(
const prevIsFollowBack =
groupedNotif.notification.reason === 'follow' &&
groupedNotif.notification.author.viewer?.following
const shouldUngroup =
(nextIsFollowBack || prevIsFollowBack) &&
options?.shouldUngroupFollowBacks?.()
const shouldUngroup = nextIsFollowBack || prevIsFollowBack
if (!shouldUngroup) {
groupedNotif.additional = groupedNotif.additional || []
groupedNotif.additional.push(notif)
+18 -1
View File
@@ -137,6 +137,7 @@ export function sortThread(
opts: UsePreferencesQueryResponse['threadViewPrefs'],
modCache: ThreadModerationCache,
currentDid: string | undefined,
justPostedUris: Set<string>,
): ThreadNode {
if (node.type !== 'post') {
return node
@@ -150,6 +151,20 @@ export function sortThread(
return -1
}
if (node.ctx.isHighlightedPost || opts.lab_treeViewEnabled) {
const aIsJustPosted =
a.post.author.did === currentDid && justPostedUris.has(a.post.uri)
const bIsJustPosted =
b.post.author.did === currentDid && justPostedUris.has(b.post.uri)
if (aIsJustPosted && bIsJustPosted) {
return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
} else if (aIsJustPosted) {
return -1 // reply while onscreen
} else if (bIsJustPosted) {
return 1 // reply while onscreen
}
}
const aIsByOp = a.post.author.did === node.post?.author.did
const bIsByOp = b.post.author.did === node.post?.author.did
if (aIsByOp && bIsByOp) {
@@ -206,7 +221,9 @@ export function sortThread(
}
return b.post.indexedAt.localeCompare(a.post.indexedAt)
})
node.replies.forEach(reply => sortThread(reply, opts, modCache, currentDid))
node.replies.forEach(reply =>
sortThread(reply, opts, modCache, currentDid, justPostedUris),
)
}
return node
}
+4 -12
View File
@@ -40,18 +40,10 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
pages: data.pages.map(page => {
return {
...page,
lists: page.lists
/*
* Starter packs use a reference list, which we do not want to
* show on profiles. At some point we could probably just filter
* this out on the backend instead of in the client.
*/
.filter(l => l.purpose !== 'app.bsky.graph.defs#referencelist')
// filter by labels
.filter(list => {
const decision = moderateUserList(list, moderationOpts!)
return !decision.ui('contentList').filter
}),
lists: page.lists.filter(list => {
const decision = moderateUserList(list, moderationOpts!)
return !decision.ui('contentList').filter
}),
}
}),
}
+15
View File
@@ -222,6 +222,7 @@ export function useProfileFollowMutationQueue(
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext'],
) {
const agent = useAgent()
const queryClient = useQueryClient()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
@@ -253,6 +254,20 @@ export function useProfileFollowMutationQueue(
updateProfileShadow(queryClient, did, {
followingUri: finalFollowingUri,
})
if (finalFollowingUri) {
agent.app.bsky.graph
.getSuggestedFollowsByActor({
actor: did,
})
.then(res => {
const dids = res.data.suggestions
.filter(a => !a.viewer?.following)
.map(a => a.did)
.slice(0, 8)
userActionHistory.followSuggestion(dids)
})
}
},
})
+1
View File
@@ -106,6 +106,7 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
const agent = useAgent()
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
gcTime: 0,
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
+3 -2
View File
@@ -1,10 +1,11 @@
import React from 'react'
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
AppBskyRichtextFacet,
ModerationDecision,
AppBskyActorDefs,
} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
export interface ComposerOptsPostRef {
@@ -31,7 +32,7 @@ export interface ComposerOptsQuote {
}
export interface ComposerOpts {
replyTo?: ComposerOptsPostRef
onPost?: () => void
onPost?: (postUri: string | undefined) => void
quote?: ComposerOptsQuote
mention?: string // handle of user to mention
openPicker?: (pos: DOMRect | undefined) => void
+1 -6
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {
ProgressGuideToast,
ProgressGuideToastRef,
@@ -61,7 +60,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const {data: preferences} = usePreferencesQuery()
const {mutateAsync, variables, isPending} =
useSetActiveProgressGuideMutation()
const gate = useGate()
const activeProgressGuide = (
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
@@ -89,9 +87,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const controls = React.useMemo(() => {
return {
startProgressGuide(guide: ProgressGuideName) {
if (!gate('new_user_progress_guide')) {
return
}
if (guide === 'like-10-and-follow-7') {
const guideObj = {
guide: 'like-10-and-follow-7',
@@ -148,7 +143,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
mutateAsync(guide?.isComplete ? undefined : guide)
},
}
}, [activeProgressGuide, mutateAsync, gate, setLocalGuideState])
}, [activeProgressGuide, mutateAsync, setLocalGuideState])
return (
<ProgressGuideContext.Provider value={localGuideState}>
+13
View File
@@ -2,6 +2,7 @@ import React from 'react'
const LIKE_WINDOW = 100
const FOLLOW_WINDOW = 100
const FOLLOW_SUGGESTION_WINDOW = 100
const SEEN_WINDOW = 100
export type SeenPost = {
@@ -22,6 +23,10 @@ export type UserActionHistory = {
* The last 100 DIDs the user has followed
*/
follows: string[]
/*
* The last 100 DIDs of suggested follows based on last follows
*/
followSuggestions: string[]
/**
* The last 100 post URIs the user has seen from the Discover feed only
*/
@@ -31,6 +36,7 @@ export type UserActionHistory = {
const userActionHistory: UserActionHistory = {
likes: [],
follows: [],
followSuggestions: [],
seen: [],
}
@@ -58,6 +64,13 @@ export function follow(dids: string[]) {
.concat(dids)
.slice(-FOLLOW_WINDOW)
}
export function followSuggestion(dids: string[]) {
userActionHistory.followSuggestions = userActionHistory.followSuggestions
.concat(dids)
.slice(-FOLLOW_SUGGESTION_WINDOW)
}
export function unfollow(dids: string[]) {
userActionHistory.follows = userActionHistory.follows.filter(
uri => !dids.includes(uri),
+2 -48
View File
@@ -1,25 +1,20 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {logEvent} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {isIOS, isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {isIOS} from '#/platform/detection'
import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
import {NavigationProp} from 'lib/routes/types'
import {useGate} from 'lib/statsig/statsig'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Text} from '#/view/com/util/text/Text'
import {Login} from '#/screens/Login'
import {Signup} from '#/screens/Signup'
import {LandingScreen} from '#/screens/StarterPack/StarterPackLandingScreen'
@@ -34,7 +29,6 @@ enum ScreenState {
export {ScreenState as LoggedOutScreenState}
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {hasSession} = useSession()
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
@@ -52,10 +46,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}
})
const {clearRequestedAccount} = useLoggedOutViewControls()
const navigation = useNavigation<NavigationProp>()
const gate = useGate()
const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
React.useEffect(() => {
screen('Login')
setMinimalShellMode(true)
@@ -68,10 +59,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
clearRequestedAccount()
}, [clearRequestedAccount, onDismiss])
const onPressSearch = React.useCallback(() => {
navigation.navigate(`SearchTab`)
}, [navigation])
return (
<View testID="noSessionView" style={[s.hContentRegion, pal.view]}>
<ErrorBoundary>
@@ -98,39 +85,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}}
/>
</Pressable>
) : isNative &&
!hasSession &&
isFirstScreen &&
!gate('native_pwi_disabled') ? (
<Pressable
accessibilityHint={_(msg`Search for users`)}
accessibilityLabel={_(msg`Search for users`)}
accessibilityRole="button"
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 4,
position: 'absolute',
top: 20,
right: 20,
paddingHorizontal: 16,
paddingVertical: 8,
zIndex: 100,
backgroundColor: pal.btn.backgroundColor,
borderRadius: 100,
}}
onPress={onPressSearch}>
<Text type="lg-bold" style={[pal.text]}>
<Trans>Search</Trans>{' '}
</Text>
<FontAwesomeIcon
icon="search"
size={16}
style={{
color: String(pal.text.color),
}}
/>
</Pressable>
) : null}
{screenState === ScreenState.S_StarterPack ? (
+1 -1
View File
@@ -392,7 +392,7 @@ export const ComposePost = observer(function ComposePost({
emitPostCreated()
}
setLangPrefs.savePostLanguageToHistory()
onPost?.()
onPost?.(postUri)
onClose()
Toast.show(
replyTo
+1 -1
View File
@@ -91,7 +91,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={styles.replyToPost}>
<Text type="xl-medium" style={t.atoms.text}>
<Text type="xl-medium" style={t.atoms.text} numberOfLines={1}>
{sanitizeDisplayName(
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
)}
+3 -7
View File
@@ -25,7 +25,6 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {logger} from '#/logger'
import {FeedNotification} from '#/state/queries/notifications/feed'
@@ -87,7 +86,6 @@ let FeedItem = ({
const pal = usePalette('default')
const {_} = useLingui()
const t = useTheme()
const gate = useGate()
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
const itemHref = useMemo(() => {
if (item.type === 'post-like' || item.type === 'repost') {
@@ -275,7 +273,7 @@ let FeedItem = ({
}
}
if (isFollowBack && gate('ungroup_follow_backs')) {
if (isFollowBack) {
a11yLabel =
authors.length > 1
? _(
@@ -404,6 +402,7 @@ let FeedItem = ({
borderColor: pal.colors.unreadNotifBorder,
},
{borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
a.overflow_hidden,
]}
href={itemHref}
noFeedback
@@ -676,7 +675,7 @@ function ExpandedAuthorsList({
}, [heightInterp, visible])
return (
<Animated.View style={[heightStyle, styles.overflowHidden]}>
<Animated.View style={[a.overflow_hidden, heightStyle]}>
{visible &&
authors.map(author => (
<NewLink
@@ -772,9 +771,6 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
}
const styles = StyleSheet.create({
overflowHidden: {
overflow: 'hidden',
},
pointer: isWeb
? {
// @ts-ignore web only
+86 -23
View File
@@ -1,11 +1,14 @@
import React, {useEffect, useRef} from 'react'
import {useWindowDimensions, View} from 'react-native'
import React, {useRef} from 'react'
import {StyleSheet, useWindowDimensions, View} from 'react-native'
import {runOnJS} from 'react-native-reanimated'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {clamp} from '#/lib/numbers'
import {ScrollProvider} from '#/lib/ScrollContext'
import {isAndroid, isNative, isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -21,7 +24,9 @@ import {
} from '#/state/queries/post-thread'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useMinimalShellFabTransform} from 'lib/hooks/useMinimalShellTransform'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -30,9 +35,9 @@ import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Text} from '#/components/Typography'
import {ComposePrompt} from '../composer/Prompt'
import {List, ListMethods} from '../util/List'
import {ViewHeader} from '../util/ViewHeader'
import {PostThreadComposePrompt} from './PostThreadComposePrompt'
import {PostThreadItem} from './PostThreadItem'
import {PostThreadLoadMore} from './PostThreadLoadMore'
import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
@@ -80,15 +85,7 @@ const keyExtractor = (item: RowItem) => {
return item._reactKey
}
export function PostThread({
uri,
onCanReply,
onPressReply,
}: {
uri: string | undefined
onCanReply: (canReply: boolean) => void
onPressReply: () => unknown
}) {
export function PostThread({uri}: {uri: string | undefined}) {
const {hasSession, currentAccount} = useSession()
const {_} = useLingui()
const t = useTheme()
@@ -163,12 +160,22 @@ export function PostThread({
return cache
}, [thread, moderationOpts])
const [justPostedUris, setJustPostedUris] = React.useState(
() => new Set<string>(),
)
const skeleton = React.useMemo(() => {
const threadViewPrefs = preferences?.threadViewPrefs
if (!threadViewPrefs || !thread) return null
return createThreadSkeleton(
sortThread(thread, threadViewPrefs, threadModerationCache, currentDid),
sortThread(
thread,
threadViewPrefs,
threadModerationCache,
currentDid,
justPostedUris,
),
!!currentDid,
treeView,
threadModerationCache,
@@ -181,6 +188,7 @@ export function PostThread({
treeView,
threadModerationCache,
hiddenRepliesState,
justPostedUris,
])
const error = React.useMemo(() => {
@@ -210,14 +218,6 @@ export function PostThread({
return null
}, [thread, skeleton?.highlightedPost, isThreadError, _, threadError])
useEffect(() => {
if (error) {
onCanReply(false)
} else if (rootPost) {
onCanReply(!rootPost.viewer?.replyDisabled)
}
}, [rootPost, onCanReply, error])
// construct content
const posts = React.useMemo(() => {
if (!skeleton) return []
@@ -313,6 +313,38 @@ export function PostThread({
setMaxReplies(prev => prev + 50)
}, [isFetching, maxReplies, posts.length])
const onPostReply = React.useCallback(
(postUri: string | undefined) => {
refetch()
if (postUri) {
setJustPostedUris(set => {
const nextSet = new Set(set)
nextSet.add(postUri)
return nextSet
})
}
},
[refetch],
)
const {openComposer} = useComposerControls()
const onPressReply = React.useCallback(() => {
if (thread?.type !== 'post') {
return
}
openComposer({
replyTo: {
uri: thread.post.uri,
cid: thread.post.cid,
text: thread.record.text,
author: thread.post.author,
embed: thread.post.embed,
},
onPost: onPostReply,
})
}, [openComposer, thread, onPostReply])
const canReply = !error && rootPost && !rootPost.viewer?.replyDisabled
const hasParents =
skeleton?.highlightedPost?.type === 'post' &&
(skeleton.highlightedPost.ctx.isParentLoading ||
@@ -324,7 +356,9 @@ export function PostThread({
if (item === REPLY_PROMPT && hasSession) {
return (
<View>
{!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
{!isMobile && (
<PostThreadComposePrompt onPressCompose={onPressReply} />
)}
</View>
)
} else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) {
@@ -406,7 +440,7 @@ export function PostThread({
HiddenRepliesState.ShowAndOverridePostHider &&
item.ctx.depth > 0
}
onPostReply={refetch}
onPostReply={onPostReply}
hideTopBorder={index === 0 && !item.ctx.isParentLoading}
/>
</View>
@@ -473,10 +507,30 @@ export function PostThread({
sideBorders={false}
/>
</ScrollProvider>
{isMobile && canReply && hasSession && (
<MobileComposePrompt onPressReply={onPressReply} />
)}
</CenteredView>
)
}
function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
const safeAreaInsets = useSafeAreaInsets()
const fabMinimalShellTransform = useMinimalShellFabTransform()
return (
<Animated.View
style={[
styles.prompt,
fabMinimalShellTransform,
{
bottom: clamp(safeAreaInsets.bottom, 15, 30),
},
]}>
<PostThreadComposePrompt onPressCompose={onPressReply} />
</Animated.View>
)
}
function isThreadPost(v: unknown): v is ThreadPost {
return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
}
@@ -622,3 +676,12 @@ function hasBranchingReplies(node?: ThreadNode) {
}
return true
}
const styles = StyleSheet.create({
prompt: {
// @ts-ignore web-only
position: isWeb ? 'fixed' : 'absolute',
left: 0,
right: 0,
},
})
@@ -10,7 +10,11 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
export function PostThreadComposePrompt({
onPressCompose,
}: {
onPressCompose: () => void
}) {
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const pal = usePalette('default')
+3 -2
View File
@@ -75,7 +75,7 @@ export function PostThreadItem({
showParentReplyLine?: boolean
hasPrecedingItem: boolean
overrideBlur: boolean
onPostReply: () => void
onPostReply: (postUri: string | undefined) => void
hideTopBorder?: boolean
}) {
const postShadowed = usePostShadow(post)
@@ -169,7 +169,7 @@ let PostThreadItemLoaded = ({
showParentReplyLine?: boolean
hasPrecedingItem: boolean
overrideBlur: boolean
onPostReply: () => void
onPostReply: (postUri: string | undefined) => void
hideTopBorder?: boolean
}): React.ReactNode => {
const pal = usePalette('default')
@@ -742,6 +742,7 @@ const styles = StyleSheet.create({
flexWrap: 'wrap',
paddingBottom: 4,
paddingRight: 10,
overflow: 'hidden',
},
postTextLargeContainer: {
paddingHorizontal: 0,
+7 -6
View File
@@ -12,17 +12,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
import {colors, s} from '#/lib/styles'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {MAX_POST_LINES} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
import {countLines} from 'lib/strings/helpers'
import {colors, s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile'
import {AviFollowButton} from '#/view/com/posts/AviFollowButton'
import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -280,6 +280,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
overflow: 'hidden',
},
replyLine: {
position: 'absolute',
+1 -3
View File
@@ -7,7 +7,6 @@ import {useNavigation} from '@react-navigation/native'
import {createHitslop} from '#/lib/constants'
import {NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useSession} from '#/state/session'
@@ -37,7 +36,6 @@ export function AviFollowButton({
profile: profile,
logContext: 'AvatarButton',
})
const gate = useGate()
const {currentAccount, hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
@@ -80,7 +78,7 @@ export function AviFollowButton({
},
]
return hasSession && gate('show_avi_follow_button') ? (
return hasSession ? (
<View style={a.relative}>
{children}
+1 -2
View File
@@ -349,8 +349,7 @@ let Feed = ({
const shouldShow =
(interstitial.type === feedInterstitialType &&
gate('suggested_feeds_interstitial')) ||
(interstitial.type === followInterstitialType &&
gate('suggested_follows_interstitial')) ||
interstitial.type === followInterstitialType ||
interstitial.type === progressGuideInterstitialType
if (shouldShow) {
+1
View File
@@ -544,6 +544,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
flexWrap: 'wrap',
paddingBottom: 2,
overflow: 'hidden',
},
contentHiderChild: {
marginTop: 6,
@@ -1,32 +1,60 @@
import React from 'react'
import {Pressable, ScrollView, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {ScrollView, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {logEvent} from '#/lib/statsig/statsig'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {isWeb} from 'platform/detection'
import {Button} from 'view/com/util/forms/Button'
import {Link} from 'view/com/util/Link'
import {Text} from 'view/com/util/text/Text'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import * as Toast from '../util/Toast'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
const OUTER_PADDING = 10
const INNER_PADDING = 14
const TOTAL_HEIGHT = 250
const OUTER_PADDING = a.p_md.padding
const INNER_PADDING = a.p_lg.padding
const TOTAL_HEIGHT = 232
const MOBILE_CARD_WIDTH = 300
function CardOuter({
children,
style,
}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.w_full,
a.p_lg,
a.rounded_md,
a.border,
t.atoms.bg,
t.atoms.border_contrast_low,
{
width: MOBILE_CARD_WIDTH,
},
style,
]}>
{children}
</View>
)
}
export function SuggestedFollowPlaceholder() {
const t = useTheme()
return (
<CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}>
<ProfileCard.Header>
<ProfileCard.AvatarPlaceholder />
<ProfileCard.NameAndHandlePlaceholder />
</ProfileCard.Header>
<ProfileCard.DescriptionPlaceholder />
</CardOuter>
)
}
export function ProfileHeaderSuggestedFollows({
actorDid,
@@ -35,47 +63,55 @@ export function ProfileHeaderSuggestedFollows({
actorDid: string
requestDismiss: () => void
}) {
const pal = usePalette('default')
const {isLoading, data} = useSuggestedFollowsByActorQuery({
did: actorDid,
})
const t = useTheme()
const {_} = useLingui()
const {isLoading: isSuggestionsLoading, data} =
useSuggestedFollowsByActorQuery({
did: actorDid,
})
const moderationOpts = useModerationOpts()
const isLoading = isSuggestionsLoading || !moderationOpts
return (
<View
style={{paddingVertical: OUTER_PADDING, height: TOTAL_HEIGHT}}
pointerEvents="box-none">
<View
pointerEvents="box-none"
style={{
backgroundColor: pal.viewLight.backgroundColor,
height: '100%',
paddingTop: INNER_PADDING / 2,
}}>
style={[
t.atoms.bg_contrast_25,
{
height: '100%',
paddingTop: INNER_PADDING / 2,
},
]}>
<View
pointerEvents="box-none"
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 4,
paddingBottom: INNER_PADDING / 2,
paddingLeft: INNER_PADDING,
paddingRight: INNER_PADDING / 2,
}}>
<Text type="sm-bold" style={[pal.textLight]}>
<Trans>Suggested for you</Trans>
style={[
a.flex_row,
a.justify_between,
a.align_center,
a.pt_xs,
{
paddingBottom: INNER_PADDING / 2,
paddingLeft: INNER_PADDING,
paddingRight: INNER_PADDING / 2,
},
]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>Similar accounts</Trans>
</Text>
<Pressable
accessibilityRole="button"
<Button
onPress={requestDismiss}
hitSlop={10}
style={{padding: INNER_PADDING / 2}}>
<FontAwesomeIcon
icon="x"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</Pressable>
label={_(msg`Dismiss`)}
size="xsmall"
variant="ghost"
color="secondary"
shape="round">
<ButtonIcon icon={X} size="sm" />
</Button>
</View>
<ScrollView
@@ -83,187 +119,72 @@ export function ProfileHeaderSuggestedFollows({
showsHorizontalScrollIndicator={isWeb}
persistentScrollbar={true}
scrollIndicatorInsets={{bottom: 0}}
scrollEnabled={true}
contentContainerStyle={{
alignItems: 'flex-start',
paddingLeft: INNER_PADDING / 2,
paddingBottom: INNER_PADDING,
}}>
{isLoading ? (
<>
<SuggestedFollowSkeleton />
<SuggestedFollowSkeleton />
<SuggestedFollowSkeleton />
<SuggestedFollowSkeleton />
<SuggestedFollowSkeleton />
<SuggestedFollowSkeleton />
</>
) : data ? (
data.suggestions
.filter(s => (s.associated?.labeler ? false : true))
.map(profile => (
<SuggestedFollow key={profile.did} profile={profile} />
))
) : (
<View />
)}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_sm.gap}
decelerationRate="fast">
<View
style={[
a.flex_row,
a.gap_sm,
{
paddingHorizontal: INNER_PADDING,
paddingBottom: INNER_PADDING,
},
]}>
{isLoading ? (
<>
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
</>
) : data ? (
data.suggestions
.filter(s => (s.associated?.labeler ? false : true))
.map(profile => (
<ProfileCard.Link
key={profile.did}
profile={profile}
onPress={() => {
logEvent('profile:header:suggestedFollowsCard:press', {})
}}
style={[a.flex_1]}>
{({hovered, pressed}) => (
<CardOuter
style={[
a.flex_1,
(hovered || pressed) && t.atoms.border_contrast_high,
]}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="ProfileHeaderSuggestedFollows"
color="secondary_inverted"
shape="round"
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} />
</ProfileCard.Outer>
</CardOuter>
)}
</ProfileCard.Link>
))
) : (
<View />
)}
</View>
</ScrollView>
</View>
</View>
)
}
function SuggestedFollowSkeleton() {
const pal = usePalette('default')
return (
<View
style={[
styles.suggestedFollowCardOuter,
{
backgroundColor: pal.view.backgroundColor,
},
]}>
<View
style={{
height: 60,
width: 60,
borderRadius: 60,
backgroundColor: pal.viewLight.backgroundColor,
opacity: 0.6,
}}
/>
<View
style={{
height: 17,
width: 70,
borderRadius: 4,
backgroundColor: pal.viewLight.backgroundColor,
marginTop: 12,
marginBottom: 4,
}}
/>
<View
style={{
height: 12,
width: 70,
borderRadius: 4,
backgroundColor: pal.viewLight.backgroundColor,
marginBottom: 12,
opacity: 0.6,
}}
/>
<View
style={{
height: 32,
borderRadius: 32,
width: '100%',
backgroundColor: pal.viewLight.backgroundColor,
}}
/>
</View>
)
}
function SuggestedFollow({
profile: profileUnshadowed,
}: {
profile: AppBskyActorDefs.ProfileView
}) {
const {track} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const profile = useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'ProfileHeaderSuggestedFollows',
)
const onPressFollow = React.useCallback(async () => {
try {
track('ProfileHeader:SuggestedFollowFollowed')
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}, [queueFollow, track, _])
const onPressUnfollow = React.useCallback(async () => {
try {
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}, [queueUnfollow, _])
if (!moderationOpts) {
return null
}
const moderation = moderateProfile(profile, moderationOpts)
const following = profile.viewer?.following
return (
<Link
href={makeProfileLink(profile)}
title={profile.handle}
asAnchor
anchorNoUnderline>
<View
style={[
styles.suggestedFollowCardOuter,
{
backgroundColor: pal.view.backgroundColor,
},
]}>
<PreviewableUserAvatar
size={60}
profile={profile}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
/>
<View style={{width: '100%', paddingVertical: 12}}>
<Text
type="xs-medium"
style={[pal.text, {textAlign: 'center'}]}
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<Text
type="xs-medium"
style={[pal.textLight, {textAlign: 'center'}]}
numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
<Button
label={following ? _(msg`Unfollow`) : _(msg`Follow`)}
type="inverted"
labelStyle={{textAlign: 'center'}}
onPress={following ? onPressUnfollow : onPressFollow}
/>
</View>
</Link>
)
}
const styles = StyleSheet.create({
suggestedFollowCardOuter: {
marginHorizontal: INNER_PADDING / 2,
paddingTop: 10,
paddingBottom: 12,
paddingHorizontal: 10,
borderRadius: 8,
width: 130,
alignItems: 'center',
overflow: 'hidden',
flexShrink: 1,
},
})
+2 -10
View File
@@ -8,7 +8,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette'
import {
@@ -179,7 +178,6 @@ let UserAvatar = ({
const pal = usePalette('default')
const backgroundColor = pal.colors.backgroundLight
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
const gate = useGate()
const aviStyle = useMemo(() => {
if (finalShape === 'square') {
@@ -223,10 +221,7 @@ let UserAvatar = ({
style={aviStyle}
resizeMode="cover"
source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
uri: hackModifyThumbnailPath(avatar, size < 90),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
@@ -236,10 +231,7 @@ let UserAvatar = ({
style={aviStyle}
contentFit="cover"
source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
uri: hackModifyThumbnailPath(avatar, size < 90),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
+3 -71
View File
@@ -1,39 +1,19 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {clamp} from 'lodash'
import {isWeb} from '#/platform/detection'
import {
RQKEY as POST_THREAD_RQKEY,
ThreadNode,
} from '#/state/queries/post-thread'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useMinimalShellFabTransform} from 'lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {s} from 'lib/styles'
import {ComposePrompt} from 'view/com/composer/Prompt'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) {
const queryClient = useQueryClient()
const {hasSession} = useSession()
const fabMinimalShellTransform = useMinimalShellFabTransform()
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
const safeAreaInsets = useSafeAreaInsets()
const {name, rkey} = route.params
const {isMobile} = useWebMediaQueries()
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const [canReply, setCanReply] = React.useState(false)
useFocusEffect(
React.useCallback(() => {
@@ -41,59 +21,11 @@ export function PostThreadScreen({route}: Props) {
}, [setMinimalShellMode]),
)
const onPressReply = React.useCallback(() => {
if (!uri) {
return
}
const thread = queryClient.getQueryData<ThreadNode>(POST_THREAD_RQKEY(uri))
if (thread?.type !== 'post') {
return
}
openComposer({
replyTo: {
uri: thread.post.uri,
cid: thread.post.cid,
text: thread.record.text,
author: thread.post.author,
embed: thread.post.embed,
},
onPost: () =>
queryClient.invalidateQueries({
queryKey: POST_THREAD_RQKEY(uri),
}),
})
}, [openComposer, queryClient, uri])
return (
<View style={s.hContentRegion}>
<View style={s.flex1}>
<PostThreadComponent
uri={uri}
onPressReply={onPressReply}
onCanReply={setCanReply}
/>
<PostThreadComponent uri={uri} />
</View>
{isMobile && canReply && hasSession && (
<Animated.View
style={[
styles.prompt,
fabMinimalShellTransform,
{
bottom: clamp(safeAreaInsets.bottom, 15, 30),
},
]}>
<ComposePrompt onPressCompose={onPressReply} />
</Animated.View>
)}
</View>
)
}
const styles = StyleSheet.create({
prompt: {
// @ts-ignore web-only
position: isWeb ? 'fixed' : 'absolute',
left: 0,
right: 0,
},
})
+2 -6
View File
@@ -10,7 +10,6 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -293,7 +292,6 @@ export function Explore() {
error: feedsError,
fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 10})
const gate = useGate()
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
const onLoadMoreProfiles = React.useCallback(async () => {
@@ -499,9 +497,7 @@ export function Explore() {
profile={item.profile}
noBg
noBorder
showKnownFollowers={gate(
'explore_page_profile_card_social_proof',
)}
showKnownFollowers
/>
</View>
)
@@ -565,7 +561,7 @@ export function Explore() {
}
}
},
[t, moderationOpts, gate],
[t, moderationOpts],
)
return (
@@ -29,7 +29,6 @@ import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {useGate} from 'lib/statsig/statsig'
import {isNative, isWeb} from 'platform/detection'
import {Deactivated} from '#/screens/Deactivated'
import {Onboarding} from '#/screens/Onboarding'
@@ -51,7 +50,6 @@ function NativeStackNavigator({
screenOptions,
...rest
}: NativeStackNavigatorProps) {
const gate = useGate()
// --- this is copy and pasted from the original native stack navigator ---
const {state, descriptors, navigation, NavigationContent} =
useNavigationBuilder<
@@ -102,12 +100,7 @@ function NativeStackNavigator({
const {showLoggedOut} = useLoggedOutView()
const {setShowLoggedOut} = useLoggedOutViewControls()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
if (
!hasSession &&
(!PWI_ENABLED ||
activeRouteRequiresAuth ||
(isNative && gate('native_pwi_disabled')))
) {
if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || isNative)) {
return <LoggedOut />
}
if (hasSession && currentAccount?.signupQueued) {