Send FeedFeedback interactions in thread view (#8414)
This commit is contained in:
+2
-2
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user