Merge branch 'main' into hailey/search-improvements
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {describe, beforeAll, it} from '@jest/globals'
|
import {beforeAll, describe, it} from '@jest/globals'
|
||||||
import {expect} from 'detox'
|
import {expect} from 'detox'
|
||||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
|
||||||
|
import {createServer, loginAsAlice, openApp, sleep} from '../util'
|
||||||
|
|
||||||
describe('Composer', () => {
|
describe('Composer', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -41,7 +42,6 @@ describe('Composer', () => {
|
|||||||
await element(by.id('composerTextInput')).typeText(
|
await element(by.id('composerTextInput')).typeText(
|
||||||
'Post with a https://example.com link card',
|
'Post with a https://example.com link card',
|
||||||
)
|
)
|
||||||
await element(by.id('addLinkCardBtn')).tap()
|
|
||||||
await element(by.id('composerPublishBtn')).tap()
|
await element(by.id('composerPublishBtn')).tap()
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -72,7 +72,6 @@ describe('Composer', () => {
|
|||||||
await element(by.id('composerTextInput')).typeText(
|
await element(by.id('composerTextInput')).typeText(
|
||||||
'Reply with a https://example.com link card',
|
'Reply with a https://example.com link card',
|
||||||
)
|
)
|
||||||
await element(by.id('addLinkCardBtn')).tap()
|
|
||||||
await element(by.id('composerPublishBtn')).tap()
|
await element(by.id('composerPublishBtn')).tap()
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -104,7 +103,6 @@ describe('Composer', () => {
|
|||||||
await element(by.id('composerTextInput')).typeText(
|
await element(by.id('composerTextInput')).typeText(
|
||||||
'QP with a https://example.com link card',
|
'QP with a https://example.com link card',
|
||||||
)
|
)
|
||||||
await element(by.id('addLinkCardBtn')).tap()
|
|
||||||
await element(by.id('composerPublishBtn')).tap()
|
await element(by.id('composerPublishBtn')).tap()
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
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 {
|
import {
|
||||||
downloadAndResize,
|
downloadAndResize,
|
||||||
DownloadAndResizeOpts,
|
DownloadAndResizeOpts,
|
||||||
} from '../../src/lib/media/manip'
|
} from '../../src/lib/media/manip'
|
||||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
|
||||||
import RNFetchBlob from 'rn-fetch-blob'
|
|
||||||
|
|
||||||
describe('downloadAndResize', () => {
|
describe('downloadAndResize', () => {
|
||||||
const errorSpy = jest.spyOn(global.console, 'error')
|
const errorSpy = jest.spyOn(global.console, 'error')
|
||||||
@@ -30,6 +31,7 @@ describe('downloadAndResize', () => {
|
|||||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||||
mockedFetch.mockResolvedValueOnce({
|
mockedFetch.mockResolvedValueOnce({
|
||||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||||
|
info: jest.fn().mockReturnValue({status: 200}),
|
||||||
flush: jest.fn(),
|
flush: jest.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -84,6 +86,7 @@ describe('downloadAndResize', () => {
|
|||||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||||
mockedFetch.mockResolvedValueOnce({
|
mockedFetch.mockResolvedValueOnce({
|
||||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||||
|
info: jest.fn().mockReturnValue({status: 200}),
|
||||||
flush: jest.fn(),
|
flush: jest.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -118,4 +121,26 @@ describe('downloadAndResize', () => {
|
|||||||
{mode: 'cover'},
|
{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?
|
// TODO: do we actually do something with width?
|
||||||
width := 550
|
width := 600
|
||||||
maxWidthParam := c.QueryParam("maxwidth")
|
maxWidthParam := c.QueryParam("maxwidth")
|
||||||
if maxWidthParam != "" {
|
if maxWidthParam != "" {
|
||||||
maxWidthInt, err := strconv.Atoi(maxWidthParam)
|
maxWidthInt, err := strconv.Atoi(maxWidthParam)
|
||||||
if err != nil || maxWidthInt < 220 || maxWidthInt > 550 {
|
if err != nil {
|
||||||
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer between 220 and 550)")
|
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
|
// NOTE: maxheight ignored
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
|
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
||||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||||
import {useNotificationsListener} from 'lib/notifications/notifications'
|
import {useNotificationsListener} from 'lib/notifications/notifications'
|
||||||
import {QueryProvider} from 'lib/react-query'
|
import {QueryProvider} from 'lib/react-query'
|
||||||
@@ -64,7 +64,7 @@ function InnerApp() {
|
|||||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||||
})
|
})
|
||||||
|
|
||||||
const account = persisted.get('session').currentAccount
|
const account = readLastActiveAccount()
|
||||||
resumeSession(account)
|
resumeSession(account)
|
||||||
}, [resumeSession, _])
|
}, [resumeSession, _])
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
|
|||||||
|
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
|
import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount'
|
||||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||||
import {QueryProvider} from 'lib/react-query'
|
import {QueryProvider} from 'lib/react-query'
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
import {ThemeProvider} from 'lib/ThemeContext'
|
||||||
@@ -42,7 +42,7 @@ function InnerApp() {
|
|||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const account = persisted.get('session').currentAccount
|
const account = readLastActiveAccount()
|
||||||
resumeSession(account)
|
resumeSession(account)
|
||||||
}, [resumeSession])
|
}, [resumeSession])
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
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 {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
@@ -35,6 +35,7 @@ export function SubmitView({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const [details, setDetails] = React.useState<string>('')
|
const [details, setDetails] = React.useState<string>('')
|
||||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||||
@@ -90,6 +91,7 @@ export function SubmitView({
|
|||||||
selectedServices,
|
selectedServices,
|
||||||
onSubmitComplete,
|
onSubmitComplete,
|
||||||
setError,
|
setError,
|
||||||
|
getAgent,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {RichText as RichTextAPI} from '@atproto/api'
|
import {RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
export function useRichText(text: string): [RichTextAPI, boolean] {
|
export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||||
const [prevText, setPrevText] = React.useState(text)
|
const [prevText, setPrevText] = React.useState(text)
|
||||||
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
||||||
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
|
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
|
||||||
|
const {getAgent} = useAgent()
|
||||||
if (text !== prevText) {
|
if (text !== prevText) {
|
||||||
setPrevText(text)
|
setPrevText(text)
|
||||||
setRawRT(new RichTextAPI({text}))
|
setRawRT(new RichTextAPI({text}))
|
||||||
@@ -27,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
|
|||||||
return () => {
|
return () => {
|
||||||
ignore = true
|
ignore = true
|
||||||
}
|
}
|
||||||
}, [text])
|
}, [text, getAgent])
|
||||||
const isResolving = resolvedRT === null
|
const isResolving = resolvedRT === null
|
||||||
return [resolvedRT ?? rawRT, isResolving]
|
return [resolvedRT ?? rawRT, isResolving]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
@@ -173,6 +173,7 @@ function AppealForm({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const [details, setDetails] = React.useState('')
|
const [details, setDetails] = React.useState('')
|
||||||
const isAccountReport = 'did' in subject
|
const isAccountReport = 'did' in subject
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||||
|
BskyAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export class AuthorFeedAPI implements FeedAPI {
|
export class AuthorFeedAPI implements FeedAPI {
|
||||||
constructor(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> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const res = await getAgent().getAuthorFeed({
|
const res = await this.getAgent().getAuthorFeed({
|
||||||
...this.params,
|
...this.params,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
@@ -23,7 +36,7 @@ export class AuthorFeedAPI implements FeedAPI {
|
|||||||
cursor: string | undefined
|
cursor: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const res = await getAgent().getAuthorFeed({
|
const res = await this.getAgent().getAuthorFeed({
|
||||||
...this.params,
|
...this.params,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -2,18 +2,30 @@ import {
|
|||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetFeed as GetCustomFeed,
|
AppBskyFeedGetFeed as GetCustomFeed,
|
||||||
AtpAgent,
|
AtpAgent,
|
||||||
|
BskyAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {getContentLanguages} from '#/state/preferences/languages'
|
import {getContentLanguages} from '#/state/preferences/languages'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
|
|
||||||
export class CustomFeedAPI implements FeedAPI {
|
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> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
const res = await getAgent().app.bsky.feed.getFeed(
|
const res = await this.getAgent().app.bsky.feed.getFeed(
|
||||||
{
|
{
|
||||||
...this.params,
|
...this.params,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@@ -31,15 +43,19 @@ export class CustomFeedAPI implements FeedAPI {
|
|||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
const agent = getAgent()
|
const agent = this.getAgent()
|
||||||
const res = agent.session
|
const res = agent.session
|
||||||
? await getAgent().app.bsky.feed.getFeed(
|
? await this.getAgent().app.bsky.feed.getFeed(
|
||||||
{
|
{
|
||||||
...this.params,
|
...this.params,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
},
|
},
|
||||||
{headers: {'Accept-Language': contentLangs}},
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': contentLangs,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: await loggedOutFetch({...this.params, cursor, limit})
|
: await loggedOutFetch({...this.params, cursor, limit})
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import {AppBskyFeedDefs} from '@atproto/api'
|
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export class FollowingFeedAPI implements FeedAPI {
|
export class FollowingFeedAPI implements FeedAPI {
|
||||||
constructor() {}
|
getAgent: () => BskyAgent
|
||||||
|
|
||||||
|
constructor({getAgent}: {getAgent: () => BskyAgent}) {
|
||||||
|
this.getAgent = getAgent
|
||||||
|
}
|
||||||
|
|
||||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const res = await getAgent().getTimeline({
|
const res = await this.getAgent().getTimeline({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
return res.data.feed[0]
|
return res.data.feed[0]
|
||||||
@@ -19,7 +23,7 @@ export class FollowingFeedAPI implements FeedAPI {
|
|||||||
cursor: string | undefined
|
cursor: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const res = await getAgent().getTimeline({
|
const res = await this.getAgent().getTimeline({
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {AppBskyFeedDefs} from '@atproto/api'
|
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
|
||||||
import {FollowingFeedAPI} from './following'
|
|
||||||
import {CustomFeedAPI} from './custom'
|
|
||||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||||
|
import {CustomFeedAPI} from './custom'
|
||||||
|
import {FollowingFeedAPI} from './following'
|
||||||
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
|
|
||||||
// HACK
|
// HACK
|
||||||
// the feed API does not include any facilities for passing down
|
// 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 {
|
export class HomeFeedAPI implements FeedAPI {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
following: FollowingFeedAPI
|
following: FollowingFeedAPI
|
||||||
discover: CustomFeedAPI
|
discover: CustomFeedAPI
|
||||||
usingDiscover = false
|
usingDiscover = false
|
||||||
itemCursor = 0
|
itemCursor = 0
|
||||||
|
|
||||||
constructor() {
|
constructor({getAgent}: {getAgent: () => BskyAgent}) {
|
||||||
this.following = new FollowingFeedAPI()
|
this.getAgent = getAgent
|
||||||
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
|
this.following = new FollowingFeedAPI({getAgent})
|
||||||
|
this.discover = new CustomFeedAPI({
|
||||||
|
getAgent,
|
||||||
|
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.following = new FollowingFeedAPI()
|
this.following = new FollowingFeedAPI({getAgent: this.getAgent})
|
||||||
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
|
this.discover = new CustomFeedAPI({
|
||||||
|
getAgent: this.getAgent,
|
||||||
|
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
|
||||||
|
})
|
||||||
this.usingDiscover = false
|
this.usingDiscover = false
|
||||||
this.itemCursor = 0
|
this.itemCursor = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetActorLikes as GetActorLikes,
|
AppBskyFeedGetActorLikes as GetActorLikes,
|
||||||
|
BskyAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export class LikesFeedAPI implements FeedAPI {
|
export class LikesFeedAPI implements FeedAPI {
|
||||||
constructor(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> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const res = await getAgent().getActorLikes({
|
const res = await this.getAgent().getActorLikes({
|
||||||
...this.params,
|
...this.params,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
@@ -23,7 +36,7 @@ export class LikesFeedAPI implements FeedAPI {
|
|||||||
cursor: string | undefined
|
cursor: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const res = await getAgent().getActorLikes({
|
const res = await this.getAgent().getActorLikes({
|
||||||
...this.params,
|
...this.params,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetListFeed as GetListFeed,
|
AppBskyFeedGetListFeed as GetListFeed,
|
||||||
|
BskyAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export class ListFeedAPI implements FeedAPI {
|
export class ListFeedAPI implements FeedAPI {
|
||||||
constructor(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> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const res = await getAgent().app.bsky.feed.getListFeed({
|
const res = await this.getAgent().app.bsky.feed.getListFeed({
|
||||||
...this.params,
|
...this.params,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
@@ -23,7 +36,7 @@ export class ListFeedAPI implements FeedAPI {
|
|||||||
cursor: string | undefined
|
cursor: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const res = await getAgent().app.bsky.feed.getListFeed({
|
const res = await this.getAgent().app.bsky.feed.getListFeed({
|
||||||
...this.params,
|
...this.params,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
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 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 {bundleAsync} from 'lib/async/bundle'
|
||||||
|
import {timeout} from 'lib/async/timeout'
|
||||||
import {feedUriToHref} from 'lib/strings/url-helpers'
|
import {feedUriToHref} from 'lib/strings/url-helpers'
|
||||||
import {FeedTuner} from '../feed-manip'
|
import {FeedTuner} from '../feed-manip'
|
||||||
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
|
|
||||||
import {FeedParams} from '#/state/queries/post-feed'
|
|
||||||
import {FeedTunerFn} from '../feed-manip'
|
import {FeedTunerFn} from '../feed-manip'
|
||||||
import {getAgent} from '#/state/session'
|
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
|
||||||
import {getContentLanguages} from '#/state/preferences/languages'
|
|
||||||
|
|
||||||
const REQUEST_WAIT_MS = 500 // 500ms
|
const REQUEST_WAIT_MS = 500 // 500ms
|
||||||
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
|
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
|
||||||
|
|
||||||
export class MergeFeedAPI implements FeedAPI {
|
export class MergeFeedAPI implements FeedAPI {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
|
params: FeedParams
|
||||||
|
feedTuners: FeedTunerFn[]
|
||||||
following: MergeFeedSource_Following
|
following: MergeFeedSource_Following
|
||||||
customFeeds: MergeFeedSource_Custom[] = []
|
customFeeds: MergeFeedSource_Custom[] = []
|
||||||
feedCursor = 0
|
feedCursor = 0
|
||||||
itemCursor = 0
|
itemCursor = 0
|
||||||
sampleCursor = 0
|
sampleCursor = 0
|
||||||
|
|
||||||
constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) {
|
constructor({
|
||||||
this.following = new MergeFeedSource_Following(this.feedTuners)
|
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() {
|
reset() {
|
||||||
this.following = new MergeFeedSource_Following(this.feedTuners)
|
this.following = new MergeFeedSource_Following({
|
||||||
|
getAgent: this.getAgent,
|
||||||
|
feedTuners: this.feedTuners,
|
||||||
|
})
|
||||||
this.customFeeds = []
|
this.customFeeds = []
|
||||||
this.feedCursor = 0
|
this.feedCursor = 0
|
||||||
this.itemCursor = 0
|
this.itemCursor = 0
|
||||||
@@ -33,7 +53,12 @@ export class MergeFeedAPI implements FeedAPI {
|
|||||||
if (this.params.mergeFeedSources) {
|
if (this.params.mergeFeedSources) {
|
||||||
this.customFeeds = shuffle(
|
this.customFeeds = shuffle(
|
||||||
this.params.mergeFeedSources.map(
|
this.params.mergeFeedSources.map(
|
||||||
feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners),
|
feedUri =>
|
||||||
|
new MergeFeedSource_Custom({
|
||||||
|
getAgent: this.getAgent,
|
||||||
|
feedUri,
|
||||||
|
feedTuners: this.feedTuners,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -42,7 +67,7 @@ export class MergeFeedAPI implements FeedAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||||
const res = await getAgent().getTimeline({
|
const res = await this.getAgent().getTimeline({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
return res.data.feed[0]
|
return res.data.feed[0]
|
||||||
@@ -136,12 +161,23 @@ export class MergeFeedAPI implements FeedAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MergeFeedSource {
|
class MergeFeedSource {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
|
feedTuners: FeedTunerFn[]
|
||||||
sourceInfo: ReasonFeedSource | undefined
|
sourceInfo: ReasonFeedSource | undefined
|
||||||
cursor: string | undefined = undefined
|
cursor: string | undefined = undefined
|
||||||
queue: AppBskyFeedDefs.FeedViewPost[] = []
|
queue: AppBskyFeedDefs.FeedViewPost[] = []
|
||||||
hasMore = true
|
hasMore = true
|
||||||
|
|
||||||
constructor(public feedTuners: FeedTunerFn[]) {}
|
constructor({
|
||||||
|
getAgent,
|
||||||
|
feedTuners,
|
||||||
|
}: {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
|
feedTuners: FeedTunerFn[]
|
||||||
|
}) {
|
||||||
|
this.getAgent = getAgent
|
||||||
|
this.feedTuners = feedTuners
|
||||||
|
}
|
||||||
|
|
||||||
get numReady() {
|
get numReady() {
|
||||||
return this.queue.length
|
return this.queue.length
|
||||||
@@ -203,7 +239,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
|
|||||||
cursor: string | undefined,
|
cursor: string | undefined,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<AppBskyFeedGetTimeline.Response> {
|
): 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
|
// run the tuner pre-emptively to ensure better mixing
|
||||||
const slices = this.tuner.tune(res.data.feed, {
|
const slices = this.tuner.tune(res.data.feed, {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
@@ -215,10 +251,25 @@ class MergeFeedSource_Following extends MergeFeedSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MergeFeedSource_Custom extends MergeFeedSource {
|
class MergeFeedSource_Custom extends MergeFeedSource {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
minDate: Date
|
minDate: Date
|
||||||
|
feedUri: string
|
||||||
|
|
||||||
constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) {
|
constructor({
|
||||||
super(feedTuners)
|
getAgent,
|
||||||
|
feedUri,
|
||||||
|
feedTuners,
|
||||||
|
}: {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
|
feedUri: string
|
||||||
|
feedTuners: FeedTunerFn[]
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
getAgent,
|
||||||
|
feedTuners,
|
||||||
|
})
|
||||||
|
this.getAgent = getAgent
|
||||||
|
this.feedUri = feedUri
|
||||||
this.sourceInfo = {
|
this.sourceInfo = {
|
||||||
$type: 'reasonFeedSource',
|
$type: 'reasonFeedSource',
|
||||||
uri: feedUri,
|
uri: feedUri,
|
||||||
@@ -233,13 +284,17 @@ class MergeFeedSource_Custom extends MergeFeedSource {
|
|||||||
): Promise<AppBskyFeedGetTimeline.Response> {
|
): Promise<AppBskyFeedGetTimeline.Response> {
|
||||||
try {
|
try {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
const res = await getAgent().app.bsky.feed.getFeed(
|
const res = await this.getAgent().app.bsky.feed.getFeed(
|
||||||
{
|
{
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
feed: this.feedUri,
|
feed: this.feedUri,
|
||||||
},
|
},
|
||||||
{headers: {'Accept-Language': contentLangs}},
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': contentLangs,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
// NOTE
|
// NOTE
|
||||||
// some custom feeds fail to enforce the pagination limit
|
// some custom feeds fail to enforce the pagination limit
|
||||||
|
|||||||
+4
-15
@@ -1,4 +1,3 @@
|
|||||||
import {deleteAsync} from 'expo-file-system'
|
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
AppBskyEmbedImages,
|
AppBskyEmbedImages,
|
||||||
@@ -20,6 +19,7 @@ import {shortenLinks} from 'lib/strings/rich-text-manip'
|
|||||||
import {isNative, isWeb} from 'platform/detection'
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
import {ImageModel} from 'state/models/media/image'
|
import {ImageModel} from 'state/models/media/image'
|
||||||
import {LinkMeta} from '../link-meta/link-meta'
|
import {LinkMeta} from '../link-meta/link-meta'
|
||||||
|
import {safeDeleteAsync} from '../media/manip'
|
||||||
|
|
||||||
export interface ExternalEmbedDraft {
|
export interface ExternalEmbedDraft {
|
||||||
uri: string
|
uri: string
|
||||||
@@ -119,15 +119,9 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
|||||||
const {width, height} = image.compressed || image
|
const {width, height} = image.compressed || image
|
||||||
logger.debug(`Uploading image`)
|
logger.debug(`Uploading image`)
|
||||||
const res = await uploadBlob(agent, path, 'image/jpeg')
|
const res = await uploadBlob(agent, path, 'image/jpeg')
|
||||||
|
|
||||||
if (isNative) {
|
if (isNative) {
|
||||||
try {
|
safeDeleteAsync(path)
|
||||||
deleteAsync(path)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push({
|
images.push({
|
||||||
image: res.data.blob,
|
image: res.data.blob,
|
||||||
alt: image.altText ?? '',
|
alt: image.altText ?? '',
|
||||||
@@ -182,13 +176,8 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
|||||||
encoding,
|
encoding,
|
||||||
)
|
)
|
||||||
thumb = thumbUploadRes.data.blob
|
thumb = thumbUploadRes.data.blob
|
||||||
|
if (isNative) {
|
||||||
try {
|
safeDeleteAsync(opts.extLink.localThumb.path)
|
||||||
if (isNative) {
|
|
||||||
deleteAsync(opts.extLink.localThumb.path)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||||
export const PWI_ENABLED = 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'
|
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
|
||||||
export const STAGING_SERVICE = 'https://staging.bsky.dev'
|
export const STAGING_SERVICE = 'https://staging.bsky.dev'
|
||||||
export const BSKY_SERVICE = 'https://bsky.social'
|
export const BSKY_SERVICE = 'https://bsky.social'
|
||||||
|
export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'
|
||||||
export const DEFAULT_SERVICE = BSKY_SERVICE
|
export const DEFAULT_SERVICE = BSKY_SERVICE
|
||||||
const HELP_DESK_LANG = 'en-us'
|
const HELP_DESK_LANG = 'en-us'
|
||||||
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
|
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 as RNImage, Share as RNShare} from 'react-native'
|
||||||
import {Image} from 'react-native-image-crop-picker'
|
import {Image} from 'react-native-image-crop-picker'
|
||||||
import * as RNFS from 'react-native-fs'
|
|
||||||
import uuid from 'react-native-uuid'
|
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 * 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 {isAndroid, isIOS} from 'platform/detection'
|
||||||
|
import {Dimensions} from './types'
|
||||||
|
|
||||||
export async function compressIfNeeded(
|
export async function compressIfNeeded(
|
||||||
img: Image,
|
img: Image,
|
||||||
@@ -23,7 +24,10 @@ export async function compressIfNeeded(
|
|||||||
mode: 'stretch',
|
mode: 'stretch',
|
||||||
maxSize,
|
maxSize,
|
||||||
})
|
})
|
||||||
const finalImageMovedPath = await moveToPermanentPath(resizedImage.path)
|
const finalImageMovedPath = await moveToPermanentPath(
|
||||||
|
resizedImage.path,
|
||||||
|
'.jpg',
|
||||||
|
)
|
||||||
const finalImg = {
|
const finalImg = {
|
||||||
...resizedImage,
|
...resizedImage,
|
||||||
path: finalImageMovedPath,
|
path: finalImageMovedPath,
|
||||||
@@ -63,13 +67,15 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
|||||||
downloadRes = await downloadResPromise
|
downloadRes = await downloadResPromise
|
||||||
clearTimeout(to1)
|
clearTimeout(to1)
|
||||||
|
|
||||||
let localUri = downloadRes.path()
|
const status = downloadRes.info().status
|
||||||
if (!localUri.startsWith('file://')) {
|
if (status !== 200) {
|
||||||
localUri = `file://${localUri}`
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localUri = normalizePath(downloadRes.path(), true)
|
||||||
return await doResize(localUri, opts)
|
return await doResize(localUri, opts)
|
||||||
} finally {
|
} finally {
|
||||||
|
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
||||||
if (downloadRes) {
|
if (downloadRes) {
|
||||||
downloadRes.flush()
|
downloadRes.flush()
|
||||||
}
|
}
|
||||||
@@ -105,7 +111,8 @@ export async function shareImageModal({uri}: {uri: string}) {
|
|||||||
UTI: 'image/png',
|
UTI: 'image/png',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
RNFS.unlink(imagePath)
|
|
||||||
|
safeDeleteAsync(imagePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||||
@@ -122,6 +129,7 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
|||||||
|
|
||||||
// save
|
// save
|
||||||
await MediaLibrary.createAssetAsync(imagePath)
|
await MediaLibrary.createAssetAsync(imagePath)
|
||||||
|
safeDeleteAsync(imagePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageDim(path: string): Promise<Dimensions> {
|
export function getImageDim(path: string): Promise<Dimensions> {
|
||||||
@@ -168,6 +176,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
|
|||||||
width: resizeRes.width,
|
width: resizeRes.width,
|
||||||
height: resizeRes.height,
|
height: resizeRes.height,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
safeDeleteAsync(resizeRes.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(
|
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.
|
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:
|
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 filename = uuid.v4()
|
||||||
|
|
||||||
const destinationPath = joinPath(
|
// cacheDirectory will not ever be null on native, but it could be on web. This function only ever gets called on
|
||||||
RNFS.TemporaryDirectoryPath,
|
// native so we assert as a string.
|
||||||
`${filename}${ext}`,
|
const destinationPath = joinPath(cacheDirectory as string, filename + ext)
|
||||||
)
|
await copyAsync({
|
||||||
await RNFS.moveFile(path, destinationPath)
|
from: normalizePath(path),
|
||||||
|
to: normalizePath(destinationPath),
|
||||||
|
})
|
||||||
|
safeDeleteAsync(path)
|
||||||
return normalizePath(destinationPath)
|
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) {
|
function joinPath(a: string, b: string) {
|
||||||
if (a.endsWith('/')) {
|
if (a.endsWith('/')) {
|
||||||
if (b.startsWith('/')) {
|
if (b.startsWith('/')) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
||||||
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
|
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
|
||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {getAgent, SessionAccount} from '#/state/session'
|
import {SessionAccount} from '#/state/session'
|
||||||
import {track} from 'lib/analytics/analytics'
|
import {track} from 'lib/analytics/analytics'
|
||||||
import {devicePlatform, isIOS} from 'platform/detection'
|
import {devicePlatform, isIOS} from 'platform/detection'
|
||||||
import {resetToTab} from '../../Navigation'
|
import {resetToTab} from '../../Navigation'
|
||||||
@@ -18,6 +19,7 @@ const SERVICE_DID = (serviceUrl?: string) =>
|
|||||||
: 'did:web:api.bsky.app'
|
: 'did:web:api.bsky.app'
|
||||||
|
|
||||||
export async function requestPermissionsAndRegisterToken(
|
export async function requestPermissionsAndRegisterToken(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
) {
|
) {
|
||||||
// request notifications permission once the user has logged in
|
// request notifications permission once the user has logged in
|
||||||
@@ -49,6 +51,7 @@ export async function requestPermissionsAndRegisterToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerTokenChangeHandler(
|
export function registerTokenChangeHandler(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
): () => void {
|
): () => void {
|
||||||
// listens for new changes to the push token
|
// listens for new changes to the push token
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ export type Gate =
|
|||||||
| 'start_session_with_following_v2'
|
| 'start_session_with_following_v2'
|
||||||
| 'test_gate_1'
|
| 'test_gate_1'
|
||||||
| 'test_gate_2'
|
| 'test_gate_2'
|
||||||
| 'use_new_suggestions_endpoint'
|
|
||||||
|
|||||||
@@ -352,8 +352,17 @@ export function parseEmbedPlayerFromUrl(
|
|||||||
|
|
||||||
if (id && filename && dimensions && id.includes('AAAAC')) {
|
if (id && filename && dimensions && id.includes('AAAAC')) {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
id = id.replace('AAAAC', 'AAAP3')
|
const isSafari = /^((?!chrome|android).)*safari/i.test(
|
||||||
filename = filename.replace('.gif', '.webm')
|
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 {
|
} else {
|
||||||
id = id.replace('AAAAC', 'AAAAM')
|
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 {Platform} from 'react-native'
|
||||||
import {getLocales} from 'expo-localization'
|
import {getLocales} from 'expo-localization'
|
||||||
|
|
||||||
import {dedupArray} from 'lib/functions'
|
import {dedupArray} from 'lib/functions'
|
||||||
|
|
||||||
export const isIOS = Platform.OS === 'ios'
|
export const isIOS = Platform.OS === 'ios'
|
||||||
@@ -18,3 +19,8 @@ export const deviceLocales = dedupArray(
|
|||||||
.map?.(locale => locale.languageCode)
|
.map?.(locale => locale.languageCode)
|
||||||
.filter(code => typeof code === 'string'),
|
.filter(code => typeof code === 'string'),
|
||||||
) as 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 React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useLingui} from '@lingui/react'
|
||||||
import {getAgent, isSessionDeactivated, useSessionApi} from '#/state/session'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {pluralize} from '#/lib/strings/helpers'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
import {pluralize} from '#/lib/strings/helpers'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {logger} from '#/logger'
|
||||||
import {Text, P} from '#/components/Typography'
|
|
||||||
import {isWeb} from '#/platform/detection'
|
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 {ScrollView} from '#/view/com/util/Views'
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Logo} from '#/view/icons/Logo'
|
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
|
const COL_WIDTH = 400
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ export function Deactivated() {
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const {logout} = useSessionApi()
|
const {logout} = useSessionApi()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
const [isProcessing, setProcessing] = React.useState(false)
|
const [isProcessing, setProcessing] = React.useState(false)
|
||||||
const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
|
const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
|
||||||
@@ -56,7 +57,13 @@ export function Deactivated() {
|
|||||||
} finally {
|
} finally {
|
||||||
setProcessing(false)
|
setProcessing(false)
|
||||||
}
|
}
|
||||||
}, [setProcessing, setEstimatedTime, setPlaceInQueue, onboardingDispatch])
|
}, [
|
||||||
|
setProcessing,
|
||||||
|
setEstimatedTime,
|
||||||
|
setPlaceInQueue,
|
||||||
|
onboardingDispatch,
|
||||||
|
getAgent,
|
||||||
|
])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
checkStatus()
|
checkStatus()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
|
|||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
|
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
@@ -38,6 +38,7 @@ export function StepFinished() {
|
|||||||
const onboardDispatch = useOnboardingDispatch()
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
const [saving, setSaving] = React.useState(false)
|
const [saving, setSaving] = React.useState(false)
|
||||||
const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation()
|
const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -57,6 +58,7 @@ export function StepFinished() {
|
|||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
bulkWriteFollows(
|
bulkWriteFollows(
|
||||||
|
getAgent,
|
||||||
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
|
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
|
||||||
),
|
),
|
||||||
// these must be serial
|
// these must be serial
|
||||||
@@ -80,7 +82,7 @@ export function StepFinished() {
|
|||||||
track('OnboardingV2:StepFinished:End')
|
track('OnboardingV2:StepFinished:End')
|
||||||
track('OnboardingV2:Complete')
|
track('OnboardingV2:Complete')
|
||||||
logEvent('onboarding:finished:nextPressed', {})
|
logEvent('onboarding:finished:nextPressed', {})
|
||||||
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track])
|
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track, getAgent])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
track('OnboardingV2:StepFinished:Start')
|
track('OnboardingV2:StepFinished:Start')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {useAnalytics} from '#/lib/analytics/analytics'
|
|||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {capitalize} from '#/lib/strings/capitalize'
|
import {capitalize} from '#/lib/strings/capitalize'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
@@ -39,6 +39,7 @@ export function StepInterests() {
|
|||||||
state.interestsStepResults.selectedInterests.map(i => i),
|
state.interestsStepResults.selectedInterests.map(i => i),
|
||||||
)
|
)
|
||||||
const onboardDispatch = useOnboardingDispatch()
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
|
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
|
||||||
queryKey: ['interests'],
|
queryKey: ['interests'],
|
||||||
queryFn: async () => {
|
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 {until} from '#/lib/async/until'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
import {PRIMARY_FEEDS} from './StepAlgoFeeds'
|
import {PRIMARY_FEEDS} from './StepAlgoFeeds'
|
||||||
|
|
||||||
function shuffle(array: any) {
|
function shuffle(array: any) {
|
||||||
@@ -63,7 +66,10 @@ export function aggregateInterestItems(
|
|||||||
return Array.from(new Set(results)).slice(0, 20)
|
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
|
const session = getAgent().session
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -87,10 +93,15 @@ export async function bulkWriteFollows(dids: string[]) {
|
|||||||
repo: session.did,
|
repo: session.did,
|
||||||
writes: followWrites,
|
writes: followWrites,
|
||||||
})
|
})
|
||||||
await whenFollowsIndexed(session.did, res => !!res.data.follows.length)
|
await whenFollowsIndexed(
|
||||||
|
getAgent,
|
||||||
|
session.did,
|
||||||
|
res => !!res.data.follows.length,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function whenFollowsIndexed(
|
async function whenFollowsIndexed(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (res: AppBskyGraphGetFollows.Response) => boolean,
|
fn: (res: AppBskyGraphGetFollows.Response) => boolean,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
|||||||
import {useRequireAuth, useSession} from '#/state/session'
|
import {useRequireAuth, useSession} from '#/state/session'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useHaptics} from 'lib/haptics'
|
import {useHaptics} from 'lib/haptics'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -164,10 +165,12 @@ let ProfileHeaderLabeler = ({
|
|||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
isPlaceholderProfile={isPlaceholderProfile}>
|
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
|
<View
|
||||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_lg]}
|
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_lg]}
|
||||||
pointerEvents="box-none">
|
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Button
|
<Button
|
||||||
testID="profileHeaderEditProfileButton"
|
testID="profileHeaderEditProfileButton"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isIOS, isWeb} from '#/platform/detection'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {
|
||||||
@@ -152,10 +152,12 @@ let ProfileHeaderStandard = ({
|
|||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
isPlaceholderProfile={isPlaceholderProfile}>
|
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
|
<View
|
||||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
|
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
|
||||||
pointerEvents="box-none">
|
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Button
|
<Button
|
||||||
testID="profileHeaderEditProfileButton"
|
testID="profileHeaderEditProfileButton"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {useSession} from '#/state/session'
|
|||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {UserBanner} from 'view/com/util/UserBanner'
|
import {UserBanner} from 'view/com/util/UserBanner'
|
||||||
@@ -61,8 +62,8 @@ let ProfileHeaderShell = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={t.atoms.bg} pointerEvents="box-none">
|
<View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
<View pointerEvents="none">
|
<View pointerEvents={isIOS ? 'auto' : 'none'}>
|
||||||
{isPlaceholderProfile ? (
|
{isPlaceholderProfile ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -80,7 +81,9 @@ let ProfileHeaderShell = ({
|
|||||||
|
|
||||||
{children}
|
{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} />
|
<ProfileHeaderAlerts moderation={moderation} />
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
|
<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 {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {createFullHandle} from '#/lib/strings/handles'
|
import {createFullHandle} from '#/lib/strings/handles'
|
||||||
import {useServiceQuery} from '#/state/queries/service'
|
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 {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
|
||||||
import {
|
import {
|
||||||
initialState,
|
initialState,
|
||||||
@@ -35,6 +35,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
|||||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||||
const submit = useSubmitSignup({state, dispatch})
|
const submit = useSubmitSignup({state, dispatch})
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: serviceInfo,
|
data: serviceInfo,
|
||||||
@@ -113,6 +114,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
|||||||
state.serviceDescription?.phoneVerificationRequired,
|
state.serviceDescription?.phoneVerificationRequired,
|
||||||
state.userDomain,
|
state.userDomain,
|
||||||
submit,
|
submit,
|
||||||
|
getAgent,
|
||||||
])
|
])
|
||||||
|
|
||||||
const onBackPress = React.useCallback(() => {
|
const onBackPress = React.useCallback(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
import {deviceLocales} from '#/platform/detection'
|
import {deviceLocales, prefersReducedMotion} from '#/platform/detection'
|
||||||
|
|
||||||
const externalEmbedOptions = ['show', 'hide'] as const
|
const externalEmbedOptions = ['show', 'hide'] as const
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ const accountSchema = z.object({
|
|||||||
refreshJwt: z.string().optional(), // optional because it can expire
|
refreshJwt: z.string().optional(), // optional because it can expire
|
||||||
accessJwt: z.string().optional(), // optional because it can expire
|
accessJwt: z.string().optional(), // optional because it can expire
|
||||||
deactivated: z.boolean().optional(),
|
deactivated: z.boolean().optional(),
|
||||||
|
pdsUrl: z.string().optional(),
|
||||||
})
|
})
|
||||||
export type PersistedAccount = z.infer<typeof accountSchema>
|
export type PersistedAccount = z.infer<typeof accountSchema>
|
||||||
|
|
||||||
@@ -98,5 +99,5 @@ export const defaults: Schema = {
|
|||||||
lastSelectedHomeFeed: undefined,
|
lastSelectedHomeFeed: undefined,
|
||||||
pdsAddressHistory: [],
|
pdsAddressHistory: [],
|
||||||
disableHaptics: false,
|
disableHaptics: false,
|
||||||
disableAutoplay: false,
|
disableAutoplay: prefersReducedMotion,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {isJustAMute} from '#/lib/moderation'
|
import {isJustAMute} from '#/lib/moderation'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
|
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
|
||||||
|
|
||||||
const DEFAULT_MOD_OPTS = {
|
const DEFAULT_MOD_OPTS = {
|
||||||
@@ -18,6 +18,7 @@ export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
|
|||||||
|
|
||||||
export function useActorAutocompleteQuery(prefix: string) {
|
export function useActorAutocompleteQuery(prefix: string) {
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
prefix = prefix.toLowerCase()
|
prefix = prefix.toLowerCase()
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn>
|
|||||||
export function useActorAutocompleteFn() {
|
export function useActorAutocompleteFn() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
async ({query, limit = 8}: {query: string; limit?: number}) => {
|
async ({query, limit = 8}: {query: string; limit?: number}) => {
|
||||||
@@ -74,7 +76,7 @@ export function useActorAutocompleteFn() {
|
|||||||
moderationOpts || DEFAULT_MOD_OPTS,
|
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 {QueryClient, useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'actor-search'
|
const RQKEY_ROOT = 'actor-search'
|
||||||
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
|
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
|
||||||
@@ -14,6 +14,7 @@ export function useActorSearch({
|
|||||||
query: string
|
query: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyActorDefs.ProfileView[]>({
|
return useQuery<AppBskyActorDefs.ProfileView[]>({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: RQKEY(query || ''),
|
queryKey: RQKEY(query || ''),
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
|
|||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '../session'
|
import {useAgent} from '../session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'app-passwords'
|
const RQKEY_ROOT = 'app-passwords'
|
||||||
export const RQKEY = () => [RQKEY_ROOT]
|
export const RQKEY = () => [RQKEY_ROOT]
|
||||||
|
|
||||||
export function useAppPasswordsQuery() {
|
export function useAppPasswordsQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
queryKey: RQKEY(),
|
queryKey: RQKEY(),
|
||||||
@@ -20,6 +21,7 @@ export function useAppPasswordsQuery() {
|
|||||||
|
|
||||||
export function useAppPasswordCreateMutation() {
|
export function useAppPasswordCreateMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
ComAtprotoServerCreateAppPassword.OutputSchema,
|
ComAtprotoServerCreateAppPassword.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
@@ -42,6 +44,7 @@ export function useAppPasswordCreateMutation() {
|
|||||||
|
|
||||||
export function useAppPasswordDeleteMutation() {
|
export function useAppPasswordDeleteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {name: string}>({
|
return useMutation<void, Error, {name: string}>({
|
||||||
mutationFn: async ({name}) => {
|
mutationFn: async ({name}) => {
|
||||||
await getAgent().com.atproto.server.revokeAppPassword({
|
await getAgent().com.atproto.server.revokeAppPassword({
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
|
|
||||||
export type FeedSourceFeedInfo = {
|
export type FeedSourceFeedInfo = {
|
||||||
@@ -140,6 +140,7 @@ export function getAvatarTypeFromUri(uri: string) {
|
|||||||
|
|
||||||
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
||||||
const type = getFeedTypeFromUri(uri)
|
const type = getFeedTypeFromUri(uri)
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.INFINITY,
|
staleTime: STALE.INFINITY,
|
||||||
@@ -166,6 +167,7 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
|||||||
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
|
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
|
||||||
|
|
||||||
export function useGetPopularFeedsQuery() {
|
export function useGetPopularFeedsQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
|
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
@@ -187,6 +189,7 @@ export function useGetPopularFeedsQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSearchPopularFeedsMutation() {
|
export function useSearchPopularFeedsMutation() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (query: string) => {
|
mutationFn: async (query: string) => {
|
||||||
const res = await getAgent().app.bsky.unspecced.getPopularFeedGenerators({
|
const res = await getAgent().app.bsky.unspecced.getPopularFeedGenerators({
|
||||||
@@ -238,6 +241,7 @@ const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
|||||||
|
|
||||||
export function usePinnedFeedsInfos() {
|
export function usePinnedFeedsInfos() {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
|
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
|
||||||
const pinnedUris = preferences?.feeds?.pinned ?? []
|
const pinnedUris = preferences?.feeds?.pinned ?? []
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const handleQueryKeyRoot = 'handle'
|
const handleQueryKeyRoot = 'handle'
|
||||||
const fetchHandleQueryKey = (handleOrDid: string) => [
|
const fetchHandleQueryKey = (handleOrDid: string) => [
|
||||||
@@ -14,6 +14,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
|
|||||||
|
|
||||||
export function useFetchHandle() {
|
export function useFetchHandle() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
async (handleOrDid: string) => {
|
async (handleOrDid: string) => {
|
||||||
@@ -27,12 +28,13 @@ export function useFetchHandle() {
|
|||||||
}
|
}
|
||||||
return handleOrDid
|
return handleOrDid
|
||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient, getAgent],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateHandleMutation() {
|
export function useUpdateHandleMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({handle}: {handle: string}) => {
|
mutationFn: async ({handle}: {handle: string}) => {
|
||||||
@@ -48,6 +50,7 @@ export function useUpdateHandleMutation() {
|
|||||||
|
|
||||||
export function useFetchDid() {
|
export function useFetchDid() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
async (handleOrDid: string) => {
|
async (handleOrDid: string) => {
|
||||||
@@ -64,6 +67,6 @@ export function useFetchDid() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient, getAgent],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {BskyAgent} from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
|
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||||
|
|
||||||
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
||||||
service: 'https://public.api.bsky.app',
|
service: PUBLIC_BSKY_SERVICE,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const STALE = {
|
export const STALE = {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {useQuery} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
|
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
|
||||||
return invite.available - invite.uses.length > 0 && !invite.disabled
|
return invite.available - invite.uses.length > 0 && !invite.disabled
|
||||||
@@ -16,6 +16,7 @@ export type InviteCodesQueryResponse = Exclude<
|
|||||||
undefined
|
undefined
|
||||||
>
|
>
|
||||||
export function useInviteCodesQuery() {
|
export function useInviteCodesQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
queryKey: [inviteCodesQueryKeyRoot],
|
queryKey: [inviteCodesQueryKeyRoot],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {z} from 'zod'
|
|||||||
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const labelerInfoQueryKeyRoot = 'labeler-info'
|
const labelerInfoQueryKeyRoot = 'labeler-info'
|
||||||
export const labelerInfoQueryKey = (did: string) => [
|
export const labelerInfoQueryKey = (did: string) => [
|
||||||
@@ -31,6 +31,7 @@ export function useLabelerInfoQuery({
|
|||||||
did?: string
|
did?: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!did && enabled !== false,
|
enabled: !!did && enabled !== false,
|
||||||
queryKey: labelerInfoQueryKey(did as string),
|
queryKey: labelerInfoQueryKey(did as string),
|
||||||
@@ -45,6 +46,7 @@ export function useLabelerInfoQuery({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!dids.length,
|
enabled: !!dids.length,
|
||||||
queryKey: labelersInfoQueryKey(dids),
|
queryKey: labelersInfoQueryKey(dids),
|
||||||
@@ -56,6 +58,7 @@ export function useLabelersInfoQuery({dids}: {dids: string[]}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!dids.length,
|
enabled: !!dids.length,
|
||||||
queryKey: labelersDetailedInfoQueryKey(dids),
|
queryKey: labelersDetailedInfoQueryKey(dids),
|
||||||
@@ -73,6 +76,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
|||||||
|
|
||||||
export function useLabelerSubscriptionMutation() {
|
export function useLabelerSubscriptionMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
|
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
export function useLikeMutation() {
|
export function useLikeMutation() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
|
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
|
||||||
const res = await getAgent().like(uri, cid)
|
const res = await getAgent().like(uri, cid)
|
||||||
@@ -12,6 +13,7 @@ export function useLikeMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useUnlikeMutation() {
|
export function useUnlikeMutation() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({uri}: {uri: string}) => {
|
mutationFn: async ({uri}: {uri: string}) => {
|
||||||
await getAgent().deleteLike(uri)
|
await getAgent().deleteLike(uri)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'list-members'
|
|||||||
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
||||||
|
|
||||||
export function useListMembersQuery(uri: string) {
|
export function useListMembersQuery(uri: string) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetList.OutputSchema,
|
AppBskyGraphGetList.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
|
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
|
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
|
||||||
const SANITY_PAGE_LIMIT = 1000
|
const SANITY_PAGE_LIMIT = 1000
|
||||||
@@ -40,6 +40,7 @@ export interface ListMembersip {
|
|||||||
*/
|
*/
|
||||||
export function useDangerousListMembershipsQuery() {
|
export function useDangerousListMembershipsQuery() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<ListMembersip[]>({
|
return useQuery<ListMembersip[]>({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
queryKey: RQKEY(),
|
queryKey: RQKEY(),
|
||||||
@@ -91,6 +92,7 @@ export function getMembership(
|
|||||||
|
|
||||||
export function useListMembershipAddMutation() {
|
export function useListMembershipAddMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
{uri: string; cid: string},
|
{uri: string; cid: string},
|
||||||
@@ -149,6 +151,7 @@ export function useListMembershipAddMutation() {
|
|||||||
|
|
||||||
export function useListMembershipRemoveMutation() {
|
export function useListMembershipRemoveMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
void,
|
void,
|
||||||
|
|||||||
+49
-21
@@ -4,6 +4,7 @@ import {
|
|||||||
AppBskyGraphGetList,
|
AppBskyGraphGetList,
|
||||||
AppBskyGraphList,
|
AppBskyGraphList,
|
||||||
AtUri,
|
AtUri,
|
||||||
|
BskyAgent,
|
||||||
Facet,
|
Facet,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
@@ -12,7 +13,7 @@ import chunk from 'lodash.chunk'
|
|||||||
import {uploadBlob} from '#/lib/api'
|
import {uploadBlob} from '#/lib/api'
|
||||||
import {until} from '#/lib/async/until'
|
import {until} from '#/lib/async/until'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent, useSession} from '../session'
|
import {useAgent, useSession} from '../session'
|
||||||
import {invalidate as invalidateMyLists} from './my-lists'
|
import {invalidate as invalidateMyLists} from './my-lists'
|
||||||
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-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 const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
|
||||||
|
|
||||||
export function useListQuery(uri?: string) {
|
export function useListQuery(uri?: string) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyGraphDefs.ListView, Error>({
|
return useQuery<AppBskyGraphDefs.ListView, Error>({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: RQKEY(uri || ''),
|
queryKey: RQKEY(uri || ''),
|
||||||
@@ -47,6 +49,7 @@ export interface ListCreateMutateParams {
|
|||||||
export function useListCreateMutation() {
|
export function useListCreateMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
|
return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
|
||||||
{
|
{
|
||||||
async mutationFn({
|
async mutationFn({
|
||||||
@@ -85,9 +88,13 @@ export function useListCreateMutation() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// wait for the appview to update
|
// wait for the appview to update
|
||||||
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
|
await whenAppViewReady(
|
||||||
return typeof v?.data?.list.uri === 'string'
|
getAgent,
|
||||||
})
|
res.uri,
|
||||||
|
(v: AppBskyGraphGetList.Response) => {
|
||||||
|
return typeof v?.data?.list.uri === 'string'
|
||||||
|
},
|
||||||
|
)
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
@@ -109,6 +116,7 @@ export interface ListMetadataMutateParams {
|
|||||||
}
|
}
|
||||||
export function useListMetadataMutation() {
|
export function useListMetadataMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
{uri: string; cid: string},
|
{uri: string; cid: string},
|
||||||
@@ -150,12 +158,16 @@ export function useListMetadataMutation() {
|
|||||||
).data
|
).data
|
||||||
|
|
||||||
// wait for the appview to update
|
// wait for the appview to update
|
||||||
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
|
await whenAppViewReady(
|
||||||
const list = v.data.list
|
getAgent,
|
||||||
return (
|
res.uri,
|
||||||
list.name === record.name && list.description === record.description
|
(v: AppBskyGraphGetList.Response) => {
|
||||||
)
|
const list = v.data.list
|
||||||
})
|
return (
|
||||||
|
list.name === record.name && list.description === record.description
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
onSuccess(data, variables) {
|
onSuccess(data, variables) {
|
||||||
@@ -172,6 +184,7 @@ export function useListMetadataMutation() {
|
|||||||
|
|
||||||
export function useListDeleteMutation() {
|
export function useListDeleteMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<void, Error, {uri: string}>({
|
return useMutation<void, Error, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
@@ -220,9 +233,13 @@ export function useListDeleteMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for the appview to update
|
// wait for the appview to update
|
||||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
await whenAppViewReady(
|
||||||
return !v?.success
|
getAgent,
|
||||||
})
|
uri,
|
||||||
|
(v: AppBskyGraphGetList.Response) => {
|
||||||
|
return !v?.success
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
invalidateMyLists(queryClient)
|
invalidateMyLists(queryClient)
|
||||||
@@ -236,6 +253,7 @@ export function useListDeleteMutation() {
|
|||||||
|
|
||||||
export function useListMuteMutation() {
|
export function useListMuteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {uri: string; mute: boolean}>({
|
return useMutation<void, Error, {uri: string; mute: boolean}>({
|
||||||
mutationFn: async ({uri, mute}) => {
|
mutationFn: async ({uri, mute}) => {
|
||||||
if (mute) {
|
if (mute) {
|
||||||
@@ -244,9 +262,13 @@ export function useListMuteMutation() {
|
|||||||
await getAgent().unmuteModList(uri)
|
await getAgent().unmuteModList(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
await whenAppViewReady(
|
||||||
return Boolean(v?.data.list.viewer?.muted) === mute
|
getAgent,
|
||||||
})
|
uri,
|
||||||
|
(v: AppBskyGraphGetList.Response) => {
|
||||||
|
return Boolean(v?.data.list.viewer?.muted) === mute
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onSuccess(data, variables) {
|
onSuccess(data, variables) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -258,6 +280,7 @@ export function useListMuteMutation() {
|
|||||||
|
|
||||||
export function useListBlockMutation() {
|
export function useListBlockMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {uri: string; block: boolean}>({
|
return useMutation<void, Error, {uri: string; block: boolean}>({
|
||||||
mutationFn: async ({uri, block}) => {
|
mutationFn: async ({uri, block}) => {
|
||||||
if (block) {
|
if (block) {
|
||||||
@@ -266,11 +289,15 @@ export function useListBlockMutation() {
|
|||||||
await getAgent().unblockModList(uri)
|
await getAgent().unblockModList(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
|
await whenAppViewReady(
|
||||||
return block
|
getAgent,
|
||||||
? typeof v?.data.list.viewer?.blocked === 'string'
|
uri,
|
||||||
: !v?.data.list.viewer?.blocked
|
(v: AppBskyGraphGetList.Response) => {
|
||||||
})
|
return block
|
||||||
|
? typeof v?.data.list.viewer?.blocked === 'string'
|
||||||
|
: !v?.data.list.viewer?.blocked
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onSuccess(data, variables) {
|
onSuccess(data, variables) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -281,6 +308,7 @@ export function useListBlockMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
uri: string,
|
uri: string,
|
||||||
fn: (res: AppBskyGraphGetList.Response) => boolean,
|
fn: (res: AppBskyGraphGetList.Response) => boolean,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'my-blocked-accounts'
|
const RQKEY_ROOT = 'my-blocked-accounts'
|
||||||
export const RQKEY = () => [RQKEY_ROOT]
|
export const RQKEY = () => [RQKEY_ROOT]
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
export function useMyBlockedAccountsQuery() {
|
export function useMyBlockedAccountsQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetBlocks.OutputSchema,
|
AppBskyGraphGetBlocks.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {QueryClient, useQuery} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {accumulate} from '#/lib/async/accumulate'
|
import {accumulate} from '#/lib/async/accumulate'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
|
||||||
export type MyListsFilter =
|
export type MyListsFilter =
|
||||||
| 'all'
|
| 'all'
|
||||||
@@ -16,6 +16,7 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
|
|||||||
|
|
||||||
export function useMyListsQuery(filter: MyListsFilter) {
|
export function useMyListsQuery(filter: MyListsFilter) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyGraphDefs.ListView[]>({
|
return useQuery<AppBskyGraphDefs.ListView[]>({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: RQKEY(filter),
|
queryKey: RQKEY(filter),
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'my-muted-accounts'
|
const RQKEY_ROOT = 'my-muted-accounts'
|
||||||
export const RQKEY = () => [RQKEY_ROOT]
|
export const RQKEY = () => [RQKEY_ROOT]
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
export function useMyMutedAccountsQuery() {
|
export function useMyMutedAccountsQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetMutes.OutputSchema,
|
AppBskyGraphGetMutes.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useMutedThreads} from '#/state/muted-threads'
|
import {useMutedThreads} from '#/state/muted-threads'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
import {STALE} from '..'
|
import {STALE} from '..'
|
||||||
import {useModerationOpts} from '../preferences'
|
import {useModerationOpts} from '../preferences'
|
||||||
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
|
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
|
||||||
@@ -46,6 +47,7 @@ export function RQKEY() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const threadMutes = useMutedThreads()
|
const threadMutes = useMutedThreads()
|
||||||
@@ -71,6 +73,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||||||
if (!page) {
|
if (!page) {
|
||||||
page = (
|
page = (
|
||||||
await fetchPage({
|
await fetchPage({
|
||||||
|
getAgent,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
cursor: pageParam,
|
cursor: pageParam,
|
||||||
queryClient,
|
queryClient,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import BroadcastChannel from '#/lib/broadcast'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useMutedThreads} from '#/state/muted-threads'
|
import {useMutedThreads} from '#/state/muted-threads'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useModerationOpts} from '../preferences'
|
import {useModerationOpts} from '../preferences'
|
||||||
import {truncateAndInvalidate} from '../util'
|
import {truncateAndInvalidate} from '../util'
|
||||||
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
||||||
@@ -46,6 +46,7 @@ const apiContext = React.createContext<ApiContext>({
|
|||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const threadMutes = useMutedThreads()
|
const threadMutes = useMutedThreads()
|
||||||
@@ -144,6 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
// count
|
// count
|
||||||
const {page, indexedAt: lastIndexed} = await fetchPage({
|
const {page, indexedAt: lastIndexed} = await fetchPage({
|
||||||
|
getAgent,
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
limit: 40,
|
limit: 40,
|
||||||
queryClient,
|
queryClient,
|
||||||
@@ -196,7 +198,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}, [setNumUnread, queryClient, moderationOpts, threadMutes])
|
}, [setNumUnread, queryClient, moderationOpts, threadMutes, getAgent])
|
||||||
checkUnreadRef.current = api.checkUnread
|
checkUnreadRef.current = api.checkUnread
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyNotificationListNotifications,
|
AppBskyEmbedRecord,
|
||||||
ModerationOpts,
|
|
||||||
moderateNotification,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
|
AppBskyFeedLike,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedLike,
|
AppBskyNotificationListNotifications,
|
||||||
AppBskyEmbedRecord,
|
BskyAgent,
|
||||||
|
moderateNotification,
|
||||||
|
ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import chunk from 'lodash.chunk'
|
|
||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
import {getAgent} from '../../session'
|
import chunk from 'lodash.chunk'
|
||||||
|
|
||||||
import {precacheProfile} from '../profile'
|
import {precacheProfile} from '../profile'
|
||||||
import {NotificationType, FeedNotification, FeedPage} from './types'
|
import {FeedNotification, FeedPage, NotificationType} from './types'
|
||||||
|
|
||||||
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||||
const MS_1HR = 1e3 * 60 * 60
|
const MS_1HR = 1e3 * 60 * 60
|
||||||
@@ -22,6 +23,7 @@ const MS_2DAY = MS_1HR * 48
|
|||||||
// =
|
// =
|
||||||
|
|
||||||
export async function fetchPage({
|
export async function fetchPage({
|
||||||
|
getAgent,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
queryClient,
|
queryClient,
|
||||||
@@ -29,6 +31,7 @@ export async function fetchPage({
|
|||||||
threadMutes,
|
threadMutes,
|
||||||
fetchAdditionalData,
|
fetchAdditionalData,
|
||||||
}: {
|
}: {
|
||||||
|
getAgent: () => BskyAgent
|
||||||
cursor: string | undefined
|
cursor: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
@@ -53,7 +56,7 @@ export async function fetchPage({
|
|||||||
// we fetch subjects of notifications (usually posts) now instead of lazily
|
// we fetch subjects of notifications (usually posts) now instead of lazily
|
||||||
// in the UI to avoid relayouts
|
// in the UI to avoid relayouts
|
||||||
if (fetchAdditionalData) {
|
if (fetchAdditionalData) {
|
||||||
const subjects = await fetchSubjects(notifsGrouped)
|
const subjects = await fetchSubjects(getAgent, notifsGrouped)
|
||||||
for (const notif of notifsGrouped) {
|
for (const notif of notifsGrouped) {
|
||||||
if (notif.subjectUri) {
|
if (notif.subjectUri) {
|
||||||
notif.subject = subjects.get(notif.subjectUri)
|
notif.subject = subjects.get(notif.subjectUri)
|
||||||
@@ -137,6 +140,7 @@ export function groupNotifications(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSubjects(
|
async function fetchSubjects(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
groupedNotifs: FeedNotification[],
|
groupedNotifs: FeedNotification[],
|
||||||
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
||||||
const uris = new Set<string>()
|
const uris = new Set<string>()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AtUri,
|
AtUri,
|
||||||
|
BskyAgent,
|
||||||
ModerationDecision,
|
ModerationDecision,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +20,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
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 {AuthorFeedAPI} from 'lib/api/feed/author'
|
||||||
import {CustomFeedAPI} from 'lib/api/feed/custom'
|
import {CustomFeedAPI} from 'lib/api/feed/custom'
|
||||||
import {FollowingFeedAPI} from 'lib/api/feed/following'
|
import {FollowingFeedAPI} from 'lib/api/feed/following'
|
||||||
@@ -104,6 +105,7 @@ export function usePostFeedQuery(
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const feedTuners = useFeedTuners(feedDesc)
|
const feedTuners = useFeedTuners(feedDesc)
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
|
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
|
||||||
const lastRun = useRef<{
|
const lastRun = useRef<{
|
||||||
data: InfiniteData<FeedPageUnselected>
|
data: InfiniteData<FeedPageUnselected>
|
||||||
@@ -135,11 +137,15 @@ export function usePostFeedQuery(
|
|||||||
queryKey: RQKEY(feedDesc, params),
|
queryKey: RQKEY(feedDesc, params),
|
||||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor})
|
logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor})
|
||||||
|
|
||||||
const {api, cursor} = pageParam
|
const {api, cursor} = pageParam
|
||||||
? pageParam
|
? pageParam
|
||||||
: {
|
: {
|
||||||
api: createApi(feedDesc, params || {}, feedTuners),
|
api: createApi({
|
||||||
|
feedDesc,
|
||||||
|
feedParams: params || {},
|
||||||
|
feedTuners,
|
||||||
|
getAgent,
|
||||||
|
}),
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,34 +371,47 @@ export async function pollLatest(page: FeedPage | undefined) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApi(
|
function createApi({
|
||||||
feedDesc: FeedDescriptor,
|
feedDesc,
|
||||||
params: FeedParams,
|
feedParams,
|
||||||
feedTuners: FeedTunerFn[],
|
feedTuners,
|
||||||
) {
|
getAgent,
|
||||||
|
}: {
|
||||||
|
feedDesc: FeedDescriptor
|
||||||
|
feedParams: FeedParams
|
||||||
|
feedTuners: FeedTunerFn[]
|
||||||
|
getAgent: () => BskyAgent
|
||||||
|
}) {
|
||||||
if (feedDesc === 'home') {
|
if (feedDesc === 'home') {
|
||||||
if (params.mergeFeedEnabled) {
|
if (feedParams.mergeFeedEnabled) {
|
||||||
return new MergeFeedAPI(params, feedTuners)
|
return new MergeFeedAPI({
|
||||||
|
getAgent,
|
||||||
|
feedParams,
|
||||||
|
feedTuners,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return new HomeFeedAPI()
|
return new HomeFeedAPI({getAgent})
|
||||||
}
|
}
|
||||||
} else if (feedDesc === 'following') {
|
} else if (feedDesc === 'following') {
|
||||||
return new FollowingFeedAPI()
|
return new FollowingFeedAPI({getAgent})
|
||||||
} else if (feedDesc.startsWith('author')) {
|
} else if (feedDesc.startsWith('author')) {
|
||||||
const [_, actor, filter] = feedDesc.split('|')
|
const [_, actor, filter] = feedDesc.split('|')
|
||||||
return new AuthorFeedAPI({actor, filter})
|
return new AuthorFeedAPI({getAgent, feedParams: {actor, filter}})
|
||||||
} else if (feedDesc.startsWith('likes')) {
|
} else if (feedDesc.startsWith('likes')) {
|
||||||
const [_, actor] = feedDesc.split('|')
|
const [_, actor] = feedDesc.split('|')
|
||||||
return new LikesFeedAPI({actor})
|
return new LikesFeedAPI({getAgent, feedParams: {actor}})
|
||||||
} else if (feedDesc.startsWith('feedgen')) {
|
} else if (feedDesc.startsWith('feedgen')) {
|
||||||
const [_, feed] = feedDesc.split('|')
|
const [_, feed] = feedDesc.split('|')
|
||||||
return new CustomFeedAPI({feed})
|
return new CustomFeedAPI({
|
||||||
|
getAgent,
|
||||||
|
feedParams: {feed},
|
||||||
|
})
|
||||||
} else if (feedDesc.startsWith('list')) {
|
} else if (feedDesc.startsWith('list')) {
|
||||||
const [_, list] = feedDesc.split('|')
|
const [_, list] = feedDesc.split('|')
|
||||||
return new ListFeedAPI({list})
|
return new ListFeedAPI({getAgent, feedParams: {list}})
|
||||||
} else {
|
} else {
|
||||||
// shouldnt happen
|
// shouldnt happen
|
||||||
return new FollowingFeedAPI()
|
return new FollowingFeedAPI({getAgent})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'liked-by'
|
|||||||
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
||||||
|
|
||||||
export function useLikedByQuery(resolvedUri: string | undefined) {
|
export function useLikedByQuery(resolvedUri: string | undefined) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyFeedGetLikes.OutputSchema,
|
AppBskyFeedGetLikes.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'post-reposted-by'
|
|||||||
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
||||||
|
|
||||||
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
|
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyFeedGetRepostedBy.OutputSchema,
|
AppBskyFeedGetRepostedBy.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
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 findAllPostsInSearchQueryData} from 'state/queries/search-posts'
|
||||||
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
|
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
|
||||||
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
|
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
|
||||||
@@ -66,6 +66,7 @@ export type ThreadNode =
|
|||||||
|
|
||||||
export function usePostThreadQuery(uri: string | undefined) {
|
export function usePostThreadQuery(uri: string | undefined) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<ThreadNode, Error>({
|
return useQuery<ThreadNode, Error>({
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
queryKey: RQKEY(uri || ''),
|
queryKey: RQKEY(uri || ''),
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
|
|||||||
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||||
import {updatePostShadow} from '#/state/cache/post-shadow'
|
import {updatePostShadow} from '#/state/cache/post-shadow'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {findProfileQueryData} from './profile'
|
import {findProfileQueryData} from './profile'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'post'
|
const RQKEY_ROOT = 'post'
|
||||||
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
|
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
|
||||||
|
|
||||||
export function usePostQuery(uri: string | undefined) {
|
export function usePostQuery(uri: string | undefined) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyFeedDefs.PostView>({
|
return useQuery<AppBskyFeedDefs.PostView>({
|
||||||
queryKey: RQKEY(uri || ''),
|
queryKey: RQKEY(uri || ''),
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
@@ -30,6 +31,7 @@ export function usePostQuery(uri: string | undefined) {
|
|||||||
|
|
||||||
export function useGetPost() {
|
export function useGetPost() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async ({uri}: {uri: string}) => {
|
async ({uri}: {uri: string}) => {
|
||||||
return queryClient.fetchQuery({
|
return queryClient.fetchQuery({
|
||||||
@@ -56,7 +58,7 @@ export function useGetPost() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient, getAgent],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +127,7 @@ function usePostLikeMutation(
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const postAuthor = post.author
|
const postAuthor = post.author
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
{uri: string}, // responds with the uri of the like
|
{uri: string}, // responds with the uri of the like
|
||||||
Error,
|
Error,
|
||||||
@@ -162,6 +165,7 @@ function usePostLikeMutation(
|
|||||||
function usePostUnlikeMutation(
|
function usePostUnlikeMutation(
|
||||||
logContext: LogEvents['post:unlike']['logContext'],
|
logContext: LogEvents['post:unlike']['logContext'],
|
||||||
) {
|
) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {postUri: string; likeUri: string}>({
|
return useMutation<void, Error, {postUri: string; likeUri: string}>({
|
||||||
mutationFn: ({likeUri}) => {
|
mutationFn: ({likeUri}) => {
|
||||||
logEvent('post:unlike', {logContext})
|
logEvent('post:unlike', {logContext})
|
||||||
@@ -234,6 +238,7 @@ export function usePostRepostMutationQueue(
|
|||||||
function usePostRepostMutation(
|
function usePostRepostMutation(
|
||||||
logContext: LogEvents['post:repost']['logContext'],
|
logContext: LogEvents['post:repost']['logContext'],
|
||||||
) {
|
) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<
|
return useMutation<
|
||||||
{uri: string}, // responds with the uri of the repost
|
{uri: string}, // responds with the uri of the repost
|
||||||
Error,
|
Error,
|
||||||
@@ -252,6 +257,7 @@ function usePostRepostMutation(
|
|||||||
function usePostUnrepostMutation(
|
function usePostUnrepostMutation(
|
||||||
logContext: LogEvents['post:unrepost']['logContext'],
|
logContext: LogEvents['post:unrepost']['logContext'],
|
||||||
) {
|
) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {postUri: string; repostUri: string}>({
|
return useMutation<void, Error, {postUri: string; repostUri: string}>({
|
||||||
mutationFn: ({repostUri}) => {
|
mutationFn: ({repostUri}) => {
|
||||||
logEvent('post:unrepost', {logContext})
|
logEvent('post:unrepost', {logContext})
|
||||||
@@ -265,6 +271,7 @@ function usePostUnrepostMutation(
|
|||||||
|
|
||||||
export function usePostDeleteMutation() {
|
export function usePostDeleteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {uri: string}>({
|
return useMutation<void, Error, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
await getAgent().deletePost(uri)
|
await getAgent().deletePost(uri)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
ThreadViewPreferences,
|
ThreadViewPreferences,
|
||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
} from '#/state/queries/preferences/types'
|
} from '#/state/queries/preferences/types'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {saveLabelers} from '#/state/session/agent-config'
|
import {saveLabelers} from '#/state/session/agent-config'
|
||||||
|
|
||||||
export * from '#/state/queries/preferences/const'
|
export * from '#/state/queries/preferences/const'
|
||||||
@@ -33,6 +33,7 @@ const preferencesQueryKeyRoot = 'getPreferences'
|
|||||||
export const preferencesQueryKey = [preferencesQueryKeyRoot]
|
export const preferencesQueryKey = [preferencesQueryKeyRoot]
|
||||||
|
|
||||||
export function usePreferencesQuery() {
|
export function usePreferencesQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.SECONDS.FIFTEEN,
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
structuralSharing: true,
|
structuralSharing: true,
|
||||||
@@ -118,6 +119,7 @@ export function useModerationOpts() {
|
|||||||
|
|
||||||
export function useClearPreferencesMutation() {
|
export function useClearPreferencesMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -131,6 +133,7 @@ export function useClearPreferencesMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePreferencesSetContentLabelMutation() {
|
export function usePreferencesSetContentLabelMutation() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
@@ -150,6 +153,7 @@ export function usePreferencesSetContentLabelMutation() {
|
|||||||
|
|
||||||
export function useSetContentLabelMutation() {
|
export function useSetContentLabelMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
@@ -172,6 +176,7 @@ export function useSetContentLabelMutation() {
|
|||||||
|
|
||||||
export function usePreferencesSetAdultContentMutation() {
|
export function usePreferencesSetAdultContentMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {enabled: boolean}>({
|
return useMutation<void, unknown, {enabled: boolean}>({
|
||||||
mutationFn: async ({enabled}) => {
|
mutationFn: async ({enabled}) => {
|
||||||
@@ -186,6 +191,7 @@ export function usePreferencesSetAdultContentMutation() {
|
|||||||
|
|
||||||
export function usePreferencesSetBirthDateMutation() {
|
export function usePreferencesSetBirthDateMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {birthDate: Date}>({
|
return useMutation<void, unknown, {birthDate: Date}>({
|
||||||
mutationFn: async ({birthDate}: {birthDate: Date}) => {
|
mutationFn: async ({birthDate}: {birthDate: Date}) => {
|
||||||
@@ -200,6 +206,7 @@ export function usePreferencesSetBirthDateMutation() {
|
|||||||
|
|
||||||
export function useSetFeedViewPreferencesMutation() {
|
export function useSetFeedViewPreferencesMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
|
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
|
||||||
mutationFn: async prefs => {
|
mutationFn: async prefs => {
|
||||||
@@ -214,6 +221,7 @@ export function useSetFeedViewPreferencesMutation() {
|
|||||||
|
|
||||||
export function useSetThreadViewPreferencesMutation() {
|
export function useSetThreadViewPreferencesMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
|
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
|
||||||
mutationFn: async prefs => {
|
mutationFn: async prefs => {
|
||||||
@@ -228,6 +236,7 @@ export function useSetThreadViewPreferencesMutation() {
|
|||||||
|
|
||||||
export function useSetSaveFeedsMutation() {
|
export function useSetSaveFeedsMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
void,
|
void,
|
||||||
@@ -246,6 +255,7 @@ export function useSetSaveFeedsMutation() {
|
|||||||
|
|
||||||
export function useSaveFeedMutation() {
|
export function useSaveFeedMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {uri: string}>({
|
return useMutation<void, unknown, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
@@ -261,6 +271,7 @@ export function useSaveFeedMutation() {
|
|||||||
|
|
||||||
export function useRemoveFeedMutation() {
|
export function useRemoveFeedMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {uri: string}>({
|
return useMutation<void, unknown, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
@@ -276,6 +287,7 @@ export function useRemoveFeedMutation() {
|
|||||||
|
|
||||||
export function usePinFeedMutation() {
|
export function usePinFeedMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {uri: string}>({
|
return useMutation<void, unknown, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
@@ -291,6 +303,7 @@ export function usePinFeedMutation() {
|
|||||||
|
|
||||||
export function useUnpinFeedMutation() {
|
export function useUnpinFeedMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation<void, unknown, {uri: string}>({
|
return useMutation<void, unknown, {uri: string}>({
|
||||||
mutationFn: async ({uri}) => {
|
mutationFn: async ({uri}) => {
|
||||||
@@ -306,6 +319,7 @@ export function useUnpinFeedMutation() {
|
|||||||
|
|
||||||
export function useUpsertMutedWordsMutation() {
|
export function useUpsertMutedWordsMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
|
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
|
||||||
@@ -320,6 +334,7 @@ export function useUpsertMutedWordsMutation() {
|
|||||||
|
|
||||||
export function useUpdateMutedWordMutation() {
|
export function useUpdateMutedWordMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
||||||
@@ -334,6 +349,7 @@ export function useUpdateMutedWordMutation() {
|
|||||||
|
|
||||||
export function useRemoveMutedWordMutation() {
|
export function useRemoveMutedWordMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
|
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
|
||||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -15,6 +15,7 @@ export function useProfileFeedgensQuery(
|
|||||||
opts?: {enabled?: boolean},
|
opts?: {enabled?: boolean},
|
||||||
) {
|
) {
|
||||||
const enabled = opts?.enabled !== false
|
const enabled = opts?.enabled !== false
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyFeedGetActorFeeds.OutputSchema,
|
AppBskyFeedGetActorFeeds.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -15,6 +15,7 @@ const RQKEY_ROOT = 'profile-followers'
|
|||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useProfileFollowersQuery(did: string | undefined) {
|
export function useProfileFollowersQuery(did: string | undefined) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetFollowers.OutputSchema,
|
AppBskyGraphGetFollowers.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -17,6 +17,7 @@ const RQKEY_ROOT = 'profile-follows'
|
|||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useProfileFollowsQuery(did: string | undefined) {
|
export function useProfileFollowsQuery(did: string | undefined) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetFollows.OutputSchema,
|
AppBskyGraphGetFollows.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {AppBskyGraphGetLists} from '@atproto/api'
|
import {AppBskyGraphGetLists} from '@atproto/api'
|
||||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
@@ -11,6 +11,7 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
|||||||
|
|
||||||
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
||||||
const enabled = opts?.enabled !== false
|
const enabled = opts?.enabled !== false
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetLists.OutputSchema,
|
AppBskyGraphGetLists.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
|
BskyAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
@@ -25,7 +26,7 @@ import {Shadow} from '#/state/cache/types'
|
|||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||||
import {updateProfileShadow} from '../cache/profile-shadow'
|
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_BLOCKED} from './my-blocked-accounts'
|
||||||
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
||||||
import {ThreadNode} from './post-thread'
|
import {ThreadNode} from './post-thread'
|
||||||
@@ -53,6 +54,7 @@ export function useProfileQuery({
|
|||||||
staleTime?: number
|
staleTime?: number
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
|
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
|
||||||
// WARNING
|
// WARNING
|
||||||
// this staleTime is load-bearing
|
// this staleTime is load-bearing
|
||||||
@@ -77,6 +79,7 @@ export function useProfileQuery({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useProfilesQuery({handles}: {handles: string[]}) {
|
export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
queryKey: profilesQueryKey(handles),
|
queryKey: profilesQueryKey(handles),
|
||||||
@@ -88,6 +91,7 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePrefetchProfileQuery() {
|
export function usePrefetchProfileQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const prefetchProfileQuery = useCallback(
|
const prefetchProfileQuery = useCallback(
|
||||||
async (did: string) => {
|
async (did: string) => {
|
||||||
@@ -99,7 +103,7 @@ export function usePrefetchProfileQuery() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient, getAgent],
|
||||||
)
|
)
|
||||||
return prefetchProfileQuery
|
return prefetchProfileQuery
|
||||||
}
|
}
|
||||||
@@ -115,6 +119,7 @@ interface ProfileUpdateParams {
|
|||||||
}
|
}
|
||||||
export function useProfileUpdateMutation() {
|
export function useProfileUpdateMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, ProfileUpdateParams>({
|
return useMutation<void, Error, ProfileUpdateParams>({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
profile,
|
profile,
|
||||||
@@ -154,6 +159,7 @@ export function useProfileUpdateMutation() {
|
|||||||
return existing
|
return existing
|
||||||
})
|
})
|
||||||
await whenAppViewReady(
|
await whenAppViewReady(
|
||||||
|
getAgent,
|
||||||
profile.did,
|
profile.did,
|
||||||
checkCommitted ||
|
checkCommitted ||
|
||||||
(res => {
|
(res => {
|
||||||
@@ -255,6 +261,7 @@ function useProfileFollowMutation(
|
|||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
|
||||||
) {
|
) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||||
mutationFn: async ({did}) => {
|
mutationFn: async ({did}) => {
|
||||||
@@ -281,6 +288,7 @@ function useProfileFollowMutation(
|
|||||||
function useProfileUnfollowMutation(
|
function useProfileUnfollowMutation(
|
||||||
logContext: LogEvents['profile:unfollow']['logContext'],
|
logContext: LogEvents['profile:unfollow']['logContext'],
|
||||||
) {
|
) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {did: string; followUri: string}>({
|
return useMutation<void, Error, {did: string; followUri: string}>({
|
||||||
mutationFn: async ({followUri}) => {
|
mutationFn: async ({followUri}) => {
|
||||||
logEvent('profile:unfollow', {logContext})
|
logEvent('profile:unfollow', {logContext})
|
||||||
@@ -341,6 +349,7 @@ export function useProfileMuteMutationQueue(
|
|||||||
|
|
||||||
function useProfileMuteMutation() {
|
function useProfileMuteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {did: string}>({
|
return useMutation<void, Error, {did: string}>({
|
||||||
mutationFn: async ({did}) => {
|
mutationFn: async ({did}) => {
|
||||||
await getAgent().mute(did)
|
await getAgent().mute(did)
|
||||||
@@ -353,6 +362,7 @@ function useProfileMuteMutation() {
|
|||||||
|
|
||||||
function useProfileUnmuteMutation() {
|
function useProfileUnmuteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useMutation<void, Error, {did: string}>({
|
return useMutation<void, Error, {did: string}>({
|
||||||
mutationFn: async ({did}) => {
|
mutationFn: async ({did}) => {
|
||||||
await getAgent().unmute(did)
|
await getAgent().unmute(did)
|
||||||
@@ -419,6 +429,7 @@ export function useProfileBlockMutationQueue(
|
|||||||
|
|
||||||
function useProfileBlockMutation() {
|
function useProfileBlockMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||||
mutationFn: async ({did}) => {
|
mutationFn: async ({did}) => {
|
||||||
@@ -439,6 +450,7 @@ function useProfileBlockMutation() {
|
|||||||
|
|
||||||
function useProfileUnblockMutation() {
|
function useProfileUnblockMutation() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation<void, Error, {did: string; blockUri: string}>({
|
return useMutation<void, Error, {did: string; blockUri: string}>({
|
||||||
mutationFn: async ({blockUri}) => {
|
mutationFn: async ({blockUri}) => {
|
||||||
@@ -516,6 +528,7 @@ export function precacheThreadPostProfiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
|
getAgent: () => BskyAgent,
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {AppBskyActorDefs, AtUri} from '@atproto/api'
|
|||||||
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
|
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
|
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'resolved-did'
|
const RQKEY_ROOT = 'resolved-did'
|
||||||
@@ -24,6 +24,7 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
|
|||||||
|
|
||||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
return useQuery<string, Error>({
|
return useQuery<string, Error>({
|
||||||
staleTime: STALE.HOURS.ONE,
|
staleTime: STALE.HOURS.ONE,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
|
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
|
||||||
|
|
||||||
const searchPostsQueryKeyRoot = 'search-posts'
|
const searchPostsQueryKeyRoot = 'search-posts'
|
||||||
@@ -25,6 +25,7 @@ export function useSearchPostsQuery({
|
|||||||
sort?: 'top' | 'latest'
|
sort?: 'top' | 'latest'
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyFeedSearchPosts.OutputSchema,
|
AppBskyFeedSearchPosts.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import {AppBskyFeedGetSuggestedFeeds} from '@atproto/api'
|
|||||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {getAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const suggestedFeedsQueryKeyRoot = 'suggestedFeeds'
|
const suggestedFeedsQueryKeyRoot = 'suggestedFeeds'
|
||||||
export const suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]
|
export const suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]
|
||||||
|
|
||||||
export function useSuggestedFeedsQuery() {
|
export function useSuggestedFeedsQuery() {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyFeedGetSuggestedFeeds.OutputSchema,
|
AppBskyFeedGetSuggestedFeeds.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
AppBskyActorGetSuggestions,
|
AppBskyActorGetSuggestions,
|
||||||
@@ -11,12 +10,11 @@ import {
|
|||||||
QueryKey,
|
QueryKey,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useModerationOpts} from '#/state/queries/preferences'
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
|
||||||
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
|
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
|
||||||
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
|
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
|
||||||
@@ -29,6 +27,7 @@ const suggestedFollowsByActorQueryKey = (did: string) => [
|
|||||||
|
|
||||||
export function useSuggestedFollowsQuery() {
|
export function useSuggestedFollowsQuery() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
@@ -79,6 +78,7 @@ export function useSuggestedFollowsQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
||||||
queryKey: suggestedFollowsByActorQueryKey(did),
|
queryKey: suggestedFollowsByActorQueryKey(did),
|
||||||
queryFn: async () => {
|
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(
|
export function* findAllProfilesInQueryData(
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
did: string,
|
did: string,
|
||||||
|
|||||||
+25
-11
@@ -23,16 +23,14 @@ import {readLabelers} from './agent-config'
|
|||||||
|
|
||||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||||
|
|
||||||
/**
|
function __getAgent() {
|
||||||
* NOTE
|
|
||||||
* Never hold on to the object returned by this function.
|
|
||||||
* Call `getAgent()` at the time of invocation to ensure
|
|
||||||
* that you never have a stale agent.
|
|
||||||
*/
|
|
||||||
export function getAgent() {
|
|
||||||
return __globalAgent
|
return __globalAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAgent() {
|
||||||
|
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionAccount = persisted.PersistedAccount
|
export type SessionAccount = persisted.PersistedAccount
|
||||||
|
|
||||||
export type SessionState = {
|
export type SessionState = {
|
||||||
@@ -117,6 +115,7 @@ const ApiContext = React.createContext<ApiContext>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createPersistSessionHandler(
|
function createPersistSessionHandler(
|
||||||
|
agent: BskyAgent,
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
persistSessionCallback: (props: {
|
persistSessionCallback: (props: {
|
||||||
expired: boolean
|
expired: boolean
|
||||||
@@ -144,6 +143,7 @@ function createPersistSessionHandler(
|
|||||||
email: session?.email || account.email,
|
email: session?.email || account.email,
|
||||||
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
||||||
deactivated: isSessionDeactivated(session?.accessJwt),
|
deactivated: isSessionDeactivated(session?.accessJwt),
|
||||||
|
pdsUrl: agent.pdsUrl?.toString(),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tokens are undefined if the session expires, or if creation fails for
|
* 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,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
deactivated,
|
deactivated,
|
||||||
|
pdsUrl: agent.pdsUrl?.toString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await configureModeration(agent, account)
|
await configureModeration(agent, account)
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(
|
createPersistSessionHandler(
|
||||||
|
agent,
|
||||||
account,
|
account,
|
||||||
({expired, refreshedAccount}) => {
|
({expired, refreshedAccount}) => {
|
||||||
upsertAccount(refreshedAccount, expired)
|
upsertAccount(refreshedAccount, expired)
|
||||||
@@ -327,12 +329,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
refreshJwt: agent.session.refreshJwt,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||||
|
pdsUrl: agent.pdsUrl?.toString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await configureModeration(agent, account)
|
await configureModeration(agent, account)
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(
|
createPersistSessionHandler(
|
||||||
|
agent,
|
||||||
account,
|
account,
|
||||||
({expired, refreshedAccount}) => {
|
({expired, refreshedAccount}) => {
|
||||||
upsertAccount(refreshedAccount, expired)
|
upsertAccount(refreshedAccount, expired)
|
||||||
@@ -379,16 +383,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
|
||||||
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
|
||||||
|
|
||||||
const agent = new BskyAgent({
|
const agent = new BskyAgent({service: account.service})
|
||||||
service: account.service,
|
|
||||||
persistSession: createPersistSessionHandler(
|
// 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,
|
account,
|
||||||
({expired, refreshedAccount}) => {
|
({expired, refreshedAccount}) => {
|
||||||
upsertAccount(refreshedAccount, expired)
|
upsertAccount(refreshedAccount, expired)
|
||||||
},
|
},
|
||||||
{networkErrorCallback: clearCurrentAccount},
|
{networkErrorCallback: clearCurrentAccount},
|
||||||
),
|
),
|
||||||
})
|
)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (IS_DEV && isWeb) window.agent = agent
|
if (IS_DEV && isWeb) window.agent = agent
|
||||||
await configureModeration(agent, account)
|
await configureModeration(agent, account)
|
||||||
@@ -421,6 +433,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logger.debug(`session: attempting to reuse previous session`)
|
logger.debug(`session: attempting to reuse previous session`)
|
||||||
|
|
||||||
agent.session = prevSession
|
agent.session = prevSession
|
||||||
|
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
await fetchingGates
|
await fetchingGates
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
@@ -498,6 +511,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
refreshJwt: agent.session.refreshJwt,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
deactivated: isSessionDeactivated(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 {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {Gif} from '#/state/queries/tenor'
|
import {Gif} from '#/state/queries/tenor'
|
||||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
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 {useComposerControls} from '#/state/shell/composer'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
@@ -53,7 +53,7 @@ import {atoms as a} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||||
import * as Prompt from '#/components/Prompt'
|
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 {Text} from '../util/text/Text'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
@@ -83,6 +83,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
imageUris: initImageUris,
|
imageUris: initImageUris,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
||||||
const {isModalActive} = useModals()
|
const {isModalActive} = useModals()
|
||||||
const {closeComposer} = useComposerControls()
|
const {closeComposer} = useComposerControls()
|
||||||
@@ -483,8 +484,13 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{quote ? (
|
{quote ? (
|
||||||
<View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
|
<View style={[s.mt5, isWeb && s.mb10]}>
|
||||||
<QuoteEmbed quote={quote} />
|
<View style={{pointerEvents: 'none'}}>
|
||||||
|
<QuoteEmbed quote={quote} />
|
||||||
|
</View>
|
||||||
|
{quote.uri !== initQuote?.uri && (
|
||||||
|
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
|||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {isIOS} from 'platform/detection'
|
import {isIOS} from 'platform/detection'
|
||||||
import {
|
import {
|
||||||
addLinkCardIfNecessary,
|
LinkFacetMatch,
|
||||||
findIndexInText,
|
suggestLinkCardUri,
|
||||||
} from 'view/com/composer/text-input/text-input-util'
|
} from 'view/com/composer/text-input/text-input-util'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {Autocomplete} from './mobile/Autocomplete'
|
import {Autocomplete} from './mobile/Autocomplete'
|
||||||
@@ -73,7 +73,6 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
||||||
const prevLength = React.useRef(richtext.length)
|
const prevLength = React.useRef(richtext.length)
|
||||||
const prevAddedLinks = useRef(new Set<string>())
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => textInput.current?.focus(),
|
focus: () => textInput.current?.focus(),
|
||||||
@@ -83,6 +82,8 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
getCursorPosition: () => undefined, // Not implemented on native
|
getCursorPosition: () => undefined, // Not implemented on native
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const pastSuggestedUris = useRef(new Set<string>())
|
||||||
|
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
|
||||||
const onChangeText = useCallback(
|
const onChangeText = useCallback(
|
||||||
(newText: string) => {
|
(newText: string) => {
|
||||||
/*
|
/*
|
||||||
@@ -112,6 +113,7 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
setAutocompletePrefix('')
|
setAutocompletePrefix('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextDetectedUris = new Map<string, LinkFacetMatch>()
|
||||||
if (newRt.facets) {
|
if (newRt.facets) {
|
||||||
for (const facet of newRt.facets) {
|
for (const facet of newRt.facets) {
|
||||||
for (const feature of facet.features) {
|
for (const feature of facet.features) {
|
||||||
@@ -130,32 +132,26 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
onPhotoPasted(res.path)
|
onPhotoPasted(res.path)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const cursorLocation = textInputSelection.current.end
|
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
|
||||||
|
|
||||||
addLinkCardIfNecessary({
|
|
||||||
uri: feature.uri,
|
|
||||||
newText,
|
|
||||||
cursorLocation,
|
|
||||||
mayBePaste,
|
|
||||||
onNewLink,
|
|
||||||
prevAddedLinks: prevAddedLinks.current,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const suggestedUri = suggestLinkCardUri(
|
||||||
for (const uri of prevAddedLinks.current.keys()) {
|
mayBePaste,
|
||||||
if (findIndexInText(uri, newText) === -1) {
|
nextDetectedUris,
|
||||||
prevAddedLinks.current.delete(uri)
|
prevDetectedUris.current,
|
||||||
}
|
pastSuggestedUris.current,
|
||||||
|
)
|
||||||
|
prevDetectedUris.current = nextDetectedUris
|
||||||
|
if (suggestedUri) {
|
||||||
|
onNewLink(suggestedUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevLength.current = newText.length
|
prevLength.current = newText.length
|
||||||
}, 1)
|
}, 1)
|
||||||
},
|
},
|
||||||
[setRichText, autocompletePrefix, onPhotoPasted, prevAddedLinks, onNewLink],
|
[setRichText, autocompletePrefix, onPhotoPasted, onNewLink],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPaste = useCallback(
|
const onPaste = useCallback(
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
|||||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||||
import {blobToDataUri, isUriImage} from 'lib/media/util'
|
import {blobToDataUri, isUriImage} from 'lib/media/util'
|
||||||
import {
|
import {
|
||||||
addLinkCardIfNecessary,
|
LinkFacetMatch,
|
||||||
findIndexInText,
|
suggestLinkCardUri,
|
||||||
} from 'view/com/composer/text-input/text-input-util'
|
} from 'view/com/composer/text-input/text-input-util'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
@@ -61,9 +61,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const autocomplete = useActorAutocompleteFn()
|
const autocomplete = useActorAutocompleteFn()
|
||||||
const prevLength = React.useRef(0)
|
|
||||||
const prevAddedLinks = useRef(new Set<string>())
|
|
||||||
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
||||||
|
|
||||||
@@ -144,6 +141,8 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
|||||||
}
|
}
|
||||||
}, [setIsDropping])
|
}, [setIsDropping])
|
||||||
|
|
||||||
|
const pastSuggestedUris = useRef(new Set<string>())
|
||||||
|
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
|
||||||
const editor = useEditor(
|
const editor = useEditor(
|
||||||
{
|
{
|
||||||
extensions,
|
extensions,
|
||||||
@@ -185,42 +184,34 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
|||||||
},
|
},
|
||||||
onUpdate({editor: editorProp}) {
|
onUpdate({editor: editorProp}) {
|
||||||
const json = editorProp.getJSON()
|
const json = editorProp.getJSON()
|
||||||
const newText = editorJsonToText(json).trimEnd()
|
const newText = editorJsonToText(json)
|
||||||
const mayBePaste = newText.length > prevLength.current + 1
|
const isPaste = window.event?.type === 'paste'
|
||||||
|
|
||||||
const newRt = new RichText({text: newText})
|
const newRt = new RichText({text: newText})
|
||||||
newRt.detectFacetsWithoutResolution()
|
newRt.detectFacetsWithoutResolution()
|
||||||
setRichText(newRt)
|
setRichText(newRt)
|
||||||
|
|
||||||
|
const nextDetectedUris = new Map<string, LinkFacetMatch>()
|
||||||
if (newRt.facets) {
|
if (newRt.facets) {
|
||||||
for (const facet of newRt.facets) {
|
for (const facet of newRt.facets) {
|
||||||
for (const feature of facet.features) {
|
for (const feature of facet.features) {
|
||||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||||
// The TipTap editor shows the position as being one character ahead, as if the start index is 1.
|
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const uri of prevAddedLinks.current.keys()) {
|
const suggestedUri = suggestLinkCardUri(
|
||||||
if (findIndexInText(uri, newText) === -1) {
|
isPaste,
|
||||||
prevAddedLinks.current.delete(uri)
|
nextDetectedUris,
|
||||||
}
|
prevDetectedUris.current,
|
||||||
|
pastSuggestedUris.current,
|
||||||
|
)
|
||||||
|
prevDetectedUris.current = nextDetectedUris
|
||||||
|
if (suggestedUri) {
|
||||||
|
onNewLink(suggestedUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevLength.current = newText.length
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[modeClass],
|
[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 = ''
|
let text = ''
|
||||||
if (json.type === 'doc' || json.type === 'paragraph') {
|
if (json.type === 'doc') {
|
||||||
if (json.content?.length) {
|
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 += editorJsonToText(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text += '\n'
|
if (!isLastDocumentChild) {
|
||||||
|
text += '\n'
|
||||||
|
}
|
||||||
} else if (json.type === 'hardBreak') {
|
} else if (json.type === 'hardBreak') {
|
||||||
text += '\n'
|
text += '\n'
|
||||||
} else if (json.type === 'text') {
|
} else if (json.type === 'text') {
|
||||||
|
|||||||
@@ -1,41 +1,85 @@
|
|||||||
export function addLinkCardIfNecessary({
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
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
|
|
||||||
|
|
||||||
// If the text being added is not from a paste, then we should only check if the cursor is one
|
export type LinkFacetMatch = {
|
||||||
// position ahead of the last character. However, if it is a paste we need to check both if it's
|
rt: RichText
|
||||||
// the same position _or_ one position ahead. That is because iOS will add a space after a paste if
|
facet: AppBskyRichtextFacet.Main
|
||||||
// pasting into the middle of a sentence!
|
}
|
||||||
const cursorLocationIsOkay =
|
|
||||||
cursorLocation === lastCharacterPosition + 1 || mayBePaste
|
|
||||||
|
|
||||||
// Checking previouslyAddedLinks keeps a card from getting added over and over i.e.
|
export function suggestLinkCardUri(
|
||||||
// Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on
|
mayBePaste: boolean,
|
||||||
|
nextDetectedUris: Map<string, LinkFacetMatch>,
|
||||||
// We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e.
|
prevDetectedUris: Map<string, LinkFacetMatch>,
|
||||||
// http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if
|
pastSuggestedUris: Set<string>,
|
||||||
// the url is a valid url _and_ domain. new URL() won't work for this check.
|
): string | undefined {
|
||||||
const shouldCheck =
|
const suggestedUris = new Set<string>()
|
||||||
cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri)
|
for (const [uri, nextMatch] of nextDetectedUris) {
|
||||||
|
if (!isValidUrlAndDomain(uri)) {
|
||||||
if (shouldCheck) {
|
continue
|
||||||
onNewLink(uri)
|
}
|
||||||
prevAddedLinks.add(uri)
|
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
|
// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
|
||||||
@@ -46,14 +90,3 @@ function isValidUrlAndDomain(value: string) {
|
|||||||
value,
|
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.
|
* 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 {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'
|
import {isValidDomain} from 'lib/strings/url-helpers'
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ function iterateUris(str: string, cb: (from: number, to: number) => void) {
|
|||||||
uri = `https://${uri}`
|
uri = `https://${uri}`
|
||||||
}
|
}
|
||||||
let from = str.indexOf(match[2], match.index)
|
let from = str.indexOf(match[2], match.index)
|
||||||
let to = from + match[2].length + 1
|
let to = from + match[2].length
|
||||||
// strip ending puncuation
|
// strip ending puncuation
|
||||||
if (/[.,;!?]$/.test(uri)) {
|
if (/[.,;!?]$/.test(uri)) {
|
||||||
uri = uri.slice(0, -1)
|
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 * as apilib from 'lib/api/index'
|
||||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||||
import {ComposerOpts} from 'state/shell/composer'
|
import {ComposerOpts} from 'state/shell/composer'
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export function useExternalLinkFetch({}: {
|
export function useExternalLinkFetch({}: {
|
||||||
setQuote: (opts: ComposerOpts['quote']) => void
|
setQuote: (opts: ComposerOpts['quote']) => void
|
||||||
}) {
|
}) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
|
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
@@ -39,7 +41,7 @@ export function useExternalLinkFetch({}: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [extLink])
|
}, [extLink, getAgent])
|
||||||
|
|
||||||
return {extLink, setExtLink}
|
return {extLink, setExtLink}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import {useState, useEffect} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {ImageModel} from 'state/models/media/image'
|
|
||||||
|
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 * as apilib from 'lib/api/index'
|
||||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
import {POST_IMG_MAX} from 'lib/constants'
|
||||||
import {
|
import {
|
||||||
getPostAsQuote,
|
|
||||||
getFeedAsEmbed,
|
getFeedAsEmbed,
|
||||||
getListAsEmbed,
|
getListAsEmbed,
|
||||||
|
getPostAsQuote,
|
||||||
} from 'lib/link-meta/bsky'
|
} from 'lib/link-meta/bsky'
|
||||||
|
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||||
import {downloadAndResize} from 'lib/media/manip'
|
import {downloadAndResize} from 'lib/media/manip'
|
||||||
import {
|
import {
|
||||||
isBskyPostUrl,
|
|
||||||
isBskyCustomFeedUrl,
|
isBskyCustomFeedUrl,
|
||||||
isBskyListUrl,
|
isBskyListUrl,
|
||||||
|
isBskyPostUrl,
|
||||||
} from 'lib/strings/url-helpers'
|
} from 'lib/strings/url-helpers'
|
||||||
|
import {ImageModel} from 'state/models/media/image'
|
||||||
import {ComposerOpts} from 'state/shell/composer'
|
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({
|
export function useExternalLinkFetch({
|
||||||
setQuote,
|
setQuote,
|
||||||
@@ -30,6 +31,7 @@ export function useExternalLinkFetch({
|
|||||||
)
|
)
|
||||||
const getPost = useGetPost()
|
const getPost = useGetPost()
|
||||||
const fetchDid = useFetchDid()
|
const fetchDid = useFetchDid()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let aborted = false
|
let aborted = false
|
||||||
@@ -135,7 +137,7 @@ export function useExternalLinkFetch({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [extLink, setQuote, getPost, fetchDid])
|
}, [extLink, setQuote, getPost, fetchDid, getAgent])
|
||||||
|
|
||||||
return {extLink, setExtLink}
|
return {extLink, setExtLink}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
|
||||||
import {ScrollView, TextInput} from './util'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {Text} from '../util/text/Text'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Button} from '../util/forms/Button'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {useModalControls} from '#/state/modals'
|
||||||
import * as Toast from '../util/Toast'
|
import {useAgent, useSession, useSessionApi} from '#/state/session'
|
||||||
import {s, colors} from 'lib/styles'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {colors, s} from 'lib/styles'
|
||||||
import {useLingui} from '@lingui/react'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {useSession, useSessionApi, getAgent} from '#/state/session'
|
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 {
|
enum Stages {
|
||||||
InputEmail,
|
InputEmail,
|
||||||
@@ -26,6 +27,7 @@ export const snapPoints = ['90%']
|
|||||||
export function Component() {
|
export function Component() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
const {updateCurrentAccount} = useSessionApi()
|
const {updateCurrentAccount} = useSessionApi()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
|
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