Send FeedFeedback interactions in thread view (#8414)
This commit is contained in:
+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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
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 {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -150,14 +151,16 @@ function InnerApp() {
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GestureHandlerRootView>
|
||||
<UnstablePostSourceProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GestureHandlerRootView>
|
||||
</UnstablePostSourceProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
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 VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -131,10 +132,12 @@ function InnerApp() {
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
<UnstablePostSourceProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</UnstablePostSourceProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
|
||||
@@ -50,6 +50,7 @@ let PostControls = ({
|
||||
logContext,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
viaRepost,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
@@ -63,13 +64,19 @@ let PostControls = ({
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
viaRepost?: {uri: string; cid: string}
|
||||
}): React.ReactNode => {
|
||||
const {_, i18n} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {openComposer} = useOpenComposer()
|
||||
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
|
||||
const [queueLike, queueUnlike] = usePostLikeMutationQueue(
|
||||
post,
|
||||
viaRepost,
|
||||
logContext,
|
||||
)
|
||||
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
|
||||
post,
|
||||
viaRepost,
|
||||
logContext,
|
||||
)
|
||||
const requireAuth = useRequireAuth()
|
||||
|
||||
@@ -1023,7 +1023,12 @@ function PlayPauseTapArea({
|
||||
const {_} = useLingui()
|
||||
const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const playHaptic = useHaptics()
|
||||
const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo')
|
||||
// TODO: implement viaRepost -sfn
|
||||
const [queueLike] = usePostLikeMutationQueue(
|
||||
post,
|
||||
undefined,
|
||||
'ImmersiveVideo',
|
||||
)
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const {isPlaying} = useEvent(player, 'playingChange', {
|
||||
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 {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import throttle from 'lodash.throttle'
|
||||
@@ -13,31 +20,36 @@ import {
|
||||
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
|
||||
import {useAgent} from './session'
|
||||
|
||||
type StateContext = {
|
||||
export type StateContext = {
|
||||
enabled: boolean
|
||||
onItemSeen: (item: any) => void
|
||||
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
feedDescriptor: FeedDescriptor | undefined
|
||||
}
|
||||
|
||||
const stateContext = React.createContext<StateContext>({
|
||||
const stateContext = createContext<StateContext>({
|
||||
enabled: false,
|
||||
onItemSeen: (_item: any) => {},
|
||||
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 enabled = isDiscoverFeed(feed) && hasSession
|
||||
|
||||
const queue = React.useRef<Set<string>>(new Set())
|
||||
const history = React.useRef<
|
||||
const queue = useRef<Set<string>>(new Set())
|
||||
const history = useRef<
|
||||
// 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.
|
||||
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
|
||||
>(new WeakSet())
|
||||
|
||||
const aggregatedStats = React.useRef<AggregatedStats | null>(null)
|
||||
const throttledFlushAggregatedStats = React.useMemo(
|
||||
const aggregatedStats = useRef<AggregatedStats | null>(null)
|
||||
const throttledFlushAggregatedStats = useMemo(
|
||||
() =>
|
||||
throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
|
||||
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)
|
||||
queue.current.clear()
|
||||
|
||||
let proxyDid = 'did:web:discover.bsky.app'
|
||||
if (STAGING_FEEDS.includes(feed)) {
|
||||
if (STAGING_FEEDS.includes(feed ?? '')) {
|
||||
proxyDid = 'did:web:algo.pop2.bsky.app'
|
||||
}
|
||||
|
||||
@@ -79,7 +91,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
throttledFlushAggregatedStats()
|
||||
}, [agent, throttledFlushAggregatedStats, feed])
|
||||
|
||||
const sendToFeed = React.useMemo(
|
||||
const sendToFeed = useMemo(
|
||||
() =>
|
||||
throttle(sendToFeedNoDelay, 10e3, {
|
||||
leading: false,
|
||||
@@ -88,7 +100,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[sendToFeedNoDelay],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +112,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
return () => sub.remove()
|
||||
}, [enabled, sendToFeed])
|
||||
|
||||
const onItemSeen = React.useCallback(
|
||||
const onItemSeen = useCallback(
|
||||
(feedItem: any) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
@@ -124,7 +136,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
const sendInteraction = React.useCallback(
|
||||
const sendInteraction = useCallback(
|
||||
(interaction: AppBskyFeedDefs.Interaction) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
@@ -138,7 +150,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
return React.useMemo(() => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
enabled,
|
||||
// pass this method to the <List> onItemSeen
|
||||
@@ -146,14 +158,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
// call on various events
|
||||
// queues the event to be sent with the throttled sendToFeed call
|
||||
sendInteraction,
|
||||
feedDescriptor: feed,
|
||||
}
|
||||
}, [enabled, onItemSeen, sendInteraction])
|
||||
}, [enabled, onItemSeen, sendInteraction, feed])
|
||||
}
|
||||
|
||||
export const FeedFeedbackProvider = stateContext.Provider
|
||||
|
||||
export function useFeedFeedbackContext() {
|
||||
return React.useContext(stateContext)
|
||||
return useContext(stateContext)
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -161,8 +174,8 @@ export function useFeedFeedbackContext() {
|
||||
// take advantage of the feed feedback API. Until that's in
|
||||
// place, we're hardcoding it to the discover feed.
|
||||
// -prf
|
||||
function isDiscoverFeed(feed: FeedDescriptor) {
|
||||
return FEEDBACK_FEEDS.includes(feed)
|
||||
function isDiscoverFeed(feed?: FeedDescriptor) {
|
||||
return !!feed && FEEDBACK_FEEDS.includes(feed)
|
||||
}
|
||||
|
||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||
|
||||
@@ -46,6 +46,8 @@ type OtherNotificationType =
|
||||
| 'feedgen-like'
|
||||
| 'verified'
|
||||
| 'unverified'
|
||||
| 'like-via-repost'
|
||||
| 'repost-via-repost'
|
||||
| 'unknown'
|
||||
|
||||
type FeedNotificationBase = {
|
||||
|
||||
@@ -244,7 +244,9 @@ function toKnownType(
|
||||
notif.reason === 'follow' ||
|
||||
notif.reason === 'starterpack-joined' ||
|
||||
notif.reason === 'verified' ||
|
||||
notif.reason === 'unverified'
|
||||
notif.reason === 'unverified' ||
|
||||
notif.reason === 'like-via-repost' ||
|
||||
notif.reason === 'repost-via-repost'
|
||||
) {
|
||||
return notif.reason as NotificationType
|
||||
}
|
||||
@@ -257,7 +259,12 @@ function getSubjectUri(
|
||||
): string | undefined {
|
||||
if (type === 'reply' || type === 'quote' || type === 'mention') {
|
||||
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 (
|
||||
bsky.dangerousIsType<AppBskyFeedRepost.Record>(
|
||||
notif.record,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 {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 {Shadow} from '#/state/cache/types'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
|
||||
@@ -98,6 +98,7 @@ export function useGetPosts() {
|
||||
|
||||
export function usePostLikeMutationQueue(
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
viaRepost: {uri: string; cid: string} | undefined,
|
||||
logContext: LogEvents['post:like']['logContext'] &
|
||||
LogEvents['post:unlike']['logContext'],
|
||||
) {
|
||||
@@ -115,6 +116,7 @@ export function usePostLikeMutationQueue(
|
||||
const {uri: likeUri} = await likeMutation.mutateAsync({
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
via: viaRepost,
|
||||
})
|
||||
userActionHistory.like([postUri])
|
||||
return likeUri
|
||||
@@ -167,9 +169,9 @@ function usePostLikeMutation(
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the like
|
||||
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
|
||||
if (currentAccount) {
|
||||
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
||||
@@ -190,7 +192,7 @@ function usePostLikeMutation(
|
||||
? toClout(post.likeCount + post.repostCount + post.replyCount)
|
||||
: undefined,
|
||||
})
|
||||
return agent.like(uri, cid)
|
||||
return agent.like(uri, cid, via)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -209,6 +211,7 @@ function usePostUnlikeMutation(
|
||||
|
||||
export function usePostRepostMutationQueue(
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
viaRepost: {uri: string; cid: string} | undefined,
|
||||
logContext: LogEvents['post:repost']['logContext'] &
|
||||
LogEvents['post:unrepost']['logContext'],
|
||||
) {
|
||||
@@ -226,6 +229,7 @@ export function usePostRepostMutationQueue(
|
||||
const {uri: repostUri} = await repostMutation.mutateAsync({
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
via: viaRepost,
|
||||
})
|
||||
return repostUri
|
||||
} else {
|
||||
@@ -272,11 +276,11 @@ function usePostRepostMutation(
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the repost
|
||||
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})
|
||||
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 {
|
||||
ActivityIndicator,
|
||||
ListRenderItemInfo,
|
||||
type ListRenderItemInfo,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
@@ -16,7 +16,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
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 {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {NotificationFeedItem} from './NotificationFeedItem'
|
||||
|
||||
@@ -446,6 +446,55 @@ let NotificationFeedItem = ({
|
||||
</Trans>
|
||||
)
|
||||
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 {
|
||||
return null
|
||||
}
|
||||
@@ -553,7 +602,10 @@ let NotificationFeedItem = ({
|
||||
</TimeElapsed>
|
||||
</Text>
|
||||
</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]}>
|
||||
<AdditionalPostText post={item.subject} />
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import {memo, useCallback, useMemo, useState} from 'react'
|
||||
import {
|
||||
type GestureResponderEvent,
|
||||
StyleSheet,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
@@ -35,10 +35,12 @@ import {
|
||||
usePostShadow,
|
||||
} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {type ThreadPost} from '#/state/queries/post-thread'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {useUnstablePostSource} from '#/state/unstable-post-source'
|
||||
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {Link, TextLink} from '#/view/com/util/Link'
|
||||
@@ -201,18 +203,21 @@ let PostThreadItemLoaded = ({
|
||||
hideTopBorder?: boolean
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const source = useUnstablePostSource(post.uri)
|
||||
const feedFeedback = useFeedFeedback(source?.feed, hasSession)
|
||||
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_, i18n} = useLingui()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const {openComposer} = useOpenComposer()
|
||||
const [limitLines, setLimitLines] = React.useState(
|
||||
const [limitLines, setLimitLines] = useState(
|
||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||
)
|
||||
const {currentAccount} = useSession()
|
||||
const shadowedPostAuthor = useProfileShadow(post.author)
|
||||
const rootUri = record.reply?.root?.uri || post.uri
|
||||
const postHref = React.useMemo(() => {
|
||||
const postHref = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
@@ -220,12 +225,12 @@ let PostThreadItemLoaded = ({
|
||||
const authorHref = makeProfileLink(post.author)
|
||||
const authorTitle = post.author.handle
|
||||
const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
|
||||
const likesHref = React.useMemo(() => {
|
||||
const likesHref = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
|
||||
}, [post.uri, post.author])
|
||||
const likesTitle = _(msg`Likes on this post`)
|
||||
const repostsHref = React.useMemo(() => {
|
||||
const repostsHref = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
|
||||
}, [post.uri, post.author])
|
||||
@@ -233,7 +238,7 @@ let PostThreadItemLoaded = ({
|
||||
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
|
||||
threadgateRecord,
|
||||
})
|
||||
const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
|
||||
const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
|
||||
const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
|
||||
const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
|
||||
return isControlledByViewer && isPostHiddenByThreadgate
|
||||
@@ -246,7 +251,7 @@ let PostThreadItemLoaded = ({
|
||||
]
|
||||
: []
|
||||
}, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
|
||||
const quotesHref = React.useMemo(() => {
|
||||
const quotesHref = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
|
||||
}, [post.uri, post.author])
|
||||
@@ -270,7 +275,15 @@ let PostThreadItemLoaded = ({
|
||||
[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({
|
||||
replyTo: {
|
||||
uri: post.uri,
|
||||
@@ -282,14 +295,46 @@ let PostThreadItemLoaded = ({
|
||||
},
|
||||
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])
|
||||
|
||||
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) {
|
||||
return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
|
||||
}
|
||||
@@ -309,10 +354,8 @@ let PostThreadItemLoaded = ({
|
||||
<View
|
||||
style={[
|
||||
styles.replyLine,
|
||||
{
|
||||
flexGrow: 1,
|
||||
backgroundColor: pal.colors.replyLine,
|
||||
},
|
||||
a.flex_grow,
|
||||
{backgroundColor: pal.colors.replyLine},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
@@ -334,13 +377,15 @@ let PostThreadItemLoaded = ({
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
live={live}
|
||||
onBeforePress={onOpenAuthor}
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Link
|
||||
style={[a.flex_shrink]}
|
||||
href={authorHref}
|
||||
title={authorTitle}>
|
||||
title={authorTitle}
|
||||
onBeforePress={onOpenAuthor}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
@@ -413,6 +458,7 @@ let PostThreadItemLoaded = ({
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||
onOpen={onOpenEmbed}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -494,16 +540,21 @@ let PostThreadItemLoaded = ({
|
||||
marginLeft: -5,
|
||||
},
|
||||
]}>
|
||||
<PostControls
|
||||
big
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
onPressReply={onPressReply}
|
||||
onPostReply={onPostReply}
|
||||
logContext="PostThreadItem"
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<PostControls
|
||||
big
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
onPressReply={onPressReply}
|
||||
onPostReply={onPostReply}
|
||||
logContext="PostThreadItem"
|
||||
threadgateRecord={threadgateRecord}
|
||||
feedContext={source?.post?.feedContext}
|
||||
reqId={source?.post?.reqId}
|
||||
viaRepost={viaRepost}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -779,7 +830,7 @@ function ExpandedPostDetails({
|
||||
const isRootPost = !('reply' in post.record)
|
||||
const langPrefs = useLanguagePrefs()
|
||||
|
||||
const onTranslatePress = React.useCallback(
|
||||
const onTranslatePress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
e.preventDefault()
|
||||
openLink(translatorUrl, true)
|
||||
|
||||
@@ -33,9 +33,10 @@ import {
|
||||
usePostShadow,
|
||||
} from '#/state/cache/post-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {unstableCacheProfileView} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {useSetUnstablePostSource} from '#/state/unstable-post-source'
|
||||
import {FeedNameText} from '#/view/com/util/FeedInfoText'
|
||||
import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
@@ -174,7 +175,8 @@ let FeedItemInner = ({
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
|
||||
const unstableSetPostSource = useSetUnstablePostSource()
|
||||
|
||||
const onPressReply = () => {
|
||||
sendInteraction({
|
||||
@@ -229,7 +231,16 @@ let FeedItemInner = ({
|
||||
feedContext,
|
||||
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 = [
|
||||
@@ -263,6 +274,15 @@ let FeedItemInner = ({
|
||||
|
||||
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 (
|
||||
<Link
|
||||
testID={`feedItem-by-${post.author.handle}`}
|
||||
@@ -450,6 +470,7 @@ let FeedItemInner = ({
|
||||
reqId={reqId}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
viaRepost={viaRepost}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user