Send FeedFeedback interactions in thread view (#8414)

This commit is contained in:
Samuel Newman
2025-05-28 22:09:28 +03:00
committed by GitHub
parent 665a0430a3
commit cf63c2ca07
15 changed files with 365 additions and 124 deletions
+2 -2
View File
@@ -69,7 +69,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.15.8", "@atproto/api": "^0.15.9",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -219,7 +219,7 @@
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@atproto/dev-env": "^0.3.132", "@atproto/dev-env": "^0.3.133",
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
+11 -8
View File
@@ -58,6 +58,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
import {TestCtrls} from '#/view/com/testing/TestCtrls' import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@@ -150,14 +151,16 @@ function InnerApp() {
<MutedThreadsProvider> <MutedThreadsProvider>
<ProgressGuideProvider> <ProgressGuideProvider>
<ServiceAccountManager> <ServiceAccountManager>
<GestureHandlerRootView <UnstablePostSourceProvider>
style={s.h100pct}> <GestureHandlerRootView
<IntentDialogProvider> style={s.h100pct}>
<TestCtrls /> <IntentDialogProvider>
<Shell /> <TestCtrls />
<NuxDialogs /> <Shell />
</IntentDialogProvider> <NuxDialogs />
</GestureHandlerRootView> </IntentDialogProvider>
</GestureHandlerRootView>
</UnstablePostSourceProvider>
</ServiceAccountManager> </ServiceAccountManager>
</ProgressGuideProvider> </ProgressGuideProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
+7 -4
View File
@@ -48,6 +48,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@@ -131,10 +132,12 @@ function InnerApp() {
<SafeAreaProvider> <SafeAreaProvider>
<ProgressGuideProvider> <ProgressGuideProvider>
<ServiceConfigProvider> <ServiceConfigProvider>
<IntentDialogProvider> <UnstablePostSourceProvider>
<Shell /> <IntentDialogProvider>
<NuxDialogs /> <Shell />
</IntentDialogProvider> <NuxDialogs />
</IntentDialogProvider>
</UnstablePostSourceProvider>
</ServiceConfigProvider> </ServiceConfigProvider>
</ProgressGuideProvider> </ProgressGuideProvider>
</SafeAreaProvider> </SafeAreaProvider>
+8 -1
View File
@@ -50,6 +50,7 @@ let PostControls = ({
logContext, logContext,
threadgateRecord, threadgateRecord,
onShowLess, onShowLess,
viaRepost,
}: { }: {
big?: boolean big?: boolean
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
@@ -63,13 +64,19 @@ let PostControls = ({
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
viaRepost?: {uri: string; cid: string}
}): React.ReactNode => { }): React.ReactNode => {
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) const [queueLike, queueUnlike] = usePostLikeMutationQueue(
post,
viaRepost,
logContext,
)
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
post, post,
viaRepost,
logContext, logContext,
) )
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
+6 -1
View File
@@ -1023,7 +1023,12 @@ function PlayPauseTapArea({
const {_} = useLingui() const {_} = useLingui()
const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null) const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const playHaptic = useHaptics() const playHaptic = useHaptics()
const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') // TODO: implement viaRepost -sfn
const [queueLike] = usePostLikeMutationQueue(
post,
undefined,
'ImmersiveVideo',
)
const {sendInteraction} = useFeedFeedbackContext() const {sendInteraction} = useFeedFeedbackContext()
const {isPlaying} = useEvent(player, 'playingChange', { const {isPlaying} = useEvent(player, 'playingChange', {
isPlaying: player.playing, isPlaying: player.playing,
+32 -19
View File
@@ -1,4 +1,11 @@
import React from 'react' import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react'
import {AppState, type AppStateStatus} from 'react-native' import {AppState, type AppStateStatus} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
@@ -13,31 +20,36 @@ import {
import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAgent} from './session' import {useAgent} from './session'
type StateContext = { export type StateContext = {
enabled: boolean enabled: boolean
onItemSeen: (item: any) => void onItemSeen: (item: any) => void
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
feedDescriptor: FeedDescriptor | undefined
} }
const stateContext = React.createContext<StateContext>({ const stateContext = createContext<StateContext>({
enabled: false, enabled: false,
onItemSeen: (_item: any) => {}, onItemSeen: (_item: any) => {},
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
feedDescriptor: undefined,
}) })
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { export function useFeedFeedback(
feed: FeedDescriptor | undefined,
hasSession: boolean,
) {
const agent = useAgent() const agent = useAgent()
const enabled = isDiscoverFeed(feed) && hasSession const enabled = isDiscoverFeed(feed) && hasSession
const queue = React.useRef<Set<string>>(new Set()) const queue = useRef<Set<string>>(new Set())
const history = React.useRef< const history = useRef<
// Use a WeakSet so that we don't need to clear it. // Use a WeakSet so that we don't need to clear it.
// This assumes that referential identity of slice items maps 1:1 to feed (re)fetches. // This assumes that referential identity of slice items maps 1:1 to feed (re)fetches.
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction> WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
>(new WeakSet()) >(new WeakSet())
const aggregatedStats = React.useRef<AggregatedStats | null>(null) const aggregatedStats = useRef<AggregatedStats | null>(null)
const throttledFlushAggregatedStats = React.useMemo( const throttledFlushAggregatedStats = useMemo(
() => () =>
throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
leading: true, // The outer call is already throttled somewhat. leading: true, // The outer call is already throttled somewhat.
@@ -46,12 +58,12 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
[], [],
) )
const sendToFeedNoDelay = React.useCallback(() => { const sendToFeedNoDelay = useCallback(() => {
const interactions = Array.from(queue.current).map(toInteraction) const interactions = Array.from(queue.current).map(toInteraction)
queue.current.clear() queue.current.clear()
let proxyDid = 'did:web:discover.bsky.app' let proxyDid = 'did:web:discover.bsky.app'
if (STAGING_FEEDS.includes(feed)) { if (STAGING_FEEDS.includes(feed ?? '')) {
proxyDid = 'did:web:algo.pop2.bsky.app' proxyDid = 'did:web:algo.pop2.bsky.app'
} }
@@ -79,7 +91,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
}, [agent, throttledFlushAggregatedStats, feed]) }, [agent, throttledFlushAggregatedStats, feed])
const sendToFeed = React.useMemo( const sendToFeed = useMemo(
() => () =>
throttle(sendToFeedNoDelay, 10e3, { throttle(sendToFeedNoDelay, 10e3, {
leading: false, leading: false,
@@ -88,7 +100,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
[sendToFeedNoDelay], [sendToFeedNoDelay],
) )
React.useEffect(() => { useEffect(() => {
if (!enabled) { if (!enabled) {
return return
} }
@@ -100,7 +112,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
return () => sub.remove() return () => sub.remove()
}, [enabled, sendToFeed]) }, [enabled, sendToFeed])
const onItemSeen = React.useCallback( const onItemSeen = useCallback(
(feedItem: any) => { (feedItem: any) => {
if (!enabled) { if (!enabled) {
return return
@@ -124,7 +136,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
[enabled, sendToFeed], [enabled, sendToFeed],
) )
const sendInteraction = React.useCallback( const sendInteraction = useCallback(
(interaction: AppBskyFeedDefs.Interaction) => { (interaction: AppBskyFeedDefs.Interaction) => {
if (!enabled) { if (!enabled) {
return return
@@ -138,7 +150,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
[enabled, sendToFeed], [enabled, sendToFeed],
) )
return React.useMemo(() => { return useMemo(() => {
return { return {
enabled, enabled,
// pass this method to the <List> onItemSeen // pass this method to the <List> onItemSeen
@@ -146,14 +158,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
// call on various events // call on various events
// queues the event to be sent with the throttled sendToFeed call // queues the event to be sent with the throttled sendToFeed call
sendInteraction, sendInteraction,
feedDescriptor: feed,
} }
}, [enabled, onItemSeen, sendInteraction]) }, [enabled, onItemSeen, sendInteraction, feed])
} }
export const FeedFeedbackProvider = stateContext.Provider export const FeedFeedbackProvider = stateContext.Provider
export function useFeedFeedbackContext() { export function useFeedFeedbackContext() {
return React.useContext(stateContext) return useContext(stateContext)
} }
// TODO // TODO
@@ -161,8 +174,8 @@ export function useFeedFeedbackContext() {
// take advantage of the feed feedback API. Until that's in // take advantage of the feed feedback API. Until that's in
// place, we're hardcoding it to the discover feed. // place, we're hardcoding it to the discover feed.
// -prf // -prf
function isDiscoverFeed(feed: FeedDescriptor) { function isDiscoverFeed(feed?: FeedDescriptor) {
return FEEDBACK_FEEDS.includes(feed) return !!feed && FEEDBACK_FEEDS.includes(feed)
} }
function toString(interaction: AppBskyFeedDefs.Interaction): string { function toString(interaction: AppBskyFeedDefs.Interaction): string {
+2
View File
@@ -46,6 +46,8 @@ type OtherNotificationType =
| 'feedgen-like' | 'feedgen-like'
| 'verified' | 'verified'
| 'unverified' | 'unverified'
| 'like-via-repost'
| 'repost-via-repost'
| 'unknown' | 'unknown'
type FeedNotificationBase = { type FeedNotificationBase = {
+9 -2
View File
@@ -244,7 +244,9 @@ function toKnownType(
notif.reason === 'follow' || notif.reason === 'follow' ||
notif.reason === 'starterpack-joined' || notif.reason === 'starterpack-joined' ||
notif.reason === 'verified' || notif.reason === 'verified' ||
notif.reason === 'unverified' notif.reason === 'unverified' ||
notif.reason === 'like-via-repost' ||
notif.reason === 'repost-via-repost'
) { ) {
return notif.reason as NotificationType return notif.reason as NotificationType
} }
@@ -257,7 +259,12 @@ function getSubjectUri(
): string | undefined { ): string | undefined {
if (type === 'reply' || type === 'quote' || type === 'mention') { if (type === 'reply' || type === 'quote' || type === 'mention') {
return notif.uri return notif.uri
} else if (type === 'post-like' || type === 'repost') { } else if (
type === 'post-like' ||
type === 'repost' ||
type === 'like-via-repost' ||
type === 'repost-via-repost'
) {
if ( if (
bsky.dangerousIsType<AppBskyFeedRepost.Record>( bsky.dangerousIsType<AppBskyFeedRepost.Record>(
notif.record, notif.record,
+13 -9
View File
@@ -1,11 +1,11 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig' import {logEvent, type 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 {type Shadow} from '#/state/cache/types'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory' import * as userActionHistory from '#/state/userActionHistory'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
@@ -98,6 +98,7 @@ export function useGetPosts() {
export function usePostLikeMutationQueue( export function usePostLikeMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>, post: Shadow<AppBskyFeedDefs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
logContext: LogEvents['post:like']['logContext'] & logContext: LogEvents['post:like']['logContext'] &
LogEvents['post:unlike']['logContext'], LogEvents['post:unlike']['logContext'],
) { ) {
@@ -115,6 +116,7 @@ export function usePostLikeMutationQueue(
const {uri: likeUri} = await likeMutation.mutateAsync({ const {uri: likeUri} = await likeMutation.mutateAsync({
uri: postUri, uri: postUri,
cid: postCid, cid: postCid,
via: viaRepost,
}) })
userActionHistory.like([postUri]) userActionHistory.like([postUri])
return likeUri return likeUri
@@ -167,9 +169,9 @@ function usePostLikeMutation(
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the like {uri: string}, // responds with the uri of the like
Error, Error,
{uri: string; cid: string} // the post's uri and cid {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({ >({
mutationFn: ({uri, cid}) => { mutationFn: ({uri, cid, via}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) { if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did) ownProfile = findProfileQueryData(queryClient, currentAccount.did)
@@ -190,7 +192,7 @@ function usePostLikeMutation(
? toClout(post.likeCount + post.repostCount + post.replyCount) ? toClout(post.likeCount + post.repostCount + post.replyCount)
: undefined, : undefined,
}) })
return agent.like(uri, cid) return agent.like(uri, cid, via)
}, },
}) })
} }
@@ -209,6 +211,7 @@ function usePostUnlikeMutation(
export function usePostRepostMutationQueue( export function usePostRepostMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>, post: Shadow<AppBskyFeedDefs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
logContext: LogEvents['post:repost']['logContext'] & logContext: LogEvents['post:repost']['logContext'] &
LogEvents['post:unrepost']['logContext'], LogEvents['post:unrepost']['logContext'],
) { ) {
@@ -226,6 +229,7 @@ export function usePostRepostMutationQueue(
const {uri: repostUri} = await repostMutation.mutateAsync({ const {uri: repostUri} = await repostMutation.mutateAsync({
uri: postUri, uri: postUri,
cid: postCid, cid: postCid,
via: viaRepost,
}) })
return repostUri return repostUri
} else { } else {
@@ -272,11 +276,11 @@ function usePostRepostMutation(
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the repost {uri: string}, // responds with the uri of the repost
Error, Error,
{uri: string; cid: string} // the post's uri and cid {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({ >({
mutationFn: post => { mutationFn: ({uri, cid, via}) => {
logEvent('post:repost', {logContext}) logEvent('post:repost', {logContext})
return agent.repost(post.uri, post.cid) return agent.repost(uri, cid, via)
}, },
}) })
} }
+73
View File
@@ -0,0 +1,73 @@
import {createContext, useCallback, useContext, useState} from 'react'
import {type AppBskyFeedDefs} from '@atproto/api'
import {type FeedDescriptor} from './queries/post-feed'
/**
* For passing the source of the post (i.e. the original post, from the feed) to the threadview,
* without using query params. Deliberately unstable to avoid using query params, use for FeedFeedback
* and other ephemeral non-critical systems.
*/
type Source = {
post: AppBskyFeedDefs.FeedViewPost
feed?: FeedDescriptor
}
const SetUnstablePostSourceContext = createContext<
(key: string, source: Source) => void
>(() => {})
const ConsumeUnstablePostSourceContext = createContext<
(uri: string) => Source | undefined
>(() => undefined)
export function Provider({children}: {children: React.ReactNode}) {
const [sources, setSources] = useState<Map<string, Source>>(() => new Map())
const setUnstablePostSource = useCallback((key: string, source: Source) => {
setSources(prev => {
const newMap = new Map(prev)
newMap.set(key, source)
return newMap
})
}, [])
const consumeUnstablePostSource = useCallback(
(uri: string) => {
const source = sources.get(uri)
if (source) {
setSources(prev => {
const newMap = new Map(prev)
newMap.delete(uri)
return newMap
})
}
return source
},
[sources],
)
return (
<SetUnstablePostSourceContext.Provider value={setUnstablePostSource}>
<ConsumeUnstablePostSourceContext.Provider
value={consumeUnstablePostSource}>
{children}
</ConsumeUnstablePostSourceContext.Provider>
</SetUnstablePostSourceContext.Provider>
)
}
export function useSetUnstablePostSource() {
return useContext(SetUnstablePostSourceContext)
}
/**
* DANGER - This hook is unstable and should only be used for FeedFeedback
* and other ephemeral non-critical systems. Does not change when the URI changes.
*/
export function useUnstablePostSource(uri: string) {
const consume = useContext(ConsumeUnstablePostSourceContext)
const [source] = useState(() => consume(uri))
return source
}
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
ListRenderItemInfo, type ListRenderItemInfo,
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } from 'react-native'
@@ -16,7 +16,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {EmptyState} from '#/view/com/util/EmptyState' import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, ListRef} from '#/view/com/util/List' import {List, type ListRef} from '#/view/com/util/List'
import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {NotificationFeedItem} from './NotificationFeedItem' import {NotificationFeedItem} from './NotificationFeedItem'
@@ -446,6 +446,55 @@ let NotificationFeedItem = ({
</Trans> </Trans>
) )
icon = <VerifiedCheck size="xl" fill={t.palette.contrast_500} /> icon = <VerifiedCheck size="xl" fill={t.palette.contrast_500} />
} else if (item.type === 'like-via-repost') {
a11yLabel = hasMultipleAuthors
? _(
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
one: `${formattedAuthorsCount} other`,
other: `${formattedAuthorsCount} others`,
})} liked your repost`,
)
: _(msg`${firstAuthorName} liked your repost`)
notificationContent = hasMultipleAuthors ? (
<Trans>
{firstAuthorLink} and{' '}
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
<Plural
value={additionalAuthorsCount}
one={`${formattedAuthorsCount} other`}
other={`${formattedAuthorsCount} others`}
/>
</Text>{' '}
liked your repost
</Trans>
) : (
<Trans>{firstAuthorLink} liked your repost</Trans>
)
} else if (item.type === 'repost-via-repost') {
a11yLabel = hasMultipleAuthors
? _(
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
one: `${formattedAuthorsCount} other`,
other: `${formattedAuthorsCount} others`,
})} reposted your repost`,
)
: _(msg`${firstAuthorName} reposted your repost`)
notificationContent = hasMultipleAuthors ? (
<Trans>
{firstAuthorLink} and{' '}
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
<Plural
value={additionalAuthorsCount}
one={`${formattedAuthorsCount} other`}
other={`${formattedAuthorsCount} others`}
/>
</Text>{' '}
reposted your repost
</Trans>
) : (
<Trans>{firstAuthorLink} reposted your repost</Trans>
)
icon = <RepostIcon size="xl" style={{color: t.palette.positive_600}} />
} else { } else {
return null return null
} }
@@ -553,7 +602,10 @@ let NotificationFeedItem = ({
</TimeElapsed> </TimeElapsed>
</Text> </Text>
</ExpandListPressable> </ExpandListPressable>
{item.type === 'post-like' || item.type === 'repost' ? ( {item.type === 'post-like' ||
item.type === 'repost' ||
item.type === 'like-via-repost' ||
item.type === 'repost-via-repost' ? (
<View style={[a.pt_2xs]}> <View style={[a.pt_2xs]}>
<AdditionalPostText post={item.subject} /> <AdditionalPostText post={item.subject} />
</View> </View>
+79 -28
View File
@@ -1,4 +1,4 @@
import React, {memo, useMemo} from 'react' import {memo, useCallback, useMemo, useState} from 'react'
import { import {
type GestureResponderEvent, type GestureResponderEvent,
StyleSheet, StyleSheet,
@@ -6,7 +6,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import { import {
type AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
@@ -35,10 +35,12 @@ import {
usePostShadow, usePostShadow,
} from '#/state/cache/post-shadow' } from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {type ThreadPost} from '#/state/queries/post-thread' import {type ThreadPost} from '#/state/queries/post-thread'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useUnstablePostSource} from '#/state/unstable-post-source'
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {Link, TextLink} from '#/view/com/util/Link' import {Link, TextLink} from '#/view/com/util/Link'
@@ -201,18 +203,21 @@ let PostThreadItemLoaded = ({
hideTopBorder?: boolean hideTopBorder?: boolean
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => { }): React.ReactNode => {
const {currentAccount, hasSession} = useSession()
const source = useUnstablePostSource(post.uri)
const feedFeedback = useFeedFeedback(source?.feed, hasSession)
const t = useTheme() const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = useState(
() => countLines(richText?.text) >= MAX_POST_LINES, () => countLines(richText?.text) >= MAX_POST_LINES,
) )
const {currentAccount} = useSession()
const shadowedPostAuthor = useProfileShadow(post.author) const shadowedPostAuthor = useProfileShadow(post.author)
const rootUri = record.reply?.root?.uri || post.uri const rootUri = record.reply?.root?.uri || post.uri
const postHref = React.useMemo(() => { const postHref = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
@@ -220,12 +225,12 @@ let PostThreadItemLoaded = ({
const authorHref = makeProfileLink(post.author) const authorHref = makeProfileLink(post.author)
const authorTitle = post.author.handle const authorTitle = post.author.handle
const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
const likesHref = React.useMemo(() => { const likesHref = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
const likesTitle = _(msg`Likes on this post`) const likesTitle = _(msg`Likes on this post`)
const repostsHref = React.useMemo(() => { const repostsHref = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
@@ -233,7 +238,7 @@ let PostThreadItemLoaded = ({
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
threadgateRecord, threadgateRecord,
}) })
const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
return isControlledByViewer && isPostHiddenByThreadgate return isControlledByViewer && isPostHiddenByThreadgate
@@ -246,7 +251,7 @@ let PostThreadItemLoaded = ({
] ]
: [] : []
}, [post, currentAccount?.did, threadgateHiddenReplies, rootUri]) }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
const quotesHref = React.useMemo(() => { const quotesHref = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'quotes') return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
}, [post.uri, post.author]) }, [post.uri, post.author])
@@ -270,7 +275,15 @@ let PostThreadItemLoaded = ({
[post, langPrefs.primaryLanguage], [post, langPrefs.primaryLanguage],
) )
const onPressReply = React.useCallback(() => { const onPressReply = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionReply',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
openComposer({ openComposer({
replyTo: { replyTo: {
uri: post.uri, uri: post.uri,
@@ -282,14 +295,46 @@ let PostThreadItemLoaded = ({
}, },
onPost: onPostReply, onPost: onPostReply,
}) })
}, [openComposer, post, record, onPostReply, moderation]) }
const onPressShowMore = React.useCallback(() => { const onOpenAuthor = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#clickthroughAuthor',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
}
const onOpenEmbed = () => {
if (source) {
feedFeedback.sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#clickthroughEmbed',
feedContext: source.post.feedContext,
reqId: source.post.reqId,
})
}
}
const onPressShowMore = useCallback(() => {
setLimitLines(false) setLimitLines(false)
}, [setLimitLines]) }, [setLimitLines])
const {isActive: live} = useActorStatus(post.author) const {isActive: live} = useActorStatus(post.author)
const reason = source?.post.reason
const viaRepost = useMemo(() => {
if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
return {
uri: reason.uri,
cid: reason.cid,
}
}
}, [reason])
if (!record) { if (!record) {
return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} /> return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
} }
@@ -309,10 +354,8 @@ let PostThreadItemLoaded = ({
<View <View
style={[ style={[
styles.replyLine, styles.replyLine,
{ a.flex_grow,
flexGrow: 1, {backgroundColor: pal.colors.replyLine},
backgroundColor: pal.colors.replyLine,
},
]} ]}
/> />
</View> </View>
@@ -334,13 +377,15 @@ let PostThreadItemLoaded = ({
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'} type={post.author.associated?.labeler ? 'labeler' : 'user'}
live={live} live={live}
onBeforePress={onOpenAuthor}
/> />
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View style={[a.flex_row, a.align_center]}> <View style={[a.flex_row, a.align_center]}>
<Link <Link
style={[a.flex_shrink]} style={[a.flex_shrink]}
href={authorHref} href={authorHref}
title={authorTitle}> title={authorTitle}
onBeforePress={onOpenAuthor}>
<Text <Text
emoji emoji
style={[ style={[
@@ -413,6 +458,7 @@ let PostThreadItemLoaded = ({
embed={post.embed} embed={post.embed}
moderation={moderation} moderation={moderation}
viewContext={PostEmbedViewContext.ThreadHighlighted} viewContext={PostEmbedViewContext.ThreadHighlighted}
onOpen={onOpenEmbed}
/> />
</View> </View>
)} )}
@@ -494,16 +540,21 @@ let PostThreadItemLoaded = ({
marginLeft: -5, marginLeft: -5,
}, },
]}> ]}>
<PostControls <FeedFeedbackProvider value={feedFeedback}>
big <PostControls
post={post} big
record={record} post={post}
richText={richText} record={record}
onPressReply={onPressReply} richText={richText}
onPostReply={onPostReply} onPressReply={onPressReply}
logContext="PostThreadItem" onPostReply={onPostReply}
threadgateRecord={threadgateRecord} logContext="PostThreadItem"
/> threadgateRecord={threadgateRecord}
feedContext={source?.post?.feedContext}
reqId={source?.post?.reqId}
viaRepost={viaRepost}
/>
</FeedFeedbackProvider>
</View> </View>
</View> </View>
</View> </View>
@@ -779,7 +830,7 @@ function ExpandedPostDetails({
const isRootPost = !('reply' in post.record) const isRootPost = !('reply' in post.record)
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const onTranslatePress = React.useCallback( const onTranslatePress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
e.preventDefault() e.preventDefault()
openLink(translatorUrl, true) openLink(translatorUrl, true)
+24 -3
View File
@@ -33,9 +33,10 @@ import {
usePostShadow, usePostShadow,
} from '#/state/cache/post-shadow' } from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {precacheProfile} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useSetUnstablePostSource} from '#/state/unstable-post-source'
import {FeedNameText} from '#/view/com/util/FeedInfoText' import {FeedNameText} from '#/view/com/util/FeedInfoText'
import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link'
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
@@ -174,7 +175,8 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
const {sendInteraction} = useFeedFeedbackContext() const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
const unstableSetPostSource = useSetUnstablePostSource()
const onPressReply = () => { const onPressReply = () => {
sendInteraction({ sendInteraction({
@@ -229,7 +231,16 @@ let FeedItemInner = ({
feedContext, feedContext,
reqId, reqId,
}) })
precacheProfile(queryClient, post.author) unstableCacheProfileView(queryClient, post.author)
unstableSetPostSource(post.uri, {
feed: feedDescriptor,
post: {
post,
reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined,
feedContext,
reqId,
},
})
} }
const outerStyles = [ const outerStyles = [
@@ -263,6 +274,15 @@ let FeedItemInner = ({
const {isActive: live} = useActorStatus(post.author) const {isActive: live} = useActorStatus(post.author)
const viaRepost = useMemo(() => {
if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
return {
uri: reason.uri,
cid: reason.cid,
}
}
}, [reason])
return ( return (
<Link <Link
testID={`feedItem-by-${post.author.handle}`} testID={`feedItem-by-${post.author.handle}`}
@@ -450,6 +470,7 @@ let FeedItemInner = ({
reqId={reqId} reqId={reqId}
threadgateRecord={threadgateRecord} threadgateRecord={threadgateRecord}
onShowLess={onShowLess} onShowLess={onShowLess}
viaRepost={viaRepost}
/> />
</View> </View>
+44 -44
View File
@@ -63,10 +63,10 @@
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.7.18"
"@atproto/api@^0.15.8": "@atproto/api@^0.15.9":
version "0.15.8" version "0.15.9"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.8.tgz#f284a9c225191ebd35b46f5695932ab649c04a61" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.9.tgz#f8c40afd6e414ab107d63d6f08d9e264bf9a149a"
integrity sha512-PsCgmV4zPjN8VuJMruxqauhn88PuS0b8t2Xsjl4617+bCPpY513jVlxgNH/XExxO7TSVvJM7EzdLY4o3fqh/xQ== integrity sha512-CyAILiIcbN+V5CFAI6MDb247epm25RGkP7HSan5LUaOHiyg1NCAmflWCN/bbMdJX9kLqjAPAG3eN4BUUbYe//Q==
dependencies: dependencies:
"@atproto/common-web" "^0.4.2" "@atproto/common-web" "^0.4.2"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
@@ -94,14 +94,14 @@
multiformats "^9.9.0" multiformats "^9.9.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/bsky@^0.0.150": "@atproto/bsky@^0.0.151":
version "0.0.150" version "0.0.151"
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.150.tgz#6626095875d805d0d3f38fa4e184b9f7d274c80f" resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.151.tgz#a0e5b59e163a3b74379fb547601be4fc66b7a133"
integrity sha512-dn1jzP1EId842+g78Q6EMdOmgEZxa9bSq20HMdd5/R8uu559mPs8zigFuddqCoT1fRaJXFC8ZP7Jk5asvBQhrA== integrity sha512-42pvUsyGw0nR6Sxlda824maY4gBxUni1cXPG+7uGe6Ixm6XAaPhfTgT1rAg++1rDXH9tT1EXAVnMxg38S6osLg==
dependencies: dependencies:
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
"@atproto-labs/xrpc-utils" "0.0.14" "@atproto-labs/xrpc-utils" "0.0.14"
"@atproto/api" "^0.15.8" "@atproto/api" "^0.15.9"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/did" "^0.1.5" "@atproto/did" "^0.1.5"
@@ -218,20 +218,20 @@
"@noble/hashes" "^1.6.1" "@noble/hashes" "^1.6.1"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/dev-env@^0.3.132": "@atproto/dev-env@^0.3.133":
version "0.3.132" version "0.3.133"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.132.tgz#78d55ef08a368a752c55b1ee7b7c08a41f27b5ac" resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.133.tgz#4ca58c9c4c99f001f26ce50629214f81d6acd3ab"
integrity sha512-RFd/9kgvmbP859N6NLu/FxCzLsj01iq22P9jNpL+dQNXbWXHYwGMUa6edf/ZrljNi3dFBNxabdDZJ2q+8uvBJQ== integrity sha512-GtKDa+q0Fx2tJZL44cDAINMCxNmt1aKkGVpW/6PTnuSSjdA7ErBUEL3opbwgaAcPRGZfscB0mQmGfWR0BUmvUw==
dependencies: dependencies:
"@atproto/api" "^0.15.8" "@atproto/api" "^0.15.9"
"@atproto/bsky" "^0.0.150" "@atproto/bsky" "^0.0.151"
"@atproto/bsync" "^0.0.19" "@atproto/bsync" "^0.0.19"
"@atproto/common-web" "^0.4.2" "@atproto/common-web" "^0.4.2"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/ozone" "^0.1.111" "@atproto/ozone" "^0.1.112"
"@atproto/pds" "^0.4.138" "@atproto/pds" "^0.4.139"
"@atproto/sync" "^0.1.23" "@atproto/sync" "^0.1.23"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.7.18"
@@ -294,24 +294,24 @@
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.1.5"
"@atproto/oauth-types" "0.2.7" "@atproto/oauth-types" "0.2.7"
"@atproto/oauth-provider-frontend@0.1.4": "@atproto/oauth-provider-frontend@0.1.5":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.4.tgz#240a2e58c29d32fa7d4ea9d142c00c23d2469452"
integrity sha512-TLKL5lTmSieHx7+3RVIx7rIxRPP1SNCwzzdTvYB46yd1XrGHdPU//M6CP5OZ1BvcxF6H4JXIkOSWvFseol+gOw==
optionalDependencies:
"@atproto/oauth-provider-api" "0.1.2"
"@atproto/oauth-provider-ui@0.1.5":
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.5.tgz#b080c5e814975821689c5976c27ac1081211106f" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.5.tgz#66fd8760fade2ac94111ad5389f33f4d8ce5bba2"
integrity sha512-pW0Vx3kvIWH1Mu3SOImNHP9JbmhSj2e3ChDvtfXCWI1oC03fiaMlJfdxrx9Plq5Z+DajnCzPzrf1Lvbopjf94Q== integrity sha512-FdDBuwy827+etjIcRwZU7dtxa8Ltso3ufVLMEi8A2V91v21XDysZjLANC6cvmNNSUcS4E/J6ZAwTrQDo7O5axw==
optionalDependencies: optionalDependencies:
"@atproto/oauth-provider-api" "0.1.2" "@atproto/oauth-provider-api" "0.1.2"
"@atproto/oauth-provider@^0.7.7": "@atproto/oauth-provider-ui@0.1.6":
version "0.7.7" version "0.1.6"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.7.tgz#dbbdeb405ab1d239fd926340f83fb41e13455011" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.6.tgz#4bae995ff57671ac3915f58fdb2cf6a76a0fe42d"
integrity sha512-ElphzmOjw1hr42HN4dD6sMAQFtpTkaJ8bBDAsbL9YBVJDEGhmHsF3Ye8mDUO4nhEdg7PUTWiCzXyqnaorAjiTA== integrity sha512-pJzV9ouNj1/TDUCl3CWEZrHoUese4lcKx5F59t2OiLFm2K7T7QrszKUIMyU5QdiQHv551B0ZJOkJ8+4b/fVGPA==
optionalDependencies:
"@atproto/oauth-provider-api" "0.1.2"
"@atproto/oauth-provider@^0.7.8":
version "0.7.8"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.8.tgz#287b15eb6b0bc0bb4b2da2339150253db006c6e0"
integrity sha512-+dEU9dTyfWKeZ/Nu7ocR6fO73RcG0vwDjT45vgcnM9L7jtuPk9zfpmiR4ODYBk9QUu2DURo9yBhtXNJI3Yz8aQ==
dependencies: dependencies:
"@atproto-labs/fetch" "0.2.3" "@atproto-labs/fetch" "0.2.3"
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
@@ -322,8 +322,8 @@
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.1.5"
"@atproto/jwk-jose" "0.1.6" "@atproto/jwk-jose" "0.1.6"
"@atproto/oauth-provider-api" "0.1.2" "@atproto/oauth-provider-api" "0.1.2"
"@atproto/oauth-provider-frontend" "0.1.4" "@atproto/oauth-provider-frontend" "0.1.5"
"@atproto/oauth-provider-ui" "0.1.5" "@atproto/oauth-provider-ui" "0.1.6"
"@atproto/oauth-types" "0.2.7" "@atproto/oauth-types" "0.2.7"
"@atproto/syntax" "0.4.0" "@atproto/syntax" "0.4.0"
"@hapi/accept" "^6.0.3" "@hapi/accept" "^6.0.3"
@@ -346,12 +346,12 @@
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.1.5"
zod "^3.23.8" zod "^3.23.8"
"@atproto/ozone@^0.1.111": "@atproto/ozone@^0.1.112":
version "0.1.111" version "0.1.112"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.111.tgz#7ef4a02f1af045ab44254fb9d44ab0e50fd94ba9" resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.112.tgz#6b6b5ac052dd4e6dfec3c88f83c9b53f4902fcbe"
integrity sha512-NY+Cn/3dY4tPFkMUoJR1KMZN/v9ZIxjx6EQBMwn/nqTiHk0E3rtGEbyL2jLQ7x+FxpPTjDgpnn3K725+8XUaAg== integrity sha512-Euut64N/4UyRXyV6m1ATE9K6o6EpCf46ozD4GG8HJ9AC5zEgBYMSkH4l6SLrhKrYYIGXkvglk1WYuuDQKYb3LA==
dependencies: dependencies:
"@atproto/api" "^0.15.8" "@atproto/api" "^0.15.9"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
@@ -376,20 +376,20 @@
undici "^6.14.1" undici "^6.14.1"
ws "^8.12.0" ws "^8.12.0"
"@atproto/pds@^0.4.138": "@atproto/pds@^0.4.139":
version "0.4.138" version "0.4.139"
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.138.tgz#437d785c83f710bf37bef8baf687b0a46ce9dc68" resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.139.tgz#70ae5afd7d90eab214c652d57a5e6478af454fbe"
integrity sha512-WLzDhmguTgs2wQNKoGxCbpKNegDnRiemSslenMbPrB7kSiXYj+XZobLyoIXHv1EnAd2pbThwNEL8z8EfkM0mDg== integrity sha512-VD1VTSAnbAme4D4Xk/Wdl05qs8YbCe39/i960EyXzw2fYNvL9jMpKm3z0lwhrYN9q7phFhr2ubU2QjfRFDbDAQ==
dependencies: dependencies:
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
"@atproto-labs/xrpc-utils" "0.0.14" "@atproto-labs/xrpc-utils" "0.0.14"
"@atproto/api" "^0.15.8" "@atproto/api" "^0.15.9"
"@atproto/aws" "^0.2.21" "@atproto/aws" "^0.2.21"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/oauth-provider" "^0.7.7" "@atproto/oauth-provider" "^0.7.8"
"@atproto/repo" "^0.8.1" "@atproto/repo" "^0.8.1"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"