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

This commit is contained in:
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', () => { describe('Home screen', () => {
beforeAll(async () => { beforeAll(async () => {
await createServer('?users&follows&posts') await createServer('?users&follows&posts&feeds')
await openApp({permissions: {notifications: 'YES'}}) await openApp({permissions: {notifications: 'YES'}})
}) })
@ -13,6 +13,23 @@ describe('Home screen', () => {
await element(by.id('homeScreenFeedTabs-Following')).tap() 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 () => { it('Can like posts', async () => {
const carlaPosts = by.id('feedItem-by-carla.test') const carlaPosts = by.id('feedItem-by-carla.test')
await expect( await expect(
@ -65,14 +82,14 @@ describe('Home screen', () => {
it('Can swipe between feeds', async () => { it('Can swipe between feeds', async () => {
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) 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 element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
await expect(element(by.id('followingFeedPage'))).toBeVisible() await expect(element(by.id('followingFeedPage'))).toBeVisible()
}) })
it('Can tap between feeds', async () => { it('Can tap between feeds', async () => {
await element(by.id("homeScreenFeedTabs-What's hot")).tap() await element(by.id('homeScreenFeedTabs-alice-favs')).tap()
await expect(element(by.id('whatshotFeedPage'))).toBeVisible() await expect(element(by.id('customFeedPage'))).toBeVisible()
await element(by.id('homeScreenFeedTabs-Following')).tap() await element(by.id('homeScreenFeedTabs-Following')).tap()
await expect(element(by.id('followingFeedPage'))).toBeVisible() await expect(element(by.id('followingFeedPage'))).toBeVisible()
}) })

View file

@ -1,12 +1,41 @@
const pkg = require('./package.json')
module.exports = function () { 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 { return {
expo: { expo: {
version: VERSION,
name: 'Bluesky', name: 'Bluesky',
slug: 'bluesky', slug: 'bluesky',
scheme: 'bluesky', scheme: 'bluesky',
owner: 'blueskysocial', owner: 'blueskysocial',
version: '1.57.0',
runtimeVersion: { runtimeVersion: {
policy: 'appVersion', policy: 'appVersion',
}, },
@ -19,7 +48,7 @@ module.exports = function () {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
}, },
ios: { ios: {
buildNumber: '4', buildNumber: IOS_BUILD_NUMBER,
supportsTablet: false, supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app', bundleIdentifier: 'xyz.blueskyweb.app',
config: { config: {
@ -43,7 +72,7 @@ module.exports = function () {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
}, },
android: { android: {
versionCode: 46, versionCode: ANDROID_VERSION_CODE,
adaptiveIcon: { adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png', foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
@ -74,7 +103,7 @@ module.exports = function () {
}, },
plugins: [ plugins: [
'expo-localization', 'expo-localization',
hasSentryToken && 'sentry-expo', Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
[ [
'expo-build-properties', 'expo-build-properties',
{ {
@ -100,11 +129,16 @@ module.exports = function () {
}, },
hooks: { hooks: {
postPublish: [ postPublish: [
/*
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/
{ {
file: 'sentry-expo/upload-sourcemaps', file: 'sentry-expo/upload-sourcemaps',
config: { config: {
organization: 'blueskyweb', organization: 'blueskyweb',
project: 'react-native', project: 'react-native',
release: VERSION,
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
}, },
}, },
], ],

View file

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

View file

@ -3,10 +3,12 @@ import 'react-native-gesture-handler' // must be first
import {LogBox} from 'react-native' import {LogBox} from 'react-native'
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
import 'platform/polyfills' import '#/platform/polyfills'
import {registerRootComponent} from 'expo' 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); // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build, // 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 {registerRootComponent} from 'expo'
import App from './src/App' import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
doPolyfill()
registerRootComponent(App) 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: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:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure", "perf:measure": "NODE_ENV=test flashlight measure",
"build:apk": "eas build -p android --profile dev-android-apk",
"intl:extract": "lingui extract", "intl:extract": "lingui extract",
"intl:compile": "lingui compile" "intl:compile": "lingui compile"
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,15 @@ export const queryClient = new QueryClient({
// so we NEVER want to enable this // so we NEVER want to enable this
// -prf // -prf
refetchOnWindowFocus: false, 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' 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({ init({
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432', 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 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." #~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "" #~ 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 #: src/view/com/modals/Repost.tsx:44
msgid "{0}" msgid "{0}"
msgstr "" msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List" msgid "{0} {purposeLabel} List"
msgstr "" 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 #: src/view/screens/Search/Search.tsx:86
msgid "{message}" msgid "{message}"
msgstr "" msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "" msgstr ""
#: src/view/com/modals/EditImage.tsx:299 #: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422 #: src/view/screens/Settings.tsx:410
msgid "Accessibility" msgid "Accessibility"
msgstr "" msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161 #: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289 #: src/view/screens/Settings.tsx:288
msgid "Account" msgid "Account"
msgstr "" msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list" msgid "Add a user to this list"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:358 #: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:367 #: src/view/screens/Settings.tsx:366
msgid "Add account" msgid "Add account"
msgstr "" msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content" msgid "Adult Content"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:574 #: src/view/screens/Settings.tsx:562
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language" msgid "App Language"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:594 #: src/view/screens/Settings.tsx:582
msgid "App passwords" msgid "App passwords"
msgstr "" msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords" msgid "App Passwords"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:437 #: src/view/screens/Settings.tsx:425
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "" msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145 #: 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/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251 #: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 #: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:466 #: src/view/screens/Settings.tsx:454
msgid "Basics" msgid "Basics"
msgstr "" msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday" msgid "Birthday"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:315 #: src/view/screens/Settings.tsx:314
msgid "Birthday:" msgid "Birthday:"
msgstr "" msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social" msgid "Bluesky.Social"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:723 #: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}" msgid "Build version {0} {1}"
msgstr "" msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup" msgid "Cancel waitlist signup"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:309 #: src/view/screens/Settings.tsx:308
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:606 #: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:615 #: src/view/screens/Settings.tsx:603
msgid "Change handle" msgid "Change handle"
msgstr "" msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password" msgid "Choose your password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:699 #: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data" msgid "Clear all legacy storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:701 #: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)" msgid "Clear all legacy storage data (restart after this)"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:711 #: src/view/screens/Settings.tsx:699
msgid "Clear all storage data" msgid "Clear all storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:713 #: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)" msgid "Clear all storage data (restart after this)"
msgstr "" msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain" msgid "Custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:620 #: src/view/screens/Settings.tsx:608
msgid "Danger Zone" msgid "Danger Zone"
msgstr "" msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark" #~ msgid "Dark"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:627 #: src/view/screens/Settings.tsx:615
msgid "Delete account" msgid "Delete account"
msgstr "" msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:637 #: src/view/screens/Settings.tsx:625
msgid "Delete my account…" msgid "Delete my account…"
msgstr "" msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server" msgid "Dev Server"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:642 #: src/view/screens/Settings.tsx:630
msgid "Developer Tools" msgid "Developer Tools"
msgstr "" msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:367 #: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75 #: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds" msgid "Edit My Feeds"
msgstr "" msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated" msgid "Email Updated"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:293 #: src/view/screens/Settings.tsx:292
msgid "Email:" msgid "Email:"
msgstr "" msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds" msgid "Failed to load recommended feeds"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:558 #: src/view/screens/Feeds.tsx:559
msgid "Feed offline" msgid "Feed offline"
msgstr "" msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences" msgid "Feed Preferences"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64 #: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411 #: src/view/shell/Drawer.tsx:411
msgid "Feedback" msgid "Feedback"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:474 #: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169 #: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342 #: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328 #: 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." msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "" 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." msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "" msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle" msgid "Handle"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93 #: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421 #: src/view/shell/Drawer.tsx:421
msgid "Help" msgid "Help"
msgstr "" msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list" msgid "Hide user list"
msgstr "" 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/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306 #: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275 #: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71 #: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95 #: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486 #: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences" msgid "Home Feed Preferences"
msgstr "" msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:386 #: src/view/screens/Settings.tsx:385
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91 #: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374 #: src/view/screens/Settings.tsx:373
msgid "Invite a Friend" msgid "Invite a Friend"
msgstr "" msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings" msgid "Language Settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:546 #: src/view/screens/Settings.tsx:534
msgid "Languages" msgid "Languages"
msgstr "" msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light" #~ msgid "Light"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625 #: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed" msgid "Like this feed"
msgstr "" msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts" msgid "Load more posts"
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:120 #: src/view/screens/Notifications.tsx:130
msgid "Load new notifications" msgid "Load new notifications"
msgstr "" msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server" msgid "Local dev server"
msgstr "" 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" msgid "Login to account that is not listed"
msgstr "" 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!" 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 "" msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr "" msgstr ""
#: src/view/screens/Moderation.tsx:51 #: 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/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346 #: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347 #: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506 #: 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 #: src/view/screens/ProfileList.tsx:506
msgid "More options" msgid "More options"
msgstr "" msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile" msgid "My Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:525 #: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds" msgid "My Saved Feeds"
msgstr "" msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187 #: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509 #: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:380 #: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:447 #: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231 #: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
#: src/view/screens/ProfileFeed.tsx:618 #: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632 #: src/view/screens/ProfileList.tsx:632
msgid "No description" msgid "No description"
msgstr "" msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result" msgid "No result"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:451 #: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\"" msgid "No results found for \"{query}\""
msgstr "" msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable." msgid "Not Applicable."
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:87 #: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:111 #: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364 #: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299 #: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51 #: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation" msgid "Open navigation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:538 #: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings" msgid "Opens configurable language settings"
msgstr "" 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 #: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain" msgid "Opens modal for using custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:563 #: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings" msgid "Opens moderation settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:519 #: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds" msgid "Opens screen with all saved feeds"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:586 #: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page" msgid "Opens the app password settings page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:478 #: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences" msgid "Opens the home feed preferences"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:669 #: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page" msgid "Opens the storybook page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:649 #: src/view/screens/Settings.tsx:637
msgid "Opens the system log page" msgid "Opens the system log page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:499 #: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences" msgid "Opens the threads preferences"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109 #: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account" msgid "Other account"
msgstr "" msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults." msgid "Pictures meant for adults."
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:79 #: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds" msgid "Pinned Feeds"
msgstr "" msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows" msgid "Prioritize Your Follows"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75 #: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:794 #: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email." msgid "Protect your account by verifying your email."
msgstr "" msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92 #: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?" msgid "Remove {0} from my feeds?"
msgstr "" msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account" msgid "Remove account"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106 #: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed" msgid "Remove feed"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278 #: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds" msgid "Remove from my feeds"
msgstr "" msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview" msgid "Remove image preview"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107 #: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?" msgid "Remove this feed from your saved feeds?"
msgstr "" msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code" msgid "Reset code"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:691 #: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state" msgid "Reset onboarding state"
msgstr "" msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:681 #: src/view/screens/Settings.tsx:669
msgid "Reset preferences state" msgid "Reset preferences state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:689 #: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state" msgid "Resets the onboarding state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:679 #: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state" msgid "Resets the preferences state"
msgstr "" msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop" msgid "Save image crop"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:105 #: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds" msgid "Saved Feeds"
msgstr "" 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." msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:280 #: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436 #: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380 #: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381 #: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link" #~ msgid "Share link"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:319 #: src/view/screens/Settings.tsx:318
msgid "Show" msgid "Show"
msgstr "" msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In" msgid "Sign In"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37 #: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}" msgid "Sign in as {0}"
msgstr "" 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 #: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..." msgid "Sign in as..."
msgstr "" msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation" msgid "Sign up or sign in to join the conversation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:330 #: src/view/screens/Settings.tsx:329
msgid "Signed in as" msgid "Signed in as"
msgstr "" msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging" msgid "Staging"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:735 #: src/view/screens/Settings.tsx:723
msgid "Status page" msgid "Status page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:671 #: src/view/screens/Settings.tsx:659
msgid "Storybook" msgid "Storybook"
msgstr "" msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System" #~ msgid "System"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:651 #: src/view/screens/Settings.tsx:639
msgid "System log" msgid "System log"
msgstr "" msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall" msgid "Tall"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84 #: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms" msgid "Terms"
msgstr "" msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "" msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508 #: src/view/screens/Settings.tsx:496
msgid "Thread Preferences" msgid "Thread Preferences"
msgstr "" msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:755 #: src/view/screens/Settings.tsx:743
msgid "Verify email" msgid "Verify email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:780 #: src/view/screens/Settings.tsx:768
msgid "Verify my email" msgid "Verify my email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:789 #: src/view/screens/Settings.tsx:777
msgid "Verify My Email" msgid "Verify My Email"
msgstr "" msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!" msgid "We're so excited to have you join us!"
msgstr "" 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 #: 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." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." 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 "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:92 #: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds." msgid "You don't have any pinned feeds."
msgstr "" msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!" msgid "You don't have any saved feeds!"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:118 #: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds." msgid "You don't have any saved feeds."
msgstr "" msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider" msgid "Your hosting provider"
msgstr "" 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 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private." msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "" 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." #~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "" #~ 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 #: src/view/com/modals/Repost.tsx:44
msgid "{0}" msgid "{0}"
msgstr "" msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List" msgid "{0} {purposeLabel} List"
msgstr "" 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 #: src/view/screens/Search/Search.tsx:86
msgid "{message}" msgid "{message}"
msgstr "" msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "" msgstr ""
#: src/view/com/modals/EditImage.tsx:299 #: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422 #: src/view/screens/Settings.tsx:410
msgid "Accessibility" msgid "Accessibility"
msgstr "" msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161 #: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289 #: src/view/screens/Settings.tsx:288
msgid "Account" msgid "Account"
msgstr "" msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list" msgid "Add a user to this list"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:358 #: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:367 #: src/view/screens/Settings.tsx:366
msgid "Add account" msgid "Add account"
msgstr "" msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content" msgid "Adult Content"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:574 #: src/view/screens/Settings.tsx:562
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language" msgid "App Language"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:594 #: src/view/screens/Settings.tsx:582
msgid "App passwords" msgid "App passwords"
msgstr "" msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords" msgid "App Passwords"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:437 #: src/view/screens/Settings.tsx:425
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "" msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145 #: 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/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251 #: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 #: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:466 #: src/view/screens/Settings.tsx:454
msgid "Basics" msgid "Basics"
msgstr "" msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday" msgid "Birthday"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:315 #: src/view/screens/Settings.tsx:314
msgid "Birthday:" msgid "Birthday:"
msgstr "" msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social" msgid "Bluesky.Social"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:723 #: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}" msgid "Build version {0} {1}"
msgstr "" msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup" msgid "Cancel waitlist signup"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:309 #: src/view/screens/Settings.tsx:308
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:606 #: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:615 #: src/view/screens/Settings.tsx:603
msgid "Change handle" msgid "Change handle"
msgstr "" msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password" msgid "Choose your password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:699 #: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data" msgid "Clear all legacy storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:701 #: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)" msgid "Clear all legacy storage data (restart after this)"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:711 #: src/view/screens/Settings.tsx:699
msgid "Clear all storage data" msgid "Clear all storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:713 #: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)" msgid "Clear all storage data (restart after this)"
msgstr "" msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain" msgid "Custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:620 #: src/view/screens/Settings.tsx:608
msgid "Danger Zone" msgid "Danger Zone"
msgstr "" msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark" #~ msgid "Dark"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:627 #: src/view/screens/Settings.tsx:615
msgid "Delete account" msgid "Delete account"
msgstr "" msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:637 #: src/view/screens/Settings.tsx:625
msgid "Delete my account…" msgid "Delete my account…"
msgstr "" msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server" msgid "Dev Server"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:642 #: src/view/screens/Settings.tsx:630
msgid "Developer Tools" msgid "Developer Tools"
msgstr "" msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:367 #: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75 #: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds" msgid "Edit My Feeds"
msgstr "" msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated" msgid "Email Updated"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:293 #: src/view/screens/Settings.tsx:292
msgid "Email:" msgid "Email:"
msgstr "" msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds" msgid "Failed to load recommended feeds"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:558 #: src/view/screens/Feeds.tsx:559
msgid "Feed offline" msgid "Feed offline"
msgstr "" msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences" msgid "Feed Preferences"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64 #: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411 #: src/view/shell/Drawer.tsx:411
msgid "Feedback" msgid "Feedback"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:474 #: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169 #: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342 #: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328 #: 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." msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "" 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." msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "" msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle" msgid "Handle"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93 #: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421 #: src/view/shell/Drawer.tsx:421
msgid "Help" msgid "Help"
msgstr "" msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list" msgid "Hide user list"
msgstr "" 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/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306 #: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275 #: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71 #: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95 #: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486 #: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences" msgid "Home Feed Preferences"
msgstr "" msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:386 #: src/view/screens/Settings.tsx:385
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91 #: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374 #: src/view/screens/Settings.tsx:373
msgid "Invite a Friend" msgid "Invite a Friend"
msgstr "" msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings" msgid "Language Settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:546 #: src/view/screens/Settings.tsx:534
msgid "Languages" msgid "Languages"
msgstr "" msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light" #~ msgid "Light"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625 #: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed" msgid "Like this feed"
msgstr "" msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts" msgid "Load more posts"
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:120 #: src/view/screens/Notifications.tsx:130
msgid "Load new notifications" msgid "Load new notifications"
msgstr "" msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server" msgid "Local dev server"
msgstr "" 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" msgid "Login to account that is not listed"
msgstr "" 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!" 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 "" msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr "" msgstr ""
#: src/view/screens/Moderation.tsx:51 #: 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/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346 #: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347 #: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506 #: 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 #: src/view/screens/ProfileList.tsx:506
msgid "More options" msgid "More options"
msgstr "" msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile" msgid "My Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:525 #: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds" msgid "My Saved Feeds"
msgstr "" msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187 #: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509 #: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:380 #: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:447 #: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231 #: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
#: src/view/screens/ProfileFeed.tsx:618 #: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632 #: src/view/screens/ProfileList.tsx:632
msgid "No description" msgid "No description"
msgstr "" msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result" msgid "No result"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:451 #: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\"" msgid "No results found for \"{query}\""
msgstr "" msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable." msgid "Not Applicable."
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:87 #: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:111 #: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364 #: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299 #: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51 #: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation" msgid "Open navigation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:538 #: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings" msgid "Opens configurable language settings"
msgstr "" 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 #: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain" msgid "Opens modal for using custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:563 #: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings" msgid "Opens moderation settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:519 #: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds" msgid "Opens screen with all saved feeds"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:586 #: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page" msgid "Opens the app password settings page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:478 #: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences" msgid "Opens the home feed preferences"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:669 #: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page" msgid "Opens the storybook page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:649 #: src/view/screens/Settings.tsx:637
msgid "Opens the system log page" msgid "Opens the system log page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:499 #: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences" msgid "Opens the threads preferences"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109 #: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account" msgid "Other account"
msgstr "" msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults." msgid "Pictures meant for adults."
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:79 #: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds" msgid "Pinned Feeds"
msgstr "" msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows" msgid "Prioritize Your Follows"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75 #: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:794 #: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email." msgid "Protect your account by verifying your email."
msgstr "" msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92 #: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?" msgid "Remove {0} from my feeds?"
msgstr "" msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account" msgid "Remove account"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106 #: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed" msgid "Remove feed"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278 #: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds" msgid "Remove from my feeds"
msgstr "" msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview" msgid "Remove image preview"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107 #: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?" msgid "Remove this feed from your saved feeds?"
msgstr "" msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code" msgid "Reset code"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:691 #: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state" msgid "Reset onboarding state"
msgstr "" msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:681 #: src/view/screens/Settings.tsx:669
msgid "Reset preferences state" msgid "Reset preferences state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:689 #: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state" msgid "Resets the onboarding state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:679 #: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state" msgid "Resets the preferences state"
msgstr "" msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop" msgid "Save image crop"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:105 #: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds" msgid "Saved Feeds"
msgstr "" 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." msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:280 #: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436 #: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380 #: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381 #: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link" #~ msgid "Share link"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:319 #: src/view/screens/Settings.tsx:318
msgid "Show" msgid "Show"
msgstr "" msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In" msgid "Sign In"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37 #: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}" msgid "Sign in as {0}"
msgstr "" 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 #: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..." msgid "Sign in as..."
msgstr "" msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation" msgid "Sign up or sign in to join the conversation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:330 #: src/view/screens/Settings.tsx:329
msgid "Signed in as" msgid "Signed in as"
msgstr "" msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging" msgid "Staging"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:735 #: src/view/screens/Settings.tsx:723
msgid "Status page" msgid "Status page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:671 #: src/view/screens/Settings.tsx:659
msgid "Storybook" msgid "Storybook"
msgstr "" msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System" #~ msgid "System"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:651 #: src/view/screens/Settings.tsx:639
msgid "System log" msgid "System log"
msgstr "" msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall" msgid "Tall"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84 #: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms" msgid "Terms"
msgstr "" msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "" msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508 #: src/view/screens/Settings.tsx:496
msgid "Thread Preferences" msgid "Thread Preferences"
msgstr "" msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:755 #: src/view/screens/Settings.tsx:743
msgid "Verify email" msgid "Verify email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:780 #: src/view/screens/Settings.tsx:768
msgid "Verify my email" msgid "Verify my email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:789 #: src/view/screens/Settings.tsx:777
msgid "Verify My Email" msgid "Verify My Email"
msgstr "" msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!" msgid "We're so excited to have you join us!"
msgstr "" 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 #: 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." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." 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 "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:92 #: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds." msgid "You don't have any pinned feeds."
msgstr "" msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!" msgid "You don't have any saved feeds!"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:118 #: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds." msgid "You don't have any saved feeds."
msgstr "" msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider" msgid "Your hosting provider"
msgstr "" 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 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private." msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "" 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." #~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "" #~ 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 #: src/view/com/modals/Repost.tsx:44
msgid "{0}" msgid "{0}"
msgstr "" msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List" msgid "{0} {purposeLabel} List"
msgstr "" 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 #: src/view/screens/Search/Search.tsx:86
msgid "{message}" msgid "{message}"
msgstr "" msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "" msgstr ""
#: src/view/com/modals/EditImage.tsx:299 #: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422 #: src/view/screens/Settings.tsx:410
msgid "Accessibility" msgid "Accessibility"
msgstr "" msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161 #: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289 #: src/view/screens/Settings.tsx:288
msgid "Account" msgid "Account"
msgstr "" msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list" msgid "Add a user to this list"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:358 #: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:367 #: src/view/screens/Settings.tsx:366
msgid "Add account" msgid "Add account"
msgstr "" msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content" msgid "Adult Content"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:574 #: src/view/screens/Settings.tsx:562
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language" msgid "App Language"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:594 #: src/view/screens/Settings.tsx:582
msgid "App passwords" msgid "App passwords"
msgstr "" msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords" msgid "App Passwords"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:437 #: src/view/screens/Settings.tsx:425
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "" msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145 #: 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/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251 #: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 #: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:466 #: src/view/screens/Settings.tsx:454
msgid "Basics" msgid "Basics"
msgstr "" msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday" msgid "Birthday"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:315 #: src/view/screens/Settings.tsx:314
msgid "Birthday:" msgid "Birthday:"
msgstr "" msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social" msgid "Bluesky.Social"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:723 #: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}" msgid "Build version {0} {1}"
msgstr "" msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup" msgid "Cancel waitlist signup"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:309 #: src/view/screens/Settings.tsx:308
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:606 #: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:615 #: src/view/screens/Settings.tsx:603
msgid "Change handle" msgid "Change handle"
msgstr "" msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password" msgid "Choose your password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:699 #: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data" msgid "Clear all legacy storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:701 #: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)" msgid "Clear all legacy storage data (restart after this)"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:711 #: src/view/screens/Settings.tsx:699
msgid "Clear all storage data" msgid "Clear all storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:713 #: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)" msgid "Clear all storage data (restart after this)"
msgstr "" msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain" msgid "Custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:620 #: src/view/screens/Settings.tsx:608
msgid "Danger Zone" msgid "Danger Zone"
msgstr "" msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark" #~ msgid "Dark"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:627 #: src/view/screens/Settings.tsx:615
msgid "Delete account" msgid "Delete account"
msgstr "" msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:637 #: src/view/screens/Settings.tsx:625
msgid "Delete my account…" msgid "Delete my account…"
msgstr "" msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server" msgid "Dev Server"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:642 #: src/view/screens/Settings.tsx:630
msgid "Developer Tools" msgid "Developer Tools"
msgstr "" msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:367 #: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75 #: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds" msgid "Edit My Feeds"
msgstr "" msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated" msgid "Email Updated"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:293 #: src/view/screens/Settings.tsx:292
msgid "Email:" msgid "Email:"
msgstr "" msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds" msgid "Failed to load recommended feeds"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:558 #: src/view/screens/Feeds.tsx:559
msgid "Feed offline" msgid "Feed offline"
msgstr "" msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences" msgid "Feed Preferences"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64 #: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411 #: src/view/shell/Drawer.tsx:411
msgid "Feedback" msgid "Feedback"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:474 #: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169 #: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342 #: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328 #: 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." msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "" 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." msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "" msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle" msgid "Handle"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93 #: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421 #: src/view/shell/Drawer.tsx:421
msgid "Help" msgid "Help"
msgstr "" msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list" msgid "Hide user list"
msgstr "" 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/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306 #: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275 #: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71 #: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95 #: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486 #: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences" msgid "Home Feed Preferences"
msgstr "" msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:386 #: src/view/screens/Settings.tsx:385
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91 #: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374 #: src/view/screens/Settings.tsx:373
msgid "Invite a Friend" msgid "Invite a Friend"
msgstr "" msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings" msgid "Language Settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:546 #: src/view/screens/Settings.tsx:534
msgid "Languages" msgid "Languages"
msgstr "" msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light" #~ msgid "Light"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625 #: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed" msgid "Like this feed"
msgstr "" msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts" msgid "Load more posts"
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:120 #: src/view/screens/Notifications.tsx:130
msgid "Load new notifications" msgid "Load new notifications"
msgstr "" msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server" msgid "Local dev server"
msgstr "" 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" msgid "Login to account that is not listed"
msgstr "" 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!" 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 "" msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr "" msgstr ""
#: src/view/screens/Moderation.tsx:51 #: 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/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346 #: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347 #: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506 #: 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 #: src/view/screens/ProfileList.tsx:506
msgid "More options" msgid "More options"
msgstr "" msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile" msgid "My Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:525 #: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds" msgid "My Saved Feeds"
msgstr "" msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187 #: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509 #: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:380 #: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:447 #: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231 #: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
#: src/view/screens/ProfileFeed.tsx:618 #: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632 #: src/view/screens/ProfileList.tsx:632
msgid "No description" msgid "No description"
msgstr "" msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result" msgid "No result"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:451 #: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\"" msgid "No results found for \"{query}\""
msgstr "" msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable." msgid "Not Applicable."
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:87 #: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:111 #: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364 #: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299 #: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51 #: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation" msgid "Open navigation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:538 #: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings" msgid "Opens configurable language settings"
msgstr "" 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 #: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain" msgid "Opens modal for using custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:563 #: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings" msgid "Opens moderation settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:519 #: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds" msgid "Opens screen with all saved feeds"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:586 #: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page" msgid "Opens the app password settings page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:478 #: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences" msgid "Opens the home feed preferences"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:669 #: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page" msgid "Opens the storybook page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:649 #: src/view/screens/Settings.tsx:637
msgid "Opens the system log page" msgid "Opens the system log page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:499 #: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences" msgid "Opens the threads preferences"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109 #: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account" msgid "Other account"
msgstr "" msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults." msgid "Pictures meant for adults."
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:79 #: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds" msgid "Pinned Feeds"
msgstr "" msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows" msgid "Prioritize Your Follows"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75 #: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:794 #: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email." msgid "Protect your account by verifying your email."
msgstr "" msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92 #: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?" msgid "Remove {0} from my feeds?"
msgstr "" msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account" msgid "Remove account"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106 #: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed" msgid "Remove feed"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278 #: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds" msgid "Remove from my feeds"
msgstr "" msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview" msgid "Remove image preview"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107 #: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?" msgid "Remove this feed from your saved feeds?"
msgstr "" msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code" msgid "Reset code"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:691 #: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state" msgid "Reset onboarding state"
msgstr "" msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:681 #: src/view/screens/Settings.tsx:669
msgid "Reset preferences state" msgid "Reset preferences state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:689 #: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state" msgid "Resets the onboarding state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:679 #: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state" msgid "Resets the preferences state"
msgstr "" msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop" msgid "Save image crop"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:105 #: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds" msgid "Saved Feeds"
msgstr "" 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." msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:280 #: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436 #: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380 #: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381 #: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link" #~ msgid "Share link"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:319 #: src/view/screens/Settings.tsx:318
msgid "Show" msgid "Show"
msgstr "" msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In" msgid "Sign In"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37 #: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}" msgid "Sign in as {0}"
msgstr "" 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 #: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..." msgid "Sign in as..."
msgstr "" msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation" msgid "Sign up or sign in to join the conversation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:330 #: src/view/screens/Settings.tsx:329
msgid "Signed in as" msgid "Signed in as"
msgstr "" msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging" msgid "Staging"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:735 #: src/view/screens/Settings.tsx:723
msgid "Status page" msgid "Status page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:671 #: src/view/screens/Settings.tsx:659
msgid "Storybook" msgid "Storybook"
msgstr "" msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System" #~ msgid "System"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:651 #: src/view/screens/Settings.tsx:639
msgid "System log" msgid "System log"
msgstr "" msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall" msgid "Tall"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84 #: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms" msgid "Terms"
msgstr "" msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "" msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508 #: src/view/screens/Settings.tsx:496
msgid "Thread Preferences" msgid "Thread Preferences"
msgstr "" msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:755 #: src/view/screens/Settings.tsx:743
msgid "Verify email" msgid "Verify email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:780 #: src/view/screens/Settings.tsx:768
msgid "Verify my email" msgid "Verify my email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:789 #: src/view/screens/Settings.tsx:777
msgid "Verify My Email" msgid "Verify My Email"
msgstr "" msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!" msgid "We're so excited to have you join us!"
msgstr "" 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 #: 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." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." 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 "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:92 #: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds." msgid "You don't have any pinned feeds."
msgstr "" msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!" msgid "You don't have any saved feeds!"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:118 #: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds." msgid "You don't have any saved feeds."
msgstr "" msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider" msgid "Your hosting provider"
msgstr "" 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 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private." msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "" 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." #~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "" #~ 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 #: src/view/com/modals/Repost.tsx:44
msgid "{0}" msgid "{0}"
msgstr "" msgstr ""
@ -35,11 +29,6 @@ msgstr ""
msgid "{0} {purposeLabel} List" msgid "{0} {purposeLabel} List"
msgstr "" 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 #: src/view/screens/Search/Search.tsx:86
msgid "{message}" msgid "{message}"
msgstr "" msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "" msgstr ""
#: src/view/com/modals/EditImage.tsx:299 #: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422 #: src/view/screens/Settings.tsx:410
msgid "Accessibility" msgid "Accessibility"
msgstr "" msgstr ""
#: src/view/com/auth/login/LoginForm.tsx:161 #: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289 #: src/view/screens/Settings.tsx:288
msgid "Account" msgid "Account"
msgstr "" msgstr ""
@ -88,8 +77,8 @@ msgstr ""
msgid "Add a user to this list" msgid "Add a user to this list"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:358 #: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:367 #: src/view/screens/Settings.tsx:366
msgid "Add account" msgid "Add account"
msgstr "" msgstr ""
@ -140,7 +129,7 @@ msgstr ""
msgid "Adult Content" msgid "Adult Content"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:574 #: src/view/screens/Settings.tsx:562
msgid "Advanced" msgid "Advanced"
msgstr "" msgstr ""
@ -172,7 +161,7 @@ msgstr ""
msgid "App Language" msgid "App Language"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:594 #: src/view/screens/Settings.tsx:582
msgid "App passwords" msgid "App passwords"
msgstr "" msgstr ""
@ -180,7 +169,7 @@ msgstr ""
msgid "App Passwords" msgid "App Passwords"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:437 #: src/view/screens/Settings.tsx:425
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "" msgstr ""
#: src/view/com/auth/create/CreateAccount.tsx:145 #: 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/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251 #: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 #: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:466 #: src/view/screens/Settings.tsx:454
msgid "Basics" msgid "Basics"
msgstr "" msgstr ""
@ -226,7 +215,7 @@ msgstr ""
msgid "Birthday" msgid "Birthday"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:315 #: src/view/screens/Settings.tsx:314
msgid "Birthday:" msgid "Birthday:"
msgstr "" msgstr ""
@ -291,7 +280,7 @@ msgstr ""
msgid "Bluesky.Social" msgid "Bluesky.Social"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:723 #: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}" msgid "Build version {0} {1}"
msgstr "" msgstr ""
@ -359,12 +348,12 @@ msgstr ""
msgid "Cancel waitlist signup" msgid "Cancel waitlist signup"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:309 #: src/view/screens/Settings.tsx:308
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:606 #: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:615 #: src/view/screens/Settings.tsx:603
msgid "Change handle" msgid "Change handle"
msgstr "" msgstr ""
@ -408,19 +397,19 @@ msgstr ""
msgid "Choose your password" msgid "Choose your password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:699 #: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data" msgid "Clear all legacy storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:701 #: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)" msgid "Clear all legacy storage data (restart after this)"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:711 #: src/view/screens/Settings.tsx:699
msgid "Clear all storage data" msgid "Clear all storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:713 #: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)" msgid "Clear all storage data (restart after this)"
msgstr "" msgstr ""
@ -561,7 +550,7 @@ msgstr ""
msgid "Custom domain" msgid "Custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:620 #: src/view/screens/Settings.tsx:608
msgid "Danger Zone" msgid "Danger Zone"
msgstr "" msgstr ""
@ -569,7 +558,7 @@ msgstr ""
#~ msgid "Dark" #~ msgid "Dark"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:627 #: src/view/screens/Settings.tsx:615
msgid "Delete account" msgid "Delete account"
msgstr "" msgstr ""
@ -591,7 +580,7 @@ msgstr ""
msgid "Delete my account" msgid "Delete my account"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:637 #: src/view/screens/Settings.tsx:625
msgid "Delete my account…" msgid "Delete my account…"
msgstr "" msgstr ""
@ -618,7 +607,7 @@ msgstr ""
msgid "Dev Server" msgid "Dev Server"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:642 #: src/view/screens/Settings.tsx:630
msgid "Developer Tools" msgid "Developer Tools"
msgstr "" msgstr ""
@ -677,7 +666,7 @@ msgid "Edit list details"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:367 #: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75 #: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds" msgid "Edit My Feeds"
msgstr "" msgstr ""
@ -712,7 +701,7 @@ msgstr ""
msgid "Email Updated" msgid "Email Updated"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:293 #: src/view/screens/Settings.tsx:292
msgid "Email:" msgid "Email:"
msgstr "" msgstr ""
@ -757,7 +746,7 @@ msgstr ""
msgid "Failed to load recommended feeds" msgid "Failed to load recommended feeds"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:558 #: src/view/screens/Feeds.tsx:559
msgid "Feed offline" msgid "Feed offline"
msgstr "" msgstr ""
@ -765,12 +754,12 @@ msgstr ""
msgid "Feed Preferences" msgid "Feed Preferences"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64 #: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411 #: src/view/shell/Drawer.tsx:411
msgid "Feedback" msgid "Feedback"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:474 #: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169 #: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342 #: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328 #: 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." msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "" 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." msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "" msgstr ""
@ -884,7 +873,7 @@ msgstr ""
msgid "Handle" msgid "Handle"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93 #: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421 #: src/view/shell/Drawer.tsx:421
msgid "Help" msgid "Help"
msgstr "" msgstr ""
@ -901,6 +890,26 @@ msgstr ""
msgid "Hide user list" msgid "Hide user list"
msgstr "" 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/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306 #: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275 #: src/view/shell/Drawer.tsx:275
@ -908,9 +917,9 @@ msgstr ""
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:71 #: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95 #: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486 #: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences" msgid "Home Feed Preferences"
msgstr "" msgstr ""
@ -953,12 +962,12 @@ msgstr ""
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:386 #: src/view/screens/Settings.tsx:385
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
#: src/view/com/modals/InviteCodes.tsx:91 #: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374 #: src/view/screens/Settings.tsx:373
msgid "Invite a Friend" msgid "Invite a Friend"
msgstr "" msgstr ""
@ -991,7 +1000,7 @@ msgstr ""
msgid "Language Settings" msgid "Language Settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:546 #: src/view/screens/Settings.tsx:534
msgid "Languages" msgid "Languages"
msgstr "" msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "Light" #~ msgid "Light"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/ProfileFeed.tsx:625 #: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed" msgid "Like this feed"
msgstr "" msgstr ""
@ -1059,7 +1068,7 @@ msgstr ""
msgid "Load more posts" msgid "Load more posts"
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:120 #: src/view/screens/Notifications.tsx:130
msgid "Load new notifications" msgid "Load new notifications"
msgstr "" msgstr ""
@ -1075,11 +1084,11 @@ msgstr ""
msgid "Local dev server" msgid "Local dev server"
msgstr "" 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" msgid "Login to account that is not listed"
msgstr "" 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!" 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 "" msgstr ""
@ -1092,7 +1101,7 @@ msgid "Menu"
msgstr "" msgstr ""
#: src/view/screens/Moderation.tsx:51 #: 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/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346 #: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347 #: src/view/shell/Drawer.tsx:347
@ -1108,7 +1117,7 @@ msgid "More feeds"
msgstr "" msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:506 #: 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 #: src/view/screens/ProfileList.tsx:506
msgid "More options" msgid "More options"
msgstr "" msgstr ""
@ -1161,7 +1170,7 @@ msgstr ""
msgid "My Profile" msgid "My Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:525 #: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds" msgid "My Saved Feeds"
msgstr "" msgstr ""
@ -1179,9 +1188,9 @@ msgid "New"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedPage.tsx:187 #: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509 #: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:380 #: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:447 #: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231 #: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:255
@ -1212,7 +1221,7 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
#: src/view/screens/ProfileFeed.tsx:618 #: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632 #: src/view/screens/ProfileList.tsx:632
msgid "No description" msgid "No description"
msgstr "" msgstr ""
@ -1221,7 +1230,7 @@ msgstr ""
msgid "No result" msgid "No result"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:451 #: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\"" msgid "No results found for \"{query}\""
msgstr "" msgstr ""
@ -1246,8 +1255,8 @@ msgstr ""
msgid "Not Applicable." msgid "Not Applicable."
msgstr "" msgstr ""
#: src/view/screens/Notifications.tsx:87 #: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:111 #: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364 #: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299 #: src/view/shell/Drawer.tsx:299
@ -1267,52 +1276,47 @@ msgstr ""
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "" msgstr ""
#: src/view/com/pager/FeedsTabBarMobile.tsx:51 #: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation" msgid "Open navigation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:538 #: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings" msgid "Opens configurable language settings"
msgstr "" 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 #: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain" msgid "Opens modal for using custom domain"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:563 #: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings" msgid "Opens moderation settings"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:519 #: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds" msgid "Opens screen with all saved feeds"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:586 #: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page" msgid "Opens the app password settings page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:478 #: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences" msgid "Opens the home feed preferences"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:669 #: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page" msgid "Opens the storybook page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:649 #: src/view/screens/Settings.tsx:637
msgid "Opens the system log page" msgid "Opens the system log page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:499 #: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences" msgid "Opens the threads preferences"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:109 #: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account" msgid "Other account"
msgstr "" msgstr ""
@ -1349,7 +1353,7 @@ msgstr ""
msgid "Pictures meant for adults." msgid "Pictures meant for adults."
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:79 #: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds" msgid "Pinned Feeds"
msgstr "" msgstr ""
@ -1415,7 +1419,7 @@ msgstr ""
msgid "Prioritize Your Follows" msgid "Prioritize Your Follows"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75 #: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -1434,7 +1438,7 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:794 #: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email." msgid "Protect your account by verifying your email."
msgstr "" msgstr ""
@ -1476,7 +1480,7 @@ msgstr ""
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:92 #: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?" msgid "Remove {0} from my feeds?"
msgstr "" msgstr ""
@ -1484,11 +1488,11 @@ msgstr ""
msgid "Remove account" msgid "Remove account"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:106 #: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed" msgid "Remove feed"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedSourceCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278 #: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds" msgid "Remove from my feeds"
msgstr "" msgstr ""
@ -1501,7 +1505,7 @@ msgstr ""
msgid "Remove image preview" msgid "Remove image preview"
msgstr "" msgstr ""
#: src/view/com/posts/FeedErrorMessage.tsx:107 #: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?" msgid "Remove this feed from your saved feeds?"
msgstr "" msgstr ""
@ -1565,7 +1569,7 @@ msgstr ""
msgid "Reset code" msgid "Reset code"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:691 #: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state" msgid "Reset onboarding state"
msgstr "" msgstr ""
@ -1573,15 +1577,15 @@ msgstr ""
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:681 #: src/view/screens/Settings.tsx:669
msgid "Reset preferences state" msgid "Reset preferences state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:689 #: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state" msgid "Resets the onboarding state"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:679 #: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state" msgid "Resets the preferences state"
msgstr "" msgstr ""
@ -1628,7 +1632,7 @@ msgstr ""
msgid "Save image crop" msgid "Save image crop"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:105 #: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds" msgid "Saved Feeds"
msgstr "" 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." msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:280 #: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436 #: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380 #: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381 #: src/view/shell/Drawer.tsx:381
@ -1751,7 +1755,7 @@ msgstr ""
#~ msgid "Share link" #~ msgid "Share link"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:319 #: src/view/screens/Settings.tsx:318
msgid "Show" msgid "Show"
msgstr "" msgstr ""
@ -1795,11 +1799,11 @@ msgstr ""
msgid "Sign In" msgid "Sign In"
msgstr "" msgstr ""
#: src/view/com/auth/login/ChooseAccountForm.tsx:37 #: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}" msgid "Sign in as {0}"
msgstr "" 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 #: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..." msgid "Sign in as..."
msgstr "" msgstr ""
@ -1823,7 +1827,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation" msgid "Sign up or sign in to join the conversation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:330 #: src/view/screens/Settings.tsx:329
msgid "Signed in as" msgid "Signed in as"
msgstr "" msgstr ""
@ -1848,11 +1852,11 @@ msgstr ""
msgid "Staging" msgid "Staging"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:735 #: src/view/screens/Settings.tsx:723
msgid "Status page" msgid "Status page"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:671 #: src/view/screens/Settings.tsx:659
msgid "Storybook" msgid "Storybook"
msgstr "" msgstr ""
@ -1881,7 +1885,7 @@ msgstr ""
#~ msgid "System" #~ msgid "System"
#~ msgstr "" #~ msgstr ""
#: src/view/screens/Settings.tsx:651 #: src/view/screens/Settings.tsx:639
msgid "System log" msgid "System log"
msgstr "" msgstr ""
@ -1889,7 +1893,7 @@ msgstr ""
msgid "Tall" msgid "Tall"
msgstr "" msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84 #: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms" msgid "Terms"
msgstr "" msgstr ""
@ -1962,7 +1966,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "" msgstr ""
#: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508 #: src/view/screens/Settings.tsx:496
msgid "Thread Preferences" msgid "Thread Preferences"
msgstr "" msgstr ""
@ -2069,15 +2073,15 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:755 #: src/view/screens/Settings.tsx:743
msgid "Verify email" msgid "Verify email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:780 #: src/view/screens/Settings.tsx:768
msgid "Verify my email" msgid "Verify my email"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:789 #: src/view/screens/Settings.tsx:777
msgid "Verify My Email" msgid "Verify My Email"
msgstr "" msgstr ""
@ -2102,6 +2106,10 @@ msgstr ""
msgid "We're so excited to have you join us!" msgid "We're so excited to have you join us!"
msgstr "" 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 #: 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." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." 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 "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:92 #: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds." msgid "You don't have any pinned feeds."
msgstr "" msgstr ""
@ -2165,7 +2173,7 @@ msgstr ""
msgid "You don't have any saved feeds!" msgid "You don't have any saved feeds!"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:118 #: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds." msgid "You don't have any saved feeds."
msgstr "" msgstr ""
@ -2228,12 +2236,6 @@ msgstr ""
msgid "Your hosting provider" msgid "Your hosting provider"
msgstr "" 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 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private." msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "" 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." #~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "यह चेतावनी केवल मीडिया वाले पोस्ट के लिए उपलब्ध है।" #~ 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 #: src/view/com/modals/Repost.tsx:44
msgid "{0}" msgid "{0}"
msgstr "{0}" msgstr "{0}"
@ -35,11 +29,6 @@ msgstr "{0}"
msgid "{0} {purposeLabel} List" msgid "{0} {purposeLabel} List"
msgstr "{0} {purposeLabel} सूची" 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 #: src/view/screens/Search/Search.tsx:86
msgid "{message}" msgid "{message}"
msgstr "" msgstr ""
@ -61,12 +50,12 @@ msgid "A new version of the app is available. Please update to continue using th
msgstr "ऐप का एक नया संस्करण उपलब्ध है. कृपया ऐप का उपयोग जारी रखने के लिए अपडेट करें।" msgstr "ऐप का एक नया संस्करण उपलब्ध है. कृपया ऐप का उपयोग जारी रखने के लिए अपडेट करें।"
#: src/view/com/modals/EditImage.tsx:299 #: src/view/com/modals/EditImage.tsx:299
#: src/view/screens/Settings.tsx:422 #: src/view/screens/Settings.tsx:410
msgid "Accessibility" msgid "Accessibility"
msgstr "प्रवेर्शयोग्यता" msgstr "प्रवेर्शयोग्यता"
#: src/view/com/auth/login/LoginForm.tsx:161 #: src/view/com/auth/login/LoginForm.tsx:161
#: src/view/screens/Settings.tsx:289 #: src/view/screens/Settings.tsx:288
msgid "Account" msgid "Account"
msgstr "अकाउंट" msgstr "अकाउंट"
@ -88,8 +77,8 @@ msgstr "सामग्री चेतावनी जोड़ें"
msgid "Add a user to this list" msgid "Add a user to this list"
msgstr "इस सूची में किसी को जोड़ें" msgstr "इस सूची में किसी को जोड़ें"
#: src/view/screens/Settings.tsx:358 #: src/view/screens/Settings.tsx:357
#: src/view/screens/Settings.tsx:367 #: src/view/screens/Settings.tsx:366
msgid "Add account" msgid "Add account"
msgstr "अकाउंट जोड़ें" msgstr "अकाउंट जोड़ें"
@ -140,7 +129,7 @@ msgstr "पसंद की संख्या को समायोजित
msgid "Adult Content" msgid "Adult Content"
msgstr "वयस्क सामग्री" msgstr "वयस्क सामग्री"
#: src/view/screens/Settings.tsx:574 #: src/view/screens/Settings.tsx:562
msgid "Advanced" msgid "Advanced"
msgstr "विकसित" msgstr "विकसित"
@ -172,7 +161,7 @@ msgstr "और"
msgid "App Language" msgid "App Language"
msgstr "ऐप भाषा" msgstr "ऐप भाषा"
#: src/view/screens/Settings.tsx:594 #: src/view/screens/Settings.tsx:582
msgid "App passwords" msgid "App passwords"
msgstr "ऐप पासवर्ड" msgstr "ऐप पासवर्ड"
@ -180,7 +169,7 @@ msgstr "ऐप पासवर्ड"
msgid "App Passwords" msgid "App Passwords"
msgstr "ऐप पासवर्ड" msgstr "ऐप पासवर्ड"
#: src/view/screens/Settings.tsx:437 #: src/view/screens/Settings.tsx:425
msgid "Appearance" msgid "Appearance"
msgstr "दिखावट" msgstr "दिखावट"
@ -205,7 +194,7 @@ msgid "Artistic or non-erotic nudity."
msgstr "कलात्मक या गैर-कामुक नग्नता।।" msgstr "कलात्मक या गैर-कामुक नग्नता।।"
#: src/view/com/auth/create/CreateAccount.tsx:145 #: 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/ForgotPasswordForm.tsx:166
#: src/view/com/auth/login/LoginForm.tsx:251 #: src/view/com/auth/login/LoginForm.tsx:251
#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 #: src/view/com/auth/login/SetNewPasswordForm.tsx:148
@ -217,7 +206,7 @@ msgstr "कलात्मक या गैर-कामुक नग्नत
msgid "Back" msgid "Back"
msgstr "वापस" msgstr "वापस"
#: src/view/screens/Settings.tsx:466 #: src/view/screens/Settings.tsx:454
msgid "Basics" msgid "Basics"
msgstr "मूल बातें" msgstr "मूल बातें"
@ -226,7 +215,7 @@ msgstr "मूल बातें"
msgid "Birthday" msgid "Birthday"
msgstr "जन्मदिन" msgstr "जन्मदिन"
#: src/view/screens/Settings.tsx:315 #: src/view/screens/Settings.tsx:314
msgid "Birthday:" msgid "Birthday:"
msgstr "जन्मदिन:" msgstr "जन्मदिन:"
@ -291,7 +280,7 @@ msgstr "ब्लूस्की एक स्वस्थ समुदाय
msgid "Bluesky.Social" msgid "Bluesky.Social"
msgstr "Bluesky.Social" msgstr "Bluesky.Social"
#: src/view/screens/Settings.tsx:723 #: src/view/screens/Settings.tsx:711
msgid "Build version {0} {1}" msgid "Build version {0} {1}"
msgstr "Build version {0} {1}" msgstr "Build version {0} {1}"
@ -359,12 +348,12 @@ msgstr "खोज मत करो"
msgid "Cancel waitlist signup" msgid "Cancel waitlist signup"
msgstr "प्रतीक्षा सूची पंजीकरण मत करो" msgstr "प्रतीक्षा सूची पंजीकरण मत करो"
#: src/view/screens/Settings.tsx:309 #: src/view/screens/Settings.tsx:308
msgid "Change" msgid "Change"
msgstr "परिवर्तन" msgstr "परिवर्तन"
#: src/view/screens/Settings.tsx:606 #: src/view/screens/Settings.tsx:594
#: src/view/screens/Settings.tsx:615 #: src/view/screens/Settings.tsx:603
msgid "Change handle" msgid "Change handle"
msgstr "हैंडल बदलें" msgstr "हैंडल बदलें"
@ -404,19 +393,19 @@ msgstr "उन एल्गोरिदम का चयन करें जो
msgid "Choose your password" msgid "Choose your password"
msgstr "अपना पासवर्ड चुनें" msgstr "अपना पासवर्ड चुनें"
#: src/view/screens/Settings.tsx:699 #: src/view/screens/Settings.tsx:687
msgid "Clear all legacy storage data" msgid "Clear all legacy storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:701 #: src/view/screens/Settings.tsx:689
msgid "Clear all legacy storage data (restart after this)" msgid "Clear all legacy storage data (restart after this)"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:711 #: src/view/screens/Settings.tsx:699
msgid "Clear all storage data" msgid "Clear all storage data"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:713 #: src/view/screens/Settings.tsx:701
msgid "Clear all storage data (restart after this)" msgid "Clear all storage data (restart after this)"
msgstr "" msgstr ""
@ -557,7 +546,7 @@ msgstr "बनाया गया {0}"
msgid "Custom domain" msgid "Custom domain"
msgstr "कस्टम डोमेन" msgstr "कस्टम डोमेन"
#: src/view/screens/Settings.tsx:620 #: src/view/screens/Settings.tsx:608
msgid "Danger Zone" msgid "Danger Zone"
msgstr "खतरा क्षेत्र" msgstr "खतरा क्षेत्र"
@ -565,7 +554,7 @@ msgstr "खतरा क्षेत्र"
#~ msgid "Dark" #~ msgid "Dark"
#~ msgstr "डार्क मोड" #~ msgstr "डार्क मोड"
#: src/view/screens/Settings.tsx:627 #: src/view/screens/Settings.tsx:615
msgid "Delete account" msgid "Delete account"
msgstr "खाता हटाएं" msgstr "खाता हटाएं"
@ -587,7 +576,7 @@ msgstr "सूची हटाएँ"
msgid "Delete my account" msgid "Delete my account"
msgstr "मेरा खाता हटाएं" msgstr "मेरा खाता हटाएं"
#: src/view/screens/Settings.tsx:637 #: src/view/screens/Settings.tsx:625
msgid "Delete my account…" msgid "Delete my account…"
msgstr "मेरा खाता हटाएं…" msgstr "मेरा खाता हटाएं…"
@ -614,7 +603,7 @@ msgstr "विवरण"
msgid "Dev Server" msgid "Dev Server"
msgstr "देव सर्वर" msgstr "देव सर्वर"
#: src/view/screens/Settings.tsx:642 #: src/view/screens/Settings.tsx:630
msgid "Developer Tools" msgid "Developer Tools"
msgstr "डेवलपर उपकरण" msgstr "डेवलपर उपकरण"
@ -673,7 +662,7 @@ msgid "Edit list details"
msgstr "सूची विवरण संपादित करें" msgstr "सूची विवरण संपादित करें"
#: src/view/screens/Feeds.tsx:367 #: src/view/screens/Feeds.tsx:367
#: src/view/screens/SavedFeeds.tsx:75 #: src/view/screens/SavedFeeds.tsx:85
msgid "Edit My Feeds" msgid "Edit My Feeds"
msgstr "मेरी फ़ीड संपादित करें" msgstr "मेरी फ़ीड संपादित करें"
@ -708,7 +697,7 @@ msgstr "ईमेल"
msgid "Email Updated" msgid "Email Updated"
msgstr "ईमेल अपडेट किया गया" msgstr "ईमेल अपडेट किया गया"
#: src/view/screens/Settings.tsx:293 #: src/view/screens/Settings.tsx:292
msgid "Email:" msgid "Email:"
msgstr "ईमेल:" msgstr "ईमेल:"
@ -753,7 +742,7 @@ msgstr "ऑल्ट टेक्स्ट"
msgid "Failed to load recommended feeds" msgid "Failed to load recommended feeds"
msgstr "अनुशंसित फ़ीड लोड करने में विफल" msgstr "अनुशंसित फ़ीड लोड करने में विफल"
#: src/view/screens/Feeds.tsx:558 #: src/view/screens/Feeds.tsx:559
msgid "Feed offline" msgid "Feed offline"
msgstr "फ़ीड ऑफ़लाइन है" msgstr "फ़ीड ऑफ़लाइन है"
@ -761,12 +750,12 @@ msgstr "फ़ीड ऑफ़लाइन है"
msgid "Feed Preferences" msgid "Feed Preferences"
msgstr "फ़ीड प्राथमिकता" msgstr "फ़ीड प्राथमिकता"
#: src/view/shell/desktop/RightNav.tsx:64 #: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:411 #: src/view/shell/Drawer.tsx:411
msgid "Feedback" msgid "Feedback"
msgstr "प्रतिक्रिया" msgstr "प्रतिक्रिया"
#: src/view/screens/Feeds.tsx:474 #: src/view/screens/Feeds.tsx:475
#: src/view/shell/bottom-bar/BottomBar.tsx:169 #: src/view/shell/bottom-bar/BottomBar.tsx:169
#: src/view/shell/desktop/LeftNav.tsx:342 #: src/view/shell/desktop/LeftNav.tsx:342
#: src/view/shell/Drawer.tsx:328 #: 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." msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "सामग्री को व्यवस्थित करने के लिए उपयोगकर्ताओं द्वारा फ़ीड बनाए जाते हैं। कुछ फ़ीड चुनें जो आपको दिलचस्प लगें।" 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." msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."
msgstr "फ़ीड कस्टम एल्गोरिदम हैं जिन्हें उपयोगकर्ता थोड़ी कोडिंग विशेषज्ञता के साथ बनाते हैं। <0/> अधिक जानकारी के लिए." msgstr "फ़ीड कस्टम एल्गोरिदम हैं जिन्हें उपयोगकर्ता थोड़ी कोडिंग विशेषज्ञता के साथ बनाते हैं। <0/> अधिक जानकारी के लिए."
@ -876,7 +865,7 @@ msgstr "अगला"
msgid "Handle" msgid "Handle"
msgstr "हैंडल" msgstr "हैंडल"
#: src/view/shell/desktop/RightNav.tsx:93 #: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:421 #: src/view/shell/Drawer.tsx:421
msgid "Help" msgid "Help"
msgstr "सहायता" msgstr "सहायता"
@ -893,6 +882,26 @@ msgstr "इसे छिपाएं"
msgid "Hide user list" msgid "Hide user list"
msgstr "उपयोगकर्ता सूची छुपाएँ" 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/bottom-bar/BottomBar.tsx:125
#: src/view/shell/desktop/LeftNav.tsx:306 #: src/view/shell/desktop/LeftNav.tsx:306
#: src/view/shell/Drawer.tsx:275 #: src/view/shell/Drawer.tsx:275
@ -900,9 +909,9 @@ msgstr "उपयोगकर्ता सूची छुपाएँ"
msgid "Home" msgid "Home"
msgstr "होम फीड" msgstr "होम फीड"
#: src/view/com/pager/FeedsTabBarMobile.tsx:71 #: src/view/com/pager/FeedsTabBarMobile.tsx:99
#: src/view/screens/PreferencesHomeFeed.tsx:95 #: src/view/screens/PreferencesHomeFeed.tsx:95
#: src/view/screens/Settings.tsx:486 #: src/view/screens/Settings.tsx:474
msgid "Home Feed Preferences" msgid "Home Feed Preferences"
msgstr "होम फ़ीड प्राथमिकताएं" msgstr "होम फ़ीड प्राथमिकताएं"
@ -945,12 +954,12 @@ msgstr "छवि विकल्प"
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "अवैध उपयोगकर्ता नाम या पासवर्ड" msgstr "अवैध उपयोगकर्ता नाम या पासवर्ड"
#: src/view/screens/Settings.tsx:386 #: src/view/screens/Settings.tsx:385
msgid "Invite" msgid "Invite"
msgstr "आमंत्रण भेजो" msgstr "आमंत्रण भेजो"
#: src/view/com/modals/InviteCodes.tsx:91 #: src/view/com/modals/InviteCodes.tsx:91
#: src/view/screens/Settings.tsx:374 #: src/view/screens/Settings.tsx:373
msgid "Invite a Friend" msgid "Invite a Friend"
msgstr "एक दोस्त को आमंत्रित करें" msgstr "एक दोस्त को आमंत्रित करें"
@ -983,7 +992,7 @@ msgstr "अपनी भाषा चुने"
msgid "Language Settings" msgid "Language Settings"
msgstr "भाषा सेटिंग्स" msgstr "भाषा सेटिंग्स"
#: src/view/screens/Settings.tsx:546 #: src/view/screens/Settings.tsx:534
msgid "Languages" msgid "Languages"
msgstr "भाषा" msgstr "भाषा"
@ -1023,7 +1032,7 @@ msgstr "चित्र पुस्तकालय"
#~ msgid "Light" #~ msgid "Light"
#~ msgstr "लाइट मोड" #~ msgstr "लाइट मोड"
#: src/view/screens/ProfileFeed.tsx:625 #: src/view/screens/ProfileFeed.tsx:626
msgid "Like this feed" msgid "Like this feed"
msgstr "इस फ़ीड को लाइक करो" msgstr "इस फ़ीड को लाइक करो"
@ -1051,7 +1060,7 @@ msgstr "सूची"
msgid "Load more posts" msgid "Load more posts"
msgstr "अधिक पोस्ट लोड करें" msgstr "अधिक पोस्ट लोड करें"
#: src/view/screens/Notifications.tsx:120 #: src/view/screens/Notifications.tsx:130
msgid "Load new notifications" msgid "Load new notifications"
msgstr "नई सूचनाएं लोड करें" msgstr "नई सूचनाएं लोड करें"
@ -1067,11 +1076,11 @@ msgstr ""
msgid "Local dev server" msgid "Local dev server"
msgstr "स्थानीय देव सर्वर" 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" msgid "Login to account that is not listed"
msgstr "उस खाते में लॉग इन करें जो सूचीबद्ध नहीं है" 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!" 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 "" msgstr ""
@ -1084,7 +1093,7 @@ msgid "Menu"
msgstr "मेनू" msgstr "मेनू"
#: src/view/screens/Moderation.tsx:51 #: 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/desktop/LeftNav.tsx:400
#: src/view/shell/Drawer.tsx:346 #: src/view/shell/Drawer.tsx:346
#: src/view/shell/Drawer.tsx:347 #: src/view/shell/Drawer.tsx:347
@ -1100,7 +1109,7 @@ msgid "More feeds"
msgstr "अधिक फ़ीड" msgstr "अधिक फ़ीड"
#: src/view/com/profile/ProfileHeader.tsx:506 #: 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 #: src/view/screens/ProfileList.tsx:506
msgid "More options" msgid "More options"
msgstr "अधिक विकल्प" msgstr "अधिक विकल्प"
@ -1153,7 +1162,7 @@ msgstr "मेरी फ़ीड"
msgid "My Profile" msgid "My Profile"
msgstr "मेरी प्रोफाइल" msgstr "मेरी प्रोफाइल"
#: src/view/screens/Settings.tsx:525 #: src/view/screens/Settings.tsx:513
msgid "My Saved Feeds" msgid "My Saved Feeds"
msgstr "मेरी फ़ीड" msgstr "मेरी फ़ीड"
@ -1171,9 +1180,9 @@ msgid "New"
msgstr "नया" msgstr "नया"
#: src/view/com/feeds/FeedPage.tsx:187 #: src/view/com/feeds/FeedPage.tsx:187
#: src/view/screens/Feeds.tsx:509 #: src/view/screens/Feeds.tsx:510
#: src/view/screens/Profile.tsx:380 #: src/view/screens/Profile.tsx:381
#: src/view/screens/ProfileFeed.tsx:447 #: src/view/screens/ProfileFeed.tsx:448
#: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:199
#: src/view/screens/ProfileList.tsx:231 #: src/view/screens/ProfileList.tsx:231
#: src/view/shell/desktop/LeftNav.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:255
@ -1204,7 +1213,7 @@ msgstr "अगली फोटो"
msgid "No" msgid "No"
msgstr "नहीं" msgstr "नहीं"
#: src/view/screens/ProfileFeed.tsx:618 #: src/view/screens/ProfileFeed.tsx:619
#: src/view/screens/ProfileList.tsx:632 #: src/view/screens/ProfileList.tsx:632
msgid "No description" msgid "No description"
msgstr "कोई विवरण नहीं" msgstr "कोई विवरण नहीं"
@ -1213,7 +1222,7 @@ msgstr "कोई विवरण नहीं"
msgid "No result" msgid "No result"
msgstr "" msgstr ""
#: src/view/screens/Feeds.tsx:451 #: src/view/screens/Feeds.tsx:452
msgid "No results found for \"{query}\"" msgid "No results found for \"{query}\""
msgstr "\"{query}\" के लिए कोई परिणाम नहीं मिला" msgstr "\"{query}\" के लिए कोई परिणाम नहीं मिला"
@ -1238,8 +1247,8 @@ msgstr ""
msgid "Not Applicable." msgid "Not Applicable."
msgstr "लागू नहीं।" msgstr "लागू नहीं।"
#: src/view/screens/Notifications.tsx:87 #: src/view/screens/Notifications.tsx:97
#: src/view/screens/Notifications.tsx:111 #: src/view/screens/Notifications.tsx:121
#: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/bottom-bar/BottomBar.tsx:196
#: src/view/shell/desktop/LeftNav.tsx:364 #: src/view/shell/desktop/LeftNav.tsx:364
#: src/view/shell/Drawer.tsx:299 #: src/view/shell/Drawer.tsx:299
@ -1259,52 +1268,47 @@ msgstr "ठीक है"
msgid "One or more images is missing alt text." msgid "One or more images is missing alt text."
msgstr "एक या अधिक छवियाँ alt पाठ याद आती हैं।।" msgstr "एक या अधिक छवियाँ alt पाठ याद आती हैं।।"
#: src/view/com/pager/FeedsTabBarMobile.tsx:51 #: src/view/com/pager/FeedsTabBarMobile.tsx:79
msgid "Open navigation" msgid "Open navigation"
msgstr "ओपन नेविगेशन" msgstr "ओपन नेविगेशन"
#: src/view/screens/Settings.tsx:538 #: src/view/screens/Settings.tsx:526
msgid "Opens configurable language settings" msgid "Opens configurable language settings"
msgstr "भाषा सेटिंग्स खोलें" 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 #: src/view/com/modals/ChangeHandle.tsx:279
msgid "Opens modal for using custom domain" msgid "Opens modal for using custom domain"
msgstr "कस्टम डोमेन का उपयोग करने के लिए मोडल खोलें" msgstr "कस्टम डोमेन का उपयोग करने के लिए मोडल खोलें"
#: src/view/screens/Settings.tsx:563 #: src/view/screens/Settings.tsx:551
msgid "Opens moderation settings" msgid "Opens moderation settings"
msgstr "मॉडरेशन सेटिंग्स खोलें" msgstr "मॉडरेशन सेटिंग्स खोलें"
#: src/view/screens/Settings.tsx:519 #: src/view/screens/Settings.tsx:507
msgid "Opens screen with all saved feeds" msgid "Opens screen with all saved feeds"
msgstr "सभी बचाया फ़ीड के साथ स्क्रीन खोलें" msgstr "सभी बचाया फ़ीड के साथ स्क्रीन खोलें"
#: src/view/screens/Settings.tsx:586 #: src/view/screens/Settings.tsx:574
msgid "Opens the app password settings page" msgid "Opens the app password settings page"
msgstr "ऐप पासवर्ड सेटिंग पेज खोलें" msgstr "ऐप पासवर्ड सेटिंग पेज खोलें"
#: src/view/screens/Settings.tsx:478 #: src/view/screens/Settings.tsx:466
msgid "Opens the home feed preferences" msgid "Opens the home feed preferences"
msgstr "होम फीड वरीयताओं को खोलता है" msgstr "होम फीड वरीयताओं को खोलता है"
#: src/view/screens/Settings.tsx:669 #: src/view/screens/Settings.tsx:657
msgid "Opens the storybook page" msgid "Opens the storybook page"
msgstr "स्टोरीबुक पेज खोलें" msgstr "स्टोरीबुक पेज खोलें"
#: src/view/screens/Settings.tsx:649 #: src/view/screens/Settings.tsx:637
msgid "Opens the system log page" msgid "Opens the system log page"
msgstr "सिस्टम लॉग पेज खोलें" msgstr "सिस्टम लॉग पेज खोलें"
#: src/view/screens/Settings.tsx:499 #: src/view/screens/Settings.tsx:487
msgid "Opens the threads preferences" msgid "Opens the threads preferences"
msgstr "धागे वरीयताओं को खोलता है" msgstr "धागे वरीयताओं को खोलता है"
#: src/view/com/auth/login/ChooseAccountForm.tsx:109 #: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account" msgid "Other account"
msgstr "अन्य खाता" msgstr "अन्य खाता"
@ -1341,7 +1345,7 @@ msgstr "पासवर्ड अद्यतन!"
msgid "Pictures meant for adults." msgid "Pictures meant for adults."
msgstr "चित्र वयस्कों के लिए थे।।" msgstr "चित्र वयस्कों के लिए थे।।"
#: src/view/screens/SavedFeeds.tsx:79 #: src/view/screens/SavedFeeds.tsx:89
msgid "Pinned Feeds" msgid "Pinned Feeds"
msgstr "पिन किया गया फ़ीड" msgstr "पिन किया गया फ़ीड"
@ -1407,7 +1411,7 @@ msgstr "प्राथमिक भाषा"
msgid "Prioritize Your Follows" msgid "Prioritize Your Follows"
msgstr "अपने फ़ॉलोअर्स को प्राथमिकता दें" msgstr "अपने फ़ॉलोअर्स को प्राथमिकता दें"
#: src/view/shell/desktop/RightNav.tsx:75 #: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy" msgid "Privacy"
msgstr "गोपनीयता" msgstr "गोपनीयता"
@ -1426,7 +1430,7 @@ msgstr "प्रसंस्करण..."
msgid "Profile" msgid "Profile"
msgstr "प्रोफ़ाइल" msgstr "प्रोफ़ाइल"
#: src/view/screens/Settings.tsx:794 #: src/view/screens/Settings.tsx:782
msgid "Protect your account by verifying your email." msgid "Protect your account by verifying your email."
msgstr "अपने ईमेल को सत्यापित करके अपने खाते को सुरक्षित रखें।।" msgstr "अपने ईमेल को सत्यापित करके अपने खाते को सुरक्षित रखें।।"
@ -1468,7 +1472,7 @@ msgstr "अनुशंसित लोग"
msgid "Remove" msgid "Remove"
msgstr "निकालें" msgstr "निकालें"
#: src/view/com/feeds/FeedSourceCard.tsx:92 #: src/view/com/feeds/FeedSourceCard.tsx:108
msgid "Remove {0} from my feeds?" msgid "Remove {0} from my feeds?"
msgstr "मेरे फ़ीड से {0} हटाएं?" msgstr "मेरे फ़ीड से {0} हटाएं?"
@ -1476,11 +1480,11 @@ msgstr "मेरे फ़ीड से {0} हटाएं?"
msgid "Remove account" msgid "Remove account"
msgstr "खाता हटाएं" msgstr "खाता हटाएं"
#: src/view/com/posts/FeedErrorMessage.tsx:106 #: src/view/com/posts/FeedErrorMessage.tsx:118
msgid "Remove feed" msgid "Remove feed"
msgstr "फ़ीड हटाएँ" msgstr "फ़ीड हटाएँ"
#: src/view/com/feeds/FeedSourceCard.tsx:91 #: src/view/com/feeds/FeedSourceCard.tsx:107
#: src/view/screens/ProfileFeed.tsx:278 #: src/view/screens/ProfileFeed.tsx:278
msgid "Remove from my feeds" msgid "Remove from my feeds"
msgstr "मेरे फ़ीड से हटाएँ" msgstr "मेरे फ़ीड से हटाएँ"
@ -1493,7 +1497,7 @@ msgstr "छवि निकालें"
msgid "Remove image preview" msgid "Remove image preview"
msgstr "छवि पूर्वावलोकन निकालें" msgstr "छवि पूर्वावलोकन निकालें"
#: src/view/com/posts/FeedErrorMessage.tsx:107 #: src/view/com/posts/FeedErrorMessage.tsx:119
msgid "Remove this feed from your saved feeds?" msgid "Remove this feed from your saved feeds?"
msgstr "इस फ़ीड को सहेजे गए फ़ीड से हटा दें?" msgstr "इस फ़ीड को सहेजे गए फ़ीड से हटा दें?"
@ -1557,7 +1561,7 @@ msgstr "इस प्रदाता के लिए आवश्यक"
msgid "Reset code" msgid "Reset code"
msgstr "कोड रीसेट करें" msgstr "कोड रीसेट करें"
#: src/view/screens/Settings.tsx:691 #: src/view/screens/Settings.tsx:679
msgid "Reset onboarding state" msgid "Reset onboarding state"
msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें" msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें"
@ -1565,15 +1569,15 @@ msgstr "ऑनबोर्डिंग स्टेट को रीसेट
msgid "Reset password" msgid "Reset password"
msgstr "पासवर्ड रीसेट" msgstr "पासवर्ड रीसेट"
#: src/view/screens/Settings.tsx:681 #: src/view/screens/Settings.tsx:669
msgid "Reset preferences state" msgid "Reset preferences state"
msgstr "प्राथमिकताओं को रीसेट करें" msgstr "प्राथमिकताओं को रीसेट करें"
#: src/view/screens/Settings.tsx:689 #: src/view/screens/Settings.tsx:677
msgid "Resets the onboarding state" msgid "Resets the onboarding state"
msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें" msgstr "ऑनबोर्डिंग स्टेट को रीसेट करें"
#: src/view/screens/Settings.tsx:679 #: src/view/screens/Settings.tsx:667
msgid "Resets the preferences state" msgid "Resets the preferences state"
msgstr "प्राथमिकताओं की स्थिति को रीसेट करें" msgstr "प्राथमिकताओं की स्थिति को रीसेट करें"
@ -1620,7 +1624,7 @@ msgstr "बदलाव सेव करो"
msgid "Save image crop" msgid "Save image crop"
msgstr "फोटो बदलाव सेव करो" msgstr "फोटो बदलाव सेव करो"
#: src/view/screens/SavedFeeds.tsx:105 #: src/view/screens/SavedFeeds.tsx:122
msgid "Saved Feeds" msgid "Saved Feeds"
msgstr "सहेजे गए फ़ीड" 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." msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
msgstr "इस सेटिंग को अपने निम्नलिखित फ़ीड में अपने सहेजे गए फ़ीड के नमूने दिखाने के लिए \"हाँ\" पर सेट करें। यह एक प्रयोगात्मक विशेषता है।।" msgstr "इस सेटिंग को अपने निम्नलिखित फ़ीड में अपने सहेजे गए फ़ीड के नमूने दिखाने के लिए \"हाँ\" पर सेट करें। यह एक प्रयोगात्मक विशेषता है।।"
#: src/view/screens/Settings.tsx:280 #: src/view/screens/Settings.tsx:279
#: src/view/shell/desktop/LeftNav.tsx:436 #: src/view/shell/desktop/LeftNav.tsx:436
#: src/view/shell/Drawer.tsx:380 #: src/view/shell/Drawer.tsx:380
#: src/view/shell/Drawer.tsx:381 #: src/view/shell/Drawer.tsx:381
@ -1743,7 +1747,7 @@ msgstr ""
#~ msgid "Share link" #~ msgid "Share link"
#~ msgstr "लिंक शेयर करें" #~ msgstr "लिंक शेयर करें"
#: src/view/screens/Settings.tsx:319 #: src/view/screens/Settings.tsx:318
msgid "Show" msgid "Show"
msgstr "दिखाओ" msgstr "दिखाओ"
@ -1787,11 +1791,11 @@ msgstr "साइन इन करें"
msgid "Sign In" msgid "Sign In"
msgstr "साइन इन करें" msgstr "साइन इन करें"
#: src/view/com/auth/login/ChooseAccountForm.tsx:37 #: src/view/com/auth/login/ChooseAccountForm.tsx:44
msgid "Sign in as {0}" msgid "Sign in as {0}"
msgstr "{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 #: src/view/com/auth/login/Login.tsx:100
msgid "Sign in as..." msgid "Sign in as..."
msgstr "... के रूप में साइन इन करें" msgstr "... के रूप में साइन इन करें"
@ -1815,7 +1819,7 @@ msgstr ""
msgid "Sign up or sign in to join the conversation" msgid "Sign up or sign in to join the conversation"
msgstr "" msgstr ""
#: src/view/screens/Settings.tsx:330 #: src/view/screens/Settings.tsx:329
msgid "Signed in as" msgid "Signed in as"
msgstr "आपने इस रूप में साइन इन करा है:" msgstr "आपने इस रूप में साइन इन करा है:"
@ -1840,11 +1844,11 @@ msgstr "स्क्वायर"
msgid "Staging" msgid "Staging"
msgstr "स्टेजिंग" msgstr "स्टेजिंग"
#: src/view/screens/Settings.tsx:735 #: src/view/screens/Settings.tsx:723
msgid "Status page" msgid "Status page"
msgstr "स्थिति पृष्ठ" msgstr "स्थिति पृष्ठ"
#: src/view/screens/Settings.tsx:671 #: src/view/screens/Settings.tsx:659
msgid "Storybook" msgid "Storybook"
msgstr "Storybook" msgstr "Storybook"
@ -1873,7 +1877,7 @@ msgstr "खाते बदलें"
#~ msgid "System" #~ msgid "System"
#~ msgstr "प्रणाली" #~ msgstr "प्रणाली"
#: src/view/screens/Settings.tsx:651 #: src/view/screens/Settings.tsx:639
msgid "System log" msgid "System log"
msgstr "सिस्टम लॉग" msgstr "सिस्टम लॉग"
@ -1881,7 +1885,7 @@ msgstr "सिस्टम लॉग"
msgid "Tall" msgid "Tall"
msgstr "लंबा" msgstr "लंबा"
#: src/view/shell/desktop/RightNav.tsx:84 #: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms" msgid "Terms"
msgstr "शर्तें" msgstr "शर्तें"
@ -1954,7 +1958,7 @@ msgid "This warning is only available for posts with media attached."
msgstr "यह चेतावनी केवल मीडिया संलग्न पोस्ट के लिए उपलब्ध है।" msgstr "यह चेतावनी केवल मीडिया संलग्न पोस्ट के लिए उपलब्ध है।"
#: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/PreferencesThreads.tsx:53
#: src/view/screens/Settings.tsx:508 #: src/view/screens/Settings.tsx:496
msgid "Thread Preferences" msgid "Thread Preferences"
msgstr "थ्रेड प्राथमिकता" msgstr "थ्रेड प्राथमिकता"
@ -2061,15 +2065,15 @@ msgstr "यूजर नाम या ईमेल पता"
msgid "Users" msgid "Users"
msgstr "यूजर लोग" msgstr "यूजर लोग"
#: src/view/screens/Settings.tsx:755 #: src/view/screens/Settings.tsx:743
msgid "Verify email" msgid "Verify email"
msgstr "ईमेल सत्यापित करें" msgstr "ईमेल सत्यापित करें"
#: src/view/screens/Settings.tsx:780 #: src/view/screens/Settings.tsx:768
msgid "Verify my email" msgid "Verify my email"
msgstr "मेरी ईमेल सत्यापित करें" msgstr "मेरी ईमेल सत्यापित करें"
#: src/view/screens/Settings.tsx:789 #: src/view/screens/Settings.tsx:777
msgid "Verify My Email" msgid "Verify My Email"
msgstr "मेरी ईमेल सत्यापित करें" msgstr "मेरी ईमेल सत्यापित करें"
@ -2094,6 +2098,10 @@ msgstr "साइट पर जाएं"
msgid "We're so excited to have you join us!" msgid "We're so excited to have you join us!"
msgstr "हम आपके हमारी सेवा में शामिल होने को लेकर बहुत उत्साहित हैं!" 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 #: 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." msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "" 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." 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 पर रहेंगे तो हम आपको कुछ भेजेंगे।" msgstr "आपके पास अभी तक कोई आमंत्रण कोड नहीं है! जब आप कुछ अधिक समय के लिए Bluesky पर रहेंगे तो हम आपको कुछ भेजेंगे।"
#: src/view/screens/SavedFeeds.tsx:92 #: src/view/screens/SavedFeeds.tsx:102
msgid "You don't have any pinned feeds." msgid "You don't have any pinned feeds."
msgstr "आपके पास कोई पिन किया हुआ फ़ीड नहीं है." msgstr "आपके पास कोई पिन किया हुआ फ़ीड नहीं है."
@ -2157,7 +2165,7 @@ msgstr "आपके पास कोई पिन किया हुआ फ़
msgid "You don't have any saved feeds!" msgid "You don't have any saved feeds!"
msgstr "" msgstr ""
#: src/view/screens/SavedFeeds.tsx:118 #: src/view/screens/SavedFeeds.tsx:135
msgid "You don't have any saved feeds." msgid "You don't have any saved feeds."
msgstr "आपके पास कोई सहेजी गई फ़ीड नहीं है." msgstr "आपके पास कोई सहेजी गई फ़ीड नहीं है."
@ -2220,12 +2228,6 @@ msgstr "आपका पूरा हैंडल होगा"
msgid "Your hosting provider" msgid "Your hosting provider"
msgstr "आपका होस्टिंग प्रदाता" 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 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private." msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "आपकी पोस्ट, पसंद और ब्लॉक सार्वजनिक हैं। म्यूट निजी हैं।।" msgstr "आपकी पोस्ट, पसंद और ब्लॉक सार्वजनिक हैं। म्यूट निजी हैं।।"

View file

@ -288,16 +288,13 @@ export class Logger {
*/ */
export const logger = new Logger() export const logger = new Logger()
/**
* Report to console in dev, Sentry in prod, nothing in test.
*/
if (env.IS_DEV && !env.IS_TEST) { if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport) 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) { } 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 {FeedTuner} from '#/lib/api/feed-manip'
import {FeedDescriptor} from '../queries/post-feed' import {FeedDescriptor} from '../queries/post-feed'
import {useLanguagePrefs} from './languages' import {useLanguagePrefs} from './languages'
import {usePreferencesQuery} from '../queries/preferences'
import {useSession} from '../session'
export function useFeedTuners(feedDesc: FeedDescriptor) { export function useFeedTuners(feedDesc: FeedDescriptor) {
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {data: preferences} = usePreferencesQuery()
const {currentAccount} = useSession()
return useMemo(() => { return useMemo(() => {
if (feedDesc.startsWith('feedgen')) { if (feedDesc.startsWith('feedgen')) {
@ -19,30 +23,30 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
if (feedDesc === 'home' || feedDesc === 'following') { if (feedDesc === 'home' || feedDesc === 'following') {
const feedTuners = [] const feedTuners = []
if (false /*TODOthis.homeFeed.hideReposts*/) { if (preferences?.feedViewPrefs.hideReposts) {
feedTuners.push(FeedTuner.removeReposts) feedTuners.push(FeedTuner.removeReposts)
} else { } else {
feedTuners.push(FeedTuner.dedupReposts) feedTuners.push(FeedTuner.dedupReposts)
} }
if (true /*TODOthis.homeFeed.hideReplies*/) { if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies) feedTuners.push(FeedTuner.removeReplies)
} /* TODO else { } else {
feedTuners.push( feedTuners.push(
FeedTuner.thresholdRepliesOnly({ FeedTuner.thresholdRepliesOnly({
userDid: this.rootStore.session.data?.did || '', userDid: currentAccount?.did || '',
minLikes: this.homeFeed.hideRepliesByLikeCount, minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
followedOnly: !!this.homeFeed.hideRepliesByUnfollowed, followedOnly: !!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
}), }),
) )
}*/ }
if (false /*TODOthis.homeFeed.hideQuotePosts*/) { if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts) feedTuners.push(FeedTuner.removeQuotePosts)
} }
return feedTuners return feedTuners
} }
return [] return []
}, [feedDesc, langPrefs]) }, [feedDesc, currentAccount, preferences, langPrefs])
} }

View file

@ -181,6 +181,9 @@ export function useIsFeedPublicQuery({uri}: {uri: string}) {
if (msg.includes('missing jwt')) { if (msg.includes('missing jwt')) {
return false 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 return true
@ -243,13 +246,19 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeUri: '', likeUri: '',
} }
export function usePinnedFeedsInfos(): FeedSourceInfo[] { export function usePinnedFeedsInfos(): {
feeds: FeedSourceInfo[]
hasPinnedCustom: boolean
} {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([ const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
FOLLOWING_FEED_STUB, FOLLOWING_FEED_STUB,
]) ])
const {data: preferences} = usePreferencesQuery() 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(() => { React.useEffect(() => {
if (!preferences?.feeds?.pinned) return if (!preferences?.feeds?.pinned) return
@ -296,13 +305,7 @@ export function usePinnedFeedsInfos(): FeedSourceInfo[] {
} }
fetchFeedInfo() fetchFeedInfo()
}, [ }, [queryClient, setTabs, preferences?.feeds?.pinned])
queryClient,
setTabs,
preferences?.feeds?.pinned,
// ensure we react to re-ordering
pinnedFeedsKey,
])
return tabs return {feeds: tabs, hasPinnedCustom}
} }

View file

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

View file

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

View file

@ -1,12 +1,22 @@
import { /**
AppBskyFeedDefs, * NOTE
AppBskyFeedPost, * The ./unread.ts API:
AppBskyFeedRepost, *
AppBskyFeedLike, * - Provides a `checkUnread()` function to sync with the server,
AppBskyNotificationListNotifications, * - Periodically calls `checkUnread()`, and
BskyAgent, * - Caches the first page of notifications.
} from '@atproto/api' *
import chunk from 'lodash.chunk' * 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 { import {
useInfiniteQuery, useInfiniteQuery,
InfiniteData, InfiniteData,
@ -14,50 +24,27 @@ import {
useQueryClient, useQueryClient,
QueryClient, QueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {getAgent} from '../../session'
import {useModerationOpts} from '../preferences' 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 {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 PAGE_SIZE = 30
const MS_1HR = 1e3 * 60 * 60
const MS_2DAY = MS_1HR * 48
type RQPageParam = string | undefined type RQPageParam = string | undefined
type NotificationType =
| 'post-like'
| 'feedgen-like'
| 'repost'
| 'mention'
| 'reply'
| 'quote'
| 'follow'
| 'unknown'
export function RQKEY() { export function RQKEY() {
return ['notification-feed'] 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}) { export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads() const threadMutes = useMutedThreads()
const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
return useInfiniteQuery< return useInfiniteQuery<
@ -69,40 +56,21 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
>({ >({
queryKey: RQKEY(), queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().listNotifications({ // 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
}
}
// do a normal fetch
return fetchPage({
limit: PAGE_SIZE, limit: PAGE_SIZE,
cursor: pageParam, cursor: pageParam,
queryClient,
moderationOpts,
threadMutes,
}) })
// 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
}
}
}
// apply thread muting
notifsGrouped = notifsGrouped.filter(
notif => !isThreadMuted(notif, threadMutes),
)
return {
cursor: res.data.cursor,
items: notifsGrouped,
}
}, },
initialPageParam: undefined, initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
@ -135,114 +103,3 @@ export function findPostInQueryData(
} }
return undefined 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 React from 'react'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import {useQueryClient} from '@tanstack/react-query'
import BroadcastChannel from '#/lib/broadcast' import BroadcastChannel from '#/lib/broadcast'
import {useSession, getAgent} from '#/state/session' import {useSession, getAgent} from '#/state/session'
import {useModerationOpts} from '../preferences' import {useModerationOpts} from '../preferences'
import {shouldFilterNotif} from './util' import {fetchPage} from './util'
import {CachedFeedPage, FeedPage} from './types'
import {isNative} from '#/platform/detection' 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 const UPDATE_INTERVAL = 30 * 1e3 // 30sec
@ -14,7 +23,8 @@ type StateContext = string
interface ApiContext { interface ApiContext {
markAllRead: () => Promise<void> markAllRead: () => Promise<void>
checkUnread: () => Promise<void> checkUnread: (opts?: {invalidate?: boolean}) => Promise<void>
getCachedUnreadPage: () => FeedPage | undefined
} }
const stateContext = React.createContext<StateContext>('') const stateContext = React.createContext<StateContext>('')
@ -22,16 +32,23 @@ const stateContext = React.createContext<StateContext>('')
const apiContext = React.createContext<ApiContext>({ const apiContext = React.createContext<ApiContext>({
async markAllRead() {}, async markAllRead() {},
async checkUnread() {}, async checkUnread() {},
getCachedUnreadPage: () => undefined,
}) })
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const {hasSession} = useSession() const {hasSession, currentAccount} = useSession()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const [numUnread, setNumUnread] = React.useState('') const [numUnread, setNumUnread] = React.useState('')
const checkUnreadRef = React.useRef<(() => Promise<void>) | null>(null) const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null)
const lastSyncRef = React.useRef<Date>(new Date()) const cacheRef = React.useRef<CachedFeedPage>({
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
})
// periodic sync // periodic sync
React.useEffect(() => { React.useEffect(() => {
@ -46,14 +63,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// listen for broadcasts // listen for broadcasts
React.useEffect(() => { React.useEffect(() => {
const listener = ({data}: MessageEvent) => { const listener = ({data}: MessageEvent) => {
lastSyncRef.current = new Date() cacheRef.current = {
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
}
setNumUnread(data.event) setNumUnread(data.event)
} }
broadcast.addEventListener('message', listener) broadcast.addEventListener('message', listener)
return () => { return () => {
broadcast.removeEventListener('message', listener) broadcast.removeEventListener('message', listener)
} }
}, [setNumUnread]) }, [setNumUnread, currentAccount])
// create API // create API
const api = React.useMemo<ApiContext>(() => { const api = React.useMemo<ApiContext>(() => {
@ -61,7 +82,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
async markAllRead() { async markAllRead() {
// update server // update server
await getAgent().updateSeenNotifications( await getAgent().updateSeenNotifications(
lastSyncRef.current.toISOString(), cacheRef.current.syncedAt.toISOString(),
) )
// update & broadcast // update & broadcast
@ -69,38 +90,59 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
broadcast.postMessage({event: ''}) broadcast.postMessage({event: ''})
}, },
async checkUnread() { async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
const agent = getAgent() try {
if (!getAgent().session) return
if (!agent.session) return
// count // count
const res = await agent.listNotifications({limit: 40}) const page = await fetchPage({
const filtered = res.data.notifications.filter( cursor: undefined,
notif => !notif.isRead && !shouldFilterNotif(notif, moderationOpts), limit: 40,
) queryClient,
const num = moderationOpts,
filtered.length >= 30 threadMutes,
})
const unreadCount = countUnread(page)
const unreadCountStr =
unreadCount >= 30
? '30+' ? '30+'
: filtered.length === 0 : unreadCount === 0
? '' ? ''
: String(filtered.length) : String(unreadCount)
if (isNative) { if (isNative) {
Notifications.setBadgeCountAsync(Math.min(filtered.length, 30)) Notifications.setBadgeCountAsync(Math.min(unreadCount, 30))
} }
// track last sync // track last sync
const now = new Date() const now = new Date()
const lastIndexed = filtered[0] && new Date(filtered[0].indexedAt) const lastIndexed =
lastSyncRef.current = page.items[0] && new Date(page.items[0].notification.indexedAt)
!lastIndexed || now > lastIndexed ? now : lastIndexed cacheRef.current = {
sessDid: currentAccount?.did || '',
data: page,
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
}
// update & broadcast // update & broadcast
setNumUnread(num) setNumUnread(unreadCountStr)
broadcast.postMessage({event: num}) if (invalidate) {
queryClient.resetQueries({queryKey: RQKEY_NOTIFS()})
}
broadcast.postMessage({event: unreadCountStr})
} catch (e) {
logger.error('Failed to check unread notifications', {error: e})
}
},
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 checkUnreadRef.current = api.checkUnread
return ( return (
@ -117,3 +159,20 @@ export function useUnreadNotifications() {
export function useUnreadNotificationsApi() { export function useUnreadNotificationsApi() {
return React.useContext(apiContext) 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, ModerationOpts,
moderateProfile, moderateProfile,
moderatePost, moderatePost,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
} from '@atproto/api' } 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 // TODO this should be in the sdk as moderateNotification -prf
export function shouldFilterNotif( function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification, notif: AppBskyNotificationListNotifications.Notification,
moderationOpts: ModerationOpts | undefined, moderationOpts: ModerationOpts | undefined,
): boolean { ): boolean {
@ -36,3 +104,116 @@ export function shouldFilterNotif(
// (this requires fetching the post) // (this requires fetching the post)
return false 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 {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
import { import {
useInfiniteQuery, useInfiniteQuery,
@ -7,9 +6,8 @@ import {
QueryClient, QueryClient,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {getAgent} from '../session'
import {useFeedTuners} from '../preferences/feed-tuners' 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 {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
import {FollowingFeedAPI} from 'lib/api/feed/following' import {FollowingFeedAPI} from 'lib/api/feed/following'
import {AuthorFeedAPI} from 'lib/api/feed/author' 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 {CustomFeedAPI} from 'lib/api/feed/custom'
import {ListFeedAPI} from 'lib/api/feed/list' import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge' import {MergeFeedAPI} from 'lib/api/feed/merge'
import {useModerationOpts} from '#/state/queries/preferences'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri' 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 ActorDid = string
type AuthorFilter = type AuthorFilter =
@ -42,7 +43,7 @@ export interface FeedParams {
mergeFeedSources?: string[] mergeFeedSources?: string[]
} }
type RQPageParam = string | undefined type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
return ['post-feed', feedDesc, params || {}] return ['post-feed', feedDesc, params || {}]
@ -63,7 +64,15 @@ export interface FeedPostSlice {
items: FeedPostSliceItem[] items: FeedPostSliceItem[]
} }
export interface FeedPageUnselected {
api: FeedAPI
cursor: string | undefined
feed: AppBskyFeedDefs.FeedViewPost[]
}
export interface FeedPage { export interface FeedPage {
api: FeedAPI
tuner: FeedTuner | NoopFeedTuner
cursor: string | undefined cursor: string | undefined
slices: FeedPostSlice[] slices: FeedPostSlice[]
} }
@ -76,84 +85,62 @@ export function usePostFeedQuery(
const queryClient = useQueryClient() const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc) const feedTuners = useFeedTuners(feedDesc)
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
const moderationOpts = useModerationOpts()
const agent = getAgent()
const api: FeedAPI = useMemo(() => { return useInfiniteQuery<
if (feedDesc === 'home') { FeedPageUnselected,
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,
Error, Error,
InfiniteData<FeedPage>, InfiniteData<FeedPage>,
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
enabled,
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
queryKey: RQKEY(feedDesc, params), queryKey: RQKEY(feedDesc, params),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
logger.debug('usePostFeedQuery', {feedDesc, pageParam}) logger.debug('usePostFeedQuery', {feedDesc, pageParam})
if (!pageParam) {
tuner.reset() const {api, cursor} = pageParam
? pageParam
: {
api: createApi(feedDesc, params || {}, feedTuners),
cursor: undefined,
} }
const res = await api.fetch({cursor: pageParam, limit: 30})
const res = await api.fetch({cursor, limit: 30})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution 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 { return {
api,
cursor: res.cursor, cursor: res.cursor,
slices: slices.map(slice => ({ feed: res.feed,
}
},
initialPageParam: undefined,
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, _reactKey: slice._reactKey,
rootUri: slice.rootItem.post.uri, rootUri: slice.rootItem.post.uri,
isThread: isThread:
@ -172,21 +159,65 @@ export function usePostFeedQuery(
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.post.record, record: item.post.record,
reason: i === 0 && slice.source ? slice.source : item.reason, reason:
i === 0 && slice.source ? slice.source : item.reason,
} }
} }
return undefined return undefined
}) })
.filter(Boolean) as FeedPostSliceItem[], .filter(Boolean) as FeedPostSliceItem[],
})), })),
})),
} }
}, },
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled,
}) })
}
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( export function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): FeedPostSliceItem | undefined { ): AppBskyFeedDefs.FeedViewPost | undefined {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({ const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
queryKey: ['post-feed'], queryKey: ['post-feed'],
}) })
for (const [_queryKey, queryData] of queryDatas) { for (const [_queryKey, queryData] of queryDatas) {
@ -205,14 +238,34 @@ export function findPostInQueryData(
continue continue
} }
for (const page of queryData?.pages) { for (const page of queryData?.pages) {
for (const slice of page.slices) { for (const item of page.feed) {
for (const item of slice.items) { if (item.post.uri === uri) {
if (item.uri === uri) {
return item return item
} }
} }
} }
} }
}
return undefined 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 {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30 const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
@ -18,7 +17,6 @@ export function usePostLikedByQuery(resolvedUri: string | undefined) {
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(resolvedUri || ''), queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getLikes({ 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 {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30 const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
@ -18,7 +17,6 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(resolvedUri || ''), queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getRepostedBy({ 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 {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {STALE} from '#/state/queries' import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
import {
findPostInQueryData as findPostInFeedQueryData,
FeedPostSliceItem,
} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri' import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
@ -68,7 +64,6 @@ export type ThreadNode =
export function usePostThreadQuery(uri: string | undefined) { export function usePostThreadQuery(uri: string | undefined) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useQuery<ThreadNode, Error>({ return useQuery<ThreadNode, Error>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''), queryKey: RQKEY(uri || ''),
async queryFn() { async queryFn() {
const res = await getAgent().getPostThread({uri: uri!}) const res = await getAgent().getPostThread({uri: uri!})
@ -93,7 +88,7 @@ export function usePostThreadQuery(uri: string | undefined) {
{ {
const item = findPostInFeedQueryData(queryClient, uri) const item = findPostInFeedQueryData(queryClient, uri)
if (item) { 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 { return {
type: 'post', type: 'post',
_reactKey: item.post.uri, _reactKey: item.post.uri,
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.record, record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
viewer: item.post.viewer, viewer: item.post.viewer,
@ -291,7 +288,7 @@ function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
hasMore: false, hasMore: false,
showChildReplyLine: false, showChildReplyLine: false,
showParentReplyLine: false, showParentReplyLine: false,
isParentLoading: !!item.record.reply, isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply,
isChildLoading: !!item.post.replyCount, isChildLoading: !!item.post.replyCount,
}, },
} }
@ -305,7 +302,7 @@ function postViewToPlaceholderThread(
_reactKey: post.uri, _reactKey: post.uri,
uri: post.uri, uri: post.uri,
post: post, post: post,
record: post.record as AppBskyFeedPost.Record, // validate in notifs record: post.record as AppBskyFeedPost.Record, // validated in notifs
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
viewer: post.viewer, viewer: post.viewer,

View file

@ -4,13 +4,11 @@ import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {updatePostShadow} from '#/state/cache/post-shadow' import {updatePostShadow} from '#/state/cache/post-shadow'
import {STALE} from '#/state/queries'
export const RQKEY = (postUri: string) => ['post', postUri] export const RQKEY = (postUri: string) => ['post', postUri]
export function usePostQuery(uri: string | undefined) { export function usePostQuery(uri: string | undefined) {
return useQuery<AppBskyFeedDefs.PostView>({ return useQuery<AppBskyFeedDefs.PostView>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''), queryKey: RQKEY(uri || ''),
async queryFn() { async queryFn() {
const res = await getAgent().getPosts({uris: [uri!]}) const res = await getAgent().getPosts({uris: [uri!]})
@ -29,7 +27,6 @@ export function useGetPost() {
return React.useCallback( return React.useCallback(
async ({uri}: {uri: string}) => { async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({ return queryClient.fetchQuery({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''), queryKey: RQKEY(uri || ''),
async queryFn() { async queryFn() {
const urip = new AtUri(uri) 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 {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30 const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
@ -22,7 +21,6 @@ export function useProfileFeedgensQuery(
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(did), queryKey: RQKEY(did),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.feed.getActorFeeds({ 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 {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
const PAGE_SIZE = 30 const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
@ -17,7 +16,6 @@ export function useProfileFollowersQuery(did: string | undefined) {
QueryKey, QueryKey,
RQPageParam RQPageParam
>({ >({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(did || ''), queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getFollowers({ const res = await getAgent().app.bsky.graph.getFollowers({

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,12 @@ import {useCloseAllActiveElements} from '#/state/util'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT 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() { export function getAgent() {
return __globalAgent return __globalAgent
} }

View file

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

View file

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

View file

@ -17,12 +17,14 @@ import {useModalControls} from '#/state/modals'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
usePinFeedMutation,
UsePreferencesQueryResponse, UsePreferencesQueryResponse,
usePreferencesQuery, usePreferencesQuery,
useSaveFeedMutation, useSaveFeedMutation,
useRemoveFeedMutation, useRemoveFeedMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@ -30,17 +32,27 @@ export function FeedSourceCard({
showSaveBtn = false, showSaveBtn = false,
showDescription = false, showDescription = false,
showLikes = false, showLikes = false,
LoadingComponent,
pinOnSave = false,
}: { }: {
feedUri: string feedUri: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showSaveBtn?: boolean showSaveBtn?: boolean
showDescription?: boolean showDescription?: boolean
showLikes?: boolean showLikes?: boolean
LoadingComponent?: JSX.Element
pinOnSave?: boolean
}) { }) {
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri}) const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
if (!feed || !preferences) return null if (!feed || !preferences) {
return LoadingComponent ? (
LoadingComponent
) : (
<FeedLoadingPlaceholder style={{flex: 1}} />
)
}
return ( return (
<FeedSourceCardLoaded <FeedSourceCardLoaded
@ -50,6 +62,7 @@ export function FeedSourceCard({
showSaveBtn={showSaveBtn} showSaveBtn={showSaveBtn}
showDescription={showDescription} showDescription={showDescription}
showLikes={showLikes} showLikes={showLikes}
pinOnSave={pinOnSave}
/> />
) )
} }
@ -61,6 +74,7 @@ export function FeedSourceCardLoaded({
showSaveBtn = false, showSaveBtn = false,
showDescription = false, showDescription = false,
showLikes = false, showLikes = false,
pinOnSave = false,
}: { }: {
feed: FeedSourceInfo feed: FeedSourceInfo
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
@ -68,6 +82,7 @@ export function FeedSourceCardLoaded({
showSaveBtn?: boolean showSaveBtn?: boolean
showDescription?: boolean showDescription?: boolean
showLikes?: boolean showLikes?: boolean
pinOnSave?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@ -78,6 +93,7 @@ export function FeedSourceCardLoaded({
useSaveFeedMutation() useSaveFeedMutation()
const {isPending: isRemovePending, mutateAsync: removeFeed} = const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation() useRemoveFeedMutation()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri)) const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri))
@ -103,14 +119,18 @@ export function FeedSourceCardLoaded({
}) })
} else { } else {
try { try {
if (pinOnSave) {
await pinFeed({uri: feed.uri})
} else {
await saveFeed({uri: feed.uri}) await saveFeed({uri: feed.uri})
}
Toast.show('Added to my feeds') Toast.show('Added to my feeds')
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show('There was an issue contacting your server')
logger.error('Failed to save feed', {error: e}) 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 if (!feed || !preferences) return null
@ -150,7 +170,7 @@ export function FeedSourceCardLoaded({
{showSaveBtn && feed.type === 'feed' && ( {showSaveBtn && feed.type === 'feed' && (
<View> <View>
<Pressable <Pressable
disabled={isSavePending || isRemovePending} disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={
isSaved ? 'Remove from my feeds' : 'Add to my feeds' 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 [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {markAllRead} = useUnreadNotificationsApi() const {markAllRead, checkUnread} = useUnreadNotificationsApi()
const { const {
data, data,
isLoading,
isFetching, isFetching,
isFetched, isFetched,
isError, isError,
error, error,
refetch,
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
fetchNextPage, fetchNextPage,
@ -52,13 +50,11 @@ export function Feed({
const firstItem = data?.pages[0]?.items[0] const firstItem = data?.pages[0]?.items[0]
// mark all read on fresh data // mark all read on fresh data
// (this will fire each time firstItem changes)
React.useEffect(() => { React.useEffect(() => {
let cleanup
if (firstItem) { if (firstItem) {
const to = setTimeout(() => markAllRead(), 250) markAllRead()
cleanup = () => clearTimeout(to)
} }
return cleanup
}, [firstItem, markAllRead]) }, [firstItem, markAllRead])
const items = React.useMemo(() => { const items = React.useMemo(() => {
@ -83,7 +79,7 @@ export function Feed({
const onRefresh = React.useCallback(async () => { const onRefresh = React.useCallback(async () => {
try { try {
setIsPTRing(true) setIsPTRing(true)
await refetch() await checkUnread({invalidate: true})
} catch (err) { } catch (err) {
logger.error('Failed to refresh notifications feed', { logger.error('Failed to refresh notifications feed', {
error: err, error: err,
@ -91,7 +87,7 @@ export function Feed({
} finally { } finally {
setIsPTRing(false) setIsPTRing(false)
} }
}, [refetch, setIsPTRing]) }, [checkUnread, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
@ -136,21 +132,6 @@ export function Feed({
[onPressRetryLoadMore, moderationOpts], [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( const FeedFooter = React.useCallback(
() => () =>
isFetchingNextPage ? ( isFetchingNextPage ? (
@ -180,7 +161,7 @@ export function Feed({
data={items} data={items}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={FeedHeader} ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl

View file

@ -12,6 +12,9 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {TextLink} from '#/view/com/util/Link' import {TextLink} from '#/view/com/util/Link'
import {CenteredView} from '../util/Views' 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( export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -79,11 +82,37 @@ function FeedsTabBarPublic() {
function FeedsTabBarTablet( function FeedsTabBarTablet(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) { ) {
const feeds = usePinnedFeedsInfos() const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const pal = usePalette('default') const pal = usePalette('default')
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const {headerMinimalShellTransform} = useMinimalShellMode() const {headerMinimalShellTransform} = useMinimalShellMode()
const {headerHeight} = useShellLayout() 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 ( return (
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf // @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 <TabBar
key={items.join(',')} key={items.join(',')}
{...props} {...props}
onSelect={onSelect}
items={items} items={items}
indicatorColor={pal.colors.link} 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 {useShellLayout} from '#/state/shell/shell-layout'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {usePinnedFeedsInfos} from '#/state/queries/feed' 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( export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -26,11 +29,36 @@ export function FeedsTabBar(
const {isSandbox, hasSession} = useSession() const {isSandbox, hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const feeds = usePinnedFeedsInfos() const navigation = useNavigation<NavigationProp>()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode() 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(() => { const onPressAvi = React.useCallback(() => {
setDrawerOpen(true) setDrawerOpen(true)
@ -84,7 +112,7 @@ export function FeedsTabBar(
key={items.join(',')} key={items.join(',')}
onPressSelected={props.onPressSelected} onPressSelected={props.onPressSelected}
selectedPage={props.selectedPage} selectedPage={props.selectedPage}
onSelect={props.onSelect} onSelect={onSelect}
testID={props.testID} testID={props.testID}
items={items} items={items}
indicatorColor={pal.colors.link} indicatorColor={pal.colors.link}

View file

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

View file

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

View file

@ -23,6 +23,7 @@ import {
FeedDescriptor, FeedDescriptor,
FeedParams, FeedParams,
usePostFeedQuery, usePostFeedQuery,
pollLatest,
} from '#/state/queries/post-feed' } from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
@ -84,22 +85,21 @@ let Feed = ({
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
fetchNextPage, fetchNextPage,
pollLatest,
} = usePostFeedQuery(feed, feedParams, opts) } = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length const isEmpty = !isFetching && !data?.pages[0]?.slices.length
const checkForNew = React.useCallback(async () => { const checkForNew = React.useCallback(async () => {
if (!isFetched || isFetching || !onHasNew) { if (!data?.pages[0] || isFetching || !onHasNew) {
return return
} }
try { try {
if (await pollLatest()) { if (await pollLatest(data.pages[0])) {
onHasNew(true) onHasNew(true)
} }
} catch (e) { } catch (e) {
logger.error('Poll latest failed', {feed, error: String(e)}) logger.error('Poll latest failed', {feed, error: String(e)})
} }
}, [feed, isFetched, isFetching, pollLatest, onHasNew]) }, [feed, data, isFetching, onHasNew])
React.useEffect(() => { React.useEffect(() => {
// we store the interval handler in a ref to avoid needless // 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 {cleanError} from '#/lib/strings/errors'
import {useRemoveFeedMutation} from '#/state/queries/preferences' import {useRemoveFeedMutation} from '#/state/queries/preferences'
enum KnownError { export enum KnownError {
Block, Block = 'Block',
FeedgenDoesNotExist, FeedgenDoesNotExist = 'FeedgenDoesNotExist',
FeedgenMisconfigured, FeedgenMisconfigured = 'FeedgenMisconfigured',
FeedgenBadResponse, FeedgenBadResponse = 'FeedgenBadResponse',
FeedgenOffline, FeedgenOffline = 'FeedgenOffline',
FeedgenUnknown, FeedgenUnknown = 'FeedgenUnknown',
Unknown, FeedNSFPublic = 'FeedNSFPublic',
} Unknown = '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 function FeedErrorMessage({ export function FeedErrorMessage({
@ -90,7 +77,32 @@ function FeedgenErrorMessage({
const pal = usePalette('default') const pal = usePalette('default')
const {_: _l} = useLingui() const {_: _l} = useLingui()
const navigation = useNavigation<NavigationProp>() 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 [_, uri] = feedDesc.split('|')
const [ownerDid] = safeParseFeedgenUri(uri) const [ownerDid] = safeParseFeedgenUri(uri)
const {openModal, closeModal} = useModalControls() const {openModal, closeModal} = useModalControls()
@ -121,6 +133,36 @@ function FeedgenErrorMessage({
}) })
}, [openModal, closeModal, uri, removeFeed, _l]) }, [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 ( return (
<View <View
style={[ style={[
@ -134,16 +176,7 @@ function FeedgenErrorMessage({
}, },
]}> ]}>
<Text style={pal.text}>{msg}</Text> <Text style={pal.text}>{msg}</Text>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> {cta}
{knownError === KnownError.FeedgenDoesNotExist && (
<Button type="inverted" label="Remove feed" onPress={onRemoveFeed} />
)}
<Button
type="default-light"
label="View profile"
onPress={onViewProfile}
/>
</View>
</View> </View>
) )
} }
@ -196,5 +229,8 @@ function detectKnownError(
if (error.includes('feed provided an invalid response')) { if (error.includes('feed provided an invalid response')) {
return KnownError.FeedgenBadResponse return KnownError.FeedgenBadResponse
} }
if (error.includes(KnownError.FeedNSFPublic)) {
return KnownError.FeedNSFPublic
}
return KnownError.FeedgenUnknown return KnownError.FeedgenUnknown
} }

View file

@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
noFeedback?: boolean noFeedback?: boolean
asAnchor?: boolean asAnchor?: boolean
anchorNoUnderline?: boolean anchorNoUnderline?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
} }
export const Link = memo(function Link({ export const Link = memo(function Link({
@ -58,6 +59,7 @@ export const Link = memo(function Link({
asAnchor, asAnchor,
accessible, accessible,
anchorNoUnderline, anchorNoUnderline,
navigationAction,
...props ...props
}: Props) { }: Props) {
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
@ -67,10 +69,16 @@ export const Link = memo(function Link({
const onPress = React.useCallback( const onPress = React.useCallback(
(e?: Event) => { (e?: Event) => {
if (typeof href === 'string') { 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) { if (noFeedback) {
@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
title, title,
onPress, onPress,
warnOnMismatchingLabel, warnOnMismatchingLabel,
navigationAction,
...orgProps ...orgProps
}: { }: {
testID?: string testID?: string
@ -158,6 +167,7 @@ export const TextLink = memo(function TextLink({
dataSet?: any dataSet?: any
title?: string title?: string
warnOnMismatchingLabel?: boolean warnOnMismatchingLabel?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
} & TextProps) { } & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)}) const {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@ -185,7 +195,13 @@ export const TextLink = memo(function TextLink({
// @ts-ignore function signature differs by platform -prf // @ts-ignore function signature differs by platform -prf
return onPress() return onPress()
} }
return onPressInner(closeModal, navigation, sanitizeUrl(href), e) return onPressInner(
closeModal,
navigation,
sanitizeUrl(href),
navigationAction,
e,
)
}, },
[ [
onPress, onPress,
@ -195,6 +211,7 @@ export const TextLink = memo(function TextLink({
href, href,
text, text,
warnOnMismatchingLabel, warnOnMismatchingLabel,
navigationAction,
], ],
) )
const hrefAttrs = useMemo(() => { const hrefAttrs = useMemo(() => {
@ -241,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
accessibilityLabel?: string accessibilityLabel?: string
accessibilityHint?: string accessibilityHint?: string
title?: string title?: string
navigationAction?: 'push' | 'replace' | 'navigate'
} }
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
testID, testID,
@ -250,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
text, text,
numberOfLines, numberOfLines,
lineHeight, lineHeight,
navigationAction,
...props ...props
}: TextLinkOnWebOnlyProps) { }: TextLinkOnWebOnlyProps) {
if (isWeb) { if (isWeb) {
@ -263,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
lineHeight={lineHeight} lineHeight={lineHeight}
title={props.title} title={props.title}
navigationAction={navigationAction}
{...props} {...props}
/> />
) )
@ -296,6 +316,7 @@ function onPressInner(
closeModal = () => {}, closeModal = () => {},
navigation: NavigationProp, navigation: NavigationProp,
href: string, href: string,
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
e?: Event, e?: Event,
) { ) {
let shouldHandle = false let shouldHandle = false
@ -328,8 +349,18 @@ function onPressInner(
} else { } else {
closeModal() // close any active modals closeModal() // close any active modals
if (navigationAction === 'push') {
// @ts-ignore we're not able to type check on this one -prf // @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href))) 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({ export function FeedLoadingPlaceholder({
style, style,
showLowerPlaceholder = true,
showTopBorder = true,
}: { }: {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showTopBorder?: boolean
showLowerPlaceholder?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
<View <View
style={[ style={[
{paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1}, {
paddingHorizontal: 12,
paddingVertical: 18,
borderTopWidth: showTopBorder ? 1 : 0,
},
pal.border, pal.border,
style, style,
]}> ]}>
@ -193,6 +201,7 @@ export function FeedLoadingPlaceholder({
<LoadingPlaceholder width={120} height={8} /> <LoadingPlaceholder width={120} height={8} />
</View> </View>
</View> </View>
{showLowerPlaceholder && (
<View style={{paddingHorizontal: 5}}> <View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder <LoadingPlaceholder
width={260} width={260}
@ -201,6 +210,7 @@ export function FeedLoadingPlaceholder({
/> />
<LoadingPlaceholder width={120} height={8} /> <LoadingPlaceholder width={120} height={8} />
</View> </View>
)}
</View> </View>
) )
} }

View file

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

View file

@ -40,6 +40,12 @@ function HomeScreenReady({
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const [selectedPage, setSelectedPage] = React.useState(0) 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 customFeeds = React.useMemo(() => {
const pinned = preferences.feeds.pinned const pinned = preferences.feeds.pinned
const feeds: FeedDescriptor[] = [] const feeds: FeedDescriptor[] = []
@ -83,7 +89,6 @@ function HomeScreenReady({
emitSoftReset() emitSoftReset()
}, []) }, [])
// TODO(pwi) may need this in public view
const onPageScrollStateChanged = React.useCallback( const onPageScrollStateChanged = React.useCallback(
(state: 'idle' | 'dragging' | 'settling') => { (state: 'idle' | 'dragging' | 'settling') => {
if (state === 'dragging') { if (state === 'dragging') {
@ -118,6 +123,7 @@ function HomeScreenReady({
return hasSession ? ( return hasSession ? (
<Pager <Pager
key={pinnedFeedOrderKey}
testID="homeScreen" testID="homeScreen"
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged} onPageScrollStateChanged={onPageScrollStateChanged}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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