deer social fork for personal usage. but you might see a use idk. github mirror

Enable show less / more buttons for third party feeds (#8672)

Co-authored-by: hailey <hailey@blueskyweb.xyz>
Co-authored-by: Hailey <me@haileyok.com>

authored by kindgracekind hailey Hailey and committed by GitHub 88e6dff4 98d96bd2

Changed files
+150 -34
src
components
PostControls
lib
screens
PostThread
Profile
ProfileFeed
VideoFeed
state
view
com
+16 -9
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 266 266 feedContext: postFeedContext, 267 267 reqId: postReqId, 268 268 }) 269 - Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 269 + Toast.show( 270 + _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), 271 + ) 270 272 } 271 273 272 274 const onPressShowLess = () => { ··· 282 284 feedContext: postFeedContext, 283 285 }) 284 286 } else { 285 - Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 287 + Toast.show( 288 + _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), 289 + ) 286 290 } 287 291 } 288 292 ··· 486 490 )} 487 491 488 492 {isDiscoverDebugUser && ( 489 - <Menu.Item 490 - testID="postDropdownReportMisclassificationBtn" 491 - label={_(msg`Assign topic for algo`)} 492 - onPress={onReportMisclassification}> 493 - <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText> 494 - <Menu.ItemIcon icon={AtomIcon} position="right" /> 495 - </Menu.Item> 493 + <> 494 + <Menu.Divider /> 495 + <Menu.Item 496 + testID="postDropdownReportMisclassificationBtn" 497 + label={_(msg`Assign topic for algo`)} 498 + onPress={onReportMisclassification}> 499 + <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText> 500 + <Menu.ItemIcon icon={AtomIcon} position="right" /> 501 + </Menu.Item> 502 + </> 496 503 )} 497 504 498 505 {hasSession && (
-2
src/lib/constants.ts
··· 90 90 `feedgen|${STAGING_DEFAULT_FEED('thevids')}`, 91 91 ] 92 92 93 - export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] 94 - 95 93 export const POST_IMG_MAX = { 96 94 width: 2000, 97 95 height: 2000,
+1 -1
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 180 180 const {openComposer} = useOpenComposer() 181 181 const {currentAccount, hasSession} = useSession() 182 182 const {gtTablet} = useBreakpoints() 183 - const feedFeedback = useFeedFeedback(postSource?.feed, hasSession) 183 + const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession) 184 184 185 185 const post = postShadow 186 186 const record = item.value.post.record
+4 -1
src/screens/PostThread/index.tsx
··· 49 49 const initialNumToRender = useInitialNumToRender() 50 50 const {height: windowHeight} = useWindowDimensions() 51 51 const anchorPostSource = useUnstablePostSource(uri) 52 - const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) 52 + const feedFeedback = useFeedFeedback( 53 + anchorPostSource?.feedSourceInfo, 54 + hasSession, 55 + ) 53 56 54 57 /* 55 58 * One query to rule them all
+1 -1
src/screens/Profile/ProfileFeed/index.tsx
··· 169 169 const [hasNew, setHasNew] = React.useState(false) 170 170 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 171 171 const queryClient = useQueryClient() 172 - const feedFeedback = useFeedFeedback(feed, hasSession) 172 + const feedFeedback = useFeedFeedback(feedInfo, hasSession) 173 173 const scrollElRef = useAnimatedRef() as ListRef 174 174 175 175 const onScrollToTop = useCallback(() => {
+4 -1
src/screens/VideoFeed/index.tsx
··· 70 70 useFeedFeedbackContext, 71 71 } from '#/state/feed-feedback' 72 72 import {useFeedFeedback} from '#/state/feed-feedback' 73 + import {useFeedInfo} from '#/state/queries/feed' 73 74 import {usePostLikeMutationQueue} from '#/state/queries/post' 74 75 import { 75 76 type AuthorFilter, ··· 199 200 throw new Error(`Invalid video feed params ${JSON.stringify(params)}`) 200 201 } 201 202 }, [params]) 202 - const feedFeedback = useFeedFeedback(feedDesc, hasSession) 203 + const feedUri = params.type === 'feedgen' ? params.uri : undefined 204 + const {data: feedInfo} = useFeedInfo(feedUri) 205 + const feedFeedback = useFeedFeedback(feedInfo, hasSession) 203 206 const {data, error, hasNextPage, isFetchingNextPage, fetchNextPage} = 204 207 usePostFeedQuery( 205 208 feedDesc,
+86 -12
src/state/feed-feedback.tsx
··· 10 10 import {type AppBskyFeedDefs} from '@atproto/api' 11 11 import throttle from 'lodash.throttle' 12 12 13 - import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants' 13 + import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants' 14 14 import {isNetworkError} from '#/lib/hooks/useCleanError' 15 15 import {logEvent} from '#/lib/statsig/statsig' 16 16 import {Logger} from '#/logger' 17 + import { 18 + type FeedSourceFeedInfo, 19 + type FeedSourceInfo, 20 + isFeedSourceFeedInfo, 21 + } from '#/state/queries/feed' 17 22 import { 18 23 type FeedDescriptor, 19 24 type FeedPostSliceItem, ··· 21 26 import {getItemsForFeedback} from '#/view/com/posts/PostFeed' 22 27 import {useAgent} from './session' 23 28 29 + export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] 30 + 31 + export const PASSIVE_FEEDBACK_INTERACTIONS = [ 32 + 'app.bsky.feed.defs#clickthroughItem', 33 + 'app.bsky.feed.defs#clickthroughAuthor', 34 + 'app.bsky.feed.defs#clickthroughReposter', 35 + 'app.bsky.feed.defs#clickthroughEmbed', 36 + 'app.bsky.feed.defs#interactionSeen', 37 + ] as const 38 + 39 + export type PassiveFeedbackInteraction = 40 + (typeof PASSIVE_FEEDBACK_INTERACTIONS)[number] 41 + 42 + export const DIRECT_FEEDBACK_INTERACTIONS = [ 43 + 'app.bsky.feed.defs#requestLess', 44 + 'app.bsky.feed.defs#requestMore', 45 + ] as const 46 + 47 + export type DirectFeedbackInteraction = 48 + (typeof DIRECT_FEEDBACK_INTERACTIONS)[number] 49 + 50 + export const ALL_FEEDBACK_INTERACTIONS = [ 51 + ...PASSIVE_FEEDBACK_INTERACTIONS, 52 + ...DIRECT_FEEDBACK_INTERACTIONS, 53 + ] as const 54 + 55 + export type FeedbackInteraction = (typeof ALL_FEEDBACK_INTERACTIONS)[number] 56 + 57 + export function isFeedbackInteraction( 58 + interactionEvent: string, 59 + ): interactionEvent is FeedbackInteraction { 60 + return ALL_FEEDBACK_INTERACTIONS.includes( 61 + interactionEvent as FeedbackInteraction, 62 + ) 63 + } 64 + 24 65 const logger = Logger.create(Logger.Context.FeedFeedback) 25 66 26 67 export type StateContext = { ··· 28 69 onItemSeen: (item: any) => void 29 70 sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void 30 71 feedDescriptor: FeedDescriptor | undefined 72 + feedSourceInfo: FeedSourceInfo | undefined 31 73 } 32 74 33 75 const stateContext = createContext<StateContext>({ ··· 35 77 onItemSeen: (_item: any) => {}, 36 78 sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, 37 79 feedDescriptor: undefined, 80 + feedSourceInfo: undefined, 38 81 }) 39 82 stateContext.displayName = 'FeedFeedbackContext' 40 83 41 84 export function useFeedFeedback( 42 - feed: FeedDescriptor | undefined, 85 + feedSourceInfo: FeedSourceInfo | undefined, 43 86 hasSession: boolean, 44 87 ) { 45 88 const agent = useAgent() 46 - const enabled = isDiscoverFeed(feed) && hasSession 89 + 90 + const feed = 91 + !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo) 92 + ? feedSourceInfo 93 + : undefined 94 + 95 + const isDiscover = isDiscoverFeed(feed?.feedDescriptor) 96 + const acceptsInteractions = Boolean(isDiscover || feed?.acceptsInteractions) 97 + const proxyDid = feed?.view?.did 98 + const enabled = 99 + Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession 100 + const enabledInteractions = getEnabledInteractions(enabled, feed, isDiscover) 47 101 48 102 const queue = useRef<Set<string>>(new Set()) 49 103 const history = useRef< ··· 66 120 const interactions = Array.from(queue.current).map(toInteraction) 67 121 queue.current.clear() 68 122 69 - let proxyDid = 'did:web:discover.bsky.app' 70 - if (STAGING_FEEDS.includes(feed ?? '')) { 71 - proxyDid = 'did:web:algo.pop2.bsky.app' 123 + const interactionsToSend = interactions.filter( 124 + interaction => 125 + interaction.event && 126 + isFeedbackInteraction(interaction.event) && 127 + enabledInteractions.includes(interaction.event), 128 + ) 129 + 130 + if (interactionsToSend.length === 0) { 131 + return 72 132 } 73 133 74 134 // Send to the feed 75 135 agent.app.bsky.feed 76 136 .sendInteractions( 77 - {interactions}, 137 + {interactions: interactionsToSend}, 78 138 { 79 139 encoding: 'application/json', 80 140 headers: { 81 - // TODO when we start sending to other feeds, we need to grab their DID -prf 82 141 'atproto-proxy': `${proxyDid}#bsky_fg`, 83 142 }, 84 143 }, ··· 93 152 if (aggregatedStats.current === null) { 94 153 aggregatedStats.current = createAggregatedStats() 95 154 } 96 - sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) 155 + sendOrAggregateInteractionsForStats( 156 + aggregatedStats.current, 157 + interactionsToSend, 158 + ) 97 159 throttledFlushAggregatedStats() 98 160 logger.debug('flushed') 99 - }, [agent, throttledFlushAggregatedStats, feed]) 161 + }, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions]) 100 162 101 163 const sendToFeed = useMemo( 102 164 () => ··· 168 230 // call on various events 169 231 // queues the event to be sent with the throttled sendToFeed call 170 232 sendInteraction, 171 - feedDescriptor: feed, 233 + feedDescriptor: feed?.feedDescriptor, 234 + feedSourceInfo: typeof feed === 'object' ? feed : undefined, 172 235 } 173 236 }, [enabled, onItemSeen, sendInteraction, feed]) 174 237 } ··· 184 247 // take advantage of the feed feedback API. Until that's in 185 248 // place, we're hardcoding it to the discover feed. 186 249 // -prf 187 - function isDiscoverFeed(feed?: FeedDescriptor) { 250 + export function isDiscoverFeed(feed?: FeedDescriptor) { 188 251 return !!feed && FEEDBACK_FEEDS.includes(feed) 252 + } 253 + 254 + function getEnabledInteractions( 255 + enabled: boolean, 256 + feed: FeedSourceFeedInfo | undefined, 257 + isDiscover: boolean, 258 + ): readonly FeedbackInteraction[] { 259 + if (!enabled || !feed) { 260 + return [] 261 + } 262 + return isDiscover ? ALL_FEEDBACK_INTERACTIONS : DIRECT_FEEDBACK_INTERACTIONS 189 263 } 190 264 191 265 function toString(interaction: AppBskyFeedDefs.Interaction): string {
+31
src/state/queries/feed.ts
··· 48 48 creatorDid: string 49 49 creatorHandle: string 50 50 likeCount: number | undefined 51 + acceptsInteractions?: boolean 51 52 likeUri: string | undefined 52 53 contentMode: AppBskyFeedDefs.GeneratorView['contentMode'] 53 54 } ··· 72 73 } 73 74 74 75 export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo 76 + 77 + export function isFeedSourceFeedInfo( 78 + feed: FeedSourceInfo, 79 + ): feed is FeedSourceFeedInfo { 80 + return feed.type === 'feed' 81 + } 75 82 76 83 const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo' 77 84 export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [ ··· 115 122 creatorDid: view.creator.did, 116 123 creatorHandle: view.creator.handle, 117 124 likeCount: view.likeCount, 125 + acceptsInteractions: view.acceptsInteractions, 118 126 likeUri: view.viewer?.like, 119 127 contentMode: view.contentMode, 120 128 } ··· 615 623 count: result.length, 616 624 feeds: result, 617 625 } 626 + }, 627 + }) 628 + } 629 + 630 + const feedInfoQueryKeyRoot = 'feedInfo' 631 + 632 + export function useFeedInfo(feedUri: string | undefined) { 633 + const agent = useAgent() 634 + 635 + return useQuery({ 636 + staleTime: STALE.INFINITY, 637 + queryKey: [feedInfoQueryKeyRoot, feedUri], 638 + queryFn: async () => { 639 + if (!feedUri) { 640 + return undefined 641 + } 642 + 643 + const res = await agent.app.bsky.feed.getFeedGenerator({ 644 + feed: feedUri, 645 + }) 646 + 647 + const feedSourceInfo = hydrateFeedGenerator(res.data.view) 648 + return feedSourceInfo 618 649 }, 619 650 }) 620 651 }
+2 -2
src/state/unstable-post-source.tsx
··· 2 2 import {type AppBskyFeedDefs, AtUri} from '@atproto/api' 3 3 4 4 import {Logger} from '#/logger' 5 - import {type FeedDescriptor} from '#/state/queries/post-feed' 5 + import {type FeedSourceInfo} from '#/state/queries/feed' 6 6 7 7 /** 8 8 * Separate logger for better debugging ··· 11 11 12 12 export type PostSource = { 13 13 post: AppBskyFeedDefs.FeedViewPost 14 - feed?: FeedDescriptor 14 + feedSourceInfo?: FeedSourceInfo 15 15 } 16 16 17 17 /**
+3 -3
src/view/com/feeds/FeedPage.tsx
··· 17 17 import {listenSoftReset} from '#/state/events' 18 18 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 19 19 import {useSetHomeBadge} from '#/state/home-badge' 20 - import {type SavedFeedSourceInfo} from '#/state/queries/feed' 20 + import {type FeedSourceInfo} from '#/state/queries/feed' 21 21 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 22 22 import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' 23 23 import {truncateAndInvalidate} from '#/state/queries/util' ··· 51 51 renderEmptyState: () => JSX.Element 52 52 renderEndOfFeed?: () => JSX.Element 53 53 savedFeedConfig?: AppBskyActorDefs.SavedFeed 54 - feedInfo: SavedFeedSourceInfo 54 + feedInfo: FeedSourceInfo 55 55 }) { 56 56 const {hasSession} = useSession() 57 57 const {_} = useLingui() ··· 61 61 const [isScrolledDown, setIsScrolledDown] = useState(false) 62 62 const setMinimalShellMode = useSetMinimalShellMode() 63 63 const headerOffset = useHeaderOffset() 64 - const feedFeedback = useFeedFeedback(feed, hasSession) 64 + const feedFeedback = useFeedFeedback(feedInfo, hasSession) 65 65 const scrollElRef = useRef<ListMethods>(null) 66 66 const [hasNew, setHasNew] = useState(false) 67 67 const setHomeBadge = useSetHomeBadge()
+2 -2
src/view/com/posts/PostFeedItem.tsx
··· 176 176 const urip = new AtUri(post.uri) 177 177 return makeProfileLink(post.author, 'post', urip.rkey) 178 178 }, [post.uri, post.author]) 179 - const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() 179 + const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() 180 180 181 181 const onPressReply = () => { 182 182 sendInteraction({ ··· 234 234 }) 235 235 unstableCacheProfileView(queryClient, post.author) 236 236 setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { 237 - feed: feedDescriptor, 237 + feedSourceInfo, 238 238 post: { 239 239 post, 240 240 reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined,