Merge branch 'bluesky-social:main' into zh

This commit is contained in:
Kuwa Lee 2024-06-22 11:33:58 +08:00 committed by GitHub
commit 21a7d47cdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 5611 additions and 770 deletions

View file

@ -24,6 +24,7 @@ import {
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as InvitesStateProvider} from '#/state/invites'
@ -152,27 +153,29 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</KeyboardProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
)
}

View file

@ -13,6 +13,7 @@ import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as InvitesStateProvider} from '#/state/invites'
@ -135,25 +136,27 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
<A11yProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</A11yProvider>
)
}

View file

@ -312,7 +312,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen name="Feeds" getComponent={() => FeedsScreen} />
<Stack.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title(msg`Feeds`)}}
/>
</>
)
}

View file

@ -1,8 +1,14 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {
@ -11,6 +17,7 @@ import {
useRemoveFeedMutation,
} from '#/state/queries/preferences'
import {sanitizeHandle} from 'lib/strings/handles'
import {precacheFeedFromGeneratorView, precacheList} from 'state/queries/feed'
import {useSession} from 'state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import * as Toast from 'view/com/util/Toast'
@ -20,41 +27,72 @@ import {Button, ButtonIcon} from '#/components/Button'
import {useRichText} from '#/components/hooks/useRichText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Link as InternalLink} from '#/components/Link'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
type Props =
| {
type: 'feed'
view: AppBskyFeedDefs.GeneratorView
}
| {
type: 'list'
view: AppBskyGraphDefs.ListView
}
export function Default(props: Props) {
const {type, view} = props
const displayName = type === 'feed' ? view.displayName : view.name
const purpose = type === 'list' ? view.purpose : undefined
return (
<Link feed={feed}>
<Link label={displayName} {...props}>
<Outer>
<Header>
<Avatar src={feed.avatar} />
<TitleAndByline title={feed.displayName} creator={feed.creator} />
<Action uri={feed.uri} pin />
<Avatar src={view.avatar} />
<TitleAndByline
title={displayName}
creator={view.creator}
type={type}
purpose={purpose}
/>
<Action uri={view.uri} pin type={type} purpose={purpose} />
</Header>
<Description description={feed.description} />
<Likes count={feed.likeCount || 0} />
<Description description={view.description} />
{type === 'feed' && <Likes count={view.likeCount || 0} />}
</Outer>
</Link>
)
}
export function Link({
type,
view,
label,
children,
feed,
}: {
children: React.ReactElement
feed: AppBskyFeedDefs.GeneratorView
}) {
}: Props & Omit<LinkProps, 'to'>) {
const queryClient = useQueryClient()
const href = React.useMemo(() => {
const urip = new AtUri(feed.uri)
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/feed/${urip.rkey}`
}, [feed])
return <InternalLink to={href}>{children}</InternalLink>
return createProfileFeedHref({feed: view})
}, [view])
return (
<InternalLink
to={href}
label={label}
onPress={() => {
if (type === 'feed') {
precacheFeedFromGeneratorView(queryClient, view)
} else {
precacheList(queryClient, view)
}
}}>
{children}
</InternalLink>
)
}
export function Outer({children}: {children: React.ReactNode}) {
@ -62,34 +100,100 @@ export function Outer({children}: {children: React.ReactNode}) {
}
export function Header({children}: {children: React.ReactNode}) {
return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View>
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_md]}>
{children}
</View>
)
}
export function Avatar({src}: {src: string | undefined}) {
return <UserAvatar type="algo" size={40} avatar={src} />
export type AvatarProps = {src: string | undefined; size?: number}
export function Avatar({src, size = 40}: AvatarProps) {
return <UserAvatar type="algo" size={size} avatar={src} />
}
export function AvatarPlaceholder({size = 40}: Omit<AvatarProps, 'src'>) {
const t = useTheme()
return (
<View
style={[
t.atoms.bg_contrast_25,
{
width: size,
height: size,
borderRadius: 8,
},
]}
/>
)
}
export function TitleAndByline({
title,
creator,
type,
purpose,
}: {
title: string
creator: AppBskyActorDefs.ProfileViewBasic
creator?: AppBskyActorDefs.ProfileViewBasic
type: 'feed' | 'list'
purpose?: AppBskyGraphDefs.ListView['purpose']
}) {
const t = useTheme()
return (
<View style={[a.flex_1]}>
<Text
style={[a.text_md, a.font_bold, a.flex_1, a.leading_snug]}
numberOfLines={1}>
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
{title}
</Text>
<Text
style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
</Text>
{creator && (
<Text
style={[a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{type === 'list' && purpose === 'app.bsky.graph.defs#curatelist' ? (
<Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans>
) : type === 'list' && purpose === 'app.bsky.graph.defs#modlist' ? (
<Trans>
Moderation list by {sanitizeHandle(creator.handle, '@')}
</Trans>
) : (
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
)}
</Text>
)}
</View>
)
}
export function TitleAndBylinePlaceholder({creator}: {creator?: boolean}) {
const t = useTheme()
return (
<View style={[a.flex_1, a.gap_xs]}>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
{
width: '60%',
height: 14,
},
]}
/>
{creator && (
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
width: '40%',
height: 10,
},
]}
/>
)}
</View>
)
}
@ -116,13 +220,31 @@ export function Likes({count}: {count: number}) {
)
}
export function Action({uri, pin}: {uri: string; pin?: boolean}) {
export function Action({
uri,
pin,
type,
purpose,
}: {
uri: string
pin?: boolean
type: 'feed' | 'list'
purpose?: AppBskyGraphDefs.ListView['purpose']
}) {
const {hasSession} = useSession()
if (!hasSession) return null
return <ActionInner uri={uri} pin={pin} />
if (!hasSession || purpose !== 'app.bsky.graph.defs#curatelist') return null
return <ActionInner uri={uri} pin={pin} type={type} />
}
function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
function ActionInner({
uri,
pin,
type,
}: {
uri: string
pin?: boolean
type: 'feed' | 'list'
}) {
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} =
@ -130,9 +252,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation()
const savedFeedConfig = React.useMemo(() => {
return preferences?.savedFeeds?.find(
feed => feed.type === 'feed' && feed.value === uri,
)
return preferences?.savedFeeds?.find(feed => feed.value === uri)
}, [preferences?.savedFeeds, uri])
const removePromptControl = Prompt.usePromptControl()
const isPending = isAddSavedFeedPending || isRemovePending
@ -148,7 +268,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
} else {
await saveFeeds([
{
type: 'feed',
type,
value: uri,
pinned: pin || false,
},
@ -160,7 +280,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
Toast.show(_(msg`Failed to update feeds`))
}
},
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig],
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
)
const onPrompRemoveFeed = React.useCallback(
@ -203,3 +323,16 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
</>
)
}
export function createProfileFeedHref({
feed,
}: {
feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
}) {
const urip = new AtUri(feed.uri)
const type = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'list'
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/${type === 'feed' ? 'feed' : 'lists'}/${
urip.rkey
}`
}

View file

@ -64,7 +64,7 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
return props.children
} else {
return (
<View onPointerMove={onPointerMove}>
<View onPointerMove={onPointerMove} style={[a.flex_shrink]}>
<ProfileHoverCardInner {...props} />
</View>
)

View file

@ -73,6 +73,22 @@ export type LogEvents = {
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
}
'discover:showMore': {
feedContext: string
}
'discover:showLess': {
feedContext: string
}
'discover:clickthrough:sampled': {
count: number
}
'discover:engaged:sampled': {
count: number
}
'discover:seen:sampled': {
count: number
}
'composer:gif:open': {}
'composer:gif:select': {}

View file

@ -1,5 +1,6 @@
export type Gate =
// Keep this alphabetic please.
| 'debug_show_feedcontext'
| 'native_pwi_disabled'
| 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button'

View file

@ -115,6 +115,9 @@ const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
'home:feedDisplayed:sampled',
'feed:endReached:sampled',
'feed:refresh:sampled',
'discover:clickthrough:sampled',
'discover:engaged:sampled',
'discover:seen:sampled',
])
const isDownsampledSession = Math.random() < 0.9 // 90% likely

View file

@ -165,7 +165,7 @@ msgstr ""
#~ msgstr "<0>{following} </0><1>following</1>"
#~ msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
#~ msgstr "<0>Scegli i tuoi</0><1>feeds</1><2>consigliati</2>"
#~ msgstr "<0>Scegli i tuoi</0><1>feed/1><2>consigliati</2>"
#~ msgid "<0>Follow some</0><1>Recommended</1><2>Users</2>"
#~ msgstr "<0>Segui alcuni</0><1>utenti</1><2>consigliati</2>"
@ -356,7 +356,7 @@ msgstr "Aggiunto alla lista"
#: src/view/com/feeds/FeedSourceCard.tsx:126
msgid "Added to my feeds"
msgstr "Aggiunto ai miei feeds"
msgstr "Aggiunto ai miei feed"
#: src/view/screens/PreferencesFollowingFeed.tsx:172
msgid "Adjust the number of likes a reply must have to be shown in your feed."
@ -395,7 +395,7 @@ msgstr ""
#: src/screens/Messages/Settings.tsx:62
#: src/screens/Messages/Settings.tsx:65
msgid "Allow new messages from"
msgstr ""
msgstr "Consenti nuovi messaggi da"
#: src/screens/Login/ForgotPasswordForm.tsx:178
#: src/view/com/modals/ChangePassword.tsx:171
@ -936,12 +936,12 @@ msgstr "Conversazione silenziata"
#: src/screens/Messages/List/index.tsx:88
#: src/view/screens/Settings/index.tsx:638
msgid "Chat settings"
msgstr ""
msgstr "Impostazioni messaggi"
#: src/screens/Messages/Settings.tsx:59
#: src/view/screens/Settings/index.tsx:647
msgid "Chat Settings"
msgstr ""
msgstr "Impostazioni messaggi"
#: src/components/dms/ConvoMenu.tsx:84
msgid "Chat unmuted"
@ -1043,7 +1043,7 @@ msgstr ""
#: src/screens/Feeds/NoFollowingFeed.tsx:46
#~ msgid "Click here to add one."
#~ msgstr ""
#~ msgstr "Clicca qui per aggiungerne uno."
#: src/components/TagMenu/index.web.tsx:138
msgid "Click here to open tag menu for {tag}"
@ -1665,14 +1665,14 @@ msgstr "Scoraggia le app dal mostrare il mio account agli utenti disconnessi"
#: src/view/com/posts/FollowingEmptyState.tsx:70
#: src/view/com/posts/FollowingEndOfFeed.tsx:71
msgid "Discover new custom feeds"
msgstr "Scopri nuovi feeds personalizzati"
msgstr "Scopri nuovi feed personalizzati"
#~ msgid "Discover new feeds"
#~ msgstr "Scopri nuovi feeds"
#~ msgstr "Scopri nuovi feed"
#: src/view/screens/Feeds.tsx:794
msgid "Discover New Feeds"
msgstr "Scopri nuovi feeds"
msgstr "Scopri nuovi feed"
#: src/view/com/modals/EditProfile.tsx:193
msgid "Display name"
@ -1831,7 +1831,7 @@ msgstr "Modifica l'elenco di moderazione"
#: src/view/screens/Feeds.tsx:469
#: src/view/screens/SavedFeeds.tsx:93
msgid "Edit My Feeds"
msgstr "Modifica i miei feeds"
msgstr "Modifica i miei feed"
#: src/view/com/modals/EditProfile.tsx:153
msgid "Edit my profile"
@ -1850,7 +1850,7 @@ msgstr "Modifica il Profilo"
#: src/view/com/home/HomeHeaderLayout.web.tsx:76
#: src/view/screens/Feeds.tsx:416
#~ msgid "Edit Saved Feeds"
#~ msgstr "Modifica i feeds memorizzati"
#~ msgstr "Modifica i feed memorizzati"
#: src/view/com/modals/CreateOrEditList.tsx:234
msgid "Edit User List"
@ -1927,7 +1927,7 @@ msgstr "Attiva il contenuto per adulti"
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79
#~ msgid "Enable adult content in your feeds"
#~ msgstr "Abilita i contenuti per adulti nei tuoi feeds"
#~ msgstr "Abilita i contenuti per adulti nei tuoi feed"
#: src/components/dialogs/EmbedConsent.tsx:82
#: src/components/dialogs/EmbedConsent.tsx:89
@ -2202,7 +2202,7 @@ msgstr "Commenti"
#: src/view/shell/Drawer.tsx:493
#: src/view/shell/Drawer.tsx:494
msgid "Feeds"
msgstr "Feeds"
msgstr "Feed"
#~ msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
#~ msgstr "I feed vengono creati dagli utenti per curare i contenuti. Scegli alcuni feed che ritieni interessanti."
@ -2213,7 +2213,7 @@ msgstr "I feed sono algoritmi personalizzati che gli utenti creano con un minimo
#: src/screens/Onboarding/StepTopicalFeeds.tsx:80
#~ msgid "Feeds can be topical as well!"
#~ msgstr "I feeds possono anche avere tematiche!"
#~ msgstr "I feed possono anche avere tematiche!"
#: src/view/com/modals/ChangeHandle.tsx:475
msgid "File Contents"
@ -3214,7 +3214,7 @@ msgstr "Il messaggio è troppo lungo"
#: src/screens/Messages/List/index.tsx:321
msgid "Message settings"
msgstr "Impostazione messaggio"
msgstr "Impostazioni messaggio"
#: src/Navigation.tsx:504
#: src/screens/Messages/List/index.tsx:164
@ -3412,7 +3412,7 @@ msgstr "Il mio Compleanno"
#: src/view/screens/Feeds.tsx:768
msgid "My Feeds"
msgstr "I miei Feeds"
msgstr "I miei Feed"
#: src/view/shell/desktop/LeftNav.tsx:84
msgid "My Profile"
@ -3424,7 +3424,7 @@ msgstr "I miei feed salvati"
#: src/view/screens/Settings/index.tsx:622
msgid "My Saved Feeds"
msgstr "I miei Feeds Salvati"
msgstr "I miei Feed Salvati"
#~ msgid "my-server.com"
#~ msgstr "my-server.com"
@ -3862,7 +3862,7 @@ msgstr "Apre la fotocamera sul dispositivo"
#: src/view/screens/Settings/index.tsx:639
msgid "Opens chat settings"
msgstr ""
msgstr "Apre impostazioni messaggi"
#: src/view/com/composer/Prompt.tsx:27
msgid "Opens composer"
@ -4112,7 +4112,7 @@ msgstr "Fissa su Home"
#: src/view/screens/SavedFeeds.tsx:103
msgid "Pinned Feeds"
msgstr "Feeds Fissi"
msgstr "Feed Fissi"
#: src/view/screens/ProfileList.tsx:289
msgid "Pinned to your feeds"
@ -4380,7 +4380,7 @@ msgstr "Elenchi pubblici e condivisibili di utenti da disattivare o bloccare in
#: src/view/screens/Lists.tsx:66
msgid "Public, shareable lists which can drive feeds."
msgstr "Liste pubbliche e condivisibili che possono impulsare i feeds."
msgstr "Liste pubbliche e condivisibili che possono impulsare i feed."
#: src/view/com/composer/Composer.tsx:462
msgid "Publish post"
@ -4431,7 +4431,7 @@ msgid "Recent Searches"
msgstr "Ricerche recenti"
#~ msgid "Recommended Feeds"
#~ msgstr "Feeds consigliati"
#~ msgstr "Feed consigliati"
#~ msgid "Recommended Users"
#~ msgstr "Utenti consigliati"
@ -4454,7 +4454,7 @@ msgid "Remove"
msgstr "Rimuovi"
#~ msgid "Remove {0} from my feeds?"
#~ msgstr "Rimuovere {0} dai miei feeds?"
#~ msgstr "Rimuovere {0} dai miei feed?"
#: src/view/com/util/AccountDropdownBtn.tsx:22
msgid "Remove account"
@ -4524,14 +4524,14 @@ msgid "Remove repost"
msgstr "Rimuovi la ripubblicazione"
#~ msgid "Remove this feed from my feeds?"
#~ msgstr "Rimuovere questo feed dai miei feeds?"
#~ msgstr "Rimuovere questo feed dai miei feed?"
#: src/view/com/posts/FeedErrorMessage.tsx:210
msgid "Remove this feed from your saved feeds"
msgstr "Rimuovi questo feed dai feed salvati"
#~ msgid "Remove this feed from your saved feeds?"
#~ msgstr "Elimina questo feed dai feeds salvati?"
#~ msgstr "Elimina questo feed dai feed salvati?"
#: src/view/com/modals/ListAddRemoveUsers.tsx:199
#: src/view/com/modals/UserAddRemoveLists.tsx:165
@ -4540,7 +4540,7 @@ msgstr "Elimina dalla lista"
#: src/view/com/feeds/FeedSourceCard.tsx:139
msgid "Removed from my feeds"
msgstr "Rimuovere dai miei feeds"
msgstr "Rimuovere dai miei feed"
#: src/view/com/posts/FeedShutdownMsg.tsx:44
#: src/view/screens/ProfileFeed.tsx:191
@ -5047,7 +5047,7 @@ msgstr "Seleziona il servizio che ospita i tuoi dati."
#: src/screens/Onboarding/StepTopicalFeeds.tsx:100
#~ msgid "Select topical feeds to follow from the list below"
#~ msgstr "Seleziona i feeds con temi da seguire dal seguente elenco"
#~ msgstr "Seleziona i feed con temi da seguire dal seguente elenco"
#: src/screens/Onboarding/StepModeration/index.tsx:63
#~ msgid "Select what you want to see (or not see), and well handle the rest."
@ -6154,7 +6154,7 @@ msgid "This will delete {0} from your muted words. You can always add it back la
msgstr "Questo eliminerà {0} dalle parole disattivate. Puoi sempre aggiungerla nuovamente in seguito."
#~ msgid "This will hide this post from your feeds."
#~ msgstr "Questo nasconderà il post dai tuoi feeds."
#~ msgstr "Questo nasconderà il post dai tuoi feed."
#: src/view/screens/Settings/index.tsx:594
msgid "Thread preferences"
@ -6783,7 +6783,7 @@ msgstr "Che lingue sono utilizzate in questo post?"
#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77
msgid "Which languages would you like to see in your algorithmic feeds?"
msgstr "Quali lingue vorresti vedere negli algoritmi dei tuoi feeds?"
msgstr "Quali lingue vorresti vedere negli algoritmi dei tuoi feed?"
#: src/components/dms/MessagesNUX.tsx:110
#: src/components/dms/MessagesNUX.tsx:124
@ -6901,7 +6901,7 @@ msgstr "Puoi modificarlo in qualsiasi momento."
#: src/screens/Messages/Settings.tsx:111
msgid "You can continue ongoing conversations regardless of which setting you choose."
msgstr ""
msgstr "Puoi proseguire le conversazioni in corso indipendentemente da quale settaggio scegli."
#: src/screens/Login/index.tsx:158
#: src/screens/Login/PasswordUpdatedForm.tsx:33
@ -6982,7 +6982,7 @@ msgstr "Non hai ancora nessuna conversazione. Avviane una!"
#: src/view/com/feeds/ProfileFeedgens.tsx:141
msgid "You have no feeds."
msgstr "Non hai feeds."
msgstr "Non hai feed."
#: src/view/com/lists/MyLists.tsx:90
#: src/view/com/lists/ProfileLists.tsx:145

View file

@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-06-05 11:06+0900\n"
"PO-Revision-Date: 2024-06-19 11:10+0900\n"
"Last-Translator: tkusano\n"
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n"
"Plural-Forms: \n"
@ -37,10 +37,6 @@ msgstr "{0, plural, other {#個のラベルがこのコンテンツに適用さ
msgid "{0, plural, one {# repost} other {# reposts}}"
msgstr "{0, plural, other {#回のリポスト}}"
#: src/components/KnownFollowers.tsx:179
msgid "{0, plural, one {and # other} other {and # others}}"
msgstr ""
#: src/components/ProfileHoverCard/index.web.tsx:376
#: src/screens/Profile/Header/Metrics.tsx:23
msgid "{0, plural, one {follower} other {followers}}"
@ -87,6 +83,26 @@ msgstr "{0}のアバター"
msgid "{count, plural, one {Liked by # user} other {Liked by # users}}"
msgstr "{count, plural, other {#人のユーザーがいいね}}"
#: src/lib/hooks/useTimeAgo.ts:69
msgid "{diff, plural, one {day} other {days}}"
msgstr "{diff, plural, other {日}}"
#: src/lib/hooks/useTimeAgo.ts:64
msgid "{diff, plural, one {hour} other {hours}}"
msgstr "{diff, plural, other {時間}}"
#: src/lib/hooks/useTimeAgo.ts:59
msgid "{diff, plural, one {minute} other {minutes}}"
msgstr "{diff, plural, other {分}}"
#: src/lib/hooks/useTimeAgo.ts:75
msgid "{diff, plural, one {month} other {months}}"
msgstr "{diff, plural, other {ヶ月}}"
#: src/lib/hooks/useTimeAgo.ts:54
msgid "{diffSeconds, plural, one {second} other {seconds}}"
msgstr "{diffSeconds, plural, other {秒}}"
#: src/screens/SignupQueued.tsx:207
msgid "{estimatedTimeHrs, plural, one {hour} other {hours}}"
msgstr "{estimatedTimeHrs, plural, other {時間}}"
@ -114,6 +130,10 @@ msgstr "{likeCount, plural, other {#人のユーザーがいいね}}"
msgid "{numUnreadNotifications} unread"
msgstr "{numUnreadNotifications}件の未読"
#: src/components/NewskieDialog.tsx:75
msgid "{profileName} joined Bluesky {0} ago"
msgstr "{profileName}はBlueskyに{0}前に参加しました"
#: src/view/screens/PreferencesFollowingFeed.tsx:67
msgid "{value, plural, =0 {Show all replies} one {Show replies with at least # like} other {Show replies with at least # likes}}"
msgstr "{value, plural, =0 {すべての返信を表示} other {#個以上のいいねがついた返信を表示}}"
@ -270,6 +290,10 @@ msgstr "フォローしているユーザーのみのデフォルトのフィー
msgid "Add the following DNS record to your domain:"
msgstr "次のDNSレコードをドメインに追加してください"
#: src/components/FeedCard.tsx:173
msgid "Add this feed to your feeds"
msgstr "このフィードをあなたのフィードに追加する"
#: src/view/com/profile/ProfileMenu.tsx:265
#: src/view/com/profile/ProfileMenu.tsx:268
msgid "Add to Lists"
@ -469,6 +493,10 @@ msgstr "この会話から退出しますか?あなたのメッセージはあ
msgid "Are you sure you want to remove {0} from your feeds?"
msgstr "あなたのフィードから{0}を削除してもよろしいですか?"
#: src/components/FeedCard.tsx:190
msgid "Are you sure you want to remove this from your feeds?"
msgstr "本当にこのフィードをあなたのフィードから削除したいですか?"
#: src/view/com/composer/Composer.tsx:630
msgid "Are you sure you'd like to discard this draft?"
msgstr "本当にこの下書きを破棄しますか?"
@ -1092,7 +1120,7 @@ msgstr "{0}として続行(現在サインイン中)"
#: src/view/com/post-thread/PostThreadLoadMore.tsx:52
msgid "Continue thread..."
msgstr ""
msgstr "スレッドの続き…"
#: src/screens/Onboarding/StepInterests/index.tsx:250
#: src/screens/Onboarding/StepProfile/index.tsx:266
@ -1419,6 +1447,10 @@ msgstr "アプリがログアウトしたユーザーに自分のアカウント
msgid "Discover new custom feeds"
msgstr "新しいカスタムフィードを見つける"
#: src/view/screens/Search/Explore.tsx:378
msgid "Discover new feeds"
msgstr "新しいフィードを探す"
#: src/view/screens/Feeds.tsx:794
msgid "Discover New Feeds"
msgstr "新しいフィードを探す"
@ -1534,16 +1566,16 @@ msgstr "例:返信として広告を繰り返し送ってくるユーザー。
msgid "Each code works once. You'll receive more invite codes periodically."
msgstr "それぞれのコードは一回限り有効です。定期的に追加の招待コードをお送りします。"
#: src/view/screens/Feeds.tsx:400
#: src/view/screens/Feeds.tsx:471
msgid "Edit"
msgstr ""
#: src/view/com/lists/ListMembers.tsx:149
msgctxt "action"
msgid "Edit"
msgstr "編集"
#: src/view/screens/Feeds.tsx:400
#: src/view/screens/Feeds.tsx:471
msgid "Edit"
msgstr "編集"
#: src/view/com/util/UserAvatar.tsx:312
#: src/view/com/util/UserBanner.tsx:92
msgid "Edit avatar"
@ -1583,11 +1615,6 @@ msgstr "プロフィールを編集"
msgid "Edit Profile"
msgstr "プロフィールを編集"
#: src/view/com/home/HomeHeaderLayout.web.tsx:76
#: src/view/screens/Feeds.tsx:416
#~ msgid "Edit Saved Feeds"
#~ msgstr "保存されたフィードを編集"
#: src/view/com/modals/CreateOrEditList.tsx:234
msgid "Edit User List"
msgstr "ユーザーリストを編集"
@ -1754,6 +1781,10 @@ msgstr "全員"
msgid "Everybody can reply"
msgstr "誰でも返信可能"
#: src/view/com/threadgate/WhoCanReply.tsx:129
msgid "Everybody can reply."
msgstr "誰でも返信可能です。"
#: src/components/dms/MessagesNUX.tsx:131
#: src/components/dms/MessagesNUX.tsx:134
#: src/screens/Messages/Settings.tsx:75
@ -1857,6 +1888,11 @@ msgstr "メッセージの削除に失敗しました"
msgid "Failed to delete post, please try again"
msgstr "投稿の削除に失敗しました。もう一度お試しください。"
#: src/view/screens/Search/Explore.tsx:414
#: src/view/screens/Search/Explore.tsx:438
msgid "Failed to load feeds preferences"
msgstr "フィードの設定の読み込みに失敗しました"
#: src/components/dialogs/GifSelect.ios.tsx:196
#: src/components/dialogs/GifSelect.tsx:212
msgid "Failed to load GIFs"
@ -1866,6 +1902,15 @@ msgstr "GIFの読み込みに失敗しました"
msgid "Failed to load past messages"
msgstr "過去のメッセージの読み込みに失敗しました"
#: src/view/screens/Search/Explore.tsx:407
#: src/view/screens/Search/Explore.tsx:431
msgid "Failed to load suggested feeds"
msgstr "おすすめのフィードの読み込みに失敗しました"
#: src/view/screens/Search/Explore.tsx:367
msgid "Failed to load suggested follows"
msgstr "おすすめのフォローの読み込みに失敗しました"
#: src/view/com/lightbox/Lightbox.tsx:84
msgid "Failed to save image: {0}"
msgstr "画像の保存に失敗しました:{0}"
@ -1879,6 +1924,14 @@ msgstr "送信に失敗"
msgid "Failed to submit appeal, please try again."
msgstr "異議申し立ての送信に失敗しました。再度試してください。"
#: src/view/com/util/forms/PostDropdownBtn.tsx:180
msgid "Failed to toggle thread mute, please try again"
msgstr "スレッドのミュートの切り替えに失敗しました。再度試してください"
#: src/components/FeedCard.tsx:153
msgid "Failed to update feeds"
msgstr "フィードの更新に失敗しました"
#: src/components/dms/MessagesNUX.tsx:60
#: src/screens/Messages/Settings.tsx:35
msgid "Failed to update settings"
@ -1914,6 +1967,10 @@ msgstr "フィード"
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "フィードはユーザーがプログラミングの専門知識を持って構築するカスタムアルゴリズムです。詳細については、<0/>を参照してください。"
#: src/components/FeedCard.tsx:150
msgid "Feeds updated!"
msgstr "フィードを更新しました!"
#: src/view/com/modals/ChangeHandle.tsx:475
msgid "File Contents"
msgstr "ファイルのコンテンツ"
@ -1996,14 +2053,30 @@ msgstr "アカウントをフォロー"
msgid "Follow Back"
msgstr "フォローバック"
#: src/components/KnownFollowers.tsx:169
msgid "Followed by"
msgstr ""
#: src/view/screens/Search/Explore.tsx:332
msgid "Follow more accounts to get connected to your interests and build your network."
msgstr "もっとたくさんのアカウントをフォローして、興味あることにつながり、ネットワークを広げましょう。"
#: src/view/com/profile/ProfileCard.tsx:227
msgid "Followed by {0}"
msgstr "{0}がフォロー中"
#: src/components/KnownFollowers.tsx:192
msgid "Followed by <0>{0}</0>"
msgstr "<0>{0}</0>がフォロー中"
#: src/components/KnownFollowers.tsx:209
msgid "Followed by <0>{0}</0> and {1, plural, one {# other} other {# others}}"
msgstr "<0>{0}</0>および{1, plural, other {他#人}}がフォロー中"
#: src/components/KnownFollowers.tsx:181
msgid "Followed by <0>{0}</0> and <1>{1}</1>"
msgstr "<0>{0}</0>と<1>{1}</1>がフォロー中"
#: src/components/KnownFollowers.tsx:168
msgid "Followed by <0>{0}</0>, <1>{1}</1>, and {2, plural, one {# other} other {# others}}"
msgstr "<0>{0}</0>、<1>{1}</1>および{2, plural, other {他#人}}がフォロー中"
#: src/view/com/modals/Threadgate.tsx:99
msgid "Followed users"
msgstr "自分がフォローしているユーザー"
@ -2023,12 +2096,12 @@ msgstr "フォロワー"
#: src/Navigation.tsx:177
msgid "Followers of @{0} that you know"
msgstr ""
msgstr "あなたが知っている@{0}のフォロワー"
#: src/screens/Profile/KnownFollowers.tsx:108
#: src/screens/Profile/KnownFollowers.tsx:118
msgid "Followers you know"
msgstr ""
msgstr "あなたが知っているフォロワー"
#: src/components/ProfileHoverCard/index.web.tsx:411
#: src/components/ProfileHoverCard/index.web.tsx:422
@ -2118,6 +2191,10 @@ msgstr "始める"
msgid "Get Started"
msgstr "開始"
#: src/view/com/util/images/ImageHorzList.tsx:35
msgid "GIF"
msgstr "GIF"
#: src/screens/Onboarding/StepProfile/index.tsx:225
msgid "Give your profile a face"
msgstr "プロフィールに顔をつける"
@ -2658,6 +2735,18 @@ msgstr "リスト"
msgid "Lists blocking this user:"
msgstr "このユーザーをブロックしているリスト:"
#: src/view/screens/Search/Explore.tsx:128
msgid "Load more"
msgstr "さらに読み込む"
#: src/view/screens/Search/Explore.tsx:216
msgid "Load more suggested feeds"
msgstr "おすすめのフィードをさらに読み込む"
#: src/view/screens/Search/Explore.tsx:214
msgid "Load more suggested follows"
msgstr "おすすめのフォローをさらに読み込む"
#: src/view/screens/Notifications.tsx:184
msgid "Load new notifications"
msgstr "最新の通知を読み込む"
@ -3062,6 +3151,10 @@ msgctxt "action"
msgid "New Post"
msgstr "新しい投稿"
#: src/components/NewskieDialog.tsx:68
msgid "New user info dialog"
msgstr "新しいユーザー情報ダイアログ"
#: src/view/com/modals/CreateOrEditList.tsx:236
msgid "New User List"
msgstr "新しいユーザーリスト"
@ -3142,7 +3235,7 @@ msgstr "誰からも受け取らない"
#: src/screens/Profile/Sections/Feed.tsx:59
msgid "No posts yet."
msgstr ""
msgstr "まだ投稿がありません。"
#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:101
#: src/view/com/composer/text-input/web/Autocomplete.tsx:195
@ -3236,6 +3329,10 @@ msgstr "通知音"
msgid "Notifications"
msgstr "通知"
#: src/lib/hooks/useTimeAgo.ts:51
msgid "now"
msgstr "今"
#: src/components/dms/MessageItem.tsx:175
msgid "Now"
msgstr "今"
@ -3274,6 +3371,10 @@ msgstr "OK"
msgid "Oldest replies first"
msgstr "古い順に返信を表示"
#: src/lib/hooks/useTimeAgo.ts:81
msgid "on {str}"
msgstr "{str}"
#: src/view/screens/Settings/index.tsx:256
msgid "Onboarding reset"
msgstr "オンボーディングのリセット"
@ -3449,11 +3550,6 @@ msgstr "モデレーションの設定を開く"
msgid "Opens password reset form"
msgstr "パスワードリセットのフォームを開く"
#: src/view/com/home/HomeHeaderLayout.web.tsx:77
#: src/view/screens/Feeds.tsx:417
#~ msgid "Opens screen to edit Saved Feeds"
#~ msgstr "保存されたフィードの編集画面を開く"
#: src/view/screens/Settings/index.tsx:617
msgid "Opens screen with all saved feeds"
msgstr "保存されたすべてのフィードで画面を開く"
@ -3777,7 +3873,7 @@ msgstr "再実行する"
#: src/components/KnownFollowers.tsx:111
msgid "Press to view followers of this account that you also follow"
msgstr ""
msgstr "あなたもフォローしているこのアカウントのフォロワーを見る"
#: src/view/com/lightbox/Lightbox.web.tsx:150
msgid "Previous image"
@ -3975,7 +4071,7 @@ msgstr "リストから削除されました"
#: src/view/com/feeds/FeedSourceCard.tsx:139
msgid "Removed from my feeds"
msgstr "フィードから削除しました"
msgstr "マイフィードから削除しました"
#: src/view/com/posts/FeedShutdownMsg.tsx:44
#: src/view/screens/ProfileFeed.tsx:191
@ -4000,9 +4096,9 @@ msgstr "Discoverで置き換える"
msgid "Replies"
msgstr "返信"
#: src/view/com/threadgate/WhoCanReply.tsx:98
msgid "Replies to this thread are disabled"
msgstr "このスレッドへの返信はできません"
#: src/view/com/threadgate/WhoCanReply.tsx:131
msgid "Replies to this thread are disabled."
msgstr "このスレッドへの返信はできません"
#: src/view/com/composer/Composer.tsx:475
msgctxt "action"
@ -4019,6 +4115,11 @@ msgctxt "description"
msgid "Reply to <0><1/></0>"
msgstr "<0><1/></0>に返信"
#: src/view/com/posts/FeedItem.tsx:437
msgctxt "description"
msgid "Reply to a blocked post"
msgstr "ブロックした投稿への返信"
#: src/components/dms/MessageMenu.tsx:132
#: src/components/dms/MessagesListBlockedFooter.tsx:77
#: src/components/dms/MessagesListBlockedFooter.tsx:84
@ -4926,9 +5027,9 @@ msgstr "このラベラーを登録"
msgid "Subscribe to this list"
msgstr "このリストに登録"
#: src/view/screens/Search/Search.tsx:425
msgid "Suggested Follows"
msgstr "おすすめのフォロー"
#: src/view/screens/Search/Explore.tsx:330
msgid "Suggested accounts"
msgstr "おすすめのアカウント"
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65
msgid "Suggested for you"
@ -5077,7 +5178,7 @@ msgstr "サービス規約は移動しました"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
msgstr "アカウントの無効化に期限はありません。いつでも戻ってこれます。"
msgstr "アカウントの無効化に期限はありません。いつでも戻ってこれます。"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:115
#: src/view/screens/ProfileFeed.tsx:541
@ -5217,7 +5318,7 @@ msgstr "このコンテンツはBlueskyのアカウントがないと閲覧で
#: src/screens/Messages/List/ChatListItem.tsx:213
msgid "This conversation is with a deleted or a deactivated account. Press for options."
msgstr ""
msgstr "削除あるいは無効化されたアカウントとの会話です。押すと選択肢が表示されます。"
#: src/view/screens/Settings/ExportCarDialog.tsx:93
msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost</0>."
@ -5227,12 +5328,6 @@ msgstr "この機能はベータ版です。リポジトリのエクスポート
msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later."
msgstr "現在このフィードにはアクセスが集中しており、一時的にご利用いただけません。時間をおいてもう一度お試しください。"
#: src/screens/Profile/Sections/Feed.tsx:59
#: src/view/screens/ProfileFeed.tsx:471
#: src/view/screens/ProfileList.tsx:729
#~ msgid "This feed is empty!"
#~ msgstr "このフィードは空です!"
#: src/view/com/posts/CustomFeedEmptyState.tsx:37
msgid "This feed is empty! You may need to follow more users or tune your language settings."
msgstr "このフィードは空です!もっと多くのユーザーをフォローするか、言語の設定を調整する必要があるかもしれません。"
@ -5240,7 +5335,7 @@ msgstr "このフィードは空です!もっと多くのユーザーをフォ
#: src/view/screens/ProfileFeed.tsx:471
#: src/view/screens/ProfileList.tsx:729
msgid "This feed is empty."
msgstr ""
msgstr "このフィードは空です。"
#: src/view/com/posts/FeedShutdownMsg.tsx:97
msgid "This feed is no longer online. We are showing <0>Discover</0> instead."
@ -5336,6 +5431,10 @@ msgstr "このユーザーはブロックした<0>{0}</0>リストに含まれ
msgid "This user is included in the <0>{0}</0> list which you have muted."
msgstr "このユーザーはミュートした<0>{0}</0>リストに含まれています。"
#: src/components/NewskieDialog.tsx:50
msgid "This user is new here. Press for more info about when they joined."
msgstr "新しいユーザーです。ここを押すといつ参加したかの情報が表示されます。"
#: src/view/com/profile/ProfileFollows.tsx:87
msgid "This user isn't following anyone."
msgstr "このユーザーは誰もフォローしていません。"
@ -5762,6 +5861,10 @@ msgstr "{0}のアバターを表示"
msgid "View {0}'s profile"
msgstr "{0}のプロフィールを表示"
#: src/components/ProfileHoverCard/index.web.tsx:417
msgid "View blocked user's profile"
msgstr "ブロック中のユーザーのプロフィールを表示"
#: src/view/screens/Log.tsx:52
msgid "View debug entry"
msgstr "デバッグエントリーを表示"
@ -5804,7 +5907,7 @@ msgstr "このフィードにいいねしたユーザーを見る"
#: src/view/com/home/HomeHeaderLayout.web.tsx:78
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:84
msgid "View your feeds and explore more"
msgstr ""
msgstr "フィードを表示し、さらにフィードを探す"
#: src/view/com/modals/LinkWarning.tsx:89
#: src/view/com/modals/LinkWarning.tsx:95
@ -5891,7 +5994,7 @@ msgstr "大変申し訳ありませんが、検索を完了できませんでし
#: src/view/com/composer/Composer.tsx:318
msgid "We're sorry! The post you are replying to has been deleted."
msgstr ""
msgstr "大変申し訳ありません!返信しようとしている投稿は削除されました。"
#: src/components/Lists.tsx:212
#: src/view/screens/NotFound.tsx:48
@ -5899,8 +6002,8 @@ msgid "We're sorry! We can't find the page you were looking for."
msgstr "大変申し訳ありません!お探しのページは見つかりません。"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:330
msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten."
msgstr "大変申し訳ありません!ラベラーは10までしか登録できず、すでに上限に達しています。"
msgid "We're sorry! You can only subscribe to twenty labelers, and you've reached your limit of twenty."
msgstr "大変申し訳ありません!ラベラーは20までしか登録できず、すでに上限に達しています。"
#: src/screens/Deactivated.tsx:128
msgid "Welcome back!"
@ -6047,7 +6150,7 @@ msgstr "あなたはまだだれもフォロワーがいません。"
#: src/screens/Profile/KnownFollowers.tsx:99
msgid "You don't follow any users who follow @{name}."
msgstr ""
msgstr "@{name}をフォローしているユーザーを誰もフォローしていません。"
#: src/view/com/modals/InviteCodes.tsx:67
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."

File diff suppressed because it is too large Load diff

65
src/state/a11y.tsx Normal file
View file

@ -0,0 +1,65 @@
import React from 'react'
import {AccessibilityInfo} from 'react-native'
import {isReducedMotion} from 'react-native-reanimated'
import {isWeb} from '#/platform/detection'
const Context = React.createContext({
reduceMotionEnabled: false,
screenReaderEnabled: false,
})
export function useA11y() {
return React.useContext(Context)
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(() =>
isReducedMotion(),
)
const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false)
React.useEffect(() => {
const reduceMotionChangedSubscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
enabled => {
setReduceMotionEnabled(enabled)
},
)
const screenReaderChangedSubscription = AccessibilityInfo.addEventListener(
'screenReaderChanged',
enabled => {
setScreenReaderEnabled(enabled)
},
)
;(async () => {
const [_reduceMotionEnabled, _screenReaderEnabled] = await Promise.all([
AccessibilityInfo.isReduceMotionEnabled(),
AccessibilityInfo.isScreenReaderEnabled(),
])
setReduceMotionEnabled(_reduceMotionEnabled)
setScreenReaderEnabled(_screenReaderEnabled)
})()
return () => {
reduceMotionChangedSubscription.remove()
screenReaderChangedSubscription.remove()
}
}, [])
const ctx = React.useMemo(() => {
return {
reduceMotionEnabled,
/**
* Always returns true on web. For now, we're using this for mobile a11y,
* so we reset to false on web.
*
* @see https://github.com/necolas/react-native-web/discussions/2072
*/
screenReaderEnabled: isWeb ? false : screenReaderEnabled,
}
}, [reduceMotionEnabled, screenReaderEnabled])
return <Context.Provider value={ctx}>{children}</Context.Provider>
}

View file

@ -4,6 +4,7 @@ import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import throttle from 'lodash.throttle'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {
FeedDescriptor,
@ -34,6 +35,16 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
>(new WeakSet())
const aggregatedStats = React.useRef<AggregatedStats | null>(null)
const throttledFlushAggregatedStats = React.useMemo(
() =>
throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
leading: true, // The outer call is already throttled somewhat.
trailing: true,
}),
[],
)
const sendToFeedNoDelay = React.useCallback(() => {
const proxyAgent = agent.withProxy(
// @ts-ignore TODO need to update withProxy() to support this key -prf
@ -45,12 +56,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
const interactions = Array.from(queue.current).map(toInteraction)
queue.current.clear()
// Send to the feed
proxyAgent.app.bsky.feed
.sendInteractions({interactions})
.catch((e: any) => {
logger.warn('Failed to send feed interactions', {error: e})
})
}, [agent])
// Send to Statsig
if (aggregatedStats.current === null) {
aggregatedStats.current = createAggregatedStats()
}
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
throttledFlushAggregatedStats()
}, [agent, throttledFlushAggregatedStats])
const sendToFeed = React.useMemo(
() =>
@ -149,3 +168,89 @@ function toInteraction(str: string): AppBskyFeedDefs.Interaction {
const [item, event, feedContext] = str.split('|')
return {item, event, feedContext}
}
type AggregatedStats = {
clickthroughCount: number
engagedCount: number
seenCount: number
}
function createAggregatedStats(): AggregatedStats {
return {
clickthroughCount: 0,
engagedCount: 0,
seenCount: 0,
}
}
function sendOrAggregateInteractionsForStats(
stats: AggregatedStats,
interactions: AppBskyFeedDefs.Interaction[],
) {
for (let interaction of interactions) {
switch (interaction.event) {
// Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them.
// This lets us send the feed context together with them.
case 'app.bsky.feed.defs#requestLess': {
logEvent('discover:showLess', {
feedContext: interaction.feedContext ?? '',
})
break
}
case 'app.bsky.feed.defs#requestMore': {
logEvent('discover:showMore', {
feedContext: interaction.feedContext ?? '',
})
break
}
// The rest of the events are aggregated and sent later in batches.
case 'app.bsky.feed.defs#clickthroughAuthor':
case 'app.bsky.feed.defs#clickthroughEmbed':
case 'app.bsky.feed.defs#clickthroughItem':
case 'app.bsky.feed.defs#clickthroughReposter': {
stats.clickthroughCount++
break
}
case 'app.bsky.feed.defs#interactionLike':
case 'app.bsky.feed.defs#interactionQuote':
case 'app.bsky.feed.defs#interactionReply':
case 'app.bsky.feed.defs#interactionRepost':
case 'app.bsky.feed.defs#interactionShare': {
stats.engagedCount++
break
}
case 'app.bsky.feed.defs#interactionSeen': {
stats.seenCount++
break
}
}
}
}
function flushToStatsig(stats: AggregatedStats | null) {
if (stats === null) {
return
}
if (stats.clickthroughCount > 0) {
logEvent('discover:clickthrough:sampled', {
count: stats.clickthroughCount,
})
stats.clickthroughCount = 0
}
if (stats.engagedCount > 0) {
logEvent('discover:engaged:sampled', {
count: stats.engagedCount,
})
stats.engagedCount = 0
}
if (stats.seenCount > 0) {
logEvent('discover:seen:sampled', {
count: stats.seenCount,
})
stats.seenCount = 0
}
}

View file

@ -9,20 +9,24 @@ import {
} from '@atproto/api'
import {
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent, useSession} from '#/state/session'
import {router} from '#/routes'
import {FeedDescriptor} from './post-feed'
import {precacheResolvedUri} from './resolve-uri'
export type FeedSourceFeedInfo = {
type: 'feed'
@ -201,6 +205,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
const agent = useAgent()
const limit = options?.limit || 10
const {data: preferences} = usePreferencesQuery()
const queryClient = useQueryClient()
// Make sure this doesn't invalidate unless really needed.
const selectArgs = useMemo(
@ -225,6 +230,13 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
limit,
cursor: pageParam,
})
// precache feeds
for (const feed of res.data.feeds) {
const hydratedFeed = hydrateFeedGenerator(feed)
precacheFeed(queryClient, hydratedFeed)
}
return res.data
},
initialPageParam: undefined,
@ -449,3 +461,138 @@ export function usePinnedFeedsInfos() {
},
})
}
export type SavedFeedItem =
| {
type: 'feed'
config: AppBskyActorDefs.SavedFeed
view: AppBskyFeedDefs.GeneratorView
}
| {
type: 'list'
config: AppBskyActorDefs.SavedFeed
view: AppBskyGraphDefs.ListView
}
| {
type: 'timeline'
config: AppBskyActorDefs.SavedFeed
view: undefined
}
export function useSavedFeeds() {
const agent = useAgent()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const savedItems = preferences?.savedFeeds ?? []
const queryClient = useQueryClient()
return useQuery({
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems],
placeholderData: previousData => {
return (
previousData || {
count: savedItems.length,
feeds: [],
}
)
},
queryFn: async () => {
const resolvedFeeds = new Map<string, AppBskyFeedDefs.GeneratorView>()
const resolvedLists = new Map<string, AppBskyGraphDefs.ListView>()
const savedFeeds = savedItems.filter(feed => feed.type === 'feed')
const savedLists = savedItems.filter(feed => feed.type === 'list')
let feedsPromise = Promise.resolve()
if (savedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed
.getFeedGenerators({
feeds: savedFeeds.map(f => f.value),
})
.then(res => {
res.data.feeds.forEach(f => {
resolvedFeeds.set(f.uri, f)
})
})
}
const listsPromises = savedLists.map(list =>
agent.app.bsky.graph
.getList({
list: list.value,
limit: 1,
})
.then(res => {
const listView = res.data.list
resolvedLists.set(listView.uri, listView)
}),
)
await Promise.allSettled([feedsPromise, ...listsPromises])
resolvedFeeds.forEach(feed => {
const hydratedFeed = hydrateFeedGenerator(feed)
precacheFeed(queryClient, hydratedFeed)
})
resolvedLists.forEach(list => {
precacheList(queryClient, list)
})
const res: SavedFeedItem[] = savedItems.map(s => {
if (s.type === 'timeline') {
return {
type: 'timeline',
config: s,
view: undefined,
}
}
return {
type: s.type,
config: s,
view:
s.type === 'feed'
? resolvedFeeds.get(s.value)
: resolvedLists.get(s.value),
}
}) as SavedFeedItem[]
return {
count: savedItems.length,
feeds: res,
}
},
})
}
function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) {
precacheResolvedUri(
queryClient,
hydratedFeed.creatorHandle,
hydratedFeed.creatorDid,
)
queryClient.setQueryData<FeedSourceInfo>(
feedSourceInfoQueryKey({uri: hydratedFeed.uri}),
hydratedFeed,
)
}
export function precacheList(
queryClient: QueryClient,
list: AppBskyGraphDefs.ListView,
) {
precacheResolvedUri(queryClient, list.creator.handle, list.creator.did)
queryClient.setQueryData<AppBskyGraphDefs.ListView>(
listQueryKey(list.uri),
list,
)
}
export function precacheFeedFromGeneratorView(
queryClient: QueryClient,
view: AppBskyFeedDefs.GeneratorView,
) {
const hydratedFeed = hydrateFeedGenerator(view)
precacheFeed(queryClient, hydratedFeed)
}

View file

@ -1,5 +1,10 @@
import {AppBskyActorDefs, AtUri} from '@atproto/api'
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
import {
QueryClient,
useQuery,
useQueryClient,
UseQueryResult,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@ -50,3 +55,11 @@ export function useResolveDidQuery(didOrHandle: string | undefined) {
enabled: !!didOrHandle,
})
}
export function precacheResolvedUri(
queryClient: QueryClient,
handle: string,
did: string,
) {
queryClient.setQueryData<string>(RQKEY(handle), did)
}

View file

@ -34,13 +34,14 @@ const suggestedFollowsByActorQueryKey = (did: string) => [
did,
]
type SuggestedFollowsOptions = {limit?: number}
type SuggestedFollowsOptions = {limit?: number; subsequentPageLimit?: number}
export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
const {currentAccount} = useSession()
const agent = useAgent()
const moderationOpts = useModerationOpts()
const {data: preferences} = usePreferencesQuery()
const limit = options?.limit || 25
return useInfiniteQuery<
AppBskyActorGetSuggestions.OutputSchema,
@ -54,9 +55,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
queryKey: suggestedFollowsQueryKey(options),
queryFn: async ({pageParam}) => {
const contentLangs = getContentLanguages().join(',')
const maybeDifferentLimit =
options?.subsequentPageLimit && pageParam
? options.subsequentPageLimit
: limit
const res = await agent.app.bsky.actor.getSuggestions(
{
limit: options?.limit || 25,
limit: maybeDifferentLimit,
cursor: pageParam,
},
{

View file

@ -19,6 +19,7 @@ import {
import {getInitialState, reducer} from './reducer'
export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging'
export type {SessionAccount} from '#/state/session/types'
import {SessionApiContext, SessionStateContext} from '#/state/session/types'
@ -40,9 +41,11 @@ const ApiContext = React.createContext<SessionApiContext>({
export function Provider({children}: React.PropsWithChildren<{}>) {
const cancelPendingTask = useOneTaskAtATime()
const [state, dispatch] = React.useReducer(reducer, null, () =>
getInitialState(persisted.get('session').accounts),
)
const [state, dispatch] = React.useReducer(reducer, null, () => {
const initialState = getInitialState(persisted.get('session').accounts)
addSessionDebugLog({type: 'reducer:init', state: initialState})
return initialState
})
const onAgentSessionChange = React.useCallback(
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
@ -63,6 +66,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
async params => {
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
const signal = cancelPendingTask()
track('Try Create Account')
logEvent('account:create:begin', {})
@ -81,12 +85,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
})
track('Create Account')
logEvent('account:create:success', {})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
},
[onAgentSessionChange, cancelPendingTask],
)
const login = React.useCallback<SessionApiContext['login']>(
async (params, logContext) => {
addSessionDebugLog({type: 'method:start', method: 'login'})
const signal = cancelPendingTask()
const {agent, account} = await createAgentAndLogin(
params,
@ -103,23 +109,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
})
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
[onAgentSessionChange, cancelPendingTask],
)
const logout = React.useCallback<SessionApiContext['logout']>(
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
dispatch({
type: 'logged-out',
})
logEvent('account:loggedOut', {logContext})
addSessionDebugLog({type: 'method:end', method: 'logout'})
},
[cancelPendingTask],
)
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
async storedAccount => {
addSessionDebugLog({
type: 'method:start',
method: 'resumeSession',
account: storedAccount,
})
const signal = cancelPendingTask()
const {agent, account} = await createAgentAndResume(
storedAccount,
@ -134,17 +148,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
newAgent: agent,
newAccount: account,
})
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
},
[onAgentSessionChange, cancelPendingTask],
)
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => {
addSessionDebugLog({
type: 'method:start',
method: 'removeAccount',
account,
})
cancelPendingTask()
dispatch({
type: 'removed-account',
accountDid: account.did,
})
addSessionDebugLog({type: 'method:end', method: 'removeAccount', account})
},
[cancelPendingTask],
)
@ -152,18 +173,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
React.useEffect(() => {
if (state.needsPersist) {
state.needsPersist = false
persisted.write('session', {
const persistedData = {
accounts: state.accounts,
currentAccount: state.accounts.find(
a => a.did === state.currentAgentState.did,
),
})
}
addSessionDebugLog({type: 'persisted:broadcast', data: persistedData})
persisted.write('session', persistedData)
}
}, [state])
React.useEffect(() => {
return persisted.onUpdate(() => {
const synced = persisted.get('session')
addSessionDebugLog({type: 'persisted:receive', data: synced})
dispatch({
type: 'synced-accounts',
syncedAccounts: synced.accounts,
@ -177,7 +201,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSession(syncedAccount)
} else {
const agent = state.currentAgentState.agent as BskyAgent
const prevSession = agent.session
agent.session = sessionAccountToSession(syncedAccount)
addSessionDebugLog({
type: 'agent:patch',
agent,
prevSession,
nextSession: agent.session,
})
}
}
})
@ -215,6 +246,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// Read the previous value and immediately advance the pointer.
const prevAgent = currentAgentRef.current
currentAgentRef.current = agent
addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent})
// We never reuse agents so let's fully neutralize the previous one.
// This ensures it won't try to consume any refresh tokens.
prevAgent.session = undefined

View file

@ -0,0 +1,137 @@
import {AtpSessionData} from '@atproto/api'
import {sha256} from 'js-sha256'
import {Statsig} from 'statsig-react-native-expo'
import {Schema} from '../persisted'
import {Action, State} from './reducer'
import {SessionAccount} from './types'
type Reducer = (state: State, action: Action) => State
type Log =
| {
type: 'reducer:init'
state: State
}
| {
type: 'reducer:call'
action: Action
prevState: State
nextState: State
}
| {
type: 'method:start'
method:
| 'createAccount'
| 'login'
| 'logout'
| 'resumeSession'
| 'removeAccount'
account?: SessionAccount
}
| {
type: 'method:end'
method:
| 'createAccount'
| 'login'
| 'logout'
| 'resumeSession'
| 'removeAccount'
account?: SessionAccount
}
| {
type: 'persisted:broadcast'
data: Schema['session']
}
| {
type: 'persisted:receive'
data: Schema['session']
}
| {
type: 'agent:switch'
prevAgent: object
nextAgent: object
}
| {
type: 'agent:patch'
agent: object
prevSession: AtpSessionData | undefined
nextSession: AtpSessionData
}
export function wrapSessionReducerForLogging(reducer: Reducer): Reducer {
return function loggingWrapper(prevState: State, action: Action): State {
const nextState = reducer(prevState, action)
addSessionDebugLog({type: 'reducer:call', prevState, action, nextState})
return nextState
}
}
let nextMessageIndex = 0
const MAX_SLICE_LENGTH = 1000
export function addSessionDebugLog(log: Log) {
try {
if (!Statsig.initializeCalled() || !Statsig.getStableID()) {
// Drop these logs for now.
return
}
if (!Statsig.checkGate('debug_session')) {
return
}
const messageIndex = nextMessageIndex++
const {type, ...content} = log
let payload = JSON.stringify(content, replacer)
let nextSliceIndex = 0
while (payload.length > 0) {
const sliceIndex = nextSliceIndex++
const slice = payload.slice(0, MAX_SLICE_LENGTH)
payload = payload.slice(MAX_SLICE_LENGTH)
Statsig.logEvent('session:debug', null, {
realmId,
messageIndex: String(messageIndex),
messageType: type,
sliceIndex: String(sliceIndex),
slice,
})
}
} catch (e) {
console.error(e)
}
}
let agentIds = new WeakMap<object, string>()
let realmId = Math.random().toString(36).slice(2)
let nextAgentId = 1
function getAgentId(agent: object) {
let id = agentIds.get(agent)
if (id === undefined) {
id = realmId + '::' + nextAgentId++
agentIds.set(agent, id)
}
return id
}
function replacer(key: string, value: unknown) {
if (typeof value === 'object' && value != null && 'api' in value) {
return getAgentId(value)
}
if (
key === 'service' ||
key === 'email' ||
key === 'emailConfirmed' ||
key === 'emailAuthFactor' ||
key === 'pdsUrl'
) {
return undefined
}
if (
typeof value === 'string' &&
(key === 'refreshJwt' || key === 'accessJwt')
) {
return sha256(value)
}
return value
}

View file

@ -1,6 +1,7 @@
import {AtpSessionEvent} from '@atproto/api'
import {createPublicAgent} from './agent'
import {wrapSessionReducerForLogging} from './logging'
import {SessionAccount} from './types'
// A hack so that the reducer can't read anything from the agent.
@ -64,7 +65,7 @@ export function getInitialState(persistedAccounts: SessionAccount[]): State {
}
}
export function reducer(state: State, action: Action): State {
let reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'received-agent-event': {
const {agent, accountDid, refreshedAccount, sessionEvent} = action
@ -166,3 +167,5 @@ export function reducer(state: State, action: Action): State {
}
}
}
reducer = wrapSessionReducerForLogging(reducer)
export {reducer}

View file

@ -24,12 +24,18 @@ import Animated, {
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {
AppBskyFeedDefs,
AppBskyFeedGetPostThread,
BskyAgent,
} from '@atproto/api'
import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {observer} from 'mobx-react-lite'
import {until} from '#/lib/async/until'
import {
createGIFDescription,
parseAltFromGIFDescription,
@ -299,6 +305,17 @@ export const ComposePost = observer(function ComposePost({
langs: toPostLanguages(langPrefs.postLanguage),
})
).uri
try {
await whenAppViewReady(agent, postUri, res => {
const thread = res.data.thread
return AppBskyFeedDefs.isThreadViewPost(thread)
})
} catch (waitErr: any) {
logger.error(waitErr, {
message: `Waiting for app view failed`,
})
// Keep going because the post *was* published.
}
} catch (e: any) {
logger.error(e, {
message: `Composer: create post failed`,
@ -756,6 +773,23 @@ function useKeyboardVerticalOffset() {
return top + 10
}
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
fn: (res: AppBskyFeedGetPostThread.Response) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() =>
agent.app.bsky.feed.getPostThread({
uri,
depth: 0,
}),
)
}
const styles = StyleSheet.create({
topbarInner: {
flexDirection: 'row',

View file

@ -3,7 +3,6 @@ import {
findNodeHandle,
ListRenderItemInfo,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
@ -12,18 +11,17 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {hydrateFeedGenerator} from '#/state/queries/feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {EmptyState} from 'view/com/util/EmptyState'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {FeedSourceCardLoaded} from './FeedSourceCard'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@ -52,7 +50,7 @@ export const ProfileFeedgens = React.forwardRef<
ref,
) {
const {_} = useLingui()
const theme = useTheme()
const t = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled])
const {
@ -79,10 +77,9 @@ export const ProfileFeedgens = React.forwardRef<
items = items.concat([EMPTY])
} else if (data?.pages) {
for (const page of data?.pages) {
items = items.concat(page.feeds.map(feed => hydrateFeedGenerator(feed)))
items = items.concat(page.feeds)
}
}
if (isError && !isEmpty) {
} else if (isError && !isEmpty) {
items = items.concat([LOAD_MORE_ERROR_ITEM])
}
return items
@ -132,48 +129,46 @@ export const ProfileFeedgens = React.forwardRef<
// rendering
// =
const renderItemInner = React.useCallback(
({item, index}: ListRenderItemInfo<any>) => {
if (item === EMPTY) {
return (
<EmptyState
icon="hashtag"
message={_(msg`You have no feeds.`)}
testID="listsEmpty"
/>
)
} else if (item === ERROR_ITEM) {
return (
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING) {
return <FeedLoadingPlaceholder />
}
if (preferences) {
return (
<FeedSourceCardLoaded
feedUri={item.uri}
feed={item}
preferences={preferences}
style={styles.item}
showLikes
hideTopBorder={index === 0 && !isWeb}
/>
)
}
return null
},
[error, refetch, onPressRetryLoadMore, preferences, _],
)
const renderItem = ({item, index}: ListRenderItemInfo<any>) => {
if (item === EMPTY) {
return (
<EmptyState
icon="hashtag"
message={_(msg`You have no feeds.`)}
testID="listsEmpty"
/>
)
} else if (item === ERROR_ITEM) {
return (
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING) {
return <FeedLoadingPlaceholder />
}
if (preferences) {
return (
<View
style={[
(index !== 0 || isWeb) && a.border_t,
t.atoms.border_contrast_low,
a.px_lg,
a.py_lg,
]}>
<FeedCard.Default type="feed" view={item} />
</View>
)
}
return null
}
React.useEffect(() => {
if (enabled && scrollElRef.current) {
@ -189,12 +184,12 @@ export const ProfileFeedgens = React.forwardRef<
ref={scrollElRef}
data={items}
keyExtractor={(item: any) => item._reactKey || item.uri}
renderItem={renderItemInner}
renderItem={renderItem}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
@ -203,9 +198,3 @@ export const ProfileFeedgens = React.forwardRef<
</View>
)
})
const styles = StyleSheet.create({
item: {
paddingHorizontal: 18,
},
})

View file

@ -3,7 +3,6 @@ import {
findNodeHandle,
ListRenderItemInfo,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
@ -12,17 +11,17 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
import {useAnalytics} from 'lib/analytics/analytics'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {EmptyState} from 'view/com/util/EmptyState'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {ListCard} from './ListCard'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@ -48,7 +47,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
ref,
) {
const theme = useTheme()
const t = useTheme()
const {track} = useAnalytics()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false)
@ -166,15 +165,18 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
return <FeedLoadingPlaceholder />
}
return (
<ListCard
list={item}
testID={`list-${item.name}`}
style={styles.item}
noBorder={index === 0 && !isWeb}
/>
<View
style={[
(index !== 0 || isWeb) && a.border_t,
t.atoms.border_contrast_low,
a.px_lg,
a.py_lg,
]}>
<FeedCard.Default type="list" view={item} />
</View>
)
},
[error, refetch, onPressRetryLoadMore, _],
[error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low],
)
React.useEffect(() => {
@ -198,7 +200,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
contentContainerStyle={
isNative && {paddingBottom: headerOffset + 100}
}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
@ -208,9 +210,3 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
)
},
)
const styles = StyleSheet.create({
item: {
paddingHorizontal: 18,
},
})

View file

@ -328,6 +328,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
justifyContent: 'center',
},
btn: {
paddingVertical: 7,

View file

@ -6,6 +6,7 @@ import {
View,
type ViewStyle,
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
@ -19,6 +20,7 @@ import {POST_CTRL_HITSLOP} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles'
import {Shadow} from '#/state/cache/types'
@ -41,6 +43,7 @@ import * as Prompt from '#/components/Prompt'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {formatCount} from '../numeric/format'
import {Text} from '../text/Text'
import * as Toast from '../Toast'
import {RepostButton} from './RepostButton'
let PostCtrls = ({
@ -75,6 +78,7 @@ let PostCtrls = ({
const loggedOutWarningPromptControl = useDialogControl()
const {sendInteraction} = useFeedFeedbackContext()
const playHaptic = useHaptics()
const gate = useGate()
const shouldShowLoggedOutWarning = React.useMemo(() => {
return (
@ -329,6 +333,31 @@ let PostCtrls = ({
timestamp={post.indexedAt}
/>
</View>
{gate('debug_show_feedcontext') && feedContext && (
<Pressable
accessible={false}
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
display: 'flex',
justifyContent: 'center',
}}
onPress={e => {
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
Toast.show(_(msg`Copied to clipboard`))
}}>
<Text
style={{
color: t.palette.contrast_400,
fontSize: 7,
}}>
{feedContext}
</Text>
</Pressable>
)}
</View>
)
}

View file

@ -1,8 +1,6 @@
import React from 'react'
import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
@ -10,12 +8,11 @@ import debounce from 'lodash.debounce'
import {isNative, isWeb} from '#/platform/detection'
import {
getAvatarTypeFromUri,
useFeedSourceInfoQuery,
SavedFeedItem,
useGetPopularFeedsQuery,
useSavedFeeds,
useSearchPopularFeedsMutation,
} from '#/state/queries/feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
@ -28,14 +25,10 @@ import {s} from 'lib/styles'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {FAB} from 'view/com/util/fab/FAB'
import {SearchInput} from 'view/com/util/forms/SearchInput'
import {Link, TextLink} from 'view/com/util/Link'
import {TextLink} from 'view/com/util/Link'
import {List} from 'view/com/util/List'
import {
FeedFeedLoadingPlaceholder,
LoadingPlaceholder,
} from 'view/com/util/LoadingPlaceholder'
import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
@ -47,6 +40,7 @@ import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkl
import hairlineWidth = StyleSheet.hairlineWidth
import {Divider} from '#/components/Divider'
import * as FeedCard from '#/components/FeedCard'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
@ -61,9 +55,8 @@ type FlatlistSlice =
key: string
}
| {
type: 'savedFeedsLoading'
type: 'savedFeedPlaceholder'
key: string
// pendingItems: number,
}
| {
type: 'savedFeedNoResults'
@ -72,8 +65,7 @@ type FlatlistSlice =
| {
type: 'savedFeed'
key: string
feedUri: string
savedFeedConfig: AppBskyActorDefs.SavedFeed
savedFeed: SavedFeedItem
}
| {
type: 'savedFeedsLoadMore'
@ -113,11 +105,11 @@ export function FeedsScreen(_props: Props) {
const [query, setQuery] = React.useState('')
const [isPTR, setIsPTR] = React.useState(false)
const {
data: preferences,
isLoading: isPreferencesLoading,
error: preferencesError,
refetch: refetchPreferences,
} = usePreferencesQuery()
data: savedFeeds,
isPlaceholderData: isSavedFeedsPlaceholder,
error: savedFeedsError,
refetch: refetchSavedFeeds,
} = useSavedFeeds()
const {
data: popularFeeds,
isFetching: isPopularFeedsFetching,
@ -173,11 +165,11 @@ export function FeedsScreen(_props: Props) {
const onPullToRefresh = React.useCallback(async () => {
setIsPTR(true)
await Promise.all([
refetchPreferences().catch(_e => undefined),
refetchSavedFeeds().catch(_e => undefined),
refetchPopularFeeds().catch(_e => undefined),
])
setIsPTR(false)
}, [setIsPTR, refetchPreferences, refetchPopularFeeds])
}, [setIsPTR, refetchSavedFeeds, refetchPopularFeeds])
const onEndReached = React.useCallback(() => {
if (
isPopularFeedsFetching ||
@ -203,6 +195,11 @@ export function FeedsScreen(_props: Props) {
const items = React.useMemo(() => {
let slices: FlatlistSlice[] = []
const hasActualSavedCount =
!isSavedFeedsPlaceholder ||
(isSavedFeedsPlaceholder && (savedFeeds?.count || 0) > 0)
const canShowDiscoverSection =
!hasSession || (hasSession && hasActualSavedCount)
if (hasSession) {
slices.push({
@ -210,47 +207,63 @@ export function FeedsScreen(_props: Props) {
type: 'savedFeedsHeader',
})
if (preferencesError) {
if (savedFeedsError) {
slices.push({
key: 'savedFeedsError',
type: 'error',
error: cleanError(preferencesError.toString()),
error: cleanError(savedFeedsError.toString()),
})
} else {
if (isPreferencesLoading || !preferences?.savedFeeds) {
slices.push({
key: 'savedFeedsLoading',
type: 'savedFeedsLoading',
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
})
if (isSavedFeedsPlaceholder && !savedFeeds?.feeds.length) {
/*
* Initial render in placeholder state is 0 on a cold page load,
* because preferences haven't loaded yet.
*
* In practice, `savedFeeds` is always defined, but we check for TS
* and for safety.
*
* In both cases, we show 4 as the the loading state.
*/
const min = 8
const count = savedFeeds
? savedFeeds.count === 0
? min
: savedFeeds.count
: min
Array(count)
.fill(0)
.forEach((_, i) => {
slices.push({
key: 'savedFeedPlaceholder' + i,
type: 'savedFeedPlaceholder',
})
})
} else {
if (preferences.savedFeeds?.length) {
const noFollowingFeed = preferences.savedFeeds.every(
if (savedFeeds?.feeds?.length) {
const noFollowingFeed = savedFeeds.feeds.every(
f => f.type !== 'timeline',
)
slices = slices.concat(
preferences.savedFeeds
.filter(f => {
return f.pinned
savedFeeds.feeds
.filter(s => {
return s.config.pinned
})
.map(feed => ({
key: `savedFeed:${feed.value}:${feed.id}`,
.map(s => ({
key: `savedFeed:${s.view?.uri}:${s.config.id}`,
type: 'savedFeed',
feedUri: feed.value,
savedFeedConfig: feed,
savedFeed: s,
})),
)
slices = slices.concat(
preferences.savedFeeds
.filter(f => {
return !f.pinned
savedFeeds.feeds
.filter(s => {
return !s.config.pinned
})
.map(feed => ({
key: `savedFeed:${feed.value}:${feed.id}`,
.map(s => ({
key: `savedFeed:${s.view?.uri}:${s.config.id}`,
type: 'savedFeed',
feedUri: feed.value,
savedFeedConfig: feed,
savedFeed: s,
})),
)
@ -270,59 +283,36 @@ export function FeedsScreen(_props: Props) {
}
}
slices.push({
key: 'popularFeedsHeader',
type: 'popularFeedsHeader',
})
if (popularFeedsError || searchError) {
if (!hasSession || (hasSession && canShowDiscoverSection)) {
slices.push({
key: 'popularFeedsError',
type: 'error',
error: cleanError(
popularFeedsError?.toString() ?? searchError?.toString() ?? '',
),
key: 'popularFeedsHeader',
type: 'popularFeedsHeader',
})
} else {
if (isUserSearching) {
if (isSearchPending || !searchResults) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (!searchResults || searchResults?.length === 0) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
slices = slices.concat(
searchResults.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
feed,
})),
)
}
}
if (popularFeedsError || searchError) {
slices.push({
key: 'popularFeedsError',
type: 'error',
error: cleanError(
popularFeedsError?.toString() ?? searchError?.toString() ?? '',
),
})
} else {
if (isPopularFeedsFetching && !popularFeeds?.pages) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (!popularFeeds?.pages) {
if (isUserSearching) {
if (isSearchPending || !searchResults) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
for (const page of popularFeeds.pages || []) {
if (!searchResults || searchResults?.length === 0) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
slices = slices.concat(
page.feeds.map(feed => ({
searchResults.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
@ -330,12 +320,37 @@ export function FeedsScreen(_props: Props) {
})),
)
}
if (isPopularFeedsFetchingNextPage) {
}
} else {
if (isPopularFeedsFetching && !popularFeeds?.pages) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (!popularFeeds?.pages) {
slices.push({
key: 'popularFeedsLoadingMore',
type: 'popularFeedsLoadingMore',
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
for (const page of popularFeeds.pages || []) {
slices = slices.concat(
page.feeds.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
feed,
})),
)
}
if (isPopularFeedsFetchingNextPage) {
slices.push({
key: 'popularFeedsLoadingMore',
type: 'popularFeedsLoadingMore',
})
}
}
}
}
@ -345,9 +360,9 @@ export function FeedsScreen(_props: Props) {
return slices
}, [
hasSession,
preferences,
isPreferencesLoading,
preferencesError,
savedFeeds,
isSavedFeedsPlaceholder,
savedFeedsError,
popularFeeds,
isPopularFeedsFetching,
popularFeedsError,
@ -407,10 +422,7 @@ export function FeedsScreen(_props: Props) {
({item}: {item: FlatlistSlice}) => {
if (item.type === 'error') {
return <ErrorMessage message={item.error} />
} else if (
item.type === 'popularFeedsLoadingMore' ||
item.type === 'savedFeedsLoading'
) {
} else if (item.type === 'popularFeedsLoadingMore') {
return (
<View style={s.p10}>
<ActivityIndicator size="large" />
@ -459,8 +471,10 @@ export function FeedsScreen(_props: Props) {
<NoSavedFeedsOfAnyType />
</View>
)
} else if (item.type === 'savedFeedPlaceholder') {
return <SavedFeedPlaceholder />
} else if (item.type === 'savedFeed') {
return <FeedOrFollowing savedFeedConfig={item.savedFeedConfig} />
return <FeedOrFollowing savedFeed={item.savedFeed} />
} else if (item.type === 'popularFeedsHeader') {
return (
<>
@ -481,7 +495,7 @@ export function FeedsScreen(_props: Props) {
} else if (item.type === 'popularFeed') {
return (
<View style={[a.px_lg, a.pt_lg, a.gap_lg]}>
<FeedCard.Default feed={item.feed} />
<FeedCard.Default type="feed" view={item.feed} />
<Divider />
</View>
)
@ -571,136 +585,106 @@ export function FeedsScreen(_props: Props) {
)
}
function FeedOrFollowing({
savedFeedConfig: feed,
}: {
savedFeedConfig: AppBskyActorDefs.SavedFeed
}) {
return feed.type === 'timeline' ? (
function FeedOrFollowing({savedFeed}: {savedFeed: SavedFeedItem}) {
return savedFeed.type === 'timeline' ? (
<FollowingFeed />
) : (
<SavedFeed savedFeedConfig={feed} />
<SavedFeed savedFeed={savedFeed} />
)
}
function FollowingFeed() {
const pal = usePalette('default')
const t = useTheme()
const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
return (
<View
testID={`saved-feed-timeline`}
style={[
pal.border,
styles.savedFeed,
isMobile && styles.savedFeedMobile,
a.flex_1,
a.px_lg,
a.py_md,
a.border_b,
t.atoms.border_contrast_low,
]}>
<View
style={[
a.align_center,
a.justify_center,
{
width: 28,
height: 28,
borderRadius: 3,
backgroundColor: t.palette.primary_500,
},
]}>
<FilterTimeline
<FeedCard.Header>
<View
style={[
a.align_center,
a.justify_center,
{
width: 18,
height: 18,
width: 28,
height: 28,
borderRadius: 3,
backgroundColor: t.palette.primary_500,
},
]}
fill={t.palette.white}
/>
</View>
<View
style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
<Text type="lg-medium" style={pal.text} numberOfLines={1}>
<Trans>Following</Trans>
</Text>
</View>
]}>
<FilterTimeline
style={[
{
width: 18,
height: 18,
},
]}
fill={t.palette.white}
/>
</View>
<FeedCard.TitleAndByline title={_(msg`Following`)} type="feed" />
</FeedCard.Header>
</View>
)
}
function SavedFeed({
savedFeedConfig: feed,
savedFeed,
}: {
savedFeedConfig: AppBskyActorDefs.SavedFeed
savedFeed: SavedFeedItem & {type: 'feed' | 'list'}
}) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const {data: info, error} = useFeedSourceInfoQuery({uri: feed.value})
const typeAvatar = getAvatarTypeFromUri(feed.value)
if (!info)
return (
<SavedFeedLoadingPlaceholder
key={`savedFeedLoadingPlaceholder:${feed.value}`}
/>
)
const t = useTheme()
const {view: feed} = savedFeed
const displayName =
savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name
return (
<Link
testID={`saved-feed-${info.displayName}`}
href={info.route.href}
style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]}
hoverStyle={pal.viewLight}
accessibilityLabel={info.displayName}
accessibilityHint=""
asAnchor
anchorNoUnderline>
{error ? (
<FeedCard.Link testID={`saved-feed-${feed.displayName}`} {...savedFeed}>
{({hovered, pressed}) => (
<View
style={{width: 28, flexDirection: 'row', justifyContent: 'center'}}>
<FontAwesomeIcon
icon="exclamation-circle"
color={pal.colors.textLight}
/>
</View>
) : (
<UserAvatar type={typeAvatar} size={28} avatar={info.avatar} />
)}
<View
style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
<Text type="lg-medium" style={pal.text} numberOfLines={1}>
{info.displayName}
</Text>
{error ? (
<View style={[styles.offlineSlug, pal.borderDark]}>
<Text type="xs" style={pal.textLight}>
<Trans>Feed offline</Trans>
</Text>
</View>
) : null}
</View>
style={[
a.flex_1,
a.px_lg,
a.py_md,
a.border_b,
t.atoms.border_contrast_low,
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<FeedCard.Header>
<FeedCard.Avatar src={feed.avatar} size={28} />
<FeedCard.TitleAndByline
title={displayName}
type={savedFeed.type}
/>
{isMobile && (
<FontAwesomeIcon
icon="chevron-right"
size={14}
style={pal.textLight as FontAwesomeIconStyle}
/>
<ChevronRight size="sm" fill={t.atoms.text_contrast_low.color} />
</FeedCard.Header>
</View>
)}
</Link>
</FeedCard.Link>
)
}
function SavedFeedLoadingPlaceholder() {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
function SavedFeedPlaceholder() {
const t = useTheme()
return (
<View
style={[
pal.border,
styles.savedFeed,
isMobile && styles.savedFeedMobile,
a.flex_1,
a.px_lg,
a.py_md,
a.border_b,
t.atoms.border_contrast_low,
]}>
<LoadingPlaceholder width={28} height={28} style={{borderRadius: 4}} />
<LoadingPlaceholder width={140} height={12} />
<FeedCard.Header>
<FeedCard.AvatarPlaceholder size={28} />
<FeedCard.TitleAndBylinePlaceholder />
</FeedCard.Header>
</View>
)
}

View file

@ -282,7 +282,7 @@ export function Explore() {
isFetchingNextPage: isFetchingNextProfilesPage,
error: profilesError,
fetchNextPage: fetchNextProfilesPage,
} = useSuggestedFollowsQuery({limit: 3})
} = useSuggestedFollowsQuery({limit: 6, subsequentPageLimit: 10})
const {
data: feeds,
hasNextPage: hasNextFeedsPage,
@ -290,7 +290,7 @@ export function Explore() {
isFetchingNextPage: isFetchingNextFeedsPage,
error: feedsError,
fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 3})
} = useGetPopularFeedsQuery({limit: 10})
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
const onLoadMoreProfiles = React.useCallback(async () => {
@ -340,11 +340,12 @@ export function Explore() {
// Currently the responses contain duplicate items.
// Needs to be fixed on backend, but let's dedupe to be safe.
let seen = new Set()
const profileItems: ExploreScreenItems[] = []
for (const page of profiles.pages) {
for (const actor of page.actors) {
if (!seen.has(actor.did)) {
seen.add(actor.did)
i.push({
profileItems.push({
type: 'profile',
key: actor.did,
profile: actor,
@ -354,13 +355,19 @@ export function Explore() {
}
if (hasNextProfilesPage) {
// splice off 3 as previews if we have a next page
const previews = profileItems.splice(-3)
// push remainder
i.push(...profileItems)
i.push({
type: 'loadMore',
key: 'loadMoreProfiles',
isLoadingMore: isLoadingMoreProfiles,
onLoadMore: onLoadMoreProfiles,
items: i.filter(item => item.type === 'profile').slice(-3),
items: previews,
})
} else {
i.push(...profileItems)
}
} else {
if (profilesError) {
@ -390,11 +397,12 @@ export function Explore() {
// Currently the responses contain duplicate items.
// Needs to be fixed on backend, but let's dedupe to be safe.
let seen = new Set()
const feedItems: ExploreScreenItems[] = []
for (const page of feeds.pages) {
for (const feed of page.feeds) {
if (!seen.has(feed.uri)) {
seen.add(feed.uri)
i.push({
feedItems.push({
type: 'feed',
key: feed.uri,
feed,
@ -403,6 +411,7 @@ export function Explore() {
}
}
// feeds errors can occur during pagination, so feeds is truthy
if (feedsError) {
i.push({
type: 'error',
@ -418,13 +427,17 @@ export function Explore() {
error: cleanError(preferencesError),
})
} else if (hasNextFeedsPage) {
const preview = feedItems.splice(-3)
i.push(...feedItems)
i.push({
type: 'loadMore',
key: 'loadMoreFeeds',
isLoadingMore: isLoadingMoreFeeds,
onLoadMore: onLoadMoreFeeds,
items: i.filter(item => item.type === 'feed').slice(-3),
items: preview,
})
} else {
i.push(...feedItems)
}
} else {
if (feedsError) {
@ -492,7 +505,7 @@ export function Explore() {
a.px_lg,
a.py_lg,
]}>
<FeedCard.Default feed={item.feed} />
<FeedCard.Default type="feed" view={item.feed} />
</View>
)
}

View file

@ -306,7 +306,7 @@ let SearchScreenFeedsResults = ({
a.px_lg,
a.py_lg,
]}>
<FeedCard.Default feed={item} />
<FeedCard.Default type="feed" view={item} />
</View>
)}
keyExtractor={item => item.uri}