Merge remote-tracking branch 'upstream/main' into invite-code-warning

zio/stable
Samuel Newman 2023-11-30 17:11:51 +00:00
commit b164f151cc
76 changed files with 1718 additions and 1240 deletions

View File

@ -4,7 +4,7 @@ import {openApp, loginAsAlice, createServer} from '../util'
describe('Home screen', () => {
beforeAll(async () => {
await createServer('?users&follows&posts')
await createServer('?users&follows&posts&feeds')
await openApp({permissions: {notifications: 'YES'}})
})
@ -13,6 +13,23 @@ describe('Home screen', () => {
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
it('Can go to feeds page using feeds button in tab bar', async () => {
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
await expect(element(by.text('Discover new feeds'))).toBeVisible()
})
it('Feeds button disappears after pinning a feed', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-4')).tap()
await element(by.id('feed-alice-favs')).tap()
await element(by.id('pinBtn')).tap()
await element(by.id('bottomBarHomeBtn')).tap()
await expect(
element(by.id('homeScreenFeedTabs-Feeds ✨')),
).not.toBeVisible()
})
it('Can like posts', async () => {
const carlaPosts = by.id('feedItem-by-carla.test')
await expect(
@ -65,14 +82,14 @@ describe('Home screen', () => {
it('Can swipe between feeds', async () => {
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
await expect(element(by.id('customFeedPage'))).toBeVisible()
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
await expect(element(by.id('followingFeedPage'))).toBeVisible()
})
it('Can tap between feeds', async () => {
await element(by.id("homeScreenFeedTabs-What's hot")).tap()
await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
await element(by.id('homeScreenFeedTabs-alice-favs')).tap()
await expect(element(by.id('customFeedPage'))).toBeVisible()
await element(by.id('homeScreenFeedTabs-Following')).tap()
await expect(element(by.id('followingFeedPage'))).toBeVisible()
})

View File

@ -1,12 +1,41 @@
const pkg = require('./package.json')
module.exports = function () {
const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN
/**
* App version number. Should be incremented as part of a release cycle.
*/
const VERSION = pkg.version
/**
* iOS build number. Must be incremented for each TestFlight version.
*/
const IOS_BUILD_NUMBER = '5'
/**
* Android build number. Must be incremented for each release.
*/
const ANDROID_VERSION_CODE = 46
/**
* Uses built-in Expo env vars
*
* @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
/**
* Additional granularity for the `dist` field
*/
const DIST_BUILD_NUMBER =
PLATFORM === 'android' ? ANDROID_VERSION_CODE : IOS_BUILD_NUMBER
return {
expo: {
version: VERSION,
name: 'Bluesky',
slug: 'bluesky',
scheme: 'bluesky',
owner: 'blueskysocial',
version: '1.57.0',
runtimeVersion: {
policy: 'appVersion',
},
@ -19,7 +48,7 @@ module.exports = function () {
backgroundColor: '#ffffff',
},
ios: {
buildNumber: '4',
buildNumber: IOS_BUILD_NUMBER,
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
config: {
@ -43,7 +72,7 @@ module.exports = function () {
backgroundColor: '#ffffff',
},
android: {
versionCode: 46,
versionCode: ANDROID_VERSION_CODE,
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
@ -74,7 +103,7 @@ module.exports = function () {
},
plugins: [
'expo-localization',
hasSentryToken && 'sentry-expo',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
[
'expo-build-properties',
{
@ -100,11 +129,16 @@ module.exports = function () {
},
hooks: {
postPublish: [
/*
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/
{
file: 'sentry-expo/upload-sourcemaps',
config: {
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
},
},
],

View File

@ -11,28 +11,19 @@
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"channel": "development",
"ios": {
"simulator": true,
"resourceClass": "large"
},
"channel": "development"
},
"development-device": {
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "large"
},
"channel": "development"
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "preview",
"ios": {
"resourceClass": "large"
},
"channel": "preview"
}
},
"production": {
"extends": "base",
@ -40,14 +31,6 @@
"resourceClass": "large"
},
"channel": "production"
},
"dev-android-apk": {
"extends": "base",
"developmentClient": true,
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease"
}
}
},
"submit": {

View File

@ -3,10 +3,12 @@ import 'react-native-gesture-handler' // must be first
import {LogBox} from 'react-native'
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
import 'platform/polyfills'
import '#/platform/polyfills'
import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill'
doPolyfill()
import App from './src/App'
import App from '#/App'
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,

View File

@ -1,4 +1,7 @@
import 'platform/polyfills'
import '#/platform/polyfills'
import {registerRootComponent} from 'expo'
import App from './src/App'
import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
doPolyfill()
registerRootComponent(App)

View File

@ -28,7 +28,6 @@
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"build:apk": "eas build -p android --profile dev-android-apk",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile"
},

View File

@ -16,14 +16,7 @@ export type FeedTunerFn = (
export class FeedViewPostsSlice {
isFlattenedReply = false
constructor(public items: FeedViewPost[] = []) {}
get _reactKey() {
const rootItem = this.isFlattenedReply ? this.items[1] : this.items[0]
return `slice-${rootItem.post.uri}-${
rootItem.reason?.indexedAt || rootItem.post.indexedAt
}`
}
constructor(public items: FeedViewPost[], public _reactKey: string) {}
get uri() {
if (this.isFlattenedReply) {
@ -118,28 +111,34 @@ export class FeedViewPostsSlice {
}
export class NoopFeedTuner {
reset() {}
private keyCounter = 0
reset() {
this.keyCounter = 0
}
tune(
feed: FeedViewPost[],
_tunerFns: FeedTunerFn[] = [],
_opts?: {dryRun: boolean; maintainOrder: boolean},
): FeedViewPostsSlice[] {
return feed.map(item => new FeedViewPostsSlice([item]))
return feed.map(
item => new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`),
)
}
}
export class FeedTuner {
private keyCounter = 0
seenUris: Set<string> = new Set()
constructor() {}
constructor(public tunerFns: FeedTunerFn[]) {}
reset() {
this.keyCounter = 0
this.seenUris.clear()
}
tune(
feed: FeedViewPost[],
tunerFns: FeedTunerFn[] = [],
{dryRun, maintainOrder}: {dryRun: boolean; maintainOrder: boolean} = {
dryRun: false,
maintainOrder: false,
@ -148,7 +147,9 @@ export class FeedTuner {
let slices: FeedViewPostsSlice[] = []
if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item]))
slices = feed.map(
item => new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`),
)
} else {
// arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) {
@ -164,12 +165,14 @@ export class FeedTuner {
continue
}
}
slices.unshift(new FeedViewPostsSlice([item]))
slices.unshift(
new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`),
)
}
}
// run the custom tuners
for (const tunerFn of tunerFns) {
for (const tunerFn of this.tunerFns) {
slices = tunerFn(this, slices.slice())
}

View File

@ -1,18 +1,15 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class AuthorFeedAPI implements FeedAPI {
constructor(
public agent: BskyAgent,
public params: GetAuthorFeed.QueryParams,
) {}
constructor(public params: GetAuthorFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.getAuthorFeed({
const res = await getAgent().getAuthorFeed({
...this.params,
limit: 1,
})
@ -26,7 +23,7 @@ export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.agent.getAuthorFeed({
const res = await getAgent().getAuthorFeed({
...this.params,
cursor,
limit,

View File

@ -1,18 +1,15 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class CustomFeedAPI implements FeedAPI {
constructor(
public agent: BskyAgent,
public params: GetCustomFeed.QueryParams,
) {}
constructor(public params: GetCustomFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.app.bsky.feed.getFeed({
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
limit: 1,
})
@ -26,7 +23,7 @@ export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.agent.app.bsky.feed.getFeed({
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
cursor,
limit,

View File

@ -1,11 +1,12 @@
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {AppBskyFeedDefs} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class FollowingFeedAPI implements FeedAPI {
constructor(public agent: BskyAgent) {}
constructor() {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.getTimeline({
const res = await getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
@ -18,7 +19,7 @@ export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.agent.getTimeline({
const res = await getAgent().getTimeline({
cursor,
limit,
})

View File

@ -1,18 +1,15 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class LikesFeedAPI implements FeedAPI {
constructor(
public agent: BskyAgent,
public params: GetActorLikes.QueryParams,
) {}
constructor(public params: GetActorLikes.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.getActorLikes({
const res = await getAgent().getActorLikes({
...this.params,
limit: 1,
})
@ -26,7 +23,7 @@ export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.agent.getActorLikes({
const res = await getAgent().getActorLikes({
...this.params,
cursor,
limit,

View File

@ -1,18 +1,15 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetListFeed as GetListFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class ListFeedAPI implements FeedAPI {
constructor(
public agent: BskyAgent,
public params: GetListFeed.QueryParams,
) {}
constructor(public params: GetListFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.app.bsky.feed.getListFeed({
const res = await getAgent().app.bsky.feed.getListFeed({
...this.params,
limit: 1,
})
@ -26,7 +23,7 @@ export class ListFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.agent.app.bsky.feed.getListFeed({
const res = await getAgent().app.bsky.feed.getListFeed({
...this.params,
cursor,
limit,

View File

@ -1,4 +1,4 @@
import {AppBskyFeedDefs, AppBskyFeedGetTimeline, BskyAgent} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedGetTimeline} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {timeout} from 'lib/async/timeout'
import {bundleAsync} from 'lib/async/bundle'
@ -7,6 +7,7 @@ import {FeedTuner} from '../feed-manip'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
@ -18,16 +19,12 @@ export class MergeFeedAPI implements FeedAPI {
itemCursor = 0
sampleCursor = 0
constructor(
public agent: BskyAgent,
public params: FeedParams,
public feedTuners: FeedTunerFn[],
) {
this.following = new MergeFeedSource_Following(this.agent, this.feedTuners)
constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) {
this.following = new MergeFeedSource_Following(this.feedTuners)
}
reset() {
this.following = new MergeFeedSource_Following(this.agent, this.feedTuners)
this.following = new MergeFeedSource_Following(this.feedTuners)
this.customFeeds = [] // just empty the array, they will be captured in _fetchNext()
this.feedCursor = 0
this.itemCursor = 0
@ -35,8 +32,7 @@ export class MergeFeedAPI implements FeedAPI {
if (this.params.mergeFeedEnabled && this.params.mergeFeedSources) {
this.customFeeds = shuffle(
this.params.mergeFeedSources.map(
feedUri =>
new MergeFeedSource_Custom(this.agent, feedUri, this.feedTuners),
feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners),
),
)
} else {
@ -45,7 +41,7 @@ export class MergeFeedAPI implements FeedAPI {
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.agent.getTimeline({
const res = await getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
@ -137,7 +133,7 @@ class MergeFeedSource {
queue: AppBskyFeedDefs.FeedViewPost[] = []
hasMore = true
constructor(public agent: BskyAgent, public feedTuners: FeedTunerFn[]) {}
constructor(public feedTuners: FeedTunerFn[]) {}
get numReady() {
return this.queue.length
@ -184,7 +180,7 @@ class MergeFeedSource {
}
class MergeFeedSource_Following extends MergeFeedSource {
tuner = new FeedTuner()
tuner = new FeedTuner(this.feedTuners)
reset() {
super.reset()
@ -199,9 +195,9 @@ class MergeFeedSource_Following extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await this.agent.getTimeline({cursor, limit})
const res = await getAgent().getTimeline({cursor, limit})
// run the tuner pre-emptively to ensure better mixing
const slices = this.tuner.tune(res.data.feed, this.feedTuners, {
const slices = this.tuner.tune(res.data.feed, {
dryRun: false,
maintainOrder: true,
})
@ -213,20 +209,16 @@ class MergeFeedSource_Following extends MergeFeedSource {
class MergeFeedSource_Custom extends MergeFeedSource {
minDate: Date
constructor(
public agent: BskyAgent,
public feedUri: string,
public feedTuners: FeedTunerFn[],
) {
super(agent, feedTuners)
constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) {
super(feedTuners)
this.sourceInfo = {
$type: 'reasonFeedSource',
displayName: feedUri.split('/').pop() || '',
uri: feedUriToHref(feedUri),
}
this.minDate = new Date(Date.now() - POST_AGE_CUTOFF)
this.agent.app.bsky.feed
.getFeedGenerator({
getAgent()
.app.bsky.feed.getFeedGenerator({
feed: feedUri,
})
.then(
@ -244,7 +236,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
try {
const res = await this.agent.app.bsky.feed.getFeed({
const res = await getAgent().app.bsky.feed.getFeed({
cursor,
limit,
feed: this.feedUri,

View File

@ -116,8 +116,8 @@ export async function DEFAULT_FEEDS(
} else {
// production
return {
pinned: [PROD_DEFAULT_FEED('whats-hot')],
saved: [PROD_DEFAULT_FEED('whats-hot')],
pinned: [],
saved: [],
}
}
}

View File

@ -7,22 +7,35 @@ import {useAnalytics} from '#/lib/analytics/analytics'
import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
export function useAccountSwitcher() {
const {track} = useAnalytics()
const {selectAccount, clearCurrentAccount} = useSessionApi()
const closeAllActiveElements = useCloseAllActiveElements()
const navigation = useNavigation<NavigationProp>()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onPressSwitchAccount = useCallback(
async (acct: SessionAccount) => {
async (account: SessionAccount) => {
track('Settings:SwitchAccountButtonClicked')
try {
closeAllActiveElements()
navigation.navigate(isWeb ? 'Home' : 'HomeTab')
await selectAccount(acct)
Toast.show(`Signed in as ${acct.handle}`)
if (account.accessJwt) {
closeAllActiveElements()
navigation.navigate(isWeb ? 'Home' : 'HomeTab')
await selectAccount(account)
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)
} else {
closeAllActiveElements()
setShowLoggedOut(true)
Toast.show(
`Please sign in as @${account.handle}`,
'circle-exclamation',
)
}
} catch (e) {
Toast.show('Sorry! We need you to enter your password.')
clearCurrentAccount() // back user out to login
@ -34,6 +47,7 @@ export function useAccountSwitcher() {
selectAccount,
closeAllActiveElements,
navigation,
setShowLoggedOut,
],
)

View File

@ -83,7 +83,7 @@ export function init(queryClient: QueryClient) {
)
if (event.request.trigger.type === 'push') {
// refresh notifications in the background
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
queryClient.resetQueries({queryKey: RQKEY_NOTIFS()})
// handle payload-based deeplinks
let payload
if (isIOS) {
@ -121,7 +121,7 @@ export function init(queryClient: QueryClient) {
logger.DebugContext.notifications,
)
track('Notificatons:OpenApp')
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
queryClient.resetQueries({queryKey: RQKEY_NOTIFS()})
resetToTab('NotificationsTab') // open notifications tab
}
},

View File

@ -8,6 +8,15 @@ export const queryClient = new QueryClient({
// so we NEVER want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
// "first seen" timestamps on objects to determine if they're fresh.
// Disable this optimization so that we can rely on "first seen" timestamps.
structuralSharing: false,
// We don't want to retry queries by default, because in most cases we
// want to fail early and show a response to the user. There are
// exceptions, and those can be made on a per-query basis. For others, we
// should give users controls to retry.
retry: false,
},
},
})

View File

@ -1,8 +1,46 @@
/**
* Importing these separately from `platform/detection` and `lib/app-info` to
* avoid future conflicts and/or circular deps
*/
import {Platform} from 'react-native'
import app from 'react-native-version-number'
import * as info from 'expo-updates'
import {init} from 'sentry-expo'
/**
* Matches the build profile `channel` props in `eas.json`
*/
const buildChannel = (info.channel || 'development') as
| 'development'
| 'preview'
| 'production'
/**
* Examples:
* - `dev`
* - `1.57.0`
*/
const release = app.appVersion ?? 'dev'
/**
* Examples:
* - `web.dev`
* - `ios.dev`
* - `android.dev`
* - `web.1.57.0`
* - `ios.1.57.0.3`
* - `android.1.57.0.46`
*/
const dist = `${Platform.OS}.${release}${
app.buildVersion ? `.${app.buildVersion}` : ''
}`
init({
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
enableInExpoDevelopment: false, // if true, Sentry will try to send events/errors in development mode.
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
environment: __DEV__ ? 'development' : 'production', // Set the environment
enableInExpoDevelopment: true,
environment: buildChannel,
dist,
release,
})

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,6 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ""
#: src/view/screens/Settings.tsx:410
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/Drawer.tsx:527
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
#: src/view/com/modals/Repost.tsx:44
msgid "{0}"
msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/Drawer.tsx:504
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
#: src/view/screens/Search/Search.tsx:86
msgid "{message}"
msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr ""
#: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422
#: src/view/screens/Settings.tsx:410
msgid "Accessibility"
msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289
#: src/view/screens/Settings.tsx:288
msgid "Account"
msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list"
msgstr ""
#: src/view/screens/Settings.tsx:358
#: src/view/screens/Settings.tsx:367
#: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:366
msgid "Add account"
msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content"
msgstr ""
#: src/view/screens/Settings.tsx:574
#: src/view/screens/Settings.tsx:562
msgid "Advanced"
msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language"
msgstr ""
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:582
msgid "App passwords"
msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords"
msgstr ""
#: src/view/screens/Settings.tsx:437
#: src/view/screens/Settings.tsx:425
msgid "Appearance"
msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145
#: src/view/com/auth/login/ChooseAccountForm.tsx:122
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:466
#: src/view/screens/Settings.tsx:454
msgid "Basics"
msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday"
msgstr ""
#: src/view/screens/Settings.tsx:315
#: src/view/screens/Settings.tsx:314
msgid "Birthday:"
msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social"
msgstr ""
#: src/view/screens/Settings.tsx:723
#: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}"
msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup"
msgstr ""
#: src/view/screens/Settings.tsx:309
#: src/view/screens/Settings.tsx:308
msgid "Change"
msgstr ""
#: src/view/screens/Settings.tsx:606
#: src/view/screens/Settings.tsx:615
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:603
msgid "Change handle"
msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password"
msgstr ""
#: src/view/screens/Settings.tsx:699
#: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data"
msgstr ""
#: src/view/screens/Settings.tsx:701
#: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)"
msgstr ""
#: src/view/screens/Settings.tsx:711
#: src/view/screens/Settings.tsx:699
msgid "Clear all storage data"
msgstr ""
#: src/view/screens/Settings.tsx:713
#: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)"
msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:620
#: src/view/screens/Settings.tsx:608
msgid "Danger Zone"
msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark"
#~ msgstr ""
#: src/view/screens/Settings.tsx:627
#: src/view/screens/Settings.tsx:615
msgid "Delete account"
msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account"
msgstr ""
#: src/view/screens/Settings.tsx:637
#: src/view/screens/Settings.tsx:625
msgid "Delete my account…"
msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server"
msgstr ""
#: src/view/screens/Settings.tsx:642
#: src/view/screens/Settings.tsx:630
msgid "Developer Tools"
msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr ""
#: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75
#: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds"
msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated"
msgstr ""
#: src/view/screens/Settings.tsx:293
#: src/view/screens/Settings.tsx:292
msgid "Email:"
msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds"
msgstr ""
#: src/view/screens/Feeds.tsx:558
#: src/view/screens/Feeds.tsx:559
msgid "Feed offline"
msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411
msgid "Feedback"
msgstr ""
#: src/view/screens/Feeds.tsx:474
#: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328
@ -782,7 +771,7 @@ msgstr ""
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:132
#: src/view/screens/SavedFeeds.tsx:156
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421
msgid "Help"
msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:101
msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:89
msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:95
msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:92
msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:86
msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
msgstr ""
#: src/view/shell/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home"
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71
#: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486
#: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences"
msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password"
msgstr ""
#: src/view/screens/Settings.tsx:386
#: src/view/screens/Settings.tsx:385
msgid "Invite"
msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374
#: src/view/screens/Settings.tsx:373
msgid "Invite a Friend"
msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings"
msgstr ""
#: src/view/screens/Settings.tsx:546
#: src/view/screens/Settings.tsx:534
msgid "Languages"
msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light"
#~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625
#: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed"
msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts"
msgstr ""
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:130
msgid "Load new notifications"
msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:104
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:477
#: src/view/screens/ProfileFeed.tsx:478
msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr ""
#: src/view/screens/Moderation.tsx:51
#: src/view/screens/Settings.tsx:568
#: src/view/screens/Settings.tsx:556
#: src/view/shell/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506
#: src/view/screens/ProfileFeed.tsx:367
#: src/view/screens/ProfileFeed.tsx:368
#: src/view/screens/ProfileList.tsx:506
msgid "More options"
msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile"
msgstr ""
#: src/view/screens/Settings.tsx:525
#: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds"
msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509
#: src/view/screens/Profile.tsx:380
#: src/view/screens/ProfileFeed.tsx:447
#: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:618
#: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632
msgid "No description"
msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result"
msgstr ""
#: src/view/screens/Feeds.tsx:451
#: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\""
msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable."
msgstr ""
#: src/view/screens/Notifications.tsx:87
#: src/view/screens/Notifications.tsx:111
#: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text."
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51
#: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation"
msgstr ""
#: src/view/screens/Settings.tsx:538
#: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/Drawer.tsx:509
msgid "Opens list of invite codes"
msgstr ""
#: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:563
#: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings"
msgstr ""
#: src/view/screens/Settings.tsx:519
#: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds"
msgstr ""
#: src/view/screens/Settings.tsx:586
#: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page"
msgstr ""
#: src/view/screens/Settings.tsx:478
#: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences"
msgstr ""
#: src/view/screens/Settings.tsx:669
#: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page"
msgstr ""
#: src/view/screens/Settings.tsx:649
#: src/view/screens/Settings.tsx:637
msgid "Opens the system log page"
msgstr ""
#: src/view/screens/Settings.tsx:499
#: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:79
#: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds"
msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile"
msgstr ""
#: src/view/screens/Settings.tsx:794
#: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email."
msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92
#: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?"
msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106
#: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91
#: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds"
msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107
#: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?"
msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code"
msgstr ""
#: src/view/screens/Settings.tsx:691
#: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state"
msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password"
msgstr ""
#: src/view/screens/Settings.tsx:681
#: src/view/screens/Settings.tsx:669
msgid "Reset preferences state"
msgstr ""
#: src/view/screens/Settings.tsx:689
#: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state"
msgstr ""
#: src/view/screens/Settings.tsx:679
#: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state"
msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:105
#: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds"
msgstr ""
@ -1726,7 +1730,7 @@ msgstr ""
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr ""
#: src/view/screens/Settings.tsx:280
#: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link"
#~ msgstr ""
#: src/view/screens/Settings.tsx:319
#: src/view/screens/Settings.tsx:318
msgid "Show"
msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37
#: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:94
#: src/view/com/auth/login/ChooseAccountForm.tsx:118
#: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..."
msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation"
msgstr ""
#: src/view/screens/Settings.tsx:330
#: src/view/screens/Settings.tsx:329
msgid "Signed in as"
msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging"
msgstr ""
#: src/view/screens/Settings.tsx:735
#: src/view/screens/Settings.tsx:723
msgid "Status page"
msgstr ""
#: src/view/screens/Settings.tsx:671
#: src/view/screens/Settings.tsx:659
msgid "Storybook"
msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System"
#~ msgstr ""
#: src/view/screens/Settings.tsx:651
#: src/view/screens/Settings.tsx:639
msgid "System log"
msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508
#: src/view/screens/Settings.tsx:496
msgid "Thread Preferences"
msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users"
msgstr ""
#: src/view/screens/Settings.tsx:755
#: src/view/screens/Settings.tsx:743
msgid "Verify email"
msgstr ""
#: src/view/screens/Settings.tsx:780
#: src/view/screens/Settings.tsx:768
msgid "Verify my email"
msgstr ""
#: src/view/screens/Settings.tsx:789
#: src/view/screens/Settings.tsx:777
msgid "Verify My Email"
msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:98
msgid "We're sorry, but this content is not viewable without a Bluesky account."
msgstr ""
#: src/view/screens/Search/Search.tsx:236
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr ""
@ -2157,7 +2165,7 @@ msgstr ""
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:92
#: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds."
msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:118
#: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds."
msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider"
msgstr ""
#: src/view/screens/Settings.tsx:405
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/Drawer.tsx:523
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr ""

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,6 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ""
#: src/view/screens/Settings.tsx:410
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/Drawer.tsx:527
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
#: src/view/com/modals/Repost.tsx:44
msgid "{0}"
msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/Drawer.tsx:504
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
#: src/view/screens/Search/Search.tsx:86
msgid "{message}"
msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr ""
#: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422
#: src/view/screens/Settings.tsx:410
msgid "Accessibility"
msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289
#: src/view/screens/Settings.tsx:288
msgid "Account"
msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list"
msgstr ""
#: src/view/screens/Settings.tsx:358
#: src/view/screens/Settings.tsx:367
#: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:366
msgid "Add account"
msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content"
msgstr ""
#: src/view/screens/Settings.tsx:574
#: src/view/screens/Settings.tsx:562
msgid "Advanced"
msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language"
msgstr ""
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:582
msgid "App passwords"
msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords"
msgstr ""
#: src/view/screens/Settings.tsx:437
#: src/view/screens/Settings.tsx:425
msgid "Appearance"
msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145
#: src/view/com/auth/login/ChooseAccountForm.tsx:122
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:466
#: src/view/screens/Settings.tsx:454
msgid "Basics"
msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday"
msgstr ""
#: src/view/screens/Settings.tsx:315
#: src/view/screens/Settings.tsx:314
msgid "Birthday:"
msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social"
msgstr ""
#: src/view/screens/Settings.tsx:723
#: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}"
msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup"
msgstr ""
#: src/view/screens/Settings.tsx:309
#: src/view/screens/Settings.tsx:308
msgid "Change"
msgstr ""
#: src/view/screens/Settings.tsx:606
#: src/view/screens/Settings.tsx:615
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:603
msgid "Change handle"
msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password"
msgstr ""
#: src/view/screens/Settings.tsx:699
#: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data"
msgstr ""
#: src/view/screens/Settings.tsx:701
#: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)"
msgstr ""
#: src/view/screens/Settings.tsx:711
#: src/view/screens/Settings.tsx:699
msgid "Clear all storage data"
msgstr ""
#: src/view/screens/Settings.tsx:713
#: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)"
msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:620
#: src/view/screens/Settings.tsx:608
msgid "Danger Zone"
msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark"
#~ msgstr ""
#: src/view/screens/Settings.tsx:627
#: src/view/screens/Settings.tsx:615
msgid "Delete account"
msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account"
msgstr ""
#: src/view/screens/Settings.tsx:637
#: src/view/screens/Settings.tsx:625
msgid "Delete my account…"
msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server"
msgstr ""
#: src/view/screens/Settings.tsx:642
#: src/view/screens/Settings.tsx:630
msgid "Developer Tools"
msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr ""
#: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75
#: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds"
msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated"
msgstr ""
#: src/view/screens/Settings.tsx:293
#: src/view/screens/Settings.tsx:292
msgid "Email:"
msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds"
msgstr ""
#: src/view/screens/Feeds.tsx:558
#: src/view/screens/Feeds.tsx:559
msgid "Feed offline"
msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411
msgid "Feedback"
msgstr ""
#: src/view/screens/Feeds.tsx:474
#: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328
@ -782,7 +771,7 @@ msgstr ""
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:132
#: src/view/screens/SavedFeeds.tsx:156
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421
msgid "Help"
msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:101
msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:89
msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:95
msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:92
msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:86
msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
msgstr ""
#: src/view/shell/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home"
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71
#: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486
#: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences"
msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password"
msgstr ""
#: src/view/screens/Settings.tsx:386
#: src/view/screens/Settings.tsx:385
msgid "Invite"
msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374
#: src/view/screens/Settings.tsx:373
msgid "Invite a Friend"
msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings"
msgstr ""
#: src/view/screens/Settings.tsx:546
#: src/view/screens/Settings.tsx:534
msgid "Languages"
msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light"
#~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625
#: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed"
msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts"
msgstr ""
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:130
msgid "Load new notifications"
msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:104
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:477
#: src/view/screens/ProfileFeed.tsx:478
msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr ""
#: src/view/screens/Moderation.tsx:51
#: src/view/screens/Settings.tsx:568
#: src/view/screens/Settings.tsx:556
#: src/view/shell/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506
#: src/view/screens/ProfileFeed.tsx:367
#: src/view/screens/ProfileFeed.tsx:368
#: src/view/screens/ProfileList.tsx:506
msgid "More options"
msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile"
msgstr ""
#: src/view/screens/Settings.tsx:525
#: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds"
msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509
#: src/view/screens/Profile.tsx:380
#: src/view/screens/ProfileFeed.tsx:447
#: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:618
#: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632
msgid "No description"
msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result"
msgstr ""
#: src/view/screens/Feeds.tsx:451
#: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\""
msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable."
msgstr ""
#: src/view/screens/Notifications.tsx:87
#: src/view/screens/Notifications.tsx:111
#: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text."
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51
#: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation"
msgstr ""
#: src/view/screens/Settings.tsx:538
#: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/Drawer.tsx:509
msgid "Opens list of invite codes"
msgstr ""
#: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:563
#: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings"
msgstr ""
#: src/view/screens/Settings.tsx:519
#: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds"
msgstr ""
#: src/view/screens/Settings.tsx:586
#: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page"
msgstr ""
#: src/view/screens/Settings.tsx:478
#: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences"
msgstr ""
#: src/view/screens/Settings.tsx:669
#: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page"
msgstr ""
#: src/view/screens/Settings.tsx:649
#: src/view/screens/Settings.tsx:637
msgid "Opens the system log page"
msgstr ""
#: src/view/screens/Settings.tsx:499
#: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:79
#: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds"
msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile"
msgstr ""
#: src/view/screens/Settings.tsx:794
#: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email."
msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92
#: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?"
msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106
#: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91
#: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds"
msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107
#: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?"
msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code"
msgstr ""
#: src/view/screens/Settings.tsx:691
#: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state"
msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password"
msgstr ""
#: src/view/screens/Settings.tsx:681
#: src/view/screens/Settings.tsx:669
msgid "Reset preferences state"
msgstr ""
#: src/view/screens/Settings.tsx:689
#: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state"
msgstr ""
#: src/view/screens/Settings.tsx:679
#: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state"
msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:105
#: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds"
msgstr ""
@ -1726,7 +1730,7 @@ msgstr ""
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr ""
#: src/view/screens/Settings.tsx:280
#: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link"
#~ msgstr ""
#: src/view/screens/Settings.tsx:319
#: src/view/screens/Settings.tsx:318
msgid "Show"
msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37
#: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:94
#: src/view/com/auth/login/ChooseAccountForm.tsx:118
#: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..."
msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation"
msgstr ""
#: src/view/screens/Settings.tsx:330
#: src/view/screens/Settings.tsx:329
msgid "Signed in as"
msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging"
msgstr ""
#: src/view/screens/Settings.tsx:735
#: src/view/screens/Settings.tsx:723
msgid "Status page"
msgstr ""
#: src/view/screens/Settings.tsx:671
#: src/view/screens/Settings.tsx:659
msgid "Storybook"
msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System"
#~ msgstr ""
#: src/view/screens/Settings.tsx:651
#: src/view/screens/Settings.tsx:639
msgid "System log"
msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508
#: src/view/screens/Settings.tsx:496
msgid "Thread Preferences"
msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users"
msgstr ""
#: src/view/screens/Settings.tsx:755
#: src/view/screens/Settings.tsx:743
msgid "Verify email"
msgstr ""
#: src/view/screens/Settings.tsx:780
#: src/view/screens/Settings.tsx:768
msgid "Verify my email"
msgstr ""
#: src/view/screens/Settings.tsx:789
#: src/view/screens/Settings.tsx:777
msgid "Verify My Email"
msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:98
msgid "We're sorry, but this content is not viewable without a Bluesky account."
msgstr ""
#: src/view/screens/Search/Search.tsx:236
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr ""
@ -2157,7 +2165,7 @@ msgstr ""
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:92
#: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds."
msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:118
#: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds."
msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider"
msgstr ""
#: src/view/screens/Settings.tsx:405
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/Drawer.tsx:523
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr ""

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,6 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ""
#: src/view/screens/Settings.tsx:410
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/Drawer.tsx:527
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
#: src/view/com/modals/Repost.tsx:44
msgid "{0}"
msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/Drawer.tsx:504
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
#: src/view/screens/Search/Search.tsx:86
msgid "{message}"
msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr ""
#: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422
#: src/view/screens/Settings.tsx:410
msgid "Accessibility"
msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289
#: src/view/screens/Settings.tsx:288
msgid "Account"
msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list"
msgstr ""
#: src/view/screens/Settings.tsx:358
#: src/view/screens/Settings.tsx:367
#: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:366
msgid "Add account"
msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content"
msgstr ""
#: src/view/screens/Settings.tsx:574
#: src/view/screens/Settings.tsx:562
msgid "Advanced"
msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language"
msgstr ""
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:582
msgid "App passwords"
msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords"
msgstr ""
#: src/view/screens/Settings.tsx:437
#: src/view/screens/Settings.tsx:425
msgid "Appearance"
msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145
#: src/view/com/auth/login/ChooseAccountForm.tsx:122
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:466
#: src/view/screens/Settings.tsx:454
msgid "Basics"
msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday"
msgstr ""
#: src/view/screens/Settings.tsx:315
#: src/view/screens/Settings.tsx:314
msgid "Birthday:"
msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social"
msgstr ""
#: src/view/screens/Settings.tsx:723
#: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}"
msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup"
msgstr ""
#: src/view/screens/Settings.tsx:309
#: src/view/screens/Settings.tsx:308
msgid "Change"
msgstr ""
#: src/view/screens/Settings.tsx:606
#: src/view/screens/Settings.tsx:615
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:603
msgid "Change handle"
msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password"
msgstr ""
#: src/view/screens/Settings.tsx:699
#: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data"
msgstr ""
#: src/view/screens/Settings.tsx:701
#: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)"
msgstr ""
#: src/view/screens/Settings.tsx:711
#: src/view/screens/Settings.tsx:699
msgid "Clear all storage data"
msgstr ""
#: src/view/screens/Settings.tsx:713
#: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)"
msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:620
#: src/view/screens/Settings.tsx:608
msgid "Danger Zone"
msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark"
#~ msgstr ""
#: src/view/screens/Settings.tsx:627
#: src/view/screens/Settings.tsx:615
msgid "Delete account"
msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account"
msgstr ""
#: src/view/screens/Settings.tsx:637
#: src/view/screens/Settings.tsx:625
msgid "Delete my account…"
msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server"
msgstr ""
#: src/view/screens/Settings.tsx:642
#: src/view/screens/Settings.tsx:630
msgid "Developer Tools"
msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr ""
#: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75
#: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds"
msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated"
msgstr ""
#: src/view/screens/Settings.tsx:293
#: src/view/screens/Settings.tsx:292
msgid "Email:"
msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds"
msgstr ""
#: src/view/screens/Feeds.tsx:558
#: src/view/screens/Feeds.tsx:559
msgid "Feed offline"
msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411
msgid "Feedback"
msgstr ""
#: src/view/screens/Feeds.tsx:474
#: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328
@ -782,7 +771,7 @@ msgstr ""
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:132
#: src/view/screens/SavedFeeds.tsx:156
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421
msgid "Help"
msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:101
msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:89
msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:95
msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:92
msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:86
msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
msgstr ""
#: src/view/shell/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home"
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71
#: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486
#: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences"
msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password"
msgstr ""
#: src/view/screens/Settings.tsx:386
#: src/view/screens/Settings.tsx:385
msgid "Invite"
msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374
#: src/view/screens/Settings.tsx:373
msgid "Invite a Friend"
msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings"
msgstr ""
#: src/view/screens/Settings.tsx:546
#: src/view/screens/Settings.tsx:534
msgid "Languages"
msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light"
#~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625
#: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed"
msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts"
msgstr ""
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:130
msgid "Load new notifications"
msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:104
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:477
#: src/view/screens/ProfileFeed.tsx:478
msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr ""
#: src/view/screens/Moderation.tsx:51
#: src/view/screens/Settings.tsx:568
#: src/view/screens/Settings.tsx:556
#: src/view/shell/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506
#: src/view/screens/ProfileFeed.tsx:367
#: src/view/screens/ProfileFeed.tsx:368
#: src/view/screens/ProfileList.tsx:506
msgid "More options"
msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile"
msgstr ""
#: src/view/screens/Settings.tsx:525
#: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds"
msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509
#: src/view/screens/Profile.tsx:380
#: src/view/screens/ProfileFeed.tsx:447
#: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:618
#: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632
msgid "No description"
msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result"
msgstr ""
#: src/view/screens/Feeds.tsx:451
#: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\""
msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable."
msgstr ""
#: src/view/screens/Notifications.tsx:87
#: src/view/screens/Notifications.tsx:111
#: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text."
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51
#: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation"
msgstr ""
#: src/view/screens/Settings.tsx:538
#: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/Drawer.tsx:509
msgid "Opens list of invite codes"
msgstr ""
#: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:563
#: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings"
msgstr ""
#: src/view/screens/Settings.tsx:519
#: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds"
msgstr ""
#: src/view/screens/Settings.tsx:586
#: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page"
msgstr ""
#: src/view/screens/Settings.tsx:478
#: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences"
msgstr ""
#: src/view/screens/Settings.tsx:669
#: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page"
msgstr ""
#: src/view/screens/Settings.tsx:649
#: src/view/screens/Settings.tsx:637
msgid "Opens the system log page"
msgstr ""
#: src/view/screens/Settings.tsx:499
#: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:79
#: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds"
msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile"
msgstr ""
#: src/view/screens/Settings.tsx:794
#: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email."
msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92
#: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?"
msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106
#: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91
#: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds"
msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107
#: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?"
msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code"
msgstr ""
#: src/view/screens/Settings.tsx:691
#: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state"
msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password"
msgstr ""
#: src/view/screens/Settings.tsx:681
#: src/view/screens/Settings.tsx:669
msgid "Reset preferences state"
msgstr ""
#: src/view/screens/Settings.tsx:689
#: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state"
msgstr ""
#: src/view/screens/Settings.tsx:679
#: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state"
msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:105
#: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds"
msgstr ""
@ -1726,7 +1730,7 @@ msgstr ""
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr ""
#: src/view/screens/Settings.tsx:280
#: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link"
#~ msgstr ""
#: src/view/screens/Settings.tsx:319
#: src/view/screens/Settings.tsx:318
msgid "Show"
msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37
#: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:94
#: src/view/com/auth/login/ChooseAccountForm.tsx:118
#: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..."
msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation"
msgstr ""
#: src/view/screens/Settings.tsx:330
#: src/view/screens/Settings.tsx:329
msgid "Signed in as"
msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging"
msgstr ""
#: src/view/screens/Settings.tsx:735
#: src/view/screens/Settings.tsx:723
msgid "Status page"
msgstr ""
#: src/view/screens/Settings.tsx:671
#: src/view/screens/Settings.tsx:659
msgid "Storybook"
msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System"
#~ msgstr ""
#: src/view/screens/Settings.tsx:651
#: src/view/screens/Settings.tsx:639
msgid "System log"
msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508
#: src/view/screens/Settings.tsx:496
msgid "Thread Preferences"
msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users"
msgstr ""
#: src/view/screens/Settings.tsx:755
#: src/view/screens/Settings.tsx:743
msgid "Verify email"
msgstr ""
#: src/view/screens/Settings.tsx:780
#: src/view/screens/Settings.tsx:768
msgid "Verify my email"
msgstr ""
#: src/view/screens/Settings.tsx:789
#: src/view/screens/Settings.tsx:777
msgid "Verify My Email"
msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:98
msgid "We're sorry, but this content is not viewable without a Bluesky account."
msgstr ""
#: src/view/screens/Search/Search.tsx:236
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr ""
@ -2157,7 +2165,7 @@ msgstr ""
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:92
#: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds."
msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:118
#: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds."
msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider"
msgstr ""
#: src/view/screens/Settings.tsx:405
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/Drawer.tsx:523
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr ""

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,6 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ""
#: src/view/screens/Settings.tsx:410
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/Drawer.tsx:527
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
#: src/view/com/modals/Repost.tsx:44
msgid "{0}"
msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/Drawer.tsx:504
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
#: src/view/screens/Search/Search.tsx:86
msgid "{message}"
msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr ""
#: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422
#: src/view/screens/Settings.tsx:410
msgid "Accessibility"
msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289
#: src/view/screens/Settings.tsx:288
msgid "Account"
msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list"
msgstr ""
#: src/view/screens/Settings.tsx:358
#: src/view/screens/Settings.tsx:367
#: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:366
msgid "Add account"
msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content"
msgstr ""
#: src/view/screens/Settings.tsx:574
#: src/view/screens/Settings.tsx:562
msgid "Advanced"
msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language"
msgstr ""
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:582
msgid "App passwords"
msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords"
msgstr ""
#: src/view/screens/Settings.tsx:437
#: src/view/screens/Settings.tsx:425
msgid "Appearance"
msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145
#: src/view/com/auth/login/ChooseAccountForm.tsx:122
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back"
msgstr ""
#: src/view/screens/Settings.tsx:466
#: src/view/screens/Settings.tsx:454
msgid "Basics"
msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday"
msgstr ""
#: src/view/screens/Settings.tsx:315
#: src/view/screens/Settings.tsx:314
msgid "Birthday:"
msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social"
msgstr ""
#: src/view/screens/Settings.tsx:723
#: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}"
msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup"
msgstr ""
#: src/view/screens/Settings.tsx:309
#: src/view/screens/Settings.tsx:308
msgid "Change"
msgstr ""
#: src/view/screens/Settings.tsx:606
#: src/view/screens/Settings.tsx:615
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:603
msgid "Change handle"
msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password"
msgstr ""
#: src/view/screens/Settings.tsx:699
#: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data"
msgstr ""
#: src/view/screens/Settings.tsx:701
#: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)"
msgstr ""
#: src/view/screens/Settings.tsx:711
#: src/view/screens/Settings.tsx:699
msgid "Clear all storage data"
msgstr ""
#: src/view/screens/Settings.tsx:713
#: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)"
msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:620
#: src/view/screens/Settings.tsx:608
msgid "Danger Zone"
msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark"
#~ msgstr ""
#: src/view/screens/Settings.tsx:627
#: src/view/screens/Settings.tsx:615
msgid "Delete account"
msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account"
msgstr ""
#: src/view/screens/Settings.tsx:637
#: src/view/screens/Settings.tsx:625
msgid "Delete my account…"
msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server"
msgstr ""
#: src/view/screens/Settings.tsx:642
#: src/view/screens/Settings.tsx:630
msgid "Developer Tools"
msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr ""
#: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75
#: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds"
msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated"
msgstr ""
#: src/view/screens/Settings.tsx:293
#: src/view/screens/Settings.tsx:292
msgid "Email:"
msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds"
msgstr ""
#: src/view/screens/Feeds.tsx:558
#: src/view/screens/Feeds.tsx:559
msgid "Feed offline"
msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411
msgid "Feedback"
msgstr ""
#: src/view/screens/Feeds.tsx:474
#: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328
@ -782,7 +771,7 @@ msgstr ""
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:132
#: src/view/screens/SavedFeeds.tsx:156
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421
msgid "Help"
msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:101
msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:89
msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:95
msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:92
msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:86
msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
msgstr ""
#: src/view/shell/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home"
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71
#: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486
#: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences"
msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password"
msgstr ""
#: src/view/screens/Settings.tsx:386
#: src/view/screens/Settings.tsx:385
msgid "Invite"
msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374
#: src/view/screens/Settings.tsx:373
msgid "Invite a Friend"
msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings"
msgstr ""
#: src/view/screens/Settings.tsx:546
#: src/view/screens/Settings.tsx:534
msgid "Languages"
msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light"
#~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625
#: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed"
msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts"
msgstr ""
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:130
msgid "Load new notifications"
msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:104
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:477
#: src/view/screens/ProfileFeed.tsx:478
msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr ""
#: src/view/screens/Moderation.tsx:51
#: src/view/screens/Settings.tsx:568
#: src/view/screens/Settings.tsx:556
#: src/view/shell/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506
#: src/view/screens/ProfileFeed.tsx:367
#: src/view/screens/ProfileFeed.tsx:368
#: src/view/screens/ProfileList.tsx:506
msgid "More options"
msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile"
msgstr ""
#: src/view/screens/Settings.tsx:525
#: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds"
msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509
#: src/view/screens/Profile.tsx:380
#: src/view/screens/ProfileFeed.tsx:447
#: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No"
msgstr ""
#: src/view/screens/ProfileFeed.tsx:618
#: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632
msgid "No description"
msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result"
msgstr ""
#: src/view/screens/Feeds.tsx:451
#: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\""
msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable."
msgstr ""
#: src/view/screens/Notifications.tsx:87
#: src/view/screens/Notifications.tsx:111
#: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text."
msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51
#: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation"
msgstr ""
#: src/view/screens/Settings.tsx:538
#: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/Drawer.tsx:509
msgid "Opens list of invite codes"
msgstr ""
#: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain"
msgstr ""
#: src/view/screens/Settings.tsx:563
#: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings"
msgstr ""
#: src/view/screens/Settings.tsx:519
#: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds"
msgstr ""
#: src/view/screens/Settings.tsx:586
#: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page"
msgstr ""
#: src/view/screens/Settings.tsx:478
#: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences"
msgstr ""
#: src/view/screens/Settings.tsx:669
#: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page"
msgstr ""
#: src/view/screens/Settings.tsx:649
#: src/view/screens/Settings.tsx:637
msgid "Opens the system log page"
msgstr ""
#: src/view/screens/Settings.tsx:499
#: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:79
#: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds"
msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile"
msgstr ""
#: src/view/screens/Settings.tsx:794
#: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email."
msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92
#: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?"
msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106
#: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed"
msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91
#: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds"
msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107
#: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?"
msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code"
msgstr ""
#: src/view/screens/Settings.tsx:691
#: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state"
msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password"
msgstr ""
#: src/view/screens/Settings.tsx:681
#: src/view/screens/Settings.tsx:669
msgid "Reset preferences state"
msgstr ""
#: src/view/screens/Settings.tsx:689
#: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state"
msgstr ""
#: src/view/screens/Settings.tsx:679
#: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state"
msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:105
#: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds"
msgstr ""
@ -1726,7 +1730,7 @@ msgstr ""
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr ""
#: src/view/screens/Settings.tsx:280
#: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link"
#~ msgstr ""
#: src/view/screens/Settings.tsx:319
#: src/view/screens/Settings.tsx:318
msgid "Show"
msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37
#: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}"
msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:94
#: src/view/com/auth/login/ChooseAccountForm.tsx:118
#: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..."
msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation"
msgstr ""
#: src/view/screens/Settings.tsx:330
#: src/view/screens/Settings.tsx:329
msgid "Signed in as"
msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging"
msgstr ""
#: src/view/screens/Settings.tsx:735
#: src/view/screens/Settings.tsx:723
msgid "Status page"
msgstr ""
#: src/view/screens/Settings.tsx:671
#: src/view/screens/Settings.tsx:659
msgid "Storybook"
msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System"
#~ msgstr ""
#: src/view/screens/Settings.tsx:651
#: src/view/screens/Settings.tsx:639
msgid "System log"
msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508
#: src/view/screens/Settings.tsx:496
msgid "Thread Preferences"
msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users"
msgstr ""
#: src/view/screens/Settings.tsx:755
#: src/view/screens/Settings.tsx:743
msgid "Verify email"
msgstr ""
#: src/view/screens/Settings.tsx:780
#: src/view/screens/Settings.tsx:768
msgid "Verify my email"
msgstr ""
#: src/view/screens/Settings.tsx:789
#: src/view/screens/Settings.tsx:777
msgid "Verify My Email"
msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!"
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:98
msgid "We're sorry, but this content is not viewable without a Bluesky account."
msgstr ""
#: src/view/screens/Search/Search.tsx:236
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr ""
@ -2157,7 +2165,7 @@ msgstr ""
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."
msgstr ""
#: src/view/screens/SavedFeeds.tsx:92
#: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds."
msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:118
#: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds."
msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider"
msgstr ""
#: src/view/screens/Settings.tsx:405
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/Drawer.tsx:523
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr ""

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,6 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "यह चेतावनी केवल मीडिया वाले पोस्ट के लिए उपलब्ध है।"
#: src/view/screens/Settings.tsx:410
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/Drawer.tsx:527
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
#: src/view/com/modals/Repost.tsx:44
msgid "{0}"
msgstr "{0}"
@ -35,11 +29,6 @@ msgstr "{0}"
msgid "{0} {purposeLabel} List"
msgstr "{0} {purposeLabel} सूची"
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/Drawer.tsx:504
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
#: src/view/screens/Search/Search.tsx:86
msgid "{message}"
msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "ऐप का एक नया संस्करण उपलब्ध है. कृपया ऐप का उपयोग जारी रखने के लिए अपडेट करें।"
#: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422
#: src/view/screens/Settings.tsx:410
msgid "Accessibility"
msgstr "प्रवेर्शयोग्यता"
#: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289
#: src/view/screens/Settings.tsx:288
msgid "Account"
msgstr "अकाउंट"
@ -88,8 +77,8 @@ msgstr "सामग्री चेतावनी जोड़ें"
msgid "Add a user to this list"
msgstr "इस सूची में किसी को जोड़ें"
#: src/view/screens/Settings.tsx:358
#: src/view/screens/Settings.tsx:367
#: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:366
msgid "Add account"
msgstr "अकाउंट जोड़ें"
@ -140,7 +129,7 @@ msgstr "पसंद की संख्या को समायोजित
msgid "Adult Content"
msgstr "वयस्क सामग्री"
#: src/view/screens/Settings.tsx:574
#: src/view/screens/Settings.tsx:562
msgid "Advanced"
msgstr "विकसित"
@ -172,7 +161,7 @@ msgstr "और"
msgid "App Language"
msgstr "ऐप भाषा"
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:582
msgid "App passwords"
msgstr "ऐप पासवर्ड"
@ -180,7 +169,7 @@ msgstr "ऐप पासवर्ड"
msgid "App Passwords"
msgstr "ऐप पासवर्ड"
#: src/view/screens/Settings.tsx:437
#: src/view/screens/Settings.tsx:425
msgid "Appearance"
msgstr "दिखावट"
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "कलात्मक या गैर-कामुक नग्नता।।"
#: src/view/com/auth/create/CreateAccount.tsx:145
#: src/view/com/auth/login/ChooseAccountForm.tsx:122
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr "कलात्मक या गैर-कामुक नग्नत
msgid "Back"
msgstr "वापस"
#: src/view/screens/Settings.tsx:466
#: src/view/screens/Settings.tsx:454
msgid "Basics"
msgstr "मूल बातें"
@ -226,7 +215,7 @@ msgstr "मूल बातें"
msgid "Birthday"
msgstr "जन्मदिन"
#: src/view/screens/Settings.tsx:315
#: src/view/screens/Settings.tsx:314
msgid "Birthday:"
msgstr "जन्मदिन:"
@ -291,7 +280,7 @@ msgstr "ब्लूस्की एक स्वस्थ समुदाय
msgid "Bluesky.Social"
msgstr "Bluesky.Social"
#: src/view/screens/Settings.tsx:723
#: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}"
msgstr "Build version {0} {1}"
@ -359,12 +348,12 @@ msgstr "खोज मत करो"
msgid "Cancel waitlist signup"
msgstr "प्रतीक्षा सूची पंजीकरण मत करो"
#: src/view/screens/Settings.tsx:309
#: src/view/screens/Settings.tsx:308
msgid "Change"
msgstr "परिवर्तन"
#: src/view/screens/Settings.tsx:606
#: src/view/screens/Settings.tsx:615
#: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:603
msgid "Change handle"
msgstr "हैंडल बदलें"
@ -404,19 +393,19 @@ msgstr "उन एल्गोरिदम का चयन करें जो
msgid "Choose your password"
msgstr "अपना पासवर्ड चुनें"
#: src/view/screens/Settings.tsx:699
#: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data"
msgstr ""
#: src/view/screens/Settings.tsx:701
#: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)"
msgstr ""
#: src/view/screens/Settings.tsx:711
#: src/view/screens/Settings.tsx:699
msgid "Clear all storage data"
msgstr ""
#: src/view/screens/Settings.tsx:713
#: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)"
msgstr ""
@ -557,7 +546,7 @@ msgstr "बनाया गया {0}"
msgid "Custom domain"
msgstr "कस्टम डोमेन"
#: src/view/screens/Settings.tsx:620
#: src/view/screens/Settings.tsx:608
msgid "Danger Zone"
msgstr "खतरा क्षेत्र"
@ -565,7 +554,7 @@ msgstr "खतरा क्षेत्र"
#~ msgid "Dark"
#~ msgstr "डार्क मोड"
#: src/view/screens/Settings.tsx:627
#: src/view/screens/Settings.tsx:615
msgid "Delete account"
msgstr "खाता हटाएं"
@ -587,7 +576,7 @@ msgstr "सूची हटाएँ"
msgid "Delete my account"
msgstr "मेरा खाता हटाएं"
#: src/view/screens/Settings.tsx:637
#: src/view/screens/Settings.tsx:625
msgid "Delete my account…"
msgstr "मेरा खाता हटाएं…"
@ -614,7 +603,7 @@ msgstr "विवरण"
msgid "Dev Server"
msgstr "देव सर्वर"
#: src/view/screens/Settings.tsx:642
#: src/view/screens/Settings.tsx:630
msgid "Developer Tools"
msgstr "डेवलपर उपकरण"
@ -673,7 +662,7 @@ msgid "Edit list details"
msgstr "सूची विवरण संपादित करें"
#: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75
#: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds"
msgstr "मेरी फ़ीड संपादित करें"
@ -708,7 +697,7 @@ msgstr "ईमेल"
msgid "Email Updated"
msgstr "ईमेल अपडेट किया गया"
#: src/view/screens/Settings.tsx:293
#: src/view/screens/Settings.tsx:292
msgid "Email:"
msgstr "ईमेल:"
@ -753,7 +742,7 @@ msgstr "ऑल्ट टेक्स्ट"
msgid "Failed to load recommended feeds"
msgstr "अनुशंसित फ़ीड लोड करने में विफल"
#: src/view/screens/Feeds.tsx:558
#: src/view/screens/Feeds.tsx:559
msgid "Feed offline"
msgstr "फ़ीड ऑफ़लाइन है"
@ -761,12 +750,12 @@ msgstr "फ़ीड ऑफ़लाइन है"
msgid "Feed Preferences"
msgstr "फ़ीड प्राथमिकता"
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411
msgid "Feedback"
msgstr "प्रतिक्रिया"
#: src/view/screens/Feeds.tsx:474
#: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328
@ -778,7 +767,7 @@ msgstr "सभी फ़ीड"
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "सामग्री को व्यवस्थित करने के लिए उपयोगकर्ताओं द्वारा फ़ीड बनाए जाते हैं। कुछ फ़ीड चुनें जो आपको दिलचस्प लगें।"
#: src/view/screens/SavedFeeds.tsx:132
#: src/view/screens/SavedFeeds.tsx:156
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "फ़ीड कस्टम एल्गोरिदम हैं जिन्हें उपयोगकर्ता थोड़ी कोडिंग विशेषज्ञता के साथ बनाते हैं। <0/> अधिक जानकारी के लिए."
@ -876,7 +865,7 @@ msgstr "अगला"
msgid "Handle"
msgstr "हैंडल"
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421
msgid "Help"
msgstr "सहायता"
@ -893,6 +882,26 @@ msgstr "इसे छिपाएं"
msgid "Hide user list"
msgstr "उपयोगकर्ता सूची छुपाएँ"
#: src/view/com/posts/FeedErrorMessage.tsx:101
msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:89
msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:95
msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:92
msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue."
msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:86
msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
msgstr ""
#: src/view/shell/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275
@ -900,9 +909,9 @@ msgstr "उपयोगकर्ता सूची छुपाएँ"
msgid "Home"
msgstr "होम फीड"
#: src/view/com/pager/FeedsTabBarMobile.tsx:71
#: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486
#: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences"
msgstr "होम फ़ीड प्राथमिकताएं"
@ -945,12 +954,12 @@ msgstr "छवि विकल्प"
msgid "Invalid username or password"
msgstr "अवैध उपयोगकर्ता नाम या पासवर्ड"
#: src/view/screens/Settings.tsx:386
#: src/view/screens/Settings.tsx:385
msgid "Invite"
msgstr "आमंत्रण भेजो"
#: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374
#: src/view/screens/Settings.tsx:373
msgid "Invite a Friend"
msgstr "एक दोस्त को आमंत्रित करें"
@ -983,7 +992,7 @@ msgstr "अपनी भाषा चुने"
msgid "Language Settings"
msgstr "भाषा सेटिंग्स"
#: src/view/screens/Settings.tsx:546
#: src/view/screens/Settings.tsx:534
msgid "Languages"
msgstr "भाषा"
@ -1023,7 +1032,7 @@ msgstr "चित्र पुस्तकालय"
#~ msgid "Light"
#~ msgstr "लाइट मोड"
#: src/view/screens/ProfileFeed.tsx:625
#: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed"
msgstr "इस फ़ीड को लाइक करो"
@ -1051,7 +1060,7 @@ msgstr "सूची"
msgid "Load more posts"
msgstr "अधिक पोस्ट लोड करें"
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:130
msgid "Load new notifications"
msgstr "नई सूचनाएं लोड करें"
@ -1067,11 +1076,11 @@ msgstr ""
msgid "Local dev server"
msgstr "स्थानीय देव सर्वर"
#: src/view/com/auth/login/ChooseAccountForm.tsx:104
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
msgstr "उस खाते में लॉग इन करें जो सूचीबद्ध नहीं है"
#: src/view/screens/ProfileFeed.tsx:477
#: src/view/screens/ProfileFeed.tsx:478
msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
msgstr ""
@ -1084,7 +1093,7 @@ msgid "Menu"
msgstr "मेनू"
#: src/view/screens/Moderation.tsx:51
#: src/view/screens/Settings.tsx:568
#: src/view/screens/Settings.tsx:556
#: src/view/shell/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347
@ -1100,7 +1109,7 @@ msgid "More feeds"
msgstr "अधिक फ़ीड"
#: src/view/com/profile/ProfileHeader.tsx:506
#: src/view/screens/ProfileFeed.tsx:367
#: src/view/screens/ProfileFeed.tsx:368
#: src/view/screens/ProfileList.tsx:506
msgid "More options"
msgstr "अधिक विकल्प"
@ -1153,7 +1162,7 @@ msgstr "मेरी फ़ीड"
msgid "My Profile"
msgstr "मेरी प्रोफाइल"
#: src/view/screens/Settings.tsx:525
#: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds"
msgstr "मेरी फ़ीड"
@ -1171,9 +1180,9 @@ msgid "New"
msgstr "नया"
#: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509
#: src/view/screens/Profile.tsx:380
#: src/view/screens/ProfileFeed.tsx:447
#: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255
@ -1204,7 +1213,7 @@ msgstr "अगली फोटो"
msgid "No"
msgstr "नहीं"
#: src/view/screens/ProfileFeed.tsx:618
#: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632
msgid "No description"
msgstr "कोई विवरण नहीं"
@ -1213,7 +1222,7 @@ msgstr "कोई विवरण नहीं"
msgid "No result"
msgstr ""
#: src/view/screens/Feeds.tsx:451
#: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\""
msgstr "\"{query}\" के लिए कोई परिणाम नहीं मिला"
@ -1238,8 +1247,8 @@ msgstr ""
msgid "Not Applicable."
msgstr "लागू नहीं।"
#: src/view/screens/Notifications.tsx:87
#: src/view/screens/Notifications.tsx:111
#: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299
@ -1259,52 +1268,47 @@ msgstr "ठीक है"
msgid "One or more images is missing alt text."
msgstr "एक या अधिक छवियाँ alt पाठ याद आती हैं।।"
#: src/view/com/pager/FeedsTabBarMobile.tsx:51
#: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation"
msgstr "ओपन नेविगेशन"
#: src/view/screens/Settings.tsx:538
#: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings"
msgstr "भाषा सेटिंग्स खोलें"
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/Drawer.tsx:509
msgid "Opens list of invite codes"
msgstr ""
#: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain"
msgstr "कस्टम डोमेन का उपयोग करने के लिए मोडल खोलें"
#: src/view/screens/Settings.tsx:563
#: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings"
msgstr "मॉडरेशन सेटिंग्स खोलें"
#: src/view/screens/Settings.tsx:519
#: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds"
msgstr "सभी बचाया फ़ीड के साथ स्क्रीन खोलें"
#: src/view/screens/Settings.tsx:586
#: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page"
msgstr "ऐप पासवर्ड सेटिंग पेज खोलें"
#: src/view/screens/Settings.tsx:478
#: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences"
msgstr "होम फीड वरीयताओं को खोलता है"
#: src/view/screens/Settings.tsx:669
#: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page"
msgstr "स्टोरीबुक पेज खोलें"
#: src/view/screens/Settings.tsx:649
#: src/view/screens/Settings.tsx:637
msgid "Opens the system log page"
msgstr "सिस्टम लॉग पेज खोलें"
#: src/view/screens/Settings.tsx:499
#: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences"
msgstr "धागे वरीयताओं को खोलता है"
#: src/view/com/auth/login/ChooseAccountForm.tsx:109
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr "अन्य खाता"
@ -1341,7 +1345,7 @@ msgstr "पासवर्ड अद्यतन!"
msgid "Pictures meant for adults."
msgstr "चित्र वयस्कों के लिए थे।।"
#: src/view/screens/SavedFeeds.tsx:79
#: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds"
msgstr "पिन किया गया फ़ीड"
@ -1407,7 +1411,7 @@ msgstr "प्राथमिक भाषा"
msgid "Prioritize Your Follows"
msgstr "अपने फ़ॉलोअर्स को प्राथमिकता दें"
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr "गोपनीयता"
@ -1426,7 +1430,7 @@ msgstr "प्रसंस्करण..."
msgid "Profile"
msgstr "प्रोफ़ाइल"
#: src/view/screens/Settings.tsx:794
#: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email."
msgstr "अपने ईमेल को सत्यापित करके अपने खाते को सुरक्षित रखें।।"
@ -1468,7 +1472,7 @@ msgstr "अनुशंसित लोग"
msgid "Remove"
msgstr "निकालें"
#: src/view/com/feeds/FeedSourceCard.tsx:92
#: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?"
msgstr "मेरे फ़ीड से {0} हटाएं?"
@ -1476,11 +1480,11 @@ msgstr "मेरे फ़ीड से {0} हटाएं?"
msgid "Remove account"
msgstr "खाता हटाएं"
#: src/view/com/posts/FeedErrorMessage.tsx:106
#: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed"
msgstr "फ़ीड हटाएँ"
#: src/view/com/feeds/FeedSourceCard.tsx:91
#: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds"
msgstr "मेरे फ़ीड से हटाएँ"
@ -1493,7 +1497,7 @@ msgstr "छवि निकालें"
msgid "Remove image preview"
msgstr "छवि पूर्वावलोकन निकालें"
#: src/view/com/posts/FeedErrorMessage.tsx:107
#: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?"
msgstr "इस फ़ीड को सहेजे गए फ़ीड से हटा दें?"
@ -1557,7 +1561,7 @@ msgstr "इस प्रदाता के लिए आवश्यक"
msgid "Reset code"
msgstr "कोड रीसेट करें"
#: src/view/screens/Settings.tsx:691
#: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state"
msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें"
@ -1565,15 +1569,15 @@ msgstr "ऑनबोर्डिंग स्टेट को रीसेट
msgid "Reset password"
msgstr "पासवर्ड रीसेट"
#: src/view/screens/Settings.tsx:681
#: src/view/screens/Settings.tsx:669
msgid "Reset preferences state"
msgstr "प्राथमिकताओं को रीसेट करें"
#: src/view/screens/Settings.tsx:689
#: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state"
msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें"
#: src/view/screens/Settings.tsx:679
#: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state"
msgstr "प्राथमिकताओं की स्थिति को रीसेट करें"
@ -1620,7 +1624,7 @@ msgstr "बदलाव सेव करो"
msgid "Save image crop"
msgstr "फोटो बदलाव सेव करो"
#: src/view/screens/SavedFeeds.tsx:105
#: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds"
msgstr "सहेजे गए फ़ीड"
@ -1718,7 +1722,7 @@ msgstr "इस सेटिंग को \"हाँ\" में सेट क
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "इस सेटिंग को अपने निम्नलिखित फ़ीड में अपने सहेजे गए फ़ीड के नमूने दिखाने के लिए \"हाँ\" पर सेट करें। यह एक प्रयोगात्मक विशेषता है।।"
#: src/view/screens/Settings.tsx:280
#: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381
@ -1743,7 +1747,7 @@ msgstr ""
#~ msgid "Share link"
#~ msgstr "लिंक शेयर करें"
#: src/view/screens/Settings.tsx:319
#: src/view/screens/Settings.tsx:318
msgid "Show"
msgstr "दिखाओ"
@ -1787,11 +1791,11 @@ msgstr "साइन इन करें"
msgid "Sign In"
msgstr "साइन इन करें"
#: src/view/com/auth/login/ChooseAccountForm.tsx:37
#: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}"
msgstr "{0} के रूप में साइन इन करें"
#: src/view/com/auth/login/ChooseAccountForm.tsx:94
#: src/view/com/auth/login/ChooseAccountForm.tsx:118
#: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..."
msgstr "... के रूप में साइन इन करें"
@ -1815,7 +1819,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation"
msgstr ""
#: src/view/screens/Settings.tsx:330
#: src/view/screens/Settings.tsx:329
msgid "Signed in as"
msgstr "आपने इस रूप में साइन इन करा है:"
@ -1840,11 +1844,11 @@ msgstr "स्क्वायर"
msgid "Staging"
msgstr "स्टेजिंग"
#: src/view/screens/Settings.tsx:735
#: src/view/screens/Settings.tsx:723
msgid "Status page"
msgstr "स्थिति पृष्ठ"
#: src/view/screens/Settings.tsx:671
#: src/view/screens/Settings.tsx:659
msgid "Storybook"
msgstr "Storybook"
@ -1873,7 +1877,7 @@ msgstr "खाते बदलें"
#~ msgid "System"
#~ msgstr "प्रणाली"
#: src/view/screens/Settings.tsx:651
#: src/view/screens/Settings.tsx:639
msgid "System log"
msgstr "सिस्टम लॉग"
@ -1881,7 +1885,7 @@ msgstr "सिस्टम लॉग"
msgid "Tall"
msgstr "लंबा"
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr "शर्तें"
@ -1954,7 +1958,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "यह चेतावनी केवल मीडिया संलग्न पोस्ट के लिए उपलब्ध है।"
#: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508
#: src/view/screens/Settings.tsx:496
msgid "Thread Preferences"
msgstr "थ्रेड प्राथमिकता"
@ -2061,15 +2065,15 @@ msgstr "यूजर नाम या ईमेल पता"
msgid "Users"
msgstr "यूजर लोग"
#: src/view/screens/Settings.tsx:755
#: src/view/screens/Settings.tsx:743
msgid "Verify email"
msgstr "ईमेल सत्यापित करें"
#: src/view/screens/Settings.tsx:780
#: src/view/screens/Settings.tsx:768
msgid "Verify my email"
msgstr "मेरी ईमेल सत्यापित करें"
#: src/view/screens/Settings.tsx:789
#: src/view/screens/Settings.tsx:777
msgid "Verify My Email"
msgstr "मेरी ईमेल सत्यापित करें"
@ -2094,6 +2098,10 @@ msgstr "साइट पर जाएं"
msgid "We're so excited to have you join us!"
msgstr "हम आपके हमारी सेवा में शामिल होने को लेकर बहुत उत्साहित हैं!"
#: src/view/com/posts/FeedErrorMessage.tsx:98
msgid "We're sorry, but this content is not viewable without a Bluesky account."
msgstr ""
#: src/view/screens/Search/Search.tsx:236
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr ""
@ -2149,7 +2157,7 @@ msgstr "अब आप अपने नए पासवर्ड के साथ
msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer."
msgstr "आपके पास अभी तक कोई आमंत्रण कोड नहीं है! जब आप कुछ अधिक समय के लिए Bluesky पर रहेंगे तो हम आपको कुछ भेजेंगे।"
#: src/view/screens/SavedFeeds.tsx:92
#: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds."
msgstr "आपके पास कोई पिन किया हुआ फ़ीड नहीं है."
@ -2157,7 +2165,7 @@ msgstr "आपके पास कोई पिन किया हुआ फ़
msgid "You don't have any saved feeds!"
msgstr ""
#: src/view/screens/SavedFeeds.tsx:118
#: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds."
msgstr "आपके पास कोई सहेजी गई फ़ीड नहीं है."
@ -2220,12 +2228,6 @@ msgstr "आपका पूरा हैंडल होगा"
msgid "Your hosting provider"
msgstr "आपका होस्टिंग प्रदाता"
#: src/view/screens/Settings.tsx:405
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/Drawer.tsx:523
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "आपकी पोस्ट, पसंद और ब्लॉक सार्वजनिक हैं। म्यूट निजी हैं।।"

View File

@ -288,16 +288,13 @@ export class Logger {
*/
export const logger = new Logger()
/**
* Report to console in dev, Sentry in prod, nothing in test.
*/
if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport)
/**
* Uncomment this to test Sentry in dev
* Comment this out to disable Sentry transport in dev
*/
// logger.addTransport(sentryTransport);
logger.addTransport(sentryTransport)
} else if (env.IS_PROD) {
// logger.addTransport(sentryTransport)
logger.addTransport(sentryTransport)
}

View File

@ -2,9 +2,13 @@ import {useMemo} from 'react'
import {FeedTuner} from '#/lib/api/feed-manip'
import {FeedDescriptor} from '../queries/post-feed'
import {useLanguagePrefs} from './languages'
import {usePreferencesQuery} from '../queries/preferences'
import {useSession} from '../session'
export function useFeedTuners(feedDesc: FeedDescriptor) {
const langPrefs = useLanguagePrefs()
const {data: preferences} = usePreferencesQuery()
const {currentAccount} = useSession()
return useMemo(() => {
if (feedDesc.startsWith('feedgen')) {
@ -19,30 +23,30 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
if (feedDesc === 'home' || feedDesc === 'following') {
const feedTuners = []
if (false /*TODOthis.homeFeed.hideReposts*/) {
if (preferences?.feedViewPrefs.hideReposts) {
feedTuners.push(FeedTuner.removeReposts)
} else {
feedTuners.push(FeedTuner.dedupReposts)
}
if (true /*TODOthis.homeFeed.hideReplies*/) {
if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies)
} /* TODO else {
} else {
feedTuners.push(
FeedTuner.thresholdRepliesOnly({
userDid: this.rootStore.session.data?.did || '',
minLikes: this.homeFeed.hideRepliesByLikeCount,
followedOnly: !!this.homeFeed.hideRepliesByUnfollowed,
userDid: currentAccount?.did || '',
minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
followedOnly: !!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
}),
)
}*/
}
if (false /*TODOthis.homeFeed.hideQuotePosts*/) {
if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts)
}
return feedTuners
}
return []
}, [feedDesc, langPrefs])
}, [feedDesc, currentAccount, preferences, langPrefs])
}

View File

@ -181,6 +181,9 @@ export function useIsFeedPublicQuery({uri}: {uri: string}) {
if (msg.includes('missing jwt')) {
return false
} else if (msg.includes('This feed requires being logged-in')) {
// e.g. https://github.com/bluesky-social/atproto/blob/99ab1ae55c463e8d5321a1eaad07a175bdd56fea/packages/bsky/src/feed-gen/best-of-follows.ts#L13
return false
}
return true
@ -243,13 +246,19 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeUri: '',
}
export function usePinnedFeedsInfos(): FeedSourceInfo[] {
export function usePinnedFeedsInfos(): {
feeds: FeedSourceInfo[]
hasPinnedCustom: boolean
} {
const queryClient = useQueryClient()
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
FOLLOWING_FEED_STUB,
])
const {data: preferences} = usePreferencesQuery()
const pinnedFeedsKey = JSON.stringify(preferences?.feeds?.pinned)
const hasPinnedCustom = React.useMemo<boolean>(() => {
return tabs.some(tab => tab !== FOLLOWING_FEED_STUB)
}, [tabs])
React.useEffect(() => {
if (!preferences?.feeds?.pinned) return
@ -296,13 +305,7 @@ export function usePinnedFeedsInfos(): FeedSourceInfo[] {
}
fetchFeedInfo()
}, [
queryClient,
setTabs,
preferences?.feeds?.pinned,
// ensure we react to re-ordering
pinnedFeedsKey,
])
}, [queryClient, setTabs, preferences?.feeds?.pinned])
return tabs
return {feeds: tabs, hasPinnedCustom}
}

View File

@ -3,7 +3,6 @@ import {
AppBskyGraphGetList,
AppBskyGraphList,
AppBskyGraphDefs,
BskyAgent,
} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
@ -75,13 +74,9 @@ export function useListCreateMutation() {
)
// wait for the appview to update
await whenAppViewReady(
getAgent(),
res.uri,
(v: AppBskyGraphGetList.Response) => {
return typeof v?.data?.list.uri === 'string'
},
)
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
return typeof v?.data?.list.uri === 'string'
})
return res
},
onSuccess() {
@ -142,16 +137,12 @@ export function useListMetadataMutation() {
).data
// wait for the appview to update
await whenAppViewReady(
getAgent(),
res.uri,
(v: AppBskyGraphGetList.Response) => {
const list = v.data.list
return (
list.name === record.name && list.description === record.description
)
},
)
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
const list = v.data.list
return (
list.name === record.name && list.description === record.description
)
})
return res
},
onSuccess(data, variables) {
@ -216,13 +207,9 @@ export function useListDeleteMutation() {
}
// wait for the appview to update
await whenAppViewReady(
getAgent(),
uri,
(v: AppBskyGraphGetList.Response) => {
return !v?.success
},
)
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
return !v?.success
})
},
onSuccess() {
invalidateMyLists(queryClient)
@ -271,7 +258,6 @@ export function useListBlockMutation() {
}
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
fn: (res: AppBskyGraphGetList.Response) => boolean,
) {
@ -280,7 +266,7 @@ async function whenAppViewReady(
1e3, // 1s delay between tries
fn,
() =>
agent.app.bsky.graph.getList({
getAgent().app.bsky.graph.getList({
list: uri,
limit: 1,
}),

View File

@ -2,7 +2,6 @@ import {AppBskyGraphGetBlocks} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
export const RQKEY = () => ['my-blocked-accounts']
type RQPageParam = string | undefined
@ -15,7 +14,6 @@ export function useMyBlockedAccountsQuery() {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getBlocks({

View File

@ -2,7 +2,6 @@ import {AppBskyGraphGetMutes} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
export const RQKEY = () => ['my-muted-accounts']
type RQPageParam = string | undefined
@ -15,7 +14,6 @@ export function useMyMutedAccountsQuery() {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getMutes({

View File

@ -1,12 +1,22 @@
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
AppBskyNotificationListNotifications,
BskyAgent,
} from '@atproto/api'
import chunk from 'lodash.chunk'
/**
* NOTE
* The ./unread.ts API:
*
* - Provides a `checkUnread()` function to sync with the server,
* - Periodically calls `checkUnread()`, and
* - Caches the first page of notifications.
*
* IMPORTANT: This query uses ./unread.ts's cache as its first page,
* IMPORTANT: which means the cache-freshness of this query is driven by the unread API.
*
* Follow these rules:
*
* 1. Call `checkUnread()` if you want to fetch latest in the background.
* 2. Call `checkUnread({invalidate: true})` if you want latest to sync into this query's results immediately.
* 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead.
*/
import {AppBskyFeedDefs} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
@ -14,50 +24,27 @@ import {
useQueryClient,
QueryClient,
} from '@tanstack/react-query'
import {getAgent} from '../../session'
import {useModerationOpts} from '../preferences'
import {shouldFilterNotif} from './util'
import {useUnreadNotificationsApi} from './unread'
import {fetchPage} from './util'
import {FeedPage} from './types'
import {useMutedThreads} from '#/state/muted-threads'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
export type {NotificationType, FeedNotification, FeedPage} from './types'
const PAGE_SIZE = 30
const MS_1HR = 1e3 * 60 * 60
const MS_2DAY = MS_1HR * 48
type RQPageParam = string | undefined
type NotificationType =
| 'post-like'
| 'feedgen-like'
| 'repost'
| 'mention'
| 'reply'
| 'quote'
| 'follow'
| 'unknown'
export function RQKEY() {
return ['notification-feed']
}
export interface FeedNotification {
_reactKey: string
type: NotificationType
notification: AppBskyNotificationListNotifications.Notification
additional?: AppBskyNotificationListNotifications.Notification[]
subjectUri?: string
subject?: AppBskyFeedDefs.PostView
}
export interface FeedPage {
cursor: string | undefined
items: FeedNotification[]
}
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false
return useInfiniteQuery<
@ -69,40 +56,21 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
>({
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().listNotifications({
limit: PAGE_SIZE,
cursor: pageParam,
})
// filter out notifs by mod rules
const notifs = res.data.notifications.filter(
notif => !shouldFilterNotif(notif, moderationOpts),
)
// group notifications which are essentially similar (follows, likes on a post)
let notifsGrouped = groupNotifications(notifs)
// we fetch subjects of notifications (usually posts) now instead of lazily
// in the UI to avoid relayouts
const subjects = await fetchSubjects(getAgent(), notifsGrouped)
for (const notif of notifsGrouped) {
if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
}
// for the first page, we check the cached page held by the unread-checker first
if (!pageParam) {
const cachedPage = unreads.getCachedUnreadPage()
if (cachedPage) {
return cachedPage
}
}
// apply thread muting
notifsGrouped = notifsGrouped.filter(
notif => !isThreadMuted(notif, threadMutes),
)
return {
cursor: res.data.cursor,
items: notifsGrouped,
}
// do a normal fetch
return fetchPage({
limit: PAGE_SIZE,
cursor: pageParam,
queryClient,
moderationOpts,
threadMutes,
})
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@ -135,114 +103,3 @@ export function findPostInQueryData(
}
return undefined
}
function groupNotifications(
notifs: AppBskyNotificationListNotifications.Notification[],
): FeedNotification[] {
const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) {
const ts = +new Date(notif.indexedAt)
let grouped = false
if (GROUPABLE_REASONS.includes(notif.reason)) {
for (const groupedNotif of groupedNotifs) {
const ts2 = +new Date(groupedNotif.notification.indexedAt)
if (
Math.abs(ts2 - ts) < MS_2DAY &&
notif.reason === groupedNotif.notification.reason &&
notif.reasonSubject === groupedNotif.notification.reasonSubject &&
notif.author.did !== groupedNotif.notification.author.did
) {
groupedNotif.additional = groupedNotif.additional || []
groupedNotif.additional.push(notif)
grouped = true
break
}
}
}
if (!grouped) {
const type = toKnownType(notif)
groupedNotifs.push({
_reactKey: `notif-${notif.uri}`,
type,
notification: notif,
subjectUri: getSubjectUri(type, notif),
})
}
}
return groupedNotifs
}
async function fetchSubjects(
agent: BskyAgent,
groupedNotifs: FeedNotification[],
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>()
for (const notif of groupedNotifs) {
if (notif.subjectUri) {
uris.add(notif.subjectUri)
}
}
const uriChunks = chunk(Array.from(uris), 25)
const postsChunks = await Promise.all(
uriChunks.map(uris =>
agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts),
),
)
const map = new Map<string, AppBskyFeedDefs.PostView>()
for (const post of postsChunks.flat()) {
if (
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
map.set(post.uri, post)
}
}
return map
}
function toKnownType(
notif: AppBskyNotificationListNotifications.Notification,
): NotificationType {
if (notif.reason === 'like') {
if (notif.reasonSubject?.includes('feed.generator')) {
return 'feedgen-like'
}
return 'post-like'
}
if (
notif.reason === 'repost' ||
notif.reason === 'mention' ||
notif.reason === 'reply' ||
notif.reason === 'quote' ||
notif.reason === 'follow'
) {
return notif.reason as NotificationType
}
return 'unknown'
}
function getSubjectUri(
type: NotificationType,
notif: AppBskyNotificationListNotifications.Notification,
): string | undefined {
if (type === 'reply' || type === 'quote' || type === 'mention') {
return notif.uri
} else if (type === 'post-like' || type === 'repost') {
if (
AppBskyFeedRepost.isRecord(notif.record) ||
AppBskyFeedLike.isRecord(notif.record)
) {
return typeof notif.record.subject?.uri === 'string'
? notif.record.subject?.uri
: undefined
}
}
}
function isThreadMuted(notif: FeedNotification, mutes: string[]): boolean {
if (!notif.subject) {
return false
}
const record = notif.subject.record as AppBskyFeedPost.Record // assured in fetchSubjects()
return mutes.includes(record.reply?.root.uri || notif.subject.uri)
}

View File

@ -0,0 +1,34 @@
import {
AppBskyNotificationListNotifications,
AppBskyFeedDefs,
} from '@atproto/api'
export type NotificationType =
| 'post-like'
| 'feedgen-like'
| 'repost'
| 'mention'
| 'reply'
| 'quote'
| 'follow'
| 'unknown'
export interface FeedNotification {
_reactKey: string
type: NotificationType
notification: AppBskyNotificationListNotifications.Notification
additional?: AppBskyNotificationListNotifications.Notification[]
subjectUri?: string
subject?: AppBskyFeedDefs.PostView
}
export interface FeedPage {
cursor: string | undefined
items: FeedNotification[]
}
export interface CachedFeedPage {
sessDid: string // used to invalidate on session changes
syncedAt: Date
data: FeedPage | undefined
}

View File

@ -1,10 +1,19 @@
/**
* A kind of companion API to ./feed.ts. See that file for more info.
*/
import React from 'react'
import * as Notifications from 'expo-notifications'
import {useQueryClient} from '@tanstack/react-query'
import BroadcastChannel from '#/lib/broadcast'
import {useSession, getAgent} from '#/state/session'
import {useModerationOpts} from '../preferences'
import {shouldFilterNotif} from './util'
import {fetchPage} from './util'
import {CachedFeedPage, FeedPage} from './types'
import {isNative} from '#/platform/detection'
import {useMutedThreads} from '#/state/muted-threads'
import {RQKEY as RQKEY_NOTIFS} from './feed'
import {logger} from '#/logger'
const UPDATE_INTERVAL = 30 * 1e3 // 30sec
@ -14,7 +23,8 @@ type StateContext = string
interface ApiContext {
markAllRead: () => Promise<void>
checkUnread: () => Promise<void>
checkUnread: (opts?: {invalidate?: boolean}) => Promise<void>
getCachedUnreadPage: () => FeedPage | undefined
}
const stateContext = React.createContext<StateContext>('')
@ -22,16 +32,23 @@ const stateContext = React.createContext<StateContext>('')
const apiContext = React.createContext<ApiContext>({
async markAllRead() {},
async checkUnread() {},
getCachedUnreadPage: () => undefined,
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const {hasSession} = useSession()
const {hasSession, currentAccount} = useSession()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const [numUnread, setNumUnread] = React.useState('')
const checkUnreadRef = React.useRef<(() => Promise<void>) | null>(null)
const lastSyncRef = React.useRef<Date>(new Date())
const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null)
const cacheRef = React.useRef<CachedFeedPage>({
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
})
// periodic sync
React.useEffect(() => {
@ -46,14 +63,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// listen for broadcasts
React.useEffect(() => {
const listener = ({data}: MessageEvent) => {
lastSyncRef.current = new Date()
cacheRef.current = {
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
}
setNumUnread(data.event)
}
broadcast.addEventListener('message', listener)
return () => {
broadcast.removeEventListener('message', listener)
}
}, [setNumUnread])
}, [setNumUnread, currentAccount])
// create API
const api = React.useMemo<ApiContext>(() => {
@ -61,7 +82,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
async markAllRead() {
// update server
await getAgent().updateSeenNotifications(
lastSyncRef.current.toISOString(),
cacheRef.current.syncedAt.toISOString(),
)
// update & broadcast
@ -69,38 +90,59 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
broadcast.postMessage({event: ''})
},
async checkUnread() {
const agent = getAgent()
async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
try {
if (!getAgent().session) return
if (!agent.session) return
// count
const page = await fetchPage({
cursor: undefined,
limit: 40,
queryClient,
moderationOpts,
threadMutes,
})
const unreadCount = countUnread(page)
const unreadCountStr =
unreadCount >= 30
? '30+'
: unreadCount === 0
? ''
: String(unreadCount)
if (isNative) {
Notifications.setBadgeCountAsync(Math.min(unreadCount, 30))
}
// count
const res = await agent.listNotifications({limit: 40})
const filtered = res.data.notifications.filter(
notif => !notif.isRead && !shouldFilterNotif(notif, moderationOpts),
)
const num =
filtered.length >= 30
? '30+'
: filtered.length === 0
? ''
: String(filtered.length)
if (isNative) {
Notifications.setBadgeCountAsync(Math.min(filtered.length, 30))
// track last sync
const now = new Date()
const lastIndexed =
page.items[0] && new Date(page.items[0].notification.indexedAt)
cacheRef.current = {
sessDid: currentAccount?.did || '',
data: page,
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
}
// update & broadcast
setNumUnread(unreadCountStr)
if (invalidate) {
queryClient.resetQueries({queryKey: RQKEY_NOTIFS()})
}
broadcast.postMessage({event: unreadCountStr})
} catch (e) {
logger.error('Failed to check unread notifications', {error: e})
}
},
// track last sync
const now = new Date()
const lastIndexed = filtered[0] && new Date(filtered[0].indexedAt)
lastSyncRef.current =
!lastIndexed || now > lastIndexed ? now : lastIndexed
// update & broadcast
setNumUnread(num)
broadcast.postMessage({event: num})
getCachedUnreadPage() {
// return cached page if was for the current user
// (protects against session changes serving data from the past session)
if (cacheRef.current.sessDid === currentAccount?.did) {
return cacheRef.current.data
}
},
}
}, [setNumUnread, moderationOpts])
}, [setNumUnread, queryClient, moderationOpts, threadMutes, currentAccount])
checkUnreadRef.current = api.checkUnread
return (
@ -117,3 +159,20 @@ export function useUnreadNotifications() {
export function useUnreadNotificationsApi() {
return React.useContext(apiContext)
}
function countUnread(page: FeedPage) {
let num = 0
for (const item of page.items) {
if (!item.notification.isRead) {
num++
}
if (item.additional) {
for (const item2 of item.additional) {
if (!item2.isRead) {
num++
}
}
}
}
return num
}

View File

@ -3,10 +3,78 @@ import {
ModerationOpts,
moderateProfile,
moderatePost,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
} from '@atproto/api'
import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
import {NotificationType, FeedNotification, FeedPage} from './types'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
const MS_1HR = 1e3 * 60 * 60
const MS_2DAY = MS_1HR * 48
// exported api
// =
export async function fetchPage({
cursor,
limit,
queryClient,
moderationOpts,
threadMutes,
}: {
cursor: string | undefined
limit: number
queryClient: QueryClient
moderationOpts: ModerationOpts | undefined
threadMutes: string[]
}): Promise<FeedPage> {
const res = await getAgent().listNotifications({
limit,
cursor,
})
// filter out notifs by mod rules
const notifs = res.data.notifications.filter(
notif => !shouldFilterNotif(notif, moderationOpts),
)
// group notifications which are essentially similar (follows, likes on a post)
let notifsGrouped = groupNotifications(notifs)
// we fetch subjects of notifications (usually posts) now instead of lazily
// in the UI to avoid relayouts
const subjects = await fetchSubjects(notifsGrouped)
for (const notif of notifsGrouped) {
if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
}
}
}
// apply thread muting
notifsGrouped = notifsGrouped.filter(
notif => !isThreadMuted(notif, threadMutes),
)
return {
cursor: res.data.cursor,
items: notifsGrouped,
}
}
// internal methods
// =
// TODO this should be in the sdk as moderateNotification -prf
export function shouldFilterNotif(
function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification,
moderationOpts: ModerationOpts | undefined,
): boolean {
@ -36,3 +104,116 @@ export function shouldFilterNotif(
// (this requires fetching the post)
return false
}
function groupNotifications(
notifs: AppBskyNotificationListNotifications.Notification[],
): FeedNotification[] {
const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) {
const ts = +new Date(notif.indexedAt)
let grouped = false
if (GROUPABLE_REASONS.includes(notif.reason)) {
for (const groupedNotif of groupedNotifs) {
const ts2 = +new Date(groupedNotif.notification.indexedAt)
if (
Math.abs(ts2 - ts) < MS_2DAY &&
notif.reason === groupedNotif.notification.reason &&
notif.reasonSubject === groupedNotif.notification.reasonSubject &&
notif.author.did !== groupedNotif.notification.author.did &&
notif.isRead === groupedNotif.notification.isRead
) {
groupedNotif.additional = groupedNotif.additional || []
groupedNotif.additional.push(notif)
grouped = true
break
}
}
}
if (!grouped) {
const type = toKnownType(notif)
groupedNotifs.push({
_reactKey: `notif-${notif.uri}`,
type,
notification: notif,
subjectUri: getSubjectUri(type, notif),
})
}
}
return groupedNotifs
}
async function fetchSubjects(
groupedNotifs: FeedNotification[],
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>()
for (const notif of groupedNotifs) {
if (notif.subjectUri) {
uris.add(notif.subjectUri)
}
}
const uriChunks = chunk(Array.from(uris), 25)
const postsChunks = await Promise.all(
uriChunks.map(uris =>
getAgent()
.app.bsky.feed.getPosts({uris})
.then(res => res.data.posts),
),
)
const map = new Map<string, AppBskyFeedDefs.PostView>()
for (const post of postsChunks.flat()) {
if (
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
map.set(post.uri, post)
}
}
return map
}
function toKnownType(
notif: AppBskyNotificationListNotifications.Notification,
): NotificationType {
if (notif.reason === 'like') {
if (notif.reasonSubject?.includes('feed.generator')) {
return 'feedgen-like'
}
return 'post-like'
}
if (
notif.reason === 'repost' ||
notif.reason === 'mention' ||
notif.reason === 'reply' ||
notif.reason === 'quote' ||
notif.reason === 'follow'
) {
return notif.reason as NotificationType
}
return 'unknown'
}
function getSubjectUri(
type: NotificationType,
notif: AppBskyNotificationListNotifications.Notification,
): string | undefined {
if (type === 'reply' || type === 'quote' || type === 'mention') {
return notif.uri
} else if (type === 'post-like' || type === 'repost') {
if (
AppBskyFeedRepost.isRecord(notif.record) ||
AppBskyFeedLike.isRecord(notif.record)
) {
return typeof notif.record.subject?.uri === 'string'
? notif.record.subject?.uri
: undefined
}
}
}
function isThreadMuted(notif: FeedNotification, mutes: string[]): boolean {
if (!notif.subject) {
return false
}
const record = notif.subject.record as AppBskyFeedPost.Record // assured in fetchSubjects()
return mutes.includes(record.reply?.root.uri || notif.subject.uri)
}

View File

@ -1,4 +1,3 @@
import {useCallback, useMemo} from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
import {
useInfiniteQuery,
@ -7,9 +6,8 @@ import {
QueryClient,
useQueryClient,
} from '@tanstack/react-query'
import {getAgent} from '../session'
import {useFeedTuners} from '../preferences/feed-tuners'
import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
import {FollowingFeedAPI} from 'lib/api/feed/following'
import {AuthorFeedAPI} from 'lib/api/feed/author'
@ -17,10 +15,13 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge'
import {useModerationOpts} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
type ActorDid = string
type AuthorFilter =
@ -42,7 +43,7 @@ export interface FeedParams {
mergeFeedSources?: string[]
}
type RQPageParam = string | undefined
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
return ['post-feed', feedDesc, params || {}]
@ -63,7 +64,15 @@ export interface FeedPostSlice {
items: FeedPostSliceItem[]
}
export interface FeedPageUnselected {
api: FeedAPI
cursor: string | undefined
feed: AppBskyFeedDefs.FeedViewPost[]
}
export interface FeedPage {
api: FeedAPI
tuner: FeedTuner | NoopFeedTuner
cursor: string | undefined
slices: FeedPostSlice[]
}
@ -76,117 +85,139 @@ export function usePostFeedQuery(
const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc)
const enabled = opts?.enabled !== false
const moderationOpts = useModerationOpts()
const agent = getAgent()
const api: FeedAPI = useMemo(() => {
if (feedDesc === 'home') {
return new MergeFeedAPI(agent, params || {}, feedTuners)
} else if (feedDesc === 'following') {
return new FollowingFeedAPI(agent)
} else if (feedDesc.startsWith('author')) {
const [_, actor, filter] = feedDesc.split('|')
return new AuthorFeedAPI(agent, {actor, filter})
} else if (feedDesc.startsWith('likes')) {
const [_, actor] = feedDesc.split('|')
return new LikesFeedAPI(agent, {actor})
} else if (feedDesc.startsWith('feedgen')) {
const [_, feed] = feedDesc.split('|')
return new CustomFeedAPI(agent, {feed})
} else if (feedDesc.startsWith('list')) {
const [_, list] = feedDesc.split('|')
return new ListFeedAPI(agent, {list})
} else {
// shouldnt happen
return new FollowingFeedAPI(agent)
}
}, [feedDesc, params, feedTuners, agent])
const disableTuner = !!params?.disableTuner
const tuner = useMemo(
() => (disableTuner ? new NoopFeedTuner() : new FeedTuner()),
[disableTuner],
)
const pollLatest = useCallback(async () => {
if (!enabled) {
return false
}
logger.debug('usePostFeedQuery: pollLatest')
const post = await api.peekLatest()
if (post && moderationOpts) {
const slices = tuner.tune([post], feedTuners, {
dryRun: true,
maintainOrder: true,
})
if (slices[0]) {
if (
!moderatePost(slices[0].items[0].post, moderationOpts).content.filter
) {
return true
}
}
}
return false
}, [api, tuner, feedTuners, moderationOpts, enabled])
const out = useInfiniteQuery<
FeedPage,
return useInfiniteQuery<
FeedPageUnselected,
Error,
InfiniteData<FeedPage>,
QueryKey,
RQPageParam
>({
enabled,
staleTime: STALE.INFINITY,
queryKey: RQKEY(feedDesc, params),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
logger.debug('usePostFeedQuery', {feedDesc, pageParam})
if (!pageParam) {
tuner.reset()
}
const res = await api.fetch({cursor: pageParam, limit: 30})
const {api, cursor} = pageParam
? pageParam
: {
api: createApi(feedDesc, params || {}, feedTuners),
cursor: undefined,
}
const res = await api.fetch({cursor, limit: 30})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
const slices = tuner.tune(res.feed, feedTuners)
/*
* If this is a public view, we need to check if posts fail moderation.
* If all fail, we throw an error. If only some fail, we continue and let
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!getAgent().session) {
assertSomePostsPassModeration(res.feed)
}
return {
api,
cursor: res.cursor,
slices: slices.map(slice => ({
_reactKey: slice._reactKey,
rootUri: slice.rootItem.post.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
item => item.post.author.did === slice.items[0].post.author.did,
),
items: slice.items
.map((item, i) => {
if (
AppBskyFeedPost.isRecord(item.post.record) &&
AppBskyFeedPost.validateRecord(item.post.record).success
) {
return {
_reactKey: `${slice._reactKey}-${i}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
reason: i === 0 && slice.source ? slice.source : item.reason,
}
}
return undefined
})
.filter(Boolean) as FeedPostSliceItem[],
})),
feed: res.feed,
}
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled,
getNextPageParam: lastPage => ({
api: lastPage.api,
cursor: lastPage.cursor,
}),
select(data) {
const tuner = params?.disableTuner
? new NoopFeedTuner()
: new FeedTuner(feedTuners)
return {
pageParams: data.pageParams,
pages: data.pages.map(page => ({
api: page.api,
tuner,
cursor: page.cursor,
slices: tuner.tune(page.feed).map(slice => ({
_reactKey: slice._reactKey,
rootUri: slice.rootItem.post.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
item => item.post.author.did === slice.items[0].post.author.did,
),
items: slice.items
.map((item, i) => {
if (
AppBskyFeedPost.isRecord(item.post.record) &&
AppBskyFeedPost.validateRecord(item.post.record).success
) {
return {
_reactKey: `${slice._reactKey}-${i}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
reason:
i === 0 && slice.source ? slice.source : item.reason,
}
}
return undefined
})
.filter(Boolean) as FeedPostSliceItem[],
})),
})),
}
},
})
}
return {...out, pollLatest}
export async function pollLatest(page: FeedPage | undefined) {
if (!page) {
return false
}
logger.debug('usePostFeedQuery: pollLatest')
const post = await page.api.peekLatest()
if (post) {
const slices = page.tuner.tune([post], {
dryRun: true,
maintainOrder: true,
})
if (slices[0]) {
return true
}
}
return false
}
function createApi(
feedDesc: FeedDescriptor,
params: FeedParams,
feedTuners: FeedTunerFn[],
) {
if (feedDesc === 'home') {
return new MergeFeedAPI(params, feedTuners)
} else if (feedDesc === 'following') {
return new FollowingFeedAPI()
} else if (feedDesc.startsWith('author')) {
const [_, actor, filter] = feedDesc.split('|')
return new AuthorFeedAPI({actor, filter})
} else if (feedDesc.startsWith('likes')) {
const [_, actor] = feedDesc.split('|')
return new LikesFeedAPI({actor})
} else if (feedDesc.startsWith('feedgen')) {
const [_, feed] = feedDesc.split('|')
return new CustomFeedAPI({feed})
} else if (feedDesc.startsWith('list')) {
const [_, list] = feedDesc.split('|')
return new ListFeedAPI({list})
} else {
// shouldnt happen
return new FollowingFeedAPI()
}
}
/**
@ -196,8 +227,10 @@ export function usePostFeedQuery(
export function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): FeedPostSliceItem | undefined {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
): AppBskyFeedDefs.FeedViewPost | undefined {
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
queryKey: ['post-feed'],
})
for (const [_queryKey, queryData] of queryDatas) {
@ -205,14 +238,34 @@ export function findPostInQueryData(
continue
}
for (const page of queryData?.pages) {
for (const slice of page.slices) {
for (const item of slice.items) {
if (item.uri === uri) {
return item
}
for (const item of page.feed) {
if (item.post.uri === uri) {
return item
}
}
}
}
return undefined
}
function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
// assume false
let somePostsPassModeration = false
for (const item of feed) {
const moderationOpts = getModerationOpts({
userDid: '',
preferences: DEFAULT_LOGGED_OUT_PREFERENCES,
})
const moderation = moderatePost(item.post, moderationOpts)
if (!moderation.content.filter) {
// we have a sfw post
somePostsPassModeration = true
}
}
if (!somePostsPassModeration) {
throw new Error(KnownError.FeedNSFPublic)
}
}

View File

@ -2,7 +2,6 @@ import {AppBskyFeedGetLikes} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@ -18,7 +17,6 @@ export function usePostLikedByQuery(resolvedUri: string | undefined) {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getLikes({

View File

@ -2,7 +2,6 @@ import {AppBskyFeedGetRepostedBy} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@ -18,7 +17,6 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getRepostedBy({

View File

@ -7,11 +7,7 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {STALE} from '#/state/queries'
import {
findPostInQueryData as findPostInFeedQueryData,
FeedPostSliceItem,
} from './post-feed'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
@ -68,7 +64,6 @@ export type ThreadNode =
export function usePostThreadQuery(uri: string | undefined) {
const queryClient = useQueryClient()
return useQuery<ThreadNode, Error>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''),
async queryFn() {
const res = await getAgent().getPostThread({uri: uri!})
@ -93,7 +88,7 @@ export function usePostThreadQuery(uri: string | undefined) {
{
const item = findPostInFeedQueryData(queryClient, uri)
if (item) {
return feedItemToPlaceholderThread(item)
return feedViewPostToPlaceholderThread(item)
}
}
{
@ -275,13 +270,15 @@ function threadNodeToPlaceholderThread(
}
}
function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
function feedViewPostToPlaceholderThread(
item: AppBskyFeedDefs.FeedViewPost,
): ThreadNode {
return {
type: 'post',
_reactKey: item.post.uri,
uri: item.post.uri,
post: item.post,
record: item.record,
record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed
parent: undefined,
replies: undefined,
viewer: item.post.viewer,
@ -291,7 +288,7 @@ function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
hasMore: false,
showChildReplyLine: false,
showParentReplyLine: false,
isParentLoading: !!item.record.reply,
isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply,
isChildLoading: !!item.post.replyCount,
},
}
@ -305,7 +302,7 @@ function postViewToPlaceholderThread(
_reactKey: post.uri,
uri: post.uri,
post: post,
record: post.record as AppBskyFeedPost.Record, // validate in notifs
record: post.record as AppBskyFeedPost.Record, // validated in notifs
parent: undefined,
replies: undefined,
viewer: post.viewer,

View File

@ -4,13 +4,11 @@ import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {STALE} from '#/state/queries'
export const RQKEY = (postUri: string) => ['post', postUri]
export function usePostQuery(uri: string | undefined) {
return useQuery<AppBskyFeedDefs.PostView>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''),
async queryFn() {
const res = await getAgent().getPosts({uris: [uri!]})
@ -29,7 +27,6 @@ export function useGetPost() {
return React.useCallback(
async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''),
async queryFn() {
const urip = new AtUri(uri)

View File

@ -2,7 +2,6 @@ import {AppBskyFeedGetActorFeeds} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@ -22,7 +21,6 @@ export function useProfileFeedgensQuery(
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(did),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.feed.getActorFeeds({

View File

@ -2,7 +2,6 @@ import {AppBskyGraphGetFollowers} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@ -17,7 +16,6 @@ export function useProfileFollowersQuery(did: string | undefined) {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getFollowers({

View File

@ -1,8 +1,6 @@
import {AppBskyGraphGetLists} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@ -18,7 +16,6 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(did),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getLists({

View File

@ -4,7 +4,6 @@ import {
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyActorGetProfile,
BskyAgent,
} from '@atproto/api'
import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
import {Image as RNImage} from 'react-native-image-crop-picker'
@ -22,6 +21,10 @@ export const RQKEY = (did: string) => ['profile', did]
export function useProfileQuery({did}: {did: string | undefined}) {
return useQuery({
// WARNING
// this staleTime is load-bearing
// if you remove it, the UI infinite-loops
// -prf
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(did || ''),
queryFn: async () => {
@ -68,7 +71,7 @@ export function useProfileUpdateMutation() {
}
return existing
})
await whenAppViewReady(getAgent(), profile.did, res => {
await whenAppViewReady(profile.did, res => {
if (typeof newUserAvatar !== 'undefined') {
if (newUserAvatar === null && res.data.avatar) {
// url hasnt cleared yet
@ -464,7 +467,6 @@ function useProfileUnblockMutation() {
}
async function whenAppViewReady(
agent: BskyAgent,
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
) {
@ -472,6 +474,6 @@ async function whenAppViewReady(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() => agent.app.bsky.actor.getProfile({actor}),
() => getAgent().app.bsky.actor.getProfile({actor}),
)
}

View File

@ -23,7 +23,7 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
export function useResolveDidQuery(didOrHandle: string | undefined) {
return useQuery<string, Error>({
staleTime: STALE.INFINITY,
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle || ''),
async queryFn() {
if (!didOrHandle) {

View File

@ -1,13 +1,10 @@
import {BskyAgent} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
export const RQKEY = (serviceUrl: string) => ['service', serviceUrl]
export function useServiceQuery(serviceUrl: string) {
return useQuery({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(serviceUrl),
queryFn: async () => {
const agent = new BskyAgent({service: serviceUrl})

View File

@ -90,7 +90,7 @@ export function useGetSuggestedFollowersByActor() {
return React.useCallback(
async (actor: string) => {
const res = await queryClient.fetchQuery({
staleTime: 60 * 1000,
staleTime: STALE.MINUTES.ONE,
queryKey: suggestedFollowsByActorQueryKey(actor),
queryFn: async () => {
const res =

View File

@ -13,6 +13,12 @@ import {useCloseAllActiveElements} from '#/state/util'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
/**
* NOTE
* Never hold on to the object returned by this function.
* Call `getAgent()` at the time of invocation to ensure
* that you never have a stale agent.
*/
export function getAgent() {
return __globalAgent
}

View File

@ -1,23 +1,30 @@
import React from 'react'
import {ScrollView, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {UserAvatar} from '../../util/UserAvatar'
import {s} from 'lib/styles'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {styles} from './styles'
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
@ -48,11 +55,19 @@ function AccountItem({
{account.handle}
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
{isCurrentAccount ? (
<FontAwesomeIcon
icon="check"
size={16}
style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
/>
) : (
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
)}
</View>
</TouchableOpacity>
)
@ -67,8 +82,9 @@ export const ChooseAccountForm = ({
const {track, screen} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const {accounts} = useSession()
const {accounts, currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
React.useEffect(() => {
screen('Choose Account')
@ -77,13 +93,21 @@ export const ChooseAccountForm = ({
const onSelect = React.useCallback(
async (account: SessionAccount) => {
if (account.accessJwt) {
await initSession(account)
track('Sign In', {resumedSession: true})
if (account.did === currentAccount?.did) {
setShowLoggedOut(false)
Toast.show(`Already signed in as @${account.handle}`)
} else {
await initSession(account)
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)
}
} else {
onSelectAccount(account)
}
},
[track, initSession, onSelectAccount],
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut],
)
return (
@ -94,7 +118,12 @@ export const ChooseAccountForm = ({
<Trans>Sign in as...</Trans>
</Text>
{accounts.map(account => (
<AccountItem key={account.did} account={account} onSelect={onSelect} />
<AccountItem
key={account.did}
account={account}
onSelect={onSelect}
isCurrentAccount={account.did === currentAccount?.did}
/>
))}
<TouchableOpacity
testID="chooseNewAccountBtn"

View File

@ -62,7 +62,7 @@ export function FeedPage({
const onSoftReset = React.useCallback(() => {
if (isPageFocused) {
scrollToTop()
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}
}, [isPageFocused, scrollToTop, queryClient, feed, setHasNew])
@ -83,7 +83,7 @@ export function FeedPage({
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollToTop, feed, queryClient, setHasNew])

View File

@ -17,12 +17,14 @@ import {useModalControls} from '#/state/modals'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
usePinFeedMutation,
UsePreferencesQueryResponse,
usePreferencesQuery,
useSaveFeedMutation,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
export function FeedSourceCard({
feedUri,
@ -30,17 +32,27 @@ export function FeedSourceCard({
showSaveBtn = false,
showDescription = false,
showLikes = false,
LoadingComponent,
pinOnSave = false,
}: {
feedUri: string
style?: StyleProp<ViewStyle>
showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
LoadingComponent?: JSX.Element
pinOnSave?: boolean
}) {
const {data: preferences} = usePreferencesQuery()
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
if (!feed || !preferences) return null
if (!feed || !preferences) {
return LoadingComponent ? (
LoadingComponent
) : (
<FeedLoadingPlaceholder style={{flex: 1}} />
)
}
return (
<FeedSourceCardLoaded
@ -50,6 +62,7 @@ export function FeedSourceCard({
showSaveBtn={showSaveBtn}
showDescription={showDescription}
showLikes={showLikes}
pinOnSave={pinOnSave}
/>
)
}
@ -61,6 +74,7 @@ export function FeedSourceCardLoaded({
showSaveBtn = false,
showDescription = false,
showLikes = false,
pinOnSave = false,
}: {
feed: FeedSourceInfo
preferences: UsePreferencesQueryResponse
@ -68,6 +82,7 @@ export function FeedSourceCardLoaded({
showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
pinOnSave?: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
@ -78,6 +93,7 @@ export function FeedSourceCardLoaded({
useSaveFeedMutation()
const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri))
@ -103,14 +119,18 @@ export function FeedSourceCardLoaded({
})
} else {
try {
await saveFeed({uri: feed.uri})
if (pinOnSave) {
await pinFeed({uri: feed.uri})
} else {
await saveFeed({uri: feed.uri})
}
Toast.show('Added to my feeds')
} catch (e) {
Toast.show('There was an issue contacting your server')
logger.error('Failed to save feed', {error: e})
}
}
}, [isSaved, openModal, feed, removeFeed, saveFeed, _])
}, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
if (!feed || !preferences) return null
@ -150,7 +170,7 @@ export function FeedSourceCardLoaded({
{showSaveBtn && feed.type === 'feed' && (
<View>
<Pressable
disabled={isSavePending || isRemovePending}
disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button"
accessibilityLabel={
isSaved ? 'Remove from my feeds' : 'Add to my feeds'

View File

@ -35,15 +35,13 @@ export function Feed({
const [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts()
const {markAllRead} = useUnreadNotificationsApi()
const {markAllRead, checkUnread} = useUnreadNotificationsApi()
const {
data,
isLoading,
isFetching,
isFetched,
isError,
error,
refetch,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
@ -52,13 +50,11 @@ export function Feed({
const firstItem = data?.pages[0]?.items[0]
// mark all read on fresh data
// (this will fire each time firstItem changes)
React.useEffect(() => {
let cleanup
if (firstItem) {
const to = setTimeout(() => markAllRead(), 250)
cleanup = () => clearTimeout(to)
markAllRead()
}
return cleanup
}, [firstItem, markAllRead])
const items = React.useMemo(() => {
@ -83,7 +79,7 @@ export function Feed({
const onRefresh = React.useCallback(async () => {
try {
setIsPTRing(true)
await refetch()
await checkUnread({invalidate: true})
} catch (err) {
logger.error('Failed to refresh notifications feed', {
error: err,
@ -91,7 +87,7 @@ export function Feed({
} finally {
setIsPTRing(false)
}
}, [refetch, setIsPTRing])
}, [checkUnread, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
@ -136,21 +132,6 @@ export function Feed({
[onPressRetryLoadMore, moderationOpts],
)
const showHeaderSpinner = !isPTRing && isFetching && !isLoading
const FeedHeader = React.useCallback(
() => (
<View>
{ListHeaderComponent ? <ListHeaderComponent /> : null}
{showHeaderSpinner ? (
<View style={{padding: 10}}>
<ActivityIndicator />
</View>
) : null}
</View>
),
[ListHeaderComponent, showHeaderSpinner],
)
const FeedFooter = React.useCallback(
() =>
isFetchingNextPage ? (
@ -180,7 +161,7 @@ export function Feed({
data={items}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListHeaderComponent={FeedHeader}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl

View File

@ -12,6 +12,9 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {TextLink} from '#/view/com/util/Link'
import {CenteredView} from '../util/Views'
import {isWeb} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -79,11 +82,37 @@ function FeedsTabBarPublic() {
function FeedsTabBarTablet(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) {
const feeds = usePinnedFeedsInfos()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const pal = usePalette('default')
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const {headerMinimalShellTransform} = useMinimalShellMode()
const {headerHeight} = useShellLayout()
const items = feeds.map(f => f.displayName)
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
const items = showFeedsLinkInTabBar
? pinnedDisplayNames.concat('Feeds ✨')
: pinnedDisplayNames
const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
const onSelect = React.useCallback(
(index: number) => {
if (showFeedsLinkInTabBar && index === items.length - 1) {
onPressDiscoverFeeds()
} else if (props.onSelect) {
props.onSelect(index)
}
},
[items.length, onPressDiscoverFeeds, props, showFeedsLinkInTabBar],
)
return (
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
@ -95,6 +124,7 @@ function FeedsTabBarTablet(
<TabBar
key={items.join(',')}
{...props}
onSelect={onSelect}
items={items}
indicatorColor={pal.colors.link}
/>

View File

@ -18,6 +18,9 @@ import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useSession} from '#/state/session'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {isWeb} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -26,11 +29,36 @@ export function FeedsTabBar(
const {isSandbox, hasSession} = useSession()
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const feeds = usePinnedFeedsInfos()
const navigation = useNavigation<NavigationProp>()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode()
const items = feeds.map(f => f.displayName)
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
const items = showFeedsLinkInTabBar
? pinnedDisplayNames.concat('Feeds ✨')
: pinnedDisplayNames
const onPressFeedsLink = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
const onSelect = React.useCallback(
(index: number) => {
if (showFeedsLinkInTabBar && index === items.length - 1) {
onPressFeedsLink()
} else if (props.onSelect) {
props.onSelect(index)
}
},
[items.length, onPressFeedsLink, props, showFeedsLinkInTabBar],
)
const onPressAvi = React.useCallback(() => {
setDrawerOpen(true)
@ -84,7 +112,7 @@ export function FeedsTabBar(
key={items.join(',')}
onPressSelected={props.onPressSelected}
selectedPage={props.selectedPage}
onSelect={props.onSelect}
onSelect={onSelect}
testID={props.testID}
items={items}
indicatorColor={pal.colors.link}

View File

@ -108,6 +108,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
pointerEvents: isHeaderReady ? 'auto' : 'none',
}}>
<TabBar
testID={testID}
items={items}
selectedPage={currentPage}
onSelect={props.onSelect}
@ -127,6 +128,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
isMobile,
onTabBarLayout,
onHeaderOnlyLayout,
testID,
],
)

View File

@ -68,6 +68,7 @@ export function TabBar({
return (
<View testID={testID} style={[pal.view, styles.outer]}>
<DraggableScrollView
testID={`${testID}-selector`}
horizontal={true}
showsHorizontalScrollIndicator={false}
ref={scrollElRef}
@ -76,6 +77,7 @@ export function TabBar({
const selected = i === selectedPage
return (
<PressableWithHover
testID={`${testID}-selector-${i}`}
key={item}
onLayout={e => onItemLayout(e, i)}
style={[styles.item, selected && indicatorStyle]}

View File

@ -23,6 +23,7 @@ import {
FeedDescriptor,
FeedParams,
usePostFeedQuery,
pollLatest,
} from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
@ -84,22 +85,21 @@ let Feed = ({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
pollLatest,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
const checkForNew = React.useCallback(async () => {
if (!isFetched || isFetching || !onHasNew) {
if (!data?.pages[0] || isFetching || !onHasNew) {
return
}
try {
if (await pollLatest()) {
if (await pollLatest(data.pages[0])) {
onHasNew(true)
}
} catch (e) {
logger.error('Poll latest failed', {feed, error: String(e)})
}
}, [feed, isFetched, isFetching, pollLatest, onHasNew])
}, [feed, data, isFetching, onHasNew])
React.useEffect(() => {
// we store the interval handler in a ref to avoid needless

View File

@ -17,28 +17,15 @@ import {EmptyState} from '../util/EmptyState'
import {cleanError} from '#/lib/strings/errors'
import {useRemoveFeedMutation} from '#/state/queries/preferences'
enum KnownError {
Block,
FeedgenDoesNotExist,
FeedgenMisconfigured,
FeedgenBadResponse,
FeedgenOffline,
FeedgenUnknown,
Unknown,
}
const MESSAGES = {
[KnownError.Unknown]: '',
[KnownError.Block]: '',
[KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`,
[KnownError.FeedgenMisconfigured]:
'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.',
[KnownError.FeedgenBadResponse]:
'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.',
[KnownError.FeedgenOffline]:
'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.',
[KnownError.FeedgenUnknown]:
'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.',
export enum KnownError {
Block = 'Block',
FeedgenDoesNotExist = 'FeedgenDoesNotExist',
FeedgenMisconfigured = 'FeedgenMisconfigured',
FeedgenBadResponse = 'FeedgenBadResponse',
FeedgenOffline = 'FeedgenOffline',
FeedgenUnknown = 'FeedgenUnknown',
FeedNSFPublic = 'FeedNSFPublic',
Unknown = 'Unknown',
}
export function FeedErrorMessage({
@ -90,7 +77,32 @@ function FeedgenErrorMessage({
const pal = usePalette('default')
const {_: _l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const msg = MESSAGES[knownError]
const msg = React.useMemo(
() =>
({
[KnownError.Unknown]: '',
[KnownError.Block]: '',
[KnownError.FeedgenDoesNotExist]: _l(
msgLingui`Hmmm, we're having trouble finding this feed. It may have been deleted.`,
),
[KnownError.FeedgenMisconfigured]: _l(
msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`,
),
[KnownError.FeedgenBadResponse]: _l(
msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`,
),
[KnownError.FeedgenOffline]: _l(
msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`,
),
[KnownError.FeedNSFPublic]: _l(
msgLingui`We're sorry, but this content is not viewable without a Bluesky account.`,
),
[KnownError.FeedgenUnknown]: _l(
msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`,
),
}[knownError]),
[_l, knownError],
)
const [_, uri] = feedDesc.split('|')
const [ownerDid] = safeParseFeedgenUri(uri)
const {openModal, closeModal} = useModalControls()
@ -121,6 +133,36 @@ function FeedgenErrorMessage({
})
}, [openModal, closeModal, uri, removeFeed, _l])
const cta = React.useMemo(() => {
switch (knownError) {
case KnownError.FeedNSFPublic: {
return null
}
case KnownError.FeedgenDoesNotExist:
case KnownError.FeedgenMisconfigured:
case KnownError.FeedgenBadResponse:
case KnownError.FeedgenOffline:
case KnownError.FeedgenUnknown: {
return (
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
{knownError === KnownError.FeedgenDoesNotExist && (
<Button
type="inverted"
label="Remove feed"
onPress={onRemoveFeed}
/>
)}
<Button
type="default-light"
label="View profile"
onPress={onViewProfile}
/>
</View>
)
}
}
}, [knownError, onViewProfile, onRemoveFeed])
return (
<View
style={[
@ -134,16 +176,7 @@ function FeedgenErrorMessage({
},
]}>
<Text style={pal.text}>{msg}</Text>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
{knownError === KnownError.FeedgenDoesNotExist && (
<Button type="inverted" label="Remove feed" onPress={onRemoveFeed} />
)}
<Button
type="default-light"
label="View profile"
onPress={onViewProfile}
/>
</View>
{cta}
</View>
)
}
@ -196,5 +229,8 @@ function detectKnownError(
if (error.includes('feed provided an invalid response')) {
return KnownError.FeedgenBadResponse
}
if (error.includes(KnownError.FeedNSFPublic)) {
return KnownError.FeedNSFPublic
}
return KnownError.FeedgenUnknown
}

View File

@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
noFeedback?: boolean
asAnchor?: boolean
anchorNoUnderline?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
}
export const Link = memo(function Link({
@ -58,6 +59,7 @@ export const Link = memo(function Link({
asAnchor,
accessible,
anchorNoUnderline,
navigationAction,
...props
}: Props) {
const {closeModal} = useModalControls()
@ -67,10 +69,16 @@ export const Link = memo(function Link({
const onPress = React.useCallback(
(e?: Event) => {
if (typeof href === 'string') {
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
return onPressInner(
closeModal,
navigation,
sanitizeUrl(href),
navigationAction,
e,
)
}
},
[closeModal, navigation, href],
[closeModal, navigation, navigationAction, href],
)
if (noFeedback) {
@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
title,
onPress,
warnOnMismatchingLabel,
navigationAction,
...orgProps
}: {
testID?: string
@ -158,6 +167,7 @@ export const TextLink = memo(function TextLink({
dataSet?: any
title?: string
warnOnMismatchingLabel?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
} & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigation<NavigationProp>()
@ -185,7 +195,13 @@ export const TextLink = memo(function TextLink({
// @ts-ignore function signature differs by platform -prf
return onPress()
}
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
return onPressInner(
closeModal,
navigation,
sanitizeUrl(href),
navigationAction,
e,
)
},
[
onPress,
@ -195,6 +211,7 @@ export const TextLink = memo(function TextLink({
href,
text,
warnOnMismatchingLabel,
navigationAction,
],
)
const hrefAttrs = useMemo(() => {
@ -241,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
accessibilityLabel?: string
accessibilityHint?: string
title?: string
navigationAction?: 'push' | 'replace' | 'navigate'
}
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
testID,
@ -250,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
text,
numberOfLines,
lineHeight,
navigationAction,
...props
}: TextLinkOnWebOnlyProps) {
if (isWeb) {
@ -263,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
numberOfLines={numberOfLines}
lineHeight={lineHeight}
title={props.title}
navigationAction={navigationAction}
{...props}
/>
)
@ -296,6 +316,7 @@ function onPressInner(
closeModal = () => {},
navigation: NavigationProp,
href: string,
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
e?: Event,
) {
let shouldHandle = false
@ -328,8 +349,18 @@ function onPressInner(
} else {
closeModal() // close any active modals
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href)))
if (navigationAction === 'push') {
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href)))
} else if (navigationAction === 'replace') {
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
} else if (navigationAction === 'navigate') {
// @ts-ignore we're not able to type check on this one -prf
navigation.navigate(...router.matchPath(href))
} else {
throw Error('Unsupported navigator action.')
}
}
}
}

View File

@ -171,14 +171,22 @@ export function ProfileCardFeedLoadingPlaceholder() {
export function FeedLoadingPlaceholder({
style,
showLowerPlaceholder = true,
showTopBorder = true,
}: {
style?: StyleProp<ViewStyle>
showTopBorder?: boolean
showLowerPlaceholder?: boolean
}) {
const pal = usePalette('default')
return (
<View
style={[
{paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
{
paddingHorizontal: 12,
paddingVertical: 18,
borderTopWidth: showTopBorder ? 1 : 0,
},
pal.border,
style,
]}>
@ -193,14 +201,16 @@ export function FeedLoadingPlaceholder({
<LoadingPlaceholder width={120} height={8} />
</View>
</View>
<View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder
width={260}
height={8}
style={{marginVertical: 12}}
/>
<LoadingPlaceholder width={120} height={8} />
</View>
{showLowerPlaceholder && (
<View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder
width={260}
height={8}
style={{marginVertical: 12}}
/>
<LoadingPlaceholder width={120} height={8} />
</View>
)}
</View>
)
}

View File

@ -437,6 +437,7 @@ export function FeedsScreen(_props: Props) {
showSaveBtn={hasSession}
showDescription
showLikes
pinOnSave
/>
)
} else if (item.type === 'popularFeedsNoResults') {

View File

@ -40,6 +40,12 @@ function HomeScreenReady({
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const [selectedPage, setSelectedPage] = React.useState(0)
/**
* Used to ensure that we re-compute `customFeeds` AND force a re-render of
* the pager with the new order of feeds.
*/
const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
const customFeeds = React.useMemo(() => {
const pinned = preferences.feeds.pinned
const feeds: FeedDescriptor[] = []
@ -83,7 +89,6 @@ function HomeScreenReady({
emitSoftReset()
}, [])
// TODO(pwi) may need this in public view
const onPageScrollStateChanged = React.useCallback(
(state: 'idle' | 'dragging' | 'settling') => {
if (state === 'dragging') {
@ -118,6 +123,7 @@ function HomeScreenReady({
return hasSession ? (
<Pager
key={pinnedFeedOrderKey}
testID="homeScreen"
onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged}

View File

@ -19,7 +19,10 @@ import {logger} from '#/logger'
import {useSetMinimalShellMode} from '#/state/shell'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {
useUnreadNotifications,
useUnreadNotificationsApi,
} from '#/state/queries/notifications/unread'
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
import {listenSoftReset, emitSoftReset} from '#/state/events'
@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) {
const {screen} = useAnalytics()
const pal = usePalette('default')
const {isDesktop} = useWebMediaQueries()
const unreadNotifs = useUnreadNotifications()
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
const unreadApi = useUnreadNotificationsApi()
const hasNew = !!unreadNotifs
// event handlers
@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) {
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
queryClient.invalidateQueries({
queryKey: NOTIFS_RQKEY(),
})
}, [scrollToTop, queryClient])
if (hasNew) {
// render what we have now
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
} else {
// check with the server
unreadApi.checkUnread({invalidate: true})
}
}, [scrollToTop, queryClient, unreadApi, hasNew])
// on-visible setup
// =

View File

@ -267,6 +267,7 @@ function ProfileScreenLoaded({
screenDescription="profile"
moderation={moderation.account}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={true}
items={sectionTitles}
onPageSelected={onPageSelected}
@ -403,7 +404,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View File

@ -353,6 +353,7 @@ export function ProfileFeedScreenInner({
style={styles.btn}
/>
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'}
@ -501,7 +502,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])

View File

@ -127,7 +127,7 @@ function ProfileListScreenLoaded({
list,
onChange() {
if (isCurateList) {
queryClient.invalidateQueries({
queryClient.resetQueries({
// TODO(eric) should construct these strings with a fn too
queryKey: FEED_RQKEY(`list|${list.uri}`),
})
@ -530,7 +530,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View File

@ -1,14 +1,7 @@
import React from 'react'
import {
StyleSheet,
View,
ActivityIndicator,
Pressable,
TouchableOpacity,
} from 'react-native'
import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
@ -32,9 +25,8 @@ import {
usePinFeedMutation,
useUnpinFeedMutation,
useSetSaveFeedsMutation,
preferencesQueryKey,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
const HITSLOP_TOP = {
top: 20,
@ -57,6 +49,24 @@ export function SavedFeeds({}: Props) {
const {screen} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: setSavedFeeds,
variables: optimisticSavedFeedsResponse,
reset: resetSaveFeedsMutationState,
error: setSavedFeedsError,
} = useSetSaveFeedsMutation()
/*
* Use optimistic data if exists and no error, otherwise fallback to remote
* data
*/
const currentFeeds =
optimisticSavedFeedsResponse && !setSavedFeedsError
? optimisticSavedFeedsResponse
: preferences?.feeds || {saved: [], pinned: []}
const unpinned = currentFeeds.saved.filter(f => {
return !currentFeeds.pinned?.includes(f)
})
useFocusEffect(
React.useCallback(() => {
@ -80,7 +90,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.pinned.length ? (
!currentFeeds.pinned.length ? (
<View
style={[
pal.border,
@ -93,8 +103,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences?.feeds?.pinned?.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned />
currentFeeds.pinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -106,7 +123,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.unpinned.length ? (
!unpinned.length ? (
<View
style={[
pal.border,
@ -119,8 +136,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences.feeds.unpinned.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned={false} />
unpinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned={false}
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -151,22 +175,30 @@ export function SavedFeeds({}: Props) {
function ListItem({
feedUri,
isPinned,
currentFeeds,
setSavedFeeds,
resetSaveFeedsMutationState,
}: {
feedUri: string // uri
isPinned: boolean
currentFeeds: {saved: string[]; pinned: string[]}
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
resetSaveFeedsMutationState: ReturnType<
typeof useSetSaveFeedsMutation
>['reset']
}) {
const pal = usePalette('default')
const queryClient = useQueryClient()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
useUnpinFeedMutation()
const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
useSetSaveFeedsMutation()
const isPending = isPinPending || isUnpinPending
const onTogglePinned = React.useCallback(async () => {
Haptics.default()
try {
resetSaveFeedsMutationState()
if (isPinned) {
await unpinFeed({uri: feedUri})
} else {
@ -176,23 +208,20 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to toggle pinned feed', {error: e})
}
}, [feedUri, isPinned, pinFeed, unpinFeed])
}, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
const onPressUp = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
// create new array, do not mutate
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index === 0) return
;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -201,23 +230,19 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
const onPressDown = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index >= pinned.length - 1) return
;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -226,7 +251,7 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
return (
<Pressable
@ -234,24 +259,30 @@ function ListItem({
style={[styles.itemContainer, pal.border]}>
{isPinned ? (
<View style={styles.webArrowButtonsContainer}>
<TouchableOpacity
disabled={isMovePending}
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressUp}
hitSlop={HITSLOP_TOP}>
hitSlop={HITSLOP_TOP}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="arrow-up"
size={12}
style={[pal.text, styles.webArrowUpButton]}
/>
</TouchableOpacity>
<TouchableOpacity
disabled={isMovePending}
</Pressable>
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressDown}
hitSlop={HITSLOP_BOTTOM}>
hitSlop={HITSLOP_BOTTOM}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
</TouchableOpacity>
</Pressable>
</View>
) : null}
<FeedSourceCard
@ -259,18 +290,28 @@ function ListItem({
feedUri={feedUri}
style={styles.noBorder}
showSaveBtn
LoadingComponent={
<FeedLoadingPlaceholder
style={{flex: 1}}
showLowerPlaceholder={false}
showTopBorder={false}
/>
}
/>
<TouchableOpacity
disabled={isPinPending || isUnpinPending}
<Pressable
disabled={isPending}
accessibilityRole="button"
hitSlop={10}
onPress={onTogglePinned}>
onPress={onTogglePinned}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="thumb-tack"
size={20}
color={isPinned ? colors.blue3 : pal.colors.icon}
/>
</TouchableOpacity>
</Pressable>
</Pressable>
)
}

View File

@ -10,11 +10,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {
useFocusEffect,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -73,6 +69,8 @@ import {STATUS_PAGE_URL} from 'lib/constants'
import {Plural, Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
function SettingsAccountCard({account}: {account: SessionAccount}) {
const pal = usePalette('default')
@ -154,13 +152,14 @@ export function SettingsScreen({}: Props) {
const {screen, track} = useAnalytics()
const {openModal} = useModalControls()
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
const {clearCurrentAccount} = useSessionApi()
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
getAgent(),
)
const {mutate: clearPreferences} = useClearPreferencesMutation()
const {data: invites} = useInviteCodesQuery()
const invitesAvailable = invites?.available?.length ?? 0
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const primaryBg = useCustomPalette<ViewStyle>({
light: {backgroundColor: colors.blue0},
@ -189,10 +188,9 @@ export function SettingsScreen({}: Props) {
const onPressAddAccount = React.useCallback(() => {
track('Settings:AddAccountButtonClicked')
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
clearCurrentAccount()
}, [track, navigation, clearCurrentAccount])
setShowLoggedOut(true)
closeAllActiveElements()
}, [track, setShowLoggedOut, closeAllActiveElements])
const onPressChangeHandle = React.useCallback(() => {
track('Settings:ChangeHandleButtonClicked')

View File

@ -141,7 +141,7 @@ export function DrawerContent() {
} else {
if (tab === 'Notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}

View File

@ -62,7 +62,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
} else {
if (tab === 'Notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}

View File

@ -137,7 +137,7 @@ const NavItem: React.FC<{
: isTab(currentRoute.name, routeName)
return (
<Link href={href} style={styles.ctrl}>
<Link href={href} style={styles.ctrl} navigationAction="navigate">
{children({isActive})}
</Link>
)

View File

@ -11,7 +11,7 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
export function DesktopFeeds() {
const pal = usePalette('default')
const {_} = useLingui()
const feeds = usePinnedFeedsInfos()
const {feeds} = usePinnedFeedsInfos()
const route = useNavigationState(state => {
if (!state) {

View File

@ -150,7 +150,7 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
} else {
if (href === '/notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}