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