Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Send Bluesky feeds and suggested follows more data (#3695)

* WIP

* Fix constructors

* Clean up

* Tweak

* Rm extra assignment

* Narrow down the argument

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Eric Bailey
Dan Abramov
and committed by
GitHub
a4e34537 d893fe00

+91 -10
+10
src/lib/api/feed/custom.ts
··· 7 7 8 8 import {getContentLanguages} from '#/state/preferences/languages' 9 9 import {FeedAPI, FeedAPIResponse} from './types' 10 + import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' 10 11 11 12 export class CustomFeedAPI implements FeedAPI { 12 13 getAgent: () => BskyAgent 13 14 params: GetCustomFeed.QueryParams 15 + userInterests?: string 14 16 15 17 constructor({ 16 18 getAgent, 17 19 feedParams, 20 + userInterests, 18 21 }: { 19 22 getAgent: () => BskyAgent 20 23 feedParams: GetCustomFeed.QueryParams 24 + userInterests?: string 21 25 }) { 22 26 this.getAgent = getAgent 23 27 this.params = feedParams 28 + this.userInterests = userInterests 24 29 } 25 30 26 31 async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { ··· 44 49 }): Promise<FeedAPIResponse> { 45 50 const contentLangs = getContentLanguages().join(',') 46 51 const agent = this.getAgent() 52 + const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) 53 + 47 54 const res = agent.session 48 55 ? await this.getAgent().app.bsky.feed.getFeed( 49 56 { ··· 53 60 }, 54 61 { 55 62 headers: { 63 + ...(isBlueskyOwned 64 + ? createBskyTopicsHeader(this.userInterests) 65 + : {}), 56 66 'Accept-Language': contentLangs, 57 67 }, 58 68 },
+10 -1
src/lib/api/feed/home.ts
··· 32 32 discover: CustomFeedAPI 33 33 usingDiscover = false 34 34 itemCursor = 0 35 + userInterests?: string 35 36 36 - constructor({getAgent}: {getAgent: () => BskyAgent}) { 37 + constructor({ 38 + userInterests, 39 + getAgent, 40 + }: { 41 + userInterests?: string 42 + getAgent: () => BskyAgent 43 + }) { 37 44 this.getAgent = getAgent 38 45 this.following = new FollowingFeedAPI({getAgent}) 39 46 this.discover = new CustomFeedAPI({ 40 47 getAgent, 41 48 feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, 42 49 }) 50 + this.userInterests = userInterests 43 51 } 44 52 45 53 reset() { ··· 47 55 this.discover = new CustomFeedAPI({ 48 56 getAgent: this.getAgent, 49 57 feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, 58 + userInterests: this.userInterests, 50 59 }) 51 60 this.usingDiscover = false 52 61 this.itemCursor = 0
+14
src/lib/api/feed/merge.ts
··· 9 9 import {FeedTuner} from '../feed-manip' 10 10 import {FeedTunerFn} from '../feed-manip' 11 11 import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types' 12 + import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' 12 13 13 14 const REQUEST_WAIT_MS = 500 // 500ms 14 15 const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours 15 16 16 17 export class MergeFeedAPI implements FeedAPI { 18 + userInterests?: string 17 19 getAgent: () => BskyAgent 18 20 params: FeedParams 19 21 feedTuners: FeedTunerFn[] ··· 27 29 getAgent, 28 30 feedParams, 29 31 feedTuners, 32 + userInterests, 30 33 }: { 31 34 getAgent: () => BskyAgent 32 35 feedParams: FeedParams 33 36 feedTuners: FeedTunerFn[] 37 + userInterests?: string 34 38 }) { 35 39 this.getAgent = getAgent 36 40 this.params = feedParams 37 41 this.feedTuners = feedTuners 42 + this.userInterests = userInterests 38 43 this.following = new MergeFeedSource_Following({ 39 44 getAgent: this.getAgent, 40 45 feedTuners: this.feedTuners, ··· 58 63 getAgent: this.getAgent, 59 64 feedUri, 60 65 feedTuners: this.feedTuners, 66 + userInterests: this.userInterests, 61 67 }), 62 68 ), 63 69 ) ··· 254 260 getAgent: () => BskyAgent 255 261 minDate: Date 256 262 feedUri: string 263 + userInterests?: string 257 264 258 265 constructor({ 259 266 getAgent, 260 267 feedUri, 261 268 feedTuners, 269 + userInterests, 262 270 }: { 263 271 getAgent: () => BskyAgent 264 272 feedUri: string 265 273 feedTuners: FeedTunerFn[] 274 + userInterests?: string 266 275 }) { 267 276 super({ 268 277 getAgent, ··· 270 279 }) 271 280 this.getAgent = getAgent 272 281 this.feedUri = feedUri 282 + this.userInterests = userInterests 273 283 this.sourceInfo = { 274 284 $type: 'reasonFeedSource', 275 285 uri: feedUri, ··· 284 294 ): Promise<AppBskyFeedGetTimeline.Response> { 285 295 try { 286 296 const contentLangs = getContentLanguages().join(',') 297 + const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri) 287 298 const res = await this.getAgent().app.bsky.feed.getFeed( 288 299 { 289 300 cursor, ··· 292 303 }, 293 304 { 294 305 headers: { 306 + ...(isBlueskyOwned 307 + ? createBskyTopicsHeader(this.userInterests) 308 + : {}), 295 309 'Accept-Language': contentLangs, 296 310 }, 297 311 },
+21
src/lib/api/feed/utils.ts
··· 1 + import {AtUri} from '@atproto/api' 2 + 3 + import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' 4 + import {UsePreferencesQueryResponse} from '#/state/queries/preferences' 5 + 6 + export function createBskyTopicsHeader(userInterests?: string) { 7 + return { 8 + 'X-Bsky-Topics': userInterests || '', 9 + } 10 + } 11 + 12 + export function aggregateUserInterests( 13 + preferences?: UsePreferencesQueryResponse, 14 + ) { 15 + return preferences?.interests?.tags?.join(',') || '' 16 + } 17 + 18 + export function isBlueskyOwnedFeed(feedUri: string) { 19 + const uri = new AtUri(feedUri) 20 + return BSKY_FEED_OWNER_DIDS.includes(uri.host) 21 + }
+12 -3
src/state/queries/post-feed.ts
··· 15 15 } from '@tanstack/react-query' 16 16 17 17 import {HomeFeedAPI} from '#/lib/api/feed/home' 18 + import {aggregateUserInterests} from '#/lib/api/feed/utils' 18 19 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 19 20 import {logger} from '#/logger' 20 21 import {STALE} from '#/state/queries' ··· 31 32 import {BSKY_FEED_OWNER_DIDS} from 'lib/constants' 32 33 import {KnownError} from '#/view/com/posts/FeedErrorMessage' 33 34 import {useFeedTuners} from '../preferences/feed-tuners' 34 - import {useModerationOpts} from './preferences' 35 + import {useModerationOpts, usePreferencesQuery} from './preferences' 35 36 import {embedViewRecordToPostView, getEmbeddedPost} from './util' 36 37 37 38 type ActorDid = string ··· 102 103 ) { 103 104 const feedTuners = useFeedTuners(feedDesc) 104 105 const moderationOpts = useModerationOpts() 106 + const {data: preferences} = usePreferencesQuery() 107 + const enabled = 108 + opts?.enabled !== false && Boolean(moderationOpts) && Boolean(preferences) 109 + const userInterests = aggregateUserInterests(preferences) 105 110 const {getAgent} = useAgent() 106 - const enabled = opts?.enabled !== false && Boolean(moderationOpts) 107 111 const lastRun = useRef<{ 108 112 data: InfiniteData<FeedPageUnselected> 109 113 args: typeof selectArgs ··· 141 145 feedDesc, 142 146 feedParams: params || {}, 143 147 feedTuners, 148 + userInterests, // Not in the query key because they don't change. 144 149 getAgent, 145 150 }), 146 151 cursor: undefined, ··· 371 376 feedDesc, 372 377 feedParams, 373 378 feedTuners, 379 + userInterests, 374 380 getAgent, 375 381 }: { 376 382 feedDesc: FeedDescriptor 377 383 feedParams: FeedParams 378 384 feedTuners: FeedTunerFn[] 385 + userInterests?: string 379 386 getAgent: () => BskyAgent 380 387 }) { 381 388 if (feedDesc === 'home') { ··· 384 391 getAgent, 385 392 feedParams, 386 393 feedTuners, 394 + userInterests, 387 395 }) 388 396 } else { 389 - return new HomeFeedAPI({getAgent}) 397 + return new HomeFeedAPI({getAgent, userInterests}) 390 398 } 391 399 } else if (feedDesc === 'following') { 392 400 return new FollowingFeedAPI({getAgent}) ··· 401 409 return new CustomFeedAPI({ 402 410 getAgent, 403 411 feedParams: {feed}, 412 + userInterests, 404 413 }) 405 414 } else if (feedDesc.startsWith('list')) { 406 415 const [_, list] = feedDesc.split('|')
+24 -6
src/state/queries/suggested-follows.ts
··· 12 12 useQuery, 13 13 } from '@tanstack/react-query' 14 14 15 + import { 16 + aggregateUserInterests, 17 + createBskyTopicsHeader, 18 + } from '#/lib/api/feed/utils' 19 + import {getContentLanguages} from '#/state/preferences/languages' 15 20 import {STALE} from '#/state/queries' 16 - import {useModerationOpts} from '#/state/queries/preferences' 21 + import { 22 + useModerationOpts, 23 + usePreferencesQuery, 24 + } from '#/state/queries/preferences' 17 25 import {useAgent, useSession} from '#/state/session' 18 26 19 27 const suggestedFollowsQueryKeyRoot = 'suggested-follows' ··· 29 37 const {currentAccount} = useSession() 30 38 const {getAgent} = useAgent() 31 39 const moderationOpts = useModerationOpts() 40 + const {data: preferences} = usePreferencesQuery() 32 41 33 42 return useInfiniteQuery< 34 43 AppBskyActorGetSuggestions.OutputSchema, ··· 37 46 QueryKey, 38 47 string | undefined 39 48 >({ 40 - enabled: !!moderationOpts, 49 + enabled: !!moderationOpts && !!preferences, 41 50 staleTime: STALE.HOURS.ONE, 42 51 queryKey: suggestedFollowsQueryKey, 43 52 queryFn: async ({pageParam}) => { 44 - const res = await getAgent().app.bsky.actor.getSuggestions({ 45 - limit: 25, 46 - cursor: pageParam, 47 - }) 53 + const contentLangs = getContentLanguages().join(',') 54 + const res = await getAgent().app.bsky.actor.getSuggestions( 55 + { 56 + limit: 25, 57 + cursor: pageParam, 58 + }, 59 + { 60 + headers: { 61 + ...createBskyTopicsHeader(aggregateUserInterests(preferences)), 62 + 'Accept-Language': contentLangs, 63 + }, 64 + }, 65 + ) 48 66 49 67 res.data.actors = res.data.actors 50 68 .filter(