Merge branch 'main' into hailey/search-improvements
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {beforeAll, describe, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
import {createServer, loginAsAlice, openApp, sleep} from '../util'
|
||||
|
||||
describe('Composer', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -41,7 +42,6 @@ describe('Composer', () => {
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Post with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
@@ -72,7 +72,6 @@ describe('Composer', () => {
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Reply with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
@@ -104,7 +103,6 @@ describe('Composer', () => {
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'QP with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
} from '../../src/lib/media/manip'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
@@ -30,6 +31,7 @@ describe('downloadAndResize', () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
@@ -84,6 +86,7 @@ describe('downloadAndResize', () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
@@ -118,4 +121,26 @@ describe('downloadAndResize', () => {
|
||||
{mode: 'cover'},
|
||||
)
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,14 +122,20 @@ func (srv *Server) WebOEmbed(c echo.Context) error {
|
||||
}
|
||||
|
||||
// TODO: do we actually do something with width?
|
||||
width := 550
|
||||
width := 600
|
||||
maxWidthParam := c.QueryParam("maxwidth")
|
||||
if maxWidthParam != "" {
|
||||
maxWidthInt, err := strconv.Atoi(maxWidthParam)
|
||||
if err != nil || maxWidthInt < 220 || maxWidthInt > 550 {
|
||||
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer between 220 and 550)")
|
||||
if err != nil {
|
||||
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer)")
|
||||
}
|
||||
if maxWidthInt < 220 {
|
||||
width = 220
|
||||
} else if maxWidthInt > 600 {
|
||||
width = 600
|
||||
} else {
|
||||
width = maxWidthInt
|
||||
}
|
||||
width = maxWidthInt
|
||||
}
|
||||
// NOTE: maxheight ignored
|
||||
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {init as initPersistedState} from '#/state/persisted'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
import {useNotificationsListener} from 'lib/notifications/notifications'
|
||||
import {QueryProvider} from 'lib/react-query'
|
||||
@@ -64,7 +64,7 @@ function InnerApp() {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
})
|
||||
|
||||
const account = persisted.get('session').currentAccount
|
||||
const account = readLastActiveAccount()
|
||||
resumeSession(account)
|
||||
}, [resumeSession, _])
|
||||
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {init as initPersistedState} from '#/state/persisted'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
import {QueryProvider} from 'lib/react-query'
|
||||
import {ThemeProvider} from 'lib/ThemeContext'
|
||||
@@ -42,7 +42,7 @@ function InnerApp() {
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
const account = persisted.get('session').currentAccount
|
||||
const account = readLastActiveAccount()
|
||||
resumeSession(account)
|
||||
}, [resumeSession])
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
@@ -35,6 +35,7 @@ export function SubmitView({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {getAgent} = useAgent()
|
||||
const [details, setDetails] = React.useState<string>('')
|
||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||
@@ -90,6 +91,7 @@ export function SubmitView({
|
||||
selectedServices,
|
||||
onSubmitComplete,
|
||||
setError,
|
||||
getAgent,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import {RichText as RichTextAPI} from '@atproto/api'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
const [prevText, setPrevText] = React.useState(text)
|
||||
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
||||
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
|
||||
const {getAgent} = useAgent()
|
||||
if (text !== prevText) {
|
||||
setPrevText(text)
|
||||
setRawRT(new RichTextAPI({text}))
|
||||
@@ -27,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
return () => {
|
||||
ignore = true
|
||||
}
|
||||
}, [text])
|
||||
}, [text, getAgent])
|
||||
const isResolving = resolvedRT === null
|
||||
return [resolvedRT ?? rawRT, isResolving]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -173,6 +173,7 @@ function AppealForm({
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const [details, setDetails] = React.useState('')
|
||||
const isAccountReport = 'did' in subject
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,3 +1,2 @@
|
||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||
export const PWI_ENABLED = true
|
||||
export const NEW_ONBOARDING_ENABLED = true
|
||||
|
||||
@@ -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
@@ -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('/')) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,4 +8,3 @@ export type Gate =
|
||||
| 'start_session_with_following_v2'
|
||||
| 'test_gate_1'
|
||||
| 'test_gate_2'
|
||||
| 'use_new_suggestions_endpoint'
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
+528
-368
File diff suppressed because it is too large
Load Diff
+527
-367
File diff suppressed because it is too large
Load Diff
+526
-366
File diff suppressed because it is too large
Load Diff
+526
-366
File diff suppressed because it is too large
Load Diff
+528
-368
File diff suppressed because it is too large
Load Diff
+529
-369
File diff suppressed because it is too large
Load Diff
+946
-2018
File diff suppressed because it is too large
Load Diff
+526
-366
File diff suppressed because it is too large
Load Diff
+527
-367
File diff suppressed because it is too large
Load Diff
+527
-367
File diff suppressed because it is too large
Load Diff
+454
-391
File diff suppressed because it is too large
Load Diff
+316
-220
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+527
-367
File diff suppressed because it is too large
Load Diff
+528
-369
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {getLocales} from 'expo-localization'
|
||||
|
||||
import {dedupArray} from 'lib/functions'
|
||||
|
||||
export const isIOS = Platform.OS === 'ios'
|
||||
@@ -18,3 +19,8 @@ export const deviceLocales = dedupArray(
|
||||
.map?.(locale => locale.languageCode)
|
||||
.filter(code => typeof code === 'string'),
|
||||
) as string[]
|
||||
|
||||
export const prefersReducedMotion =
|
||||
isWeb &&
|
||||
// @ts-ignore we know window exists -prf
|
||||
!global.window.matchMedia('(prefers-reduced-motion: no-preference)')?.matches
|
||||
|
||||
+17
-10
@@ -1,20 +1,20 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {getAgent, isSessionDeactivated, useSessionApi} from '#/state/session'
|
||||
import {logger} from '#/logger'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Text, P} from '#/components/Typography'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
|
||||
const COL_WIDTH = 400
|
||||
|
||||
@@ -25,6 +25,7 @@ export function Deactivated() {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
const {logout} = useSessionApi()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const [isProcessing, setProcessing] = React.useState(false)
|
||||
const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
|
||||
@@ -56,7 +57,13 @@ export function Deactivated() {
|
||||
} finally {
|
||||
setProcessing(false)
|
||||
}
|
||||
}, [setProcessing, setEstimatedTime, setPlaceInQueue, onboardingDispatch])
|
||||
}, [
|
||||
setProcessing,
|
||||
setEstimatedTime,
|
||||
setPlaceInQueue,
|
||||
onboardingDispatch,
|
||||
getAgent,
|
||||
])
|
||||
|
||||
React.useEffect(() => {
|
||||
checkStatus()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {
|
||||
DescriptionText,
|
||||
@@ -38,6 +38,7 @@ export function StepFinished() {
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const [saving, setSaving] = React.useState(false)
|
||||
const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const finishOnboarding = React.useCallback(async () => {
|
||||
setSaving(true)
|
||||
@@ -57,6 +58,7 @@ export function StepFinished() {
|
||||
try {
|
||||
await Promise.all([
|
||||
bulkWriteFollows(
|
||||
getAgent,
|
||||
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
|
||||
),
|
||||
// these must be serial
|
||||
@@ -80,7 +82,7 @@ export function StepFinished() {
|
||||
track('OnboardingV2:StepFinished:End')
|
||||
track('OnboardingV2:Complete')
|
||||
logEvent('onboarding:finished:nextPressed', {})
|
||||
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track])
|
||||
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track, getAgent])
|
||||
|
||||
React.useEffect(() => {
|
||||
track('OnboardingV2:StepFinished:Start')
|
||||
|
||||
@@ -8,7 +8,7 @@ import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {capitalize} from '#/lib/strings/capitalize'
|
||||
import {logger} from '#/logger'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {
|
||||
DescriptionText,
|
||||
@@ -39,6 +39,7 @@ export function StepInterests() {
|
||||
state.interestsStepResults.selectedInterests.map(i => i),
|
||||
)
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const {getAgent} = useAgent()
|
||||
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
|
||||
queryKey: ['interests'],
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {AppBskyGraphFollow, AppBskyGraphGetFollows} from '@atproto/api'
|
||||
import {
|
||||
AppBskyGraphFollow,
|
||||
AppBskyGraphGetFollows,
|
||||
BskyAgent,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {until} from '#/lib/async/until'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {PRIMARY_FEEDS} from './StepAlgoFeeds'
|
||||
|
||||
function shuffle(array: any) {
|
||||
@@ -63,7 +66,10 @@ export function aggregateInterestItems(
|
||||
return Array.from(new Set(results)).slice(0, 20)
|
||||
}
|
||||
|
||||
export async function bulkWriteFollows(dids: string[]) {
|
||||
export async function bulkWriteFollows(
|
||||
getAgent: () => BskyAgent,
|
||||
dids: string[],
|
||||
) {
|
||||
const session = getAgent().session
|
||||
|
||||
if (!session) {
|
||||
@@ -87,10 +93,15 @@ export async function bulkWriteFollows(dids: string[]) {
|
||||
repo: session.did,
|
||||
writes: followWrites,
|
||||
})
|
||||
await whenFollowsIndexed(session.did, res => !!res.data.follows.length)
|
||||
await whenFollowsIndexed(
|
||||
getAgent,
|
||||
session.did,
|
||||
res => !!res.data.follows.length,
|
||||
)
|
||||
}
|
||||
|
||||
async function whenFollowsIndexed(
|
||||
getAgent: () => BskyAgent,
|
||||
actor: string,
|
||||
fn: (res: AppBskyGraphGetFollows.Response) => boolean,
|
||||
) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useHaptics} from 'lib/haptics'
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -164,10 +165,12 @@ let ProfileHeaderLabeler = ({
|
||||
moderation={moderation}
|
||||
hideBackButton={hideBackButton}
|
||||
isPlaceholderProfile={isPlaceholderProfile}>
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents="box-none">
|
||||
<View
|
||||
style={[a.px_lg, a.pt_md, a.pb_sm]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_lg]}
|
||||
pointerEvents="box-none">
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
{isMe ? (
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
|
||||
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isIOS, isWeb} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
@@ -152,10 +152,12 @@ let ProfileHeaderStandard = ({
|
||||
moderation={moderation}
|
||||
hideBackButton={hideBackButton}
|
||||
isPlaceholderProfile={isPlaceholderProfile}>
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents="box-none">
|
||||
<View
|
||||
style={[a.px_lg, a.pt_md, a.pb_sm]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
|
||||
pointerEvents="box-none">
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
{isMe ? (
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {useSession} from '#/state/session'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {UserBanner} from 'view/com/util/UserBanner'
|
||||
@@ -61,8 +62,8 @@ let ProfileHeaderShell = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={t.atoms.bg} pointerEvents="box-none">
|
||||
<View pointerEvents="none">
|
||||
<View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<View pointerEvents={isIOS ? 'auto' : 'none'}>
|
||||
{isPlaceholderProfile ? (
|
||||
<LoadingPlaceholder
|
||||
width="100%"
|
||||
@@ -80,7 +81,9 @@ let ProfileHeaderShell = ({
|
||||
|
||||
{children}
|
||||
|
||||
<View style={[a.px_lg, a.pb_sm]} pointerEvents="box-none">
|
||||
<View
|
||||
style={[a.px_lg, a.pb_sm]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<ProfileHeaderAlerts moderation={moderation} />
|
||||
{isMe && (
|
||||
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
|
||||
|
||||
@@ -9,7 +9,7 @@ import {FEEDBACK_FORM_URL} from '#/lib/constants'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {createFullHandle} from '#/lib/strings/handles'
|
||||
import {useServiceQuery} from '#/state/queries/service'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
|
||||
import {
|
||||
initialState,
|
||||
@@ -35,6 +35,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||
const submit = useSubmitSignup({state, dispatch})
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const {
|
||||
data: serviceInfo,
|
||||
@@ -113,6 +114,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
state.serviceDescription?.phoneVerificationRequired,
|
||||
state.userDomain,
|
||||
submit,
|
||||
getAgent,
|
||||
])
|
||||
|
||||
const onBackPress = React.useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {z} from 'zod'
|
||||
|
||||
import {deviceLocales} from '#/platform/detection'
|
||||
import {deviceLocales, prefersReducedMotion} from '#/platform/detection'
|
||||
|
||||
const externalEmbedOptions = ['show', 'hide'] as const
|
||||
|
||||
@@ -15,6 +15,7 @@ const accountSchema = z.object({
|
||||
refreshJwt: z.string().optional(), // optional because it can expire
|
||||
accessJwt: z.string().optional(), // optional because it can expire
|
||||
deactivated: z.boolean().optional(),
|
||||
pdsUrl: z.string().optional(),
|
||||
})
|
||||
export type PersistedAccount = z.infer<typeof accountSchema>
|
||||
|
||||
@@ -98,5 +99,5 @@ export const defaults: Schema = {
|
||||
lastSelectedHomeFeed: undefined,
|
||||
pdsAddressHistory: [],
|
||||
disableHaptics: false,
|
||||
disableAutoplay: false,
|
||||
disableAutoplay: prefersReducedMotion,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {isJustAMute} from '#/lib/moderation'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
|
||||
|
||||
const DEFAULT_MOD_OPTS = {
|
||||
@@ -18,6 +18,7 @@ export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
|
||||
|
||||
export function useActorAutocompleteQuery(prefix: string) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
prefix = prefix.toLowerCase()
|
||||
|
||||
@@ -46,6 +47,7 @@ export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn>
|
||||
export function useActorAutocompleteFn() {
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return React.useCallback(
|
||||
async ({query, limit = 8}: {query: string; limit?: number}) => {
|
||||
@@ -74,7 +76,7 @@ export function useActorAutocompleteFn() {
|
||||
moderationOpts || DEFAULT_MOD_OPTS,
|
||||
)
|
||||
},
|
||||
[queryClient, moderationOpts],
|
||||
[queryClient, moderationOpts, getAgent],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const RQKEY_ROOT = 'actor-search'
|
||||
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
|
||||
@@ -14,6 +14,7 @@ export function useActorSearch({
|
||||
query: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyActorDefs.ProfileView[]>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(query || ''),
|
||||
|
||||
@@ -2,12 +2,13 @@ import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '../session'
|
||||
import {useAgent} from '../session'
|
||||
|
||||
const RQKEY_ROOT = 'app-passwords'
|
||||
export const RQKEY = () => [RQKEY_ROOT]
|
||||
|
||||
export function useAppPasswordsQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: RQKEY(),
|
||||
@@ -20,6 +21,7 @@ export function useAppPasswordsQuery() {
|
||||
|
||||
export function useAppPasswordCreateMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<
|
||||
ComAtprotoServerCreateAppPassword.OutputSchema,
|
||||
Error,
|
||||
@@ -42,6 +44,7 @@ export function useAppPasswordCreateMutation() {
|
||||
|
||||
export function useAppPasswordDeleteMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {name: string}>({
|
||||
mutationFn: async ({name}) => {
|
||||
await getAgent().com.atproto.server.revokeAppPassword({
|
||||
|
||||
@@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {router} from '#/routes'
|
||||
|
||||
export type FeedSourceFeedInfo = {
|
||||
@@ -140,6 +140,7 @@ export function getAvatarTypeFromUri(uri: string) {
|
||||
|
||||
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
||||
const type = getFeedTypeFromUri(uri)
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useQuery({
|
||||
staleTime: STALE.INFINITY,
|
||||
@@ -166,6 +167,7 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
||||
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
|
||||
|
||||
export function useGetPopularFeedsQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
|
||||
Error,
|
||||
@@ -187,6 +189,7 @@ export function useGetPopularFeedsQuery() {
|
||||
}
|
||||
|
||||
export function useSearchPopularFeedsMutation() {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation({
|
||||
mutationFn: async (query: string) => {
|
||||
const res = await getAgent().app.bsky.unspecced.getPopularFeedGenerators({
|
||||
@@ -238,6 +241,7 @@ const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
||||
|
||||
export function usePinnedFeedsInfos() {
|
||||
const {hasSession} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
|
||||
const pinnedUris = preferences?.feeds?.pinned ?? []
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const handleQueryKeyRoot = 'handle'
|
||||
const fetchHandleQueryKey = (handleOrDid: string) => [
|
||||
@@ -14,6 +14,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
|
||||
|
||||
export function useFetchHandle() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return React.useCallback(
|
||||
async (handleOrDid: string) => {
|
||||
@@ -27,12 +28,13 @@ export function useFetchHandle() {
|
||||
}
|
||||
return handleOrDid
|
||||
},
|
||||
[queryClient],
|
||||
[queryClient, getAgent],
|
||||
)
|
||||
}
|
||||
|
||||
export function useUpdateHandleMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({handle}: {handle: string}) => {
|
||||
@@ -48,6 +50,7 @@ export function useUpdateHandleMutation() {
|
||||
|
||||
export function useFetchDid() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return React.useCallback(
|
||||
async (handleOrDid: string) => {
|
||||
@@ -64,6 +67,6 @@ export function useFetchDid() {
|
||||
},
|
||||
})
|
||||
},
|
||||
[queryClient],
|
||||
[queryClient, getAgent],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
|
||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||
|
||||
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
service: PUBLIC_BSKY_SERVICE,
|
||||
})
|
||||
|
||||
export const STALE = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
|
||||
return invite.available - invite.uses.length > 0 && !invite.disabled
|
||||
@@ -16,6 +16,7 @@ export type InviteCodesQueryResponse = Exclude<
|
||||
undefined
|
||||
>
|
||||
export function useInviteCodesQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: [inviteCodesQueryKeyRoot],
|
||||
|
||||
@@ -5,7 +5,7 @@ import {z} from 'zod'
|
||||
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const labelerInfoQueryKeyRoot = 'labeler-info'
|
||||
export const labelerInfoQueryKey = (did: string) => [
|
||||
@@ -31,6 +31,7 @@ export function useLabelerInfoQuery({
|
||||
did?: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
enabled: !!did && enabled !== false,
|
||||
queryKey: labelerInfoQueryKey(did as string),
|
||||
@@ -45,6 +46,7 @@ export function useLabelerInfoQuery({
|
||||
}
|
||||
|
||||
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
enabled: !!dids.length,
|
||||
queryKey: labelersInfoQueryKey(dids),
|
||||
@@ -56,6 +58,7 @@ export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
||||
}
|
||||
|
||||
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
enabled: !!dids.length,
|
||||
queryKey: labelersDetailedInfoQueryKey(dids),
|
||||
@@ -73,6 +76,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||
|
||||
export function useLabelerSubscriptionMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export function useLikeMutation() {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation({
|
||||
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
|
||||
const res = await getAgent().like(uri, cid)
|
||||
@@ -12,6 +13,7 @@ export function useLikeMutation() {
|
||||
}
|
||||
|
||||
export function useUnlikeMutation() {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation({
|
||||
mutationFn: async ({uri}: {uri: string}) => {
|
||||
await getAgent().deleteLike(uri)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'list-members'
|
||||
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
||||
|
||||
export function useListMembersQuery(uri: string) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetList.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
|
||||
const SANITY_PAGE_LIMIT = 1000
|
||||
@@ -40,6 +40,7 @@ export interface ListMembersip {
|
||||
*/
|
||||
export function useDangerousListMembershipsQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<ListMembersip[]>({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: RQKEY(),
|
||||
@@ -91,6 +92,7 @@ export function getMembership(
|
||||
|
||||
export function useListMembershipAddMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<
|
||||
{uri: string; cid: string},
|
||||
@@ -149,6 +151,7 @@ export function useListMembershipAddMutation() {
|
||||
|
||||
export function useListMembershipRemoveMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<
|
||||
void,
|
||||
|
||||
+49
-21
@@ -4,6 +4,7 @@ import {
|
||||
AppBskyGraphGetList,
|
||||
AppBskyGraphList,
|
||||
AtUri,
|
||||
BskyAgent,
|
||||
Facet,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
@@ -12,7 +13,7 @@ import chunk from 'lodash.chunk'
|
||||
import {uploadBlob} from '#/lib/api'
|
||||
import {until} from '#/lib/async/until'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent, useSession} from '../session'
|
||||
import {useAgent, useSession} from '../session'
|
||||
import {invalidate as invalidateMyLists} from './my-lists'
|
||||
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
|
||||
|
||||
@@ -20,6 +21,7 @@ const RQKEY_ROOT = 'list'
|
||||
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
||||
|
||||
export function useListQuery(uri?: string) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyGraphDefs.ListView, Error>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(uri || ''),
|
||||
@@ -47,6 +49,7 @@ export interface ListCreateMutateParams {
|
||||
export function useListCreateMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
|
||||
{
|
||||
async mutationFn({
|
||||
@@ -85,9 +88,13 @@ export function useListCreateMutation() {
|
||||
)
|
||||
|
||||
// wait for the appview to update
|
||||
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
|
||||
return typeof v?.data?.list.uri === 'string'
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
res.uri,
|
||||
(v: AppBskyGraphGetList.Response) => {
|
||||
return typeof v?.data?.list.uri === 'string'
|
||||
},
|
||||
)
|
||||
return res
|
||||
},
|
||||
onSuccess() {
|
||||
@@ -109,6 +116,7 @@ export interface ListMetadataMutateParams {
|
||||
}
|
||||
export function useListMetadataMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<
|
||||
{uri: string; cid: string},
|
||||
@@ -150,12 +158,16 @@ export function useListMetadataMutation() {
|
||||
).data
|
||||
|
||||
// wait for the appview to update
|
||||
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
|
||||
const list = v.data.list
|
||||
return (
|
||||
list.name === record.name && list.description === record.description
|
||||
)
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
res.uri,
|
||||
(v: AppBskyGraphGetList.Response) => {
|
||||
const list = v.data.list
|
||||
return (
|
||||
list.name === record.name && list.description === record.description
|
||||
)
|
||||
},
|
||||
)
|
||||
return res
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
@@ -172,6 +184,7 @@ export function useListMetadataMutation() {
|
||||
|
||||
export function useListDeleteMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<void, Error, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
@@ -220,9 +233,13 @@ export function useListDeleteMutation() {
|
||||
}
|
||||
|
||||
// wait for the appview to update
|
||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
||||
return !v?.success
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
uri,
|
||||
(v: AppBskyGraphGetList.Response) => {
|
||||
return !v?.success
|
||||
},
|
||||
)
|
||||
},
|
||||
onSuccess() {
|
||||
invalidateMyLists(queryClient)
|
||||
@@ -236,6 +253,7 @@ export function useListDeleteMutation() {
|
||||
|
||||
export function useListMuteMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {uri: string; mute: boolean}>({
|
||||
mutationFn: async ({uri, mute}) => {
|
||||
if (mute) {
|
||||
@@ -244,9 +262,13 @@ export function useListMuteMutation() {
|
||||
await getAgent().unmuteModList(uri)
|
||||
}
|
||||
|
||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
||||
return Boolean(v?.data.list.viewer?.muted) === mute
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
uri,
|
||||
(v: AppBskyGraphGetList.Response) => {
|
||||
return Boolean(v?.data.list.viewer?.muted) === mute
|
||||
},
|
||||
)
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -258,6 +280,7 @@ export function useListMuteMutation() {
|
||||
|
||||
export function useListBlockMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {uri: string; block: boolean}>({
|
||||
mutationFn: async ({uri, block}) => {
|
||||
if (block) {
|
||||
@@ -266,11 +289,15 @@ export function useListBlockMutation() {
|
||||
await getAgent().unblockModList(uri)
|
||||
}
|
||||
|
||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
||||
return block
|
||||
? typeof v?.data.list.viewer?.blocked === 'string'
|
||||
: !v?.data.list.viewer?.blocked
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
uri,
|
||||
(v: AppBskyGraphGetList.Response) => {
|
||||
return block
|
||||
? typeof v?.data.list.viewer?.blocked === 'string'
|
||||
: !v?.data.list.viewer?.blocked
|
||||
},
|
||||
)
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -281,6 +308,7 @@ export function useListBlockMutation() {
|
||||
}
|
||||
|
||||
async function whenAppViewReady(
|
||||
getAgent: () => BskyAgent,
|
||||
uri: string,
|
||||
fn: (res: AppBskyGraphGetList.Response) => boolean,
|
||||
) {
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const RQKEY_ROOT = 'my-blocked-accounts'
|
||||
export const RQKEY = () => [RQKEY_ROOT]
|
||||
type RQPageParam = string | undefined
|
||||
|
||||
export function useMyBlockedAccountsQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetBlocks.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {accumulate} from '#/lib/async/accumulate'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
export type MyListsFilter =
|
||||
| 'all'
|
||||
@@ -16,6 +16,7 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
|
||||
|
||||
export function useMyListsQuery(filter: MyListsFilter) {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyGraphDefs.ListView[]>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(filter),
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const RQKEY_ROOT = 'my-muted-accounts'
|
||||
export const RQKEY = () => [RQKEY_ROOT]
|
||||
type RQPageParam = string | undefined
|
||||
|
||||
export function useMyMutedAccountsQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetMutes.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useMutedThreads} from '#/state/muted-threads'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {STALE} from '..'
|
||||
import {useModerationOpts} from '../preferences'
|
||||
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
|
||||
@@ -46,6 +47,7 @@ export function RQKEY() {
|
||||
}
|
||||
|
||||
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const threadMutes = useMutedThreads()
|
||||
@@ -71,6 +73,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
if (!page) {
|
||||
page = (
|
||||
await fetchPage({
|
||||
getAgent,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
queryClient,
|
||||
|
||||
@@ -12,7 +12,7 @@ import BroadcastChannel from '#/lib/broadcast'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useMutedThreads} from '#/state/muted-threads'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useModerationOpts} from '../preferences'
|
||||
import {truncateAndInvalidate} from '../util'
|
||||
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
||||
@@ -46,6 +46,7 @@ const apiContext = React.createContext<ApiContext>({
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const {hasSession} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const threadMutes = useMutedThreads()
|
||||
@@ -144,6 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
// count
|
||||
const {page, indexedAt: lastIndexed} = await fetchPage({
|
||||
getAgent,
|
||||
cursor: undefined,
|
||||
limit: 40,
|
||||
queryClient,
|
||||
@@ -196,7 +198,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
},
|
||||
}
|
||||
}, [setNumUnread, queryClient, moderationOpts, threadMutes])
|
||||
}, [setNumUnread, queryClient, moderationOpts, threadMutes, getAgent])
|
||||
checkUnreadRef.current = api.checkUnread
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
AppBskyNotificationListNotifications,
|
||||
ModerationOpts,
|
||||
moderateNotification,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedLike,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedRepost,
|
||||
AppBskyFeedLike,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyNotificationListNotifications,
|
||||
BskyAgent,
|
||||
moderateNotification,
|
||||
ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import chunk from 'lodash.chunk'
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
import {getAgent} from '../../session'
|
||||
import chunk from 'lodash.chunk'
|
||||
|
||||
import {precacheProfile} from '../profile'
|
||||
import {NotificationType, FeedNotification, FeedPage} from './types'
|
||||
import {FeedNotification, FeedPage, NotificationType} from './types'
|
||||
|
||||
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||
const MS_1HR = 1e3 * 60 * 60
|
||||
@@ -22,6 +23,7 @@ const MS_2DAY = MS_1HR * 48
|
||||
// =
|
||||
|
||||
export async function fetchPage({
|
||||
getAgent,
|
||||
cursor,
|
||||
limit,
|
||||
queryClient,
|
||||
@@ -29,6 +31,7 @@ export async function fetchPage({
|
||||
threadMutes,
|
||||
fetchAdditionalData,
|
||||
}: {
|
||||
getAgent: () => BskyAgent
|
||||
cursor: string | undefined
|
||||
limit: number
|
||||
queryClient: QueryClient
|
||||
@@ -53,7 +56,7 @@ export async function fetchPage({
|
||||
// we fetch subjects of notifications (usually posts) now instead of lazily
|
||||
// in the UI to avoid relayouts
|
||||
if (fetchAdditionalData) {
|
||||
const subjects = await fetchSubjects(notifsGrouped)
|
||||
const subjects = await fetchSubjects(getAgent, notifsGrouped)
|
||||
for (const notif of notifsGrouped) {
|
||||
if (notif.subjectUri) {
|
||||
notif.subject = subjects.get(notif.subjectUri)
|
||||
@@ -137,6 +140,7 @@ export function groupNotifications(
|
||||
}
|
||||
|
||||
async function fetchSubjects(
|
||||
getAgent: () => BskyAgent,
|
||||
groupedNotifs: FeedNotification[],
|
||||
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
||||
const uris = new Set<string>()
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AtUri,
|
||||
BskyAgent,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
@@ -19,7 +20,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {AuthorFeedAPI} from 'lib/api/feed/author'
|
||||
import {CustomFeedAPI} from 'lib/api/feed/custom'
|
||||
import {FollowingFeedAPI} from 'lib/api/feed/following'
|
||||
@@ -104,6 +105,7 @@ export function usePostFeedQuery(
|
||||
const queryClient = useQueryClient()
|
||||
const feedTuners = useFeedTuners(feedDesc)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {getAgent} = useAgent()
|
||||
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
|
||||
const lastRun = useRef<{
|
||||
data: InfiniteData<FeedPageUnselected>
|
||||
@@ -135,11 +137,15 @@ export function usePostFeedQuery(
|
||||
queryKey: RQKEY(feedDesc, params),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor})
|
||||
|
||||
const {api, cursor} = pageParam
|
||||
? pageParam
|
||||
: {
|
||||
api: createApi(feedDesc, params || {}, feedTuners),
|
||||
api: createApi({
|
||||
feedDesc,
|
||||
feedParams: params || {},
|
||||
feedTuners,
|
||||
getAgent,
|
||||
}),
|
||||
cursor: undefined,
|
||||
}
|
||||
|
||||
@@ -365,34 +371,47 @@ export async function pollLatest(page: FeedPage | undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
function createApi(
|
||||
feedDesc: FeedDescriptor,
|
||||
params: FeedParams,
|
||||
feedTuners: FeedTunerFn[],
|
||||
) {
|
||||
function createApi({
|
||||
feedDesc,
|
||||
feedParams,
|
||||
feedTuners,
|
||||
getAgent,
|
||||
}: {
|
||||
feedDesc: FeedDescriptor
|
||||
feedParams: FeedParams
|
||||
feedTuners: FeedTunerFn[]
|
||||
getAgent: () => BskyAgent
|
||||
}) {
|
||||
if (feedDesc === 'home') {
|
||||
if (params.mergeFeedEnabled) {
|
||||
return new MergeFeedAPI(params, feedTuners)
|
||||
if (feedParams.mergeFeedEnabled) {
|
||||
return new MergeFeedAPI({
|
||||
getAgent,
|
||||
feedParams,
|
||||
feedTuners,
|
||||
})
|
||||
} else {
|
||||
return new HomeFeedAPI()
|
||||
return new HomeFeedAPI({getAgent})
|
||||
}
|
||||
} else if (feedDesc === 'following') {
|
||||
return new FollowingFeedAPI()
|
||||
return new FollowingFeedAPI({getAgent})
|
||||
} else if (feedDesc.startsWith('author')) {
|
||||
const [_, actor, filter] = feedDesc.split('|')
|
||||
return new AuthorFeedAPI({actor, filter})
|
||||
return new AuthorFeedAPI({getAgent, feedParams: {actor, filter}})
|
||||
} else if (feedDesc.startsWith('likes')) {
|
||||
const [_, actor] = feedDesc.split('|')
|
||||
return new LikesFeedAPI({actor})
|
||||
return new LikesFeedAPI({getAgent, feedParams: {actor}})
|
||||
} else if (feedDesc.startsWith('feedgen')) {
|
||||
const [_, feed] = feedDesc.split('|')
|
||||
return new CustomFeedAPI({feed})
|
||||
return new CustomFeedAPI({
|
||||
getAgent,
|
||||
feedParams: {feed},
|
||||
})
|
||||
} else if (feedDesc.startsWith('list')) {
|
||||
const [_, list] = feedDesc.split('|')
|
||||
return new ListFeedAPI({list})
|
||||
return new ListFeedAPI({getAgent, feedParams: {list}})
|
||||
} else {
|
||||
// shouldnt happen
|
||||
return new FollowingFeedAPI()
|
||||
return new FollowingFeedAPI({getAgent})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'liked-by'
|
||||
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
||||
|
||||
export function useLikedByQuery(resolvedUri: string | undefined) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetLikes.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'post-reposted-by'
|
||||
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
||||
|
||||
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetRepostedBy.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
|
||||
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
|
||||
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
|
||||
@@ -66,6 +66,7 @@ export type ThreadNode =
|
||||
|
||||
export function usePostThreadQuery(uri: string | undefined) {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<ThreadNode, Error>({
|
||||
gcTime: 0,
|
||||
queryKey: RQKEY(uri || ''),
|
||||
|
||||
@@ -7,13 +7,14 @@ import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
|
||||
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||
import {updatePostShadow} from '#/state/cache/post-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {findProfileQueryData} from './profile'
|
||||
|
||||
const RQKEY_ROOT = 'post'
|
||||
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
|
||||
|
||||
export function usePostQuery(uri: string | undefined) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyFeedDefs.PostView>({
|
||||
queryKey: RQKEY(uri || ''),
|
||||
async queryFn() {
|
||||
@@ -30,6 +31,7 @@ export function usePostQuery(uri: string | undefined) {
|
||||
|
||||
export function useGetPost() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useCallback(
|
||||
async ({uri}: {uri: string}) => {
|
||||
return queryClient.fetchQuery({
|
||||
@@ -56,7 +58,7 @@ export function useGetPost() {
|
||||
},
|
||||
})
|
||||
},
|
||||
[queryClient],
|
||||
[queryClient, getAgent],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,6 +127,7 @@ function usePostLikeMutation(
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
const postAuthor = post.author
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the like
|
||||
Error,
|
||||
@@ -162,6 +165,7 @@ function usePostLikeMutation(
|
||||
function usePostUnlikeMutation(
|
||||
logContext: LogEvents['post:unlike']['logContext'],
|
||||
) {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {postUri: string; likeUri: string}>({
|
||||
mutationFn: ({likeUri}) => {
|
||||
logEvent('post:unlike', {logContext})
|
||||
@@ -234,6 +238,7 @@ export function usePostRepostMutationQueue(
|
||||
function usePostRepostMutation(
|
||||
logContext: LogEvents['post:repost']['logContext'],
|
||||
) {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the repost
|
||||
Error,
|
||||
@@ -252,6 +257,7 @@ function usePostRepostMutation(
|
||||
function usePostUnrepostMutation(
|
||||
logContext: LogEvents['post:unrepost']['logContext'],
|
||||
) {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {postUri: string; repostUri: string}>({
|
||||
mutationFn: ({repostUri}) => {
|
||||
logEvent('post:unrepost', {logContext})
|
||||
@@ -265,6 +271,7 @@ function usePostUnrepostMutation(
|
||||
|
||||
export function usePostDeleteMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
await getAgent().deletePost(uri)
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ThreadViewPreferences,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {saveLabelers} from '#/state/session/agent-config'
|
||||
|
||||
export * from '#/state/queries/preferences/const'
|
||||
@@ -33,6 +33,7 @@ const preferencesQueryKeyRoot = 'getPreferences'
|
||||
export const preferencesQueryKey = [preferencesQueryKeyRoot]
|
||||
|
||||
export function usePreferencesQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
structuralSharing: true,
|
||||
@@ -118,6 +119,7 @@ export function useModerationOpts() {
|
||||
|
||||
export function useClearPreferencesMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -131,6 +133,7 @@ export function useClearPreferencesMutation() {
|
||||
}
|
||||
|
||||
export function usePreferencesSetContentLabelMutation() {
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
@@ -150,6 +153,7 @@ export function usePreferencesSetContentLabelMutation() {
|
||||
|
||||
export function useSetContentLabelMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
@@ -172,6 +176,7 @@ export function useSetContentLabelMutation() {
|
||||
|
||||
export function usePreferencesSetAdultContentMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {enabled: boolean}>({
|
||||
mutationFn: async ({enabled}) => {
|
||||
@@ -186,6 +191,7 @@ export function usePreferencesSetAdultContentMutation() {
|
||||
|
||||
export function usePreferencesSetBirthDateMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {birthDate: Date}>({
|
||||
mutationFn: async ({birthDate}: {birthDate: Date}) => {
|
||||
@@ -200,6 +206,7 @@ export function usePreferencesSetBirthDateMutation() {
|
||||
|
||||
export function useSetFeedViewPreferencesMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
|
||||
mutationFn: async prefs => {
|
||||
@@ -214,6 +221,7 @@ export function useSetFeedViewPreferencesMutation() {
|
||||
|
||||
export function useSetThreadViewPreferencesMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
|
||||
mutationFn: async prefs => {
|
||||
@@ -228,6 +236,7 @@ export function useSetThreadViewPreferencesMutation() {
|
||||
|
||||
export function useSetSaveFeedsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
@@ -246,6 +255,7 @@ export function useSetSaveFeedsMutation() {
|
||||
|
||||
export function useSaveFeedMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
@@ -261,6 +271,7 @@ export function useSaveFeedMutation() {
|
||||
|
||||
export function useRemoveFeedMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
@@ -276,6 +287,7 @@ export function useRemoveFeedMutation() {
|
||||
|
||||
export function usePinFeedMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
@@ -291,6 +303,7 @@ export function usePinFeedMutation() {
|
||||
|
||||
export function useUnpinFeedMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
@@ -306,6 +319,7 @@ export function useUnpinFeedMutation() {
|
||||
|
||||
export function useUpsertMutedWordsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
|
||||
@@ -320,6 +334,7 @@ export function useUpsertMutedWordsMutation() {
|
||||
|
||||
export function useUpdateMutedWordMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
||||
@@ -334,6 +349,7 @@ export function useUpdateMutedWordMutation() {
|
||||
|
||||
export function useRemoveMutedWordMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
|
||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -15,6 +15,7 @@ export function useProfileFeedgensQuery(
|
||||
opts?: {enabled?: boolean},
|
||||
) {
|
||||
const enabled = opts?.enabled !== false
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetActorFeeds.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -15,6 +15,7 @@ const RQKEY_ROOT = 'profile-followers'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useProfileFollowersQuery(did: string | undefined) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetFollowers.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -17,6 +17,7 @@ const RQKEY_ROOT = 'profile-follows'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useProfileFollowsQuery(did: string | undefined) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetFollows.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {AppBskyGraphGetLists} from '@atproto/api'
|
||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -11,6 +11,7 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
||||
const enabled = opts?.enabled !== false
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetLists.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AtUri,
|
||||
BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
QueryClient,
|
||||
@@ -25,7 +26,7 @@ import {Shadow} from '#/state/cache/types'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||
import {updateProfileShadow} from '../cache/profile-shadow'
|
||||
import {getAgent, useSession} from '../session'
|
||||
import {useAgent, useSession} from '../session'
|
||||
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
||||
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
||||
import {ThreadNode} from './post-thread'
|
||||
@@ -53,6 +54,7 @@ export function useProfileQuery({
|
||||
staleTime?: number
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
|
||||
// WARNING
|
||||
// this staleTime is load-bearing
|
||||
@@ -77,6 +79,7 @@ export function useProfileQuery({
|
||||
}
|
||||
|
||||
export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
queryKey: profilesQueryKey(handles),
|
||||
@@ -88,6 +91,7 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||
}
|
||||
|
||||
export function usePrefetchProfileQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const prefetchProfileQuery = useCallback(
|
||||
async (did: string) => {
|
||||
@@ -99,7 +103,7 @@ export function usePrefetchProfileQuery() {
|
||||
},
|
||||
})
|
||||
},
|
||||
[queryClient],
|
||||
[queryClient, getAgent],
|
||||
)
|
||||
return prefetchProfileQuery
|
||||
}
|
||||
@@ -115,6 +119,7 @@ interface ProfileUpdateParams {
|
||||
}
|
||||
export function useProfileUpdateMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, ProfileUpdateParams>({
|
||||
mutationFn: async ({
|
||||
profile,
|
||||
@@ -154,6 +159,7 @@ export function useProfileUpdateMutation() {
|
||||
return existing
|
||||
})
|
||||
await whenAppViewReady(
|
||||
getAgent,
|
||||
profile.did,
|
||||
checkCommitted ||
|
||||
(res => {
|
||||
@@ -255,6 +261,7 @@ function useProfileFollowMutation(
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
|
||||
) {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
@@ -281,6 +288,7 @@ function useProfileFollowMutation(
|
||||
function useProfileUnfollowMutation(
|
||||
logContext: LogEvents['profile:unfollow']['logContext'],
|
||||
) {
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {did: string; followUri: string}>({
|
||||
mutationFn: async ({followUri}) => {
|
||||
logEvent('profile:unfollow', {logContext})
|
||||
@@ -341,6 +349,7 @@ export function useProfileMuteMutationQueue(
|
||||
|
||||
function useProfileMuteMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
await getAgent().mute(did)
|
||||
@@ -353,6 +362,7 @@ function useProfileMuteMutation() {
|
||||
|
||||
function useProfileUnmuteMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
return useMutation<void, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
await getAgent().unmute(did)
|
||||
@@ -419,6 +429,7 @@ export function useProfileBlockMutationQueue(
|
||||
|
||||
function useProfileBlockMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
@@ -439,6 +450,7 @@ function useProfileBlockMutation() {
|
||||
|
||||
function useProfileUnblockMutation() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<void, Error, {did: string; blockUri: string}>({
|
||||
mutationFn: async ({blockUri}) => {
|
||||
@@ -516,6 +528,7 @@ export function precacheThreadPostProfiles(
|
||||
}
|
||||
|
||||
async function whenAppViewReady(
|
||||
getAgent: () => BskyAgent,
|
||||
actor: string,
|
||||
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
||||
) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {AppBskyActorDefs, AtUri} from '@atproto/api'
|
||||
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
|
||||
|
||||
const RQKEY_ROOT = 'resolved-did'
|
||||
@@ -24,6 +24,7 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
|
||||
|
||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
return useQuery<string, Error>({
|
||||
staleTime: STALE.HOURS.ONE,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
|
||||
|
||||
const searchPostsQueryKeyRoot = 'search-posts'
|
||||
@@ -25,6 +25,7 @@ export function useSearchPostsQuery({
|
||||
sort?: 'top' | 'latest'
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedSearchPosts.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -2,12 +2,13 @@ import {AppBskyFeedGetSuggestedFeeds} from '@atproto/api'
|
||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const suggestedFeedsQueryKeyRoot = 'suggestedFeeds'
|
||||
export const suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]
|
||||
|
||||
export function useSuggestedFeedsQuery() {
|
||||
const {getAgent} = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetSuggestedFeeds.OutputSchema,
|
||||
Error,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyActorGetSuggestions,
|
||||
@@ -11,12 +10,11 @@ import {
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
|
||||
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
|
||||
@@ -29,6 +27,7 @@ const suggestedFollowsByActorQueryKey = (did: string) => [
|
||||
|
||||
export function useSuggestedFollowsQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
return useInfiniteQuery<
|
||||
@@ -79,6 +78,7 @@ export function useSuggestedFollowsQuery() {
|
||||
}
|
||||
|
||||
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
||||
const {getAgent} = useAgent()
|
||||
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
||||
queryKey: suggestedFollowsByActorQueryKey(did),
|
||||
queryFn: async () => {
|
||||
@@ -90,29 +90,6 @@ export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
||||
})
|
||||
}
|
||||
|
||||
export function useGetSuggestedFollowersByActor() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return React.useCallback(
|
||||
async (actor: string) => {
|
||||
const res = await queryClient.fetchQuery({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: suggestedFollowsByActorQueryKey(actor),
|
||||
queryFn: async () => {
|
||||
const res =
|
||||
await getAgent().app.bsky.graph.getSuggestedFollowsByActor({
|
||||
actor: actor,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
return res
|
||||
},
|
||||
[queryClient],
|
||||
)
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
|
||||
+25
-11
@@ -23,16 +23,14 @@ import {readLabelers} from './agent-config'
|
||||
|
||||
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() {
|
||||
function __getAgent() {
|
||||
return __globalAgent
|
||||
}
|
||||
|
||||
export function useAgent() {
|
||||
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
||||
}
|
||||
|
||||
export type SessionAccount = persisted.PersistedAccount
|
||||
|
||||
export type SessionState = {
|
||||
@@ -117,6 +115,7 @@ const ApiContext = React.createContext<ApiContext>({
|
||||
})
|
||||
|
||||
function createPersistSessionHandler(
|
||||
agent: BskyAgent,
|
||||
account: SessionAccount,
|
||||
persistSessionCallback: (props: {
|
||||
expired: boolean
|
||||
@@ -144,6 +143,7 @@ function createPersistSessionHandler(
|
||||
email: session?.email || account.email,
|
||||
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
||||
deactivated: isSessionDeactivated(session?.accessJwt),
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
|
||||
/*
|
||||
* Tokens are undefined if the session expires, or if creation fails for
|
||||
@@ -276,12 +276,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
refreshJwt: agent.session.refreshJwt,
|
||||
accessJwt: agent.session.accessJwt,
|
||||
deactivated,
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
}
|
||||
|
||||
await configureModeration(agent, account)
|
||||
|
||||
agent.setPersistSessionHandler(
|
||||
createPersistSessionHandler(
|
||||
agent,
|
||||
account,
|
||||
({expired, refreshedAccount}) => {
|
||||
upsertAccount(refreshedAccount, expired)
|
||||
@@ -327,12 +329,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
refreshJwt: agent.session.refreshJwt,
|
||||
accessJwt: agent.session.accessJwt,
|
||||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
}
|
||||
|
||||
await configureModeration(agent, account)
|
||||
|
||||
agent.setPersistSessionHandler(
|
||||
createPersistSessionHandler(
|
||||
agent,
|
||||
account,
|
||||
({expired, refreshedAccount}) => {
|
||||
upsertAccount(refreshedAccount, expired)
|
||||
@@ -379,16 +383,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
||||
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
||||
|
||||
const agent = new BskyAgent({
|
||||
service: account.service,
|
||||
persistSession: createPersistSessionHandler(
|
||||
const agent = new BskyAgent({service: account.service})
|
||||
|
||||
// restore the correct PDS URL if available
|
||||
if (account.pdsUrl) {
|
||||
agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl)
|
||||
}
|
||||
|
||||
agent.setPersistSessionHandler(
|
||||
createPersistSessionHandler(
|
||||
agent,
|
||||
account,
|
||||
({expired, refreshedAccount}) => {
|
||||
upsertAccount(refreshedAccount, expired)
|
||||
},
|
||||
{networkErrorCallback: clearCurrentAccount},
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = agent
|
||||
await configureModeration(agent, account)
|
||||
@@ -421,6 +433,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
logger.debug(`session: attempting to reuse previous session`)
|
||||
|
||||
agent.session = prevSession
|
||||
|
||||
__globalAgent = agent
|
||||
await fetchingGates
|
||||
upsertAccount(account)
|
||||
@@ -498,6 +511,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
refreshJwt: agent.session.refreshJwt,
|
||||
accessJwt: agent.session.accessJwt,
|
||||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
export function readLastActiveAccount() {
|
||||
const {currentAccount, accounts} = persisted.get('session')
|
||||
return accounts.find(a => a.did === currentAccount?.did)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react'
|
||||
import {SafeAreaView, Platform} from 'react-native'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Welcome} from './onboarding/Welcome'
|
||||
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
|
||||
import {RecommendedFollows} from './onboarding/RecommendedFollows'
|
||||
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
|
||||
import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
|
||||
|
||||
export function Onboarding() {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const onboardingState = useOnboardingState()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
|
||||
React.useEffect(() => {
|
||||
setMinimalShellMode(true)
|
||||
}, [setMinimalShellMode])
|
||||
|
||||
const next = () => onboardingDispatch({type: 'next'})
|
||||
const skip = () => onboardingDispatch({type: 'skip'})
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
testID="onboardingView"
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.view,
|
||||
// @ts-ignore web only -esb
|
||||
Platform.select({
|
||||
web: {
|
||||
height: '100vh',
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<ErrorBoundary>
|
||||
{onboardingState.step === 'Welcome' && (
|
||||
<Welcome skip={skip} next={next} />
|
||||
)}
|
||||
{onboardingState.step === 'RecommendedFeeds' && (
|
||||
<RecommendedFeeds next={next} />
|
||||
)}
|
||||
{onboardingState.step === 'RecommendedFollows' && (
|
||||
<RecommendedFollows next={next} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import React from 'react'
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {RecommendedFeedsItem} from './RecommendedFeedsItem'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
}
|
||||
export function RecommendedFeeds({next}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
const {isLoading, data} = useSuggestedFeedsQuery()
|
||||
|
||||
const hasFeeds = data && data.pages[0].feeds.length
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Choose your
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Feeds
|
||||
</Text>
|
||||
</Trans>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
<Trans>
|
||||
Feeds are created by users to curate content. Choose some feeds that
|
||||
you find interesting.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabletOrDesktop>
|
||||
<TitleColumnLayout
|
||||
testID="recommendedFeedsOnboarding"
|
||||
title={title}
|
||||
horizontal
|
||||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
||||
contentStyle={{paddingHorizontal: 0}}>
|
||||
{hasFeeds ? (
|
||||
<FlatList
|
||||
data={data.pages[0].feeds}
|
||||
renderItem={({item}) => <RecommendedFeedsItem item={item} />}
|
||||
keyExtractor={item => item.uri}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
) : isLoading ? (
|
||||
<View>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
) : (
|
||||
<ErrorMessage message={_(msg`Failed to load recommended feeds`)} />
|
||||
)}
|
||||
</TitleColumnLayout>
|
||||
</TabletOrDesktop>
|
||||
<Mobile>
|
||||
<View style={[mStyles.container]} testID="recommendedFeedsOnboarding">
|
||||
<ViewHeader
|
||||
title={_(msg`Recommended Feeds`)}
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
<Trans>
|
||||
Check out some recommended feeds. Tap + to add them to your list
|
||||
of pinned feeds.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
{hasFeeds ? (
|
||||
<FlatList
|
||||
data={data.pages[0].feeds}
|
||||
renderItem={({item}) => <RecommendedFeedsItem item={item} />}
|
||||
keyExtractor={item => item.uri}
|
||||
style={{flex: 1}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
) : isLoading ? (
|
||||
<View style={{flex: 1}}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
) : (
|
||||
<View style={{flex: 1}}>
|
||||
<ErrorMessage
|
||||
message={_(msg`Failed to load recommended feeds`)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label={_(msg`Continue`)}
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
</Mobile>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const tdStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title1: {
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title1Small: {
|
||||
fontSize: 24,
|
||||
},
|
||||
title2: {
|
||||
fontSize: 58,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title2Small: {
|
||||
fontSize: 36,
|
||||
},
|
||||
description: {
|
||||
maxWidth: 400,
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
||||
const mStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
})
|
||||
@@ -1,172 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {RichText} from 'view/com/util/text/RichText'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {HeartIcon} from 'lib/icons'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
usePinFeedMutation,
|
||||
useRemoveFeedMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {logger} from '#/logger'
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function RecommendedFeedsItem({
|
||||
item,
|
||||
}: {
|
||||
item: AppBskyFeedDefs.GeneratorView
|
||||
}) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {
|
||||
mutateAsync: pinFeed,
|
||||
variables: pinnedFeed,
|
||||
reset: resetPinFeed,
|
||||
} = usePinFeedMutation()
|
||||
const {
|
||||
mutateAsync: removeFeed,
|
||||
variables: removedFeed,
|
||||
reset: resetRemoveFeed,
|
||||
} = useRemoveFeedMutation()
|
||||
const {track} = useAnalytics()
|
||||
|
||||
if (!item || !preferences) return null
|
||||
|
||||
const isPinned =
|
||||
!removedFeed?.uri &&
|
||||
(pinnedFeed?.uri || preferences.feeds.saved.includes(item.uri))
|
||||
|
||||
const onToggle = async () => {
|
||||
if (isPinned) {
|
||||
try {
|
||||
await removeFeed({uri: item.uri})
|
||||
resetRemoveFeed()
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting your server`))
|
||||
logger.error('Failed to unsave feed', {message: e})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await pinFeed({uri: item.uri})
|
||||
resetPinFeed()
|
||||
track('Onboarding:CustomFeedAdded')
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting your server`))
|
||||
logger.error('Failed to pin feed', {message: e})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID={`feed-${item.displayName}`}>
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
{
|
||||
flex: isMobile ? 1 : undefined,
|
||||
flexDirection: 'row',
|
||||
gap: 18,
|
||||
maxWidth: isMobile ? undefined : 670,
|
||||
borderRightWidth: isMobile ? undefined : 1,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: isMobile ? 12 : 24,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
]}>
|
||||
<View style={{marginTop: 2}}>
|
||||
<UserAvatar type="algo" size={42} avatar={item.avatar} />
|
||||
</View>
|
||||
<View style={{flex: isMobile ? 1 : undefined}}>
|
||||
<Text
|
||||
type="2xl-bold"
|
||||
numberOfLines={1}
|
||||
style={[pal.text, {fontSize: 19}]}>
|
||||
{item.displayName}
|
||||
</Text>
|
||||
|
||||
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
|
||||
<Trans>by {sanitizeHandle(item.creator.handle, '@')}</Trans>
|
||||
</Text>
|
||||
|
||||
{item.description ? (
|
||||
<RichText
|
||||
type="xl"
|
||||
style={[
|
||||
pal.text,
|
||||
{
|
||||
flex: isMobile ? 1 : undefined,
|
||||
maxWidth: 550,
|
||||
marginBottom: 18,
|
||||
},
|
||||
]}
|
||||
richText={new BskRichText({text: item.description || ''})}
|
||||
numberOfLines={6}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
|
||||
<Button
|
||||
type="inverted"
|
||||
style={{paddingVertical: 6}}
|
||||
onPress={onToggle}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
{isPinned ? (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
size={16}
|
||||
color={pal.colors.textInverted}
|
||||
/>
|
||||
<Text type="lg-medium" style={pal.textInverted}>
|
||||
<Trans>Added</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
size={16}
|
||||
color={pal.colors.textInverted}
|
||||
/>
|
||||
<Text type="lg-medium" style={pal.textInverted}>
|
||||
<Trans>Add</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Button>
|
||||
|
||||
<View style={{flexDirection: 'row', gap: 4}}>
|
||||
<HeartIcon
|
||||
size={16}
|
||||
strokeWidth={2.5}
|
||||
style={[pal.textLight, {position: 'relative', top: 2}]}
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, pal.textLight]}>
|
||||
{item.likeCount || 0}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import React from 'react'
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
||||
import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {RecommendedFollowsItem} from './RecommendedFollowsItem'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
}
|
||||
export function RecommendedFollows({next}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
const {data: suggestedFollows} = useSuggestedFollowsQuery()
|
||||
const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor()
|
||||
const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{
|
||||
[did: string]: AppBskyActorDefs.ProfileView[]
|
||||
}>({})
|
||||
const existingDids = React.useRef<string[]>([])
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Follow some
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Users
|
||||
</Text>
|
||||
</Trans>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
<Trans>
|
||||
Follow some users to get started. We can recommend you more users
|
||||
based on who you find interesting.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
<Trans context="action">Done</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
const suggestions = React.useMemo(() => {
|
||||
if (!suggestedFollows) return []
|
||||
|
||||
const additional = Object.entries(additionalSuggestions)
|
||||
const items = suggestedFollows.pages.flatMap(page => page.actors)
|
||||
|
||||
outer: while (additional.length) {
|
||||
const additionalAccount = additional.shift()
|
||||
|
||||
if (!additionalAccount) break
|
||||
|
||||
const [followedUser, relatedAccounts] = additionalAccount
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].did === followedUser) {
|
||||
items.splice(i + 1, 0, ...relatedAccounts)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existingDids.current = items.map(i => i.did)
|
||||
|
||||
return items
|
||||
}, [suggestedFollows, additionalSuggestions])
|
||||
|
||||
const onFollowStateChange = React.useCallback(
|
||||
async ({following, did}: {following: boolean; did: string}) => {
|
||||
if (following) {
|
||||
try {
|
||||
const {suggestions: results} = await getSuggestedFollowsByActor(did)
|
||||
|
||||
if (results.length) {
|
||||
const deduped = results.filter(
|
||||
r => !existingDids.current.find(did => did === r.did),
|
||||
)
|
||||
setAdditionalSuggestions(s => ({
|
||||
...s,
|
||||
[did]: deduped.slice(0, 3),
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('RecommendedFollows: failed to get suggestions', {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// not handling the unfollow case
|
||||
},
|
||||
[existingDids, getSuggestedFollowsByActor, setAdditionalSuggestions],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabletOrDesktop>
|
||||
<TitleColumnLayout
|
||||
testID="recommendedFollowsOnboarding"
|
||||
title={title}
|
||||
horizontal
|
||||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
||||
contentStyle={{paddingHorizontal: 0}}>
|
||||
{!suggestedFollows || !moderationOpts ? (
|
||||
<ActivityIndicator size="large" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={suggestions}
|
||||
renderItem={({item}) => (
|
||||
<RecommendedFollowsItem
|
||||
profile={item}
|
||||
onFollowStateChange={onFollowStateChange}
|
||||
moderation={moderateProfile(item, moderationOpts)}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.did}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
</TitleColumnLayout>
|
||||
</TabletOrDesktop>
|
||||
|
||||
<Mobile>
|
||||
<View style={[mStyles.container]} testID="recommendedFollowsOnboarding">
|
||||
<View>
|
||||
<ViewHeader
|
||||
title={_(msg`Recommended Users`)}
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
<Trans>
|
||||
Check out some recommended users. Follow them to see similar
|
||||
users.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{!suggestedFollows || !moderationOpts ? (
|
||||
<ActivityIndicator size="large" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={suggestions}
|
||||
renderItem={({item}) => (
|
||||
<RecommendedFollowsItem
|
||||
profile={item}
|
||||
onFollowStateChange={onFollowStateChange}
|
||||
moderation={moderateProfile(item, moderationOpts)}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.did}
|
||||
style={{flex: 1}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={next}
|
||||
label={_(msg`Continue`)}
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
</Mobile>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const tdStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title1: {
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title1Small: {
|
||||
fontSize: 24,
|
||||
},
|
||||
title2: {
|
||||
fontSize: 58,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title2Small: {
|
||||
fontSize: 36,
|
||||
},
|
||||
description: {
|
||||
maxWidth: 400,
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
||||
const mStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View, StyleSheet, ActivityIndicator} from 'react-native'
|
||||
import {ModerationDecision, AppBskyActorDefs} from '@atproto/api'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {s} from 'lib/styles'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import Animated, {FadeInRight} from 'react-native-reanimated'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
type Props = {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
onFollowStateChange: (props: {
|
||||
did: string
|
||||
following: boolean
|
||||
}) => Promise<void>
|
||||
}
|
||||
|
||||
export function RecommendedFollowsItem({
|
||||
profile,
|
||||
moderation,
|
||||
onFollowStateChange,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const shadowedProfile = useProfileShadow(profile)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInRight}
|
||||
style={[
|
||||
styles.cardContainer,
|
||||
pal.view,
|
||||
pal.border,
|
||||
{
|
||||
maxWidth: isMobile ? undefined : 670,
|
||||
borderRightWidth: isMobile ? undefined : 1,
|
||||
},
|
||||
]}>
|
||||
<ProfileCard
|
||||
key={profile.did}
|
||||
profile={shadowedProfile}
|
||||
onFollowStateChange={onFollowStateChange}
|
||||
moderation={moderation}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileCard({
|
||||
profile,
|
||||
onFollowStateChange,
|
||||
moderation,
|
||||
}: {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
moderation: ModerationDecision
|
||||
onFollowStateChange: (props: {
|
||||
did: string
|
||||
following: boolean
|
||||
}) => Promise<void>
|
||||
}) {
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [addingMoreSuggestions, setAddingMoreSuggestions] =
|
||||
React.useState(false)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||
profile,
|
||||
'RecommendedFollowsItem',
|
||||
)
|
||||
|
||||
const onToggleFollow = React.useCallback(async () => {
|
||||
try {
|
||||
if (profile.viewer?.following) {
|
||||
await queueUnfollow()
|
||||
} else {
|
||||
setAddingMoreSuggestions(true)
|
||||
await queueFollow()
|
||||
await onFollowStateChange({did: profile.did, following: true})
|
||||
setAddingMoreSuggestions(false)
|
||||
track('Onboarding:SuggestedFollowFollowed')
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('RecommendedFollows: failed to toggle following', {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setAddingMoreSuggestions(false)
|
||||
}
|
||||
}, [
|
||||
profile,
|
||||
queueFollow,
|
||||
queueUnfollow,
|
||||
setAddingMoreSuggestions,
|
||||
track,
|
||||
onFollowStateChange,
|
||||
])
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="2xl-bold"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
type={profile.viewer?.following ? 'default' : 'inverted'}
|
||||
labelStyle={styles.followButton}
|
||||
onPress={onToggleFollow}
|
||||
label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)}
|
||||
/>
|
||||
</View>
|
||||
{profile.description ? (
|
||||
<View style={styles.details}>
|
||||
<Text type="lg" style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
{addingMoreSuggestions ? (
|
||||
<View style={styles.addingMoreContainer}>
|
||||
<ActivityIndicator size="small" color={pal.colors.text} />
|
||||
<Text style={[pal.text]}>
|
||||
<Trans>Finding similar accounts...</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardContainer: {
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
card: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 54,
|
||||
paddingLeft: 4,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
details: {
|
||||
paddingLeft: 54,
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
addingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 54,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 12,
|
||||
gap: 4,
|
||||
},
|
||||
followButton: {
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import 'react'
|
||||
import {withBreakpoints} from 'view/com/util/layouts/withBreakpoints'
|
||||
import {WelcomeDesktop} from './WelcomeDesktop'
|
||||
import {WelcomeMobile} from './WelcomeMobile'
|
||||
|
||||
export const Welcome = withBreakpoints(
|
||||
WelcomeMobile,
|
||||
WelcomeDesktop,
|
||||
WelcomeDesktop,
|
||||
)
|
||||
@@ -1,126 +0,0 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useMediaQuery} from 'react-responsive'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
skip: () => void
|
||||
}
|
||||
|
||||
export function WelcomeDesktop({next}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const horizontal = useMediaQuery({minWidth: 1300})
|
||||
const title = (
|
||||
<Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
{
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: horizontal ? 'right' : 'left',
|
||||
},
|
||||
]}>
|
||||
Welcome to
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
{
|
||||
fontSize: 72,
|
||||
fontWeight: '800',
|
||||
textAlign: horizontal ? 'right' : 'left',
|
||||
},
|
||||
]}>
|
||||
Bluesky
|
||||
</Text>
|
||||
</Trans>
|
||||
)
|
||||
return (
|
||||
<TitleColumnLayout
|
||||
testID="welcomeOnboarding"
|
||||
title={title}
|
||||
horizontal={horizontal}
|
||||
titleStyle={horizontal ? {paddingBottom: 160} : undefined}>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is public.</Trans>
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is open.</Trans>
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
<Trans>Never lose access to your followers and data.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is flexible.</Trans>
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.spacer} />
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
<Trans context="action">Next</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</TitleColumnLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
columnGap: 20,
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
})
|
||||
@@ -1,136 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
skip: () => void
|
||||
}
|
||||
|
||||
export function WelcomeMobile({next, skip}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<View style={[styles.container]} testID="welcomeOnboarding">
|
||||
<ViewHeader
|
||||
showOnDesktop
|
||||
showBorder={false}
|
||||
showBackButton={false}
|
||||
title=""
|
||||
renderButton={() => {
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
style={[s.flexRow, s.alignCenter]}
|
||||
onPress={skip}>
|
||||
<Text style={[pal.link]}>
|
||||
<Trans>Skip</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon={'chevron-right'}
|
||||
size={14}
|
||||
color={pal.colors.link}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>
|
||||
Welcome to{' '}
|
||||
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={styles.spacer} />
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is public.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is open.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
<Trans>Never lose access to your followers and data.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
<Trans>Bluesky is flexible.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label={_(msg`Continue`)}
|
||||
testID="continueBtn"
|
||||
style={[styles.buttonContainer]}
|
||||
labelStyle={styles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: 60,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title: {
|
||||
fontSize: 42,
|
||||
fontWeight: '800',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
columnGap: 20,
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
marginVertical: 4,
|
||||
},
|
||||
})
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import * as apilib from 'lib/api/index'
|
||||
@@ -53,7 +53,7 @@ import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
|
||||
import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
@@ -83,6 +83,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
imageUris: initImageUris,
|
||||
}: Props) {
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
||||
const {isModalActive} = useModals()
|
||||
const {closeComposer} = useComposerControls()
|
||||
@@ -483,8 +484,13 @@ export const ComposePost = observer(function ComposePost({
|
||||
/>
|
||||
)}
|
||||
{quote ? (
|
||||
<View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
<View style={[s.mt5, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : undefined}
|
||||
</ScrollView>
|
||||
|
||||
@@ -28,8 +28,8 @@ import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {
|
||||
addLinkCardIfNecessary,
|
||||
findIndexInText,
|
||||
LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from 'view/com/composer/text-input/text-input-util'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Autocomplete} from './mobile/Autocomplete'
|
||||
@@ -73,7 +73,6 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
const theme = useTheme()
|
||||
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
||||
const prevLength = React.useRef(richtext.length)
|
||||
const prevAddedLinks = useRef(new Set<string>())
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => textInput.current?.focus(),
|
||||
@@ -83,6 +82,8 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
getCursorPosition: () => undefined, // Not implemented on native
|
||||
}))
|
||||
|
||||
const pastSuggestedUris = useRef(new Set<string>())
|
||||
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
|
||||
const onChangeText = useCallback(
|
||||
(newText: string) => {
|
||||
/*
|
||||
@@ -112,6 +113,7 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
setAutocompletePrefix('')
|
||||
}
|
||||
|
||||
const nextDetectedUris = new Map<string, LinkFacetMatch>()
|
||||
if (newRt.facets) {
|
||||
for (const facet of newRt.facets) {
|
||||
for (const feature of facet.features) {
|
||||
@@ -130,32 +132,26 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
onPhotoPasted(res.path)
|
||||
}
|
||||
} else {
|
||||
const cursorLocation = textInputSelection.current.end
|
||||
|
||||
addLinkCardIfNecessary({
|
||||
uri: feature.uri,
|
||||
newText,
|
||||
cursorLocation,
|
||||
mayBePaste,
|
||||
onNewLink,
|
||||
prevAddedLinks: prevAddedLinks.current,
|
||||
})
|
||||
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const uri of prevAddedLinks.current.keys()) {
|
||||
if (findIndexInText(uri, newText) === -1) {
|
||||
prevAddedLinks.current.delete(uri)
|
||||
}
|
||||
const suggestedUri = suggestLinkCardUri(
|
||||
mayBePaste,
|
||||
nextDetectedUris,
|
||||
prevDetectedUris.current,
|
||||
pastSuggestedUris.current,
|
||||
)
|
||||
prevDetectedUris.current = nextDetectedUris
|
||||
if (suggestedUri) {
|
||||
onNewLink(suggestedUri)
|
||||
}
|
||||
|
||||
prevLength.current = newText.length
|
||||
}, 1)
|
||||
},
|
||||
[setRichText, autocompletePrefix, onPhotoPasted, prevAddedLinks, onNewLink],
|
||||
[setRichText, autocompletePrefix, onPhotoPasted, onNewLink],
|
||||
)
|
||||
|
||||
const onPaste = useCallback(
|
||||
|
||||
@@ -19,8 +19,8 @@ import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {blobToDataUri, isUriImage} from 'lib/media/util'
|
||||
import {
|
||||
addLinkCardIfNecessary,
|
||||
findIndexInText,
|
||||
LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from 'view/com/composer/text-input/text-input-util'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {Text} from '../../util/text/Text'
|
||||
@@ -61,9 +61,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
ref,
|
||||
) {
|
||||
const autocomplete = useActorAutocompleteFn()
|
||||
const prevLength = React.useRef(0)
|
||||
const prevAddedLinks = useRef(new Set<string>())
|
||||
|
||||
const pal = usePalette('default')
|
||||
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
||||
|
||||
@@ -144,6 +141,8 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
}
|
||||
}, [setIsDropping])
|
||||
|
||||
const pastSuggestedUris = useRef(new Set<string>())
|
||||
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
|
||||
const editor = useEditor(
|
||||
{
|
||||
extensions,
|
||||
@@ -185,42 +184,34 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
},
|
||||
onUpdate({editor: editorProp}) {
|
||||
const json = editorProp.getJSON()
|
||||
const newText = editorJsonToText(json).trimEnd()
|
||||
const mayBePaste = newText.length > prevLength.current + 1
|
||||
const newText = editorJsonToText(json)
|
||||
const isPaste = window.event?.type === 'paste'
|
||||
|
||||
const newRt = new RichText({text: newText})
|
||||
newRt.detectFacetsWithoutResolution()
|
||||
setRichText(newRt)
|
||||
|
||||
const nextDetectedUris = new Map<string, LinkFacetMatch>()
|
||||
if (newRt.facets) {
|
||||
for (const facet of newRt.facets) {
|
||||
for (const feature of facet.features) {
|
||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||
// The TipTap editor shows the position as being one character ahead, as if the start index is 1.
|
||||
// Subtracting 1 from the pos gives us the same behavior as the native impl.
|
||||
let cursorLocation = editor?.state.selection.$anchor.pos ?? 1
|
||||
cursorLocation -= 1
|
||||
|
||||
addLinkCardIfNecessary({
|
||||
uri: feature.uri,
|
||||
newText,
|
||||
cursorLocation,
|
||||
mayBePaste,
|
||||
onNewLink,
|
||||
prevAddedLinks: prevAddedLinks.current,
|
||||
})
|
||||
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const uri of prevAddedLinks.current.keys()) {
|
||||
if (findIndexInText(uri, newText) === -1) {
|
||||
prevAddedLinks.current.delete(uri)
|
||||
}
|
||||
const suggestedUri = suggestLinkCardUri(
|
||||
isPaste,
|
||||
nextDetectedUris,
|
||||
prevDetectedUris.current,
|
||||
pastSuggestedUris.current,
|
||||
)
|
||||
prevDetectedUris.current = nextDetectedUris
|
||||
if (suggestedUri) {
|
||||
onNewLink(suggestedUri)
|
||||
}
|
||||
|
||||
prevLength.current = newText.length
|
||||
},
|
||||
},
|
||||
[modeClass],
|
||||
@@ -277,15 +268,29 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
)
|
||||
})
|
||||
|
||||
function editorJsonToText(json: JSONContent): string {
|
||||
function editorJsonToText(
|
||||
json: JSONContent,
|
||||
isLastDocumentChild: boolean = false,
|
||||
): string {
|
||||
let text = ''
|
||||
if (json.type === 'doc' || json.type === 'paragraph') {
|
||||
if (json.type === 'doc') {
|
||||
if (json.content?.length) {
|
||||
for (const node of json.content) {
|
||||
for (let i = 0; i < json.content.length; i++) {
|
||||
const node = json.content[i]
|
||||
const isLastNode = i === json.content.length - 1
|
||||
text += editorJsonToText(node, isLastNode)
|
||||
}
|
||||
}
|
||||
} else if (json.type === 'paragraph') {
|
||||
if (json.content?.length) {
|
||||
for (let i = 0; i < json.content.length; i++) {
|
||||
const node = json.content[i]
|
||||
text += editorJsonToText(node)
|
||||
}
|
||||
}
|
||||
text += '\n'
|
||||
if (!isLastDocumentChild) {
|
||||
text += '\n'
|
||||
}
|
||||
} else if (json.type === 'hardBreak') {
|
||||
text += '\n'
|
||||
} else if (json.type === 'text') {
|
||||
|
||||
@@ -1,41 +1,85 @@
|
||||
export function addLinkCardIfNecessary({
|
||||
uri,
|
||||
newText,
|
||||
cursorLocation,
|
||||
mayBePaste,
|
||||
onNewLink,
|
||||
prevAddedLinks,
|
||||
}: {
|
||||
uri: string
|
||||
newText: string
|
||||
cursorLocation: number
|
||||
mayBePaste: boolean
|
||||
onNewLink: (uri: string) => void
|
||||
prevAddedLinks: Set<string>
|
||||
}) {
|
||||
// It would be cool if we could just use facet.index.byteEnd, but you know... *upside down smiley*
|
||||
const lastCharacterPosition = findIndexInText(uri, newText) + uri.length
|
||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
|
||||
// If the text being added is not from a paste, then we should only check if the cursor is one
|
||||
// position ahead of the last character. However, if it is a paste we need to check both if it's
|
||||
// the same position _or_ one position ahead. That is because iOS will add a space after a paste if
|
||||
// pasting into the middle of a sentence!
|
||||
const cursorLocationIsOkay =
|
||||
cursorLocation === lastCharacterPosition + 1 || mayBePaste
|
||||
export type LinkFacetMatch = {
|
||||
rt: RichText
|
||||
facet: AppBskyRichtextFacet.Main
|
||||
}
|
||||
|
||||
// Checking previouslyAddedLinks keeps a card from getting added over and over i.e.
|
||||
// Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on
|
||||
|
||||
// We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e.
|
||||
// http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if
|
||||
// the url is a valid url _and_ domain. new URL() won't work for this check.
|
||||
const shouldCheck =
|
||||
cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri)
|
||||
|
||||
if (shouldCheck) {
|
||||
onNewLink(uri)
|
||||
prevAddedLinks.add(uri)
|
||||
export function suggestLinkCardUri(
|
||||
mayBePaste: boolean,
|
||||
nextDetectedUris: Map<string, LinkFacetMatch>,
|
||||
prevDetectedUris: Map<string, LinkFacetMatch>,
|
||||
pastSuggestedUris: Set<string>,
|
||||
): string | undefined {
|
||||
const suggestedUris = new Set<string>()
|
||||
for (const [uri, nextMatch] of nextDetectedUris) {
|
||||
if (!isValidUrlAndDomain(uri)) {
|
||||
continue
|
||||
}
|
||||
if (pastSuggestedUris.has(uri)) {
|
||||
// Don't suggest already added or already dismissed link cards.
|
||||
continue
|
||||
}
|
||||
if (mayBePaste) {
|
||||
// Immediately add the pasted link without waiting to type more.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
const prevMatch = prevDetectedUris.get(uri)
|
||||
if (!prevMatch) {
|
||||
// If the same exact link wasn't already detected during the last keystroke,
|
||||
// it means you're probably still typing it. Disregard until it stabilizes.
|
||||
continue
|
||||
}
|
||||
const prevTextAfterUri = prevMatch.rt.unicodeText.slice(
|
||||
prevMatch.facet.index.byteEnd,
|
||||
)
|
||||
const nextTextAfterUri = nextMatch.rt.unicodeText.slice(
|
||||
nextMatch.facet.index.byteEnd,
|
||||
)
|
||||
if (prevTextAfterUri === nextTextAfterUri) {
|
||||
// The text you're editing is before the link, e.g.
|
||||
// "abc google.com" -> "abcd google.com".
|
||||
// This is a good time to add the link.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
if (/^\s/m.test(nextTextAfterUri)) {
|
||||
// The link is followed by a space, e.g.
|
||||
// "google.com" -> "google.com " or
|
||||
// "google.com." -> "google.com ".
|
||||
// This is a clear indicator we can linkify it.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
if (
|
||||
/^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) &&
|
||||
/^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri)
|
||||
) {
|
||||
// The link was *already* being followed by punctuation,
|
||||
// and now it's followed both by punctuation and a space.
|
||||
// This means you're typing after punctuation, e.g.
|
||||
// "google.com." -> "google.com. " or
|
||||
// "google.com.foo" -> "google.com. foo".
|
||||
// This means you're not typing the link anymore, so we can linkify it.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
}
|
||||
for (const uri of pastSuggestedUris) {
|
||||
if (!nextDetectedUris.has(uri)) {
|
||||
// If a link is no longer detected, it's eligible for suggestions next time.
|
||||
pastSuggestedUris.delete(uri)
|
||||
}
|
||||
}
|
||||
|
||||
let suggestedUri: string | undefined
|
||||
if (suggestedUris.size > 0) {
|
||||
suggestedUri = Array.from(suggestedUris)[0]
|
||||
pastSuggestedUris.add(suggestedUri)
|
||||
}
|
||||
|
||||
return suggestedUri
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
|
||||
@@ -46,14 +90,3 @@ function isValidUrlAndDomain(value: string) {
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
export function findIndexInText(term: string, text: string) {
|
||||
// This should find patterns like:
|
||||
// HELLO SENTENCE http://google.com/ HELLO
|
||||
// HELLO SENTENCE http://google.com HELLO
|
||||
// http://google.com/ HELLO.
|
||||
// http://google.com/.
|
||||
const pattern = new RegExp(`\\b(${term})(?![/w])`, 'i')
|
||||
const match = pattern.exec(text)
|
||||
return match ? match.index : -1
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
* the facet-set.
|
||||
*/
|
||||
|
||||
import {Mark} from '@tiptap/core'
|
||||
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
|
||||
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||
import {URL_REGEX} from '@atproto/api'
|
||||
import {Mark} from '@tiptap/core'
|
||||
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
|
||||
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||
|
||||
import {isValidDomain} from 'lib/strings/url-helpers'
|
||||
|
||||
@@ -91,7 +91,7 @@ function iterateUris(str: string, cb: (from: number, to: number) => void) {
|
||||
uri = `https://${uri}`
|
||||
}
|
||||
let from = str.indexOf(match[2], match.index)
|
||||
let to = from + match[2].length + 1
|
||||
let to = from + match[2].length
|
||||
// strip ending puncuation
|
||||
if (/[.,;!?]$/.test(uri)) {
|
||||
uri = uri.slice(0, -1)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
import {getAgent} from '#/state/session'
|
||||
|
||||
export function useExternalLinkFetch({}: {
|
||||
setQuote: (opts: ComposerOpts['quote']) => void
|
||||
}) {
|
||||
const {getAgent} = useAgent()
|
||||
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
|
||||
undefined,
|
||||
)
|
||||
@@ -39,7 +41,7 @@ export function useExternalLinkFetch({}: {
|
||||
})
|
||||
}
|
||||
return cleanup
|
||||
}, [extLink])
|
||||
}, [extLink, getAgent])
|
||||
|
||||
return {extLink, setExtLink}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useFetchDid} from '#/state/queries/handle'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import {
|
||||
getPostAsQuote,
|
||||
getFeedAsEmbed,
|
||||
getListAsEmbed,
|
||||
getPostAsQuote,
|
||||
} from 'lib/link-meta/bsky'
|
||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||
import {downloadAndResize} from 'lib/media/manip'
|
||||
import {
|
||||
isBskyPostUrl,
|
||||
isBskyCustomFeedUrl,
|
||||
isBskyListUrl,
|
||||
isBskyPostUrl,
|
||||
} from 'lib/strings/url-helpers'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {getAgent} from '#/state/session'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {useFetchDid} from '#/state/queries/handle'
|
||||
|
||||
export function useExternalLinkFetch({
|
||||
setQuote,
|
||||
@@ -30,6 +31,7 @@ export function useExternalLinkFetch({
|
||||
)
|
||||
const getPost = useGetPost()
|
||||
const fetchDid = useFetchDid()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
useEffect(() => {
|
||||
let aborted = false
|
||||
@@ -135,7 +137,7 @@ export function useExternalLinkFetch({
|
||||
})
|
||||
}
|
||||
return cleanup
|
||||
}, [extLink, setQuote, getPost, fetchDid])
|
||||
}, [extLink, setQuote, getPost, fetchDid, getAgent])
|
||||
|
||||
return {extLink, setExtLink}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, {useState} from 'react'
|
||||
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useAgent, useSession, useSessionApi} from '#/state/session'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useSession, useSessionApi, getAgent} from '#/state/session'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
|
||||
enum Stages {
|
||||
InputEmail,
|
||||
@@ -26,6 +27,7 @@ export const snapPoints = ['90%']
|
||||
export function Component() {
|
||||
const pal = usePalette('default')
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const {updateCurrentAccount} = useSessionApi()
|
||||
const {_} = useLingui()
|
||||
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user