Merge branch 'main' into hailey/search-improvements

This commit is contained in:
Dan Abramov
2024-04-26 16:25:34 +01:00
118 changed files with 10075 additions and 9585 deletions
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class AuthorFeedAPI implements FeedAPI {
constructor(public params: GetAuthorFeed.QueryParams) {}
getAgent: () => BskyAgent
params: GetAuthorFeed.QueryParams
constructor({
getAgent,
feedParams,
}: {
getAgent: () => BskyAgent
feedParams: GetAuthorFeed.QueryParams
}) {
this.getAgent = getAgent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getAuthorFeed({
const res = await this.getAgent().getAuthorFeed({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getAuthorFeed({
const res = await this.getAgent().getAuthorFeed({
...this.params,
cursor,
limit,
+22 -6
View File
@@ -2,18 +2,30 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
AtpAgent,
BskyAgent,
} from '@atproto/api'
import {getContentLanguages} from '#/state/preferences/languages'
import {getAgent} from '#/state/session'
import {FeedAPI, FeedAPIResponse} from './types'
export class CustomFeedAPI implements FeedAPI {
constructor(public params: GetCustomFeed.QueryParams) {}
getAgent: () => BskyAgent
params: GetCustomFeed.QueryParams
constructor({
getAgent,
feedParams,
}: {
getAgent: () => BskyAgent
feedParams: GetCustomFeed.QueryParams
}) {
this.getAgent = getAgent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
const res = await this.getAgent().app.bsky.feed.getFeed(
{
...this.params,
limit: 1,
@@ -31,15 +43,19 @@ export class CustomFeedAPI implements FeedAPI {
limit: number
}): Promise<FeedAPIResponse> {
const contentLangs = getContentLanguages().join(',')
const agent = getAgent()
const agent = this.getAgent()
const res = agent.session
? await getAgent().app.bsky.feed.getFeed(
? await this.getAgent().app.bsky.feed.getFeed(
{
...this.params,
cursor,
limit,
},
{headers: {'Accept-Language': contentLangs}},
{
headers: {
'Accept-Language': contentLangs,
},
},
)
: await loggedOutFetch({...this.params, cursor, limit})
if (res.success) {
+9 -5
View File
@@ -1,12 +1,16 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class FollowingFeedAPI implements FeedAPI {
constructor() {}
getAgent: () => BskyAgent
constructor({getAgent}: {getAgent: () => BskyAgent}) {
this.getAgent = getAgent
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getTimeline({
const res = await this.getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -19,7 +23,7 @@ export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getTimeline({
const res = await this.getAgent().getTimeline({
cursor,
limit,
})
+18 -9
View File
@@ -1,8 +1,9 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {FollowingFeedAPI} from './following'
import {CustomFeedAPI} from './custom'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {CustomFeedAPI} from './custom'
import {FollowingFeedAPI} from './following'
import {FeedAPI, FeedAPIResponse} from './types'
// HACK
// the feed API does not include any facilities for passing down
@@ -26,19 +27,27 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
}
export class HomeFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
itemCursor = 0
constructor() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
constructor({getAgent}: {getAgent: () => BskyAgent}) {
this.getAgent = getAgent
this.following = new FollowingFeedAPI({getAgent})
this.discover = new CustomFeedAPI({
getAgent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
})
}
reset() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
this.following = new FollowingFeedAPI({getAgent: this.getAgent})
this.discover = new CustomFeedAPI({
getAgent: this.getAgent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
})
this.usingDiscover = false
this.itemCursor = 0
}
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class LikesFeedAPI implements FeedAPI {
constructor(public params: GetActorLikes.QueryParams) {}
getAgent: () => BskyAgent
params: GetActorLikes.QueryParams
constructor({
getAgent,
feedParams,
}: {
getAgent: () => BskyAgent
feedParams: GetActorLikes.QueryParams
}) {
this.getAgent = getAgent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getActorLikes({
const res = await this.getAgent().getActorLikes({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getActorLikes({
const res = await this.getAgent().getActorLikes({
...this.params,
cursor,
limit,
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetListFeed as GetListFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class ListFeedAPI implements FeedAPI {
constructor(public params: GetListFeed.QueryParams) {}
getAgent: () => BskyAgent
params: GetListFeed.QueryParams
constructor({
getAgent,
feedParams,
}: {
getAgent: () => BskyAgent
feedParams: GetListFeed.QueryParams
}) {
this.getAgent = getAgent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().app.bsky.feed.getListFeed({
const res = await this.getAgent().app.bsky.feed.getListFeed({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class ListFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getListFeed({
const res = await this.getAgent().app.bsky.feed.getListFeed({
...this.params,
cursor,
limit,
+72 -17
View File
@@ -1,31 +1,51 @@
import {AppBskyFeedDefs, AppBskyFeedGetTimeline} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedGetTimeline, BskyAgent} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {timeout} from 'lib/async/timeout'
import {getContentLanguages} from '#/state/preferences/languages'
import {FeedParams} from '#/state/queries/post-feed'
import {bundleAsync} from 'lib/async/bundle'
import {timeout} from 'lib/async/timeout'
import {feedUriToHref} from 'lib/strings/url-helpers'
import {FeedTuner} from '../feed-manip'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
export class MergeFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
params: FeedParams
feedTuners: FeedTunerFn[]
following: MergeFeedSource_Following
customFeeds: MergeFeedSource_Custom[] = []
feedCursor = 0
itemCursor = 0
sampleCursor = 0
constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) {
this.following = new MergeFeedSource_Following(this.feedTuners)
constructor({
getAgent,
feedParams,
feedTuners,
}: {
getAgent: () => BskyAgent
feedParams: FeedParams
feedTuners: FeedTunerFn[]
}) {
this.getAgent = getAgent
this.params = feedParams
this.feedTuners = feedTuners
this.following = new MergeFeedSource_Following({
getAgent: this.getAgent,
feedTuners: this.feedTuners,
})
}
reset() {
this.following = new MergeFeedSource_Following(this.feedTuners)
this.following = new MergeFeedSource_Following({
getAgent: this.getAgent,
feedTuners: this.feedTuners,
})
this.customFeeds = []
this.feedCursor = 0
this.itemCursor = 0
@@ -33,7 +53,12 @@ export class MergeFeedAPI implements FeedAPI {
if (this.params.mergeFeedSources) {
this.customFeeds = shuffle(
this.params.mergeFeedSources.map(
feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners),
feedUri =>
new MergeFeedSource_Custom({
getAgent: this.getAgent,
feedUri,
feedTuners: this.feedTuners,
}),
),
)
} else {
@@ -42,7 +67,7 @@ export class MergeFeedAPI implements FeedAPI {
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getTimeline({
const res = await this.getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -136,12 +161,23 @@ export class MergeFeedAPI implements FeedAPI {
}
class MergeFeedSource {
getAgent: () => BskyAgent
feedTuners: FeedTunerFn[]
sourceInfo: ReasonFeedSource | undefined
cursor: string | undefined = undefined
queue: AppBskyFeedDefs.FeedViewPost[] = []
hasMore = true
constructor(public feedTuners: FeedTunerFn[]) {}
constructor({
getAgent,
feedTuners,
}: {
getAgent: () => BskyAgent
feedTuners: FeedTunerFn[]
}) {
this.getAgent = getAgent
this.feedTuners = feedTuners
}
get numReady() {
return this.queue.length
@@ -203,7 +239,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await getAgent().getTimeline({cursor, limit})
const res = await this.getAgent().getTimeline({cursor, limit})
// run the tuner pre-emptively to ensure better mixing
const slices = this.tuner.tune(res.data.feed, {
dryRun: false,
@@ -215,10 +251,25 @@ class MergeFeedSource_Following extends MergeFeedSource {
}
class MergeFeedSource_Custom extends MergeFeedSource {
getAgent: () => BskyAgent
minDate: Date
feedUri: string
constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) {
super(feedTuners)
constructor({
getAgent,
feedUri,
feedTuners,
}: {
getAgent: () => BskyAgent
feedUri: string
feedTuners: FeedTunerFn[]
}) {
super({
getAgent,
feedTuners,
})
this.getAgent = getAgent
this.feedUri = feedUri
this.sourceInfo = {
$type: 'reasonFeedSource',
uri: feedUri,
@@ -233,13 +284,17 @@ class MergeFeedSource_Custom extends MergeFeedSource {
): Promise<AppBskyFeedGetTimeline.Response> {
try {
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
const res = await this.getAgent().app.bsky.feed.getFeed(
{
cursor,
limit,
feed: this.feedUri,
},
{headers: {'Accept-Language': contentLangs}},
{
headers: {
'Accept-Language': contentLangs,
},
},
)
// NOTE
// some custom feeds fail to enforce the pagination limit
+4 -15
View File
@@ -1,4 +1,3 @@
import {deleteAsync} from 'expo-file-system'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
@@ -20,6 +19,7 @@ import {shortenLinks} from 'lib/strings/rich-text-manip'
import {isNative, isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
import {LinkMeta} from '../link-meta/link-meta'
import {safeDeleteAsync} from '../media/manip'
export interface ExternalEmbedDraft {
uri: string
@@ -119,15 +119,9 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
const {width, height} = image.compressed || image
logger.debug(`Uploading image`)
const res = await uploadBlob(agent, path, 'image/jpeg')
if (isNative) {
try {
deleteAsync(path)
} catch (e) {
console.error(e)
}
safeDeleteAsync(path)
}
images.push({
image: res.data.blob,
alt: image.altText ?? '',
@@ -182,13 +176,8 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
encoding,
)
thumb = thumbUploadRes.data.blob
try {
if (isNative) {
deleteAsync(opts.extLink.localThumb.path)
}
} catch (e) {
console.error(e)
if (isNative) {
safeDeleteAsync(opts.extLink.localThumb.path)
}
}
}
-1
View File
@@ -1,3 +1,2 @@
export const LOGIN_INCLUDE_DEV_SERVERS = true
export const PWI_ENABLED = true
export const NEW_ONBOARDING_ENABLED = true
+1
View File
@@ -4,6 +4,7 @@ export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const BSKY_SERVICE = 'https://bsky.social'
export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'
export const DEFAULT_SERVICE = BSKY_SERVICE
const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
+45 -16
View File
@@ -1,13 +1,14 @@
import RNFetchBlob from 'rn-fetch-blob'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker'
import * as RNFS from 'react-native-fs'
import uuid from 'react-native-uuid'
import * as Sharing from 'expo-sharing'
import {cacheDirectory, copyAsync, deleteAsync} from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library'
import {Dimensions} from './types'
import * as Sharing from 'expo-sharing'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
import {isAndroid, isIOS} from 'platform/detection'
import {Dimensions} from './types'
export async function compressIfNeeded(
img: Image,
@@ -23,7 +24,10 @@ export async function compressIfNeeded(
mode: 'stretch',
maxSize,
})
const finalImageMovedPath = await moveToPermanentPath(resizedImage.path)
const finalImageMovedPath = await moveToPermanentPath(
resizedImage.path,
'.jpg',
)
const finalImg = {
...resizedImage,
path: finalImageMovedPath,
@@ -63,13 +67,15 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
downloadRes = await downloadResPromise
clearTimeout(to1)
let localUri = downloadRes.path()
if (!localUri.startsWith('file://')) {
localUri = `file://${localUri}`
const status = downloadRes.info().status
if (status !== 200) {
return
}
const localUri = normalizePath(downloadRes.path(), true)
return await doResize(localUri, opts)
} finally {
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
if (downloadRes) {
downloadRes.flush()
}
@@ -105,7 +111,8 @@ export async function shareImageModal({uri}: {uri: string}) {
UTI: 'image/png',
})
}
RNFS.unlink(imagePath)
safeDeleteAsync(imagePath)
}
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
@@ -122,6 +129,7 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// save
await MediaLibrary.createAssetAsync(imagePath)
safeDeleteAsync(imagePath)
}
export function getImageDim(path: string): Promise<Dimensions> {
@@ -168,6 +176,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
width: resizeRes.width,
height: resizeRes.height,
}
} else {
safeDeleteAsync(resizeRes.path)
}
}
throw new Error(
@@ -175,7 +185,7 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
)
}
async function moveToPermanentPath(path: string, ext = ''): Promise<string> {
async function moveToPermanentPath(path: string, ext = 'jpg'): Promise<string> {
/*
Since this package stores images in a temp directory, we need to move the file to a permanent location.
Relevant: IOS bug when trying to open a second time:
@@ -183,14 +193,33 @@ async function moveToPermanentPath(path: string, ext = ''): Promise<string> {
*/
const filename = uuid.v4()
const destinationPath = joinPath(
RNFS.TemporaryDirectoryPath,
`${filename}${ext}`,
)
await RNFS.moveFile(path, destinationPath)
// cacheDirectory will not ever be null on native, but it could be on web. This function only ever gets called on
// native so we assert as a string.
const destinationPath = joinPath(cacheDirectory as string, filename + ext)
await copyAsync({
from: normalizePath(path),
to: normalizePath(destinationPath),
})
safeDeleteAsync(path)
return normalizePath(destinationPath)
}
export async function safeDeleteAsync(path: string) {
// Normalize is necessary for Android, otherwise it doesn't delete.
const normalizedPath = normalizePath(path)
try {
await Promise.allSettled([
deleteAsync(normalizedPath, {idempotent: true}),
// HACK: Try this one too. Might exist due to api-polyfill hack.
deleteAsync(normalizedPath.replace(/\.jpe?g$/, '.bin'), {
idempotent: true,
}),
])
} catch (e) {
console.error('Failed to delete file', e)
}
}
function joinPath(a: string, b: string) {
if (a.endsWith('/')) {
if (b.startsWith('/')) {
+4 -1
View File
@@ -1,12 +1,13 @@
import {useEffect} from 'react'
import * as Notifications from 'expo-notifications'
import {BskyAgent} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
import {truncateAndInvalidate} from '#/state/queries/util'
import {getAgent, SessionAccount} from '#/state/session'
import {SessionAccount} from '#/state/session'
import {track} from 'lib/analytics/analytics'
import {devicePlatform, isIOS} from 'platform/detection'
import {resetToTab} from '../../Navigation'
@@ -18,6 +19,7 @@ const SERVICE_DID = (serviceUrl?: string) =>
: 'did:web:api.bsky.app'
export async function requestPermissionsAndRegisterToken(
getAgent: () => BskyAgent,
account: SessionAccount,
) {
// request notifications permission once the user has logged in
@@ -49,6 +51,7 @@ export async function requestPermissionsAndRegisterToken(
}
export function registerTokenChangeHandler(
getAgent: () => BskyAgent,
account: SessionAccount,
): () => void {
// listens for new changes to the push token
-1
View File
@@ -8,4 +8,3 @@ export type Gate =
| 'start_session_with_following_v2'
| 'test_gate_1'
| 'test_gate_2'
| 'use_new_suggestions_endpoint'
+11 -2
View File
@@ -352,8 +352,17 @@ export function parseEmbedPlayerFromUrl(
if (id && filename && dimensions && id.includes('AAAAC')) {
if (Platform.OS === 'web') {
id = id.replace('AAAAC', 'AAAP3')
filename = filename.replace('.gif', '.webm')
const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent,
)
if (isSafari) {
id = id.replace('AAAAC', 'AAAP1')
filename = filename.replace('.gif', '.mp4')
} else {
id = id.replace('AAAAC', 'AAAP3')
filename = filename.replace('.gif', '.webm')
}
} else {
id = id.replace('AAAAC', 'AAAAM')
}