mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at rm-proxy 262 lines 7.3 kB view raw
1import { 2 AppBskyFeedDefs, 3 AppBskyFeedLike, 4 AppBskyFeedPost, 5 AppBskyFeedRepost, 6 AppBskyGraphDefs, 7 AppBskyGraphStarterpack, 8 AppBskyNotificationListNotifications, 9 BskyAgent, 10 moderateNotification, 11 ModerationOpts, 12} from '@atproto/api' 13import {QueryClient} from '@tanstack/react-query' 14import chunk from 'lodash.chunk' 15 16import {precacheProfile} from '../profile' 17import {FeedNotification, FeedPage, NotificationType} from './types' 18 19const GROUPABLE_REASONS = ['like', 'repost', 'follow'] 20const MS_1HR = 1e3 * 60 * 60 21const MS_2DAY = MS_1HR * 48 22 23// exported api 24// = 25 26export async function fetchPage({ 27 agent, 28 cursor, 29 limit, 30 queryClient, 31 moderationOpts, 32 fetchAdditionalData, 33}: { 34 agent: BskyAgent 35 cursor: string | undefined 36 limit: number 37 queryClient: QueryClient 38 moderationOpts: ModerationOpts | undefined 39 fetchAdditionalData: boolean 40 priority?: boolean 41}): Promise<{ 42 page: FeedPage 43 indexedAt: string | undefined 44}> { 45 const res = await agent.listNotifications({ 46 limit, 47 cursor, 48 // priority, 49 }) 50 51 const indexedAt = res.data.notifications[0]?.indexedAt 52 53 // filter out notifs by mod rules 54 const notifs = res.data.notifications.filter( 55 notif => !shouldFilterNotif(notif, moderationOpts), 56 ) 57 58 // group notifications which are essentially similar (follows, likes on a post) 59 let notifsGrouped = groupNotifications(notifs) 60 61 // we fetch subjects of notifications (usually posts) now instead of lazily 62 // in the UI to avoid relayouts 63 if (fetchAdditionalData) { 64 const subjects = await fetchSubjects(agent, notifsGrouped) 65 for (const notif of notifsGrouped) { 66 if (notif.subjectUri) { 67 if ( 68 notif.type === 'starterpack-joined' && 69 notif.notification.reasonSubject 70 ) { 71 notif.subject = subjects.starterPacks.get( 72 notif.notification.reasonSubject, 73 ) 74 } else { 75 notif.subject = subjects.posts.get(notif.subjectUri) 76 if (notif.subject) { 77 precacheProfile(queryClient, notif.subject.author) 78 } 79 } 80 } 81 } 82 } 83 84 let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() 85 if (Number.isNaN(seenAt.getTime())) { 86 seenAt = new Date() 87 } 88 89 return { 90 page: { 91 cursor: res.data.cursor, 92 seenAt, 93 items: notifsGrouped, 94 priority: res.data.priority ?? false, 95 }, 96 indexedAt, 97 } 98} 99 100// internal methods 101// = 102 103export function shouldFilterNotif( 104 notif: AppBskyNotificationListNotifications.Notification, 105 moderationOpts: ModerationOpts | undefined, 106): boolean { 107 if (!moderationOpts) { 108 return false 109 } 110 if (notif.author.viewer?.following) { 111 return false 112 } 113 return moderateNotification(notif, moderationOpts).ui('contentList').filter 114} 115 116export function groupNotifications( 117 notifs: AppBskyNotificationListNotifications.Notification[], 118): FeedNotification[] { 119 const groupedNotifs: FeedNotification[] = [] 120 for (const notif of notifs) { 121 const ts = +new Date(notif.indexedAt) 122 let grouped = false 123 if (GROUPABLE_REASONS.includes(notif.reason)) { 124 for (const groupedNotif of groupedNotifs) { 125 const ts2 = +new Date(groupedNotif.notification.indexedAt) 126 if ( 127 Math.abs(ts2 - ts) < MS_2DAY && 128 notif.reason === groupedNotif.notification.reason && 129 notif.reasonSubject === groupedNotif.notification.reasonSubject && 130 notif.author.did !== groupedNotif.notification.author.did 131 ) { 132 const nextIsFollowBack = 133 notif.reason === 'follow' && notif.author.viewer?.following 134 const prevIsFollowBack = 135 groupedNotif.notification.reason === 'follow' && 136 groupedNotif.notification.author.viewer?.following 137 const shouldUngroup = nextIsFollowBack || prevIsFollowBack 138 if (!shouldUngroup) { 139 groupedNotif.additional = groupedNotif.additional || [] 140 groupedNotif.additional.push(notif) 141 grouped = true 142 break 143 } 144 } 145 } 146 } 147 if (!grouped) { 148 const type = toKnownType(notif) 149 if (type !== 'starterpack-joined') { 150 groupedNotifs.push({ 151 _reactKey: `notif-${notif.uri}`, 152 type, 153 notification: notif, 154 subjectUri: getSubjectUri(type, notif), 155 }) 156 } else { 157 groupedNotifs.push({ 158 _reactKey: `notif-${notif.uri}`, 159 type: 'starterpack-joined', 160 notification: notif, 161 subjectUri: notif.uri, 162 }) 163 } 164 } 165 } 166 return groupedNotifs 167} 168 169async function fetchSubjects( 170 agent: BskyAgent, 171 groupedNotifs: FeedNotification[], 172): Promise<{ 173 posts: Map<string, AppBskyFeedDefs.PostView> 174 starterPacks: Map<string, AppBskyGraphDefs.StarterPackViewBasic> 175}> { 176 const postUris = new Set<string>() 177 const packUris = new Set<string>() 178 for (const notif of groupedNotifs) { 179 if (notif.subjectUri?.includes('app.bsky.feed.post')) { 180 postUris.add(notif.subjectUri) 181 } else if ( 182 notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack') 183 ) { 184 packUris.add(notif.notification.reasonSubject) 185 } 186 } 187 const postUriChunks = chunk(Array.from(postUris), 25) 188 const packUriChunks = chunk(Array.from(packUris), 25) 189 const postsChunks = await Promise.all( 190 postUriChunks.map(uris => 191 agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts), 192 ), 193 ) 194 const packsChunks = await Promise.all( 195 packUriChunks.map(uris => 196 agent.app.bsky.graph 197 .getStarterPacks({uris}) 198 .then(res => res.data.starterPacks), 199 ), 200 ) 201 const postsMap = new Map<string, AppBskyFeedDefs.PostView>() 202 const packsMap = new Map<string, AppBskyGraphDefs.StarterPackView>() 203 for (const post of postsChunks.flat()) { 204 if ( 205 AppBskyFeedPost.isRecord(post.record) && 206 AppBskyFeedPost.validateRecord(post.record).success 207 ) { 208 postsMap.set(post.uri, post) 209 } 210 } 211 for (const pack of packsChunks.flat()) { 212 if (AppBskyGraphStarterpack.isRecord(pack.record)) { 213 packsMap.set(pack.uri, pack) 214 } 215 } 216 return { 217 posts: postsMap, 218 starterPacks: packsMap, 219 } 220} 221 222function toKnownType( 223 notif: AppBskyNotificationListNotifications.Notification, 224): NotificationType { 225 if (notif.reason === 'like') { 226 if (notif.reasonSubject?.includes('feed.generator')) { 227 return 'feedgen-like' 228 } 229 return 'post-like' 230 } 231 if ( 232 notif.reason === 'repost' || 233 notif.reason === 'mention' || 234 notif.reason === 'reply' || 235 notif.reason === 'quote' || 236 notif.reason === 'follow' || 237 notif.reason === 'starterpack-joined' 238 ) { 239 return notif.reason as NotificationType 240 } 241 return 'unknown' 242} 243 244function getSubjectUri( 245 type: NotificationType, 246 notif: AppBskyNotificationListNotifications.Notification, 247): string | undefined { 248 if (type === 'reply' || type === 'quote' || type === 'mention') { 249 return notif.uri 250 } else if (type === 'post-like' || type === 'repost') { 251 if ( 252 AppBskyFeedRepost.isRecord(notif.record) || 253 AppBskyFeedLike.isRecord(notif.record) 254 ) { 255 return typeof notif.record.subject?.uri === 'string' 256 ? notif.record.subject?.uri 257 : undefined 258 } 259 } else if (type === 'feedgen-like') { 260 return notif.reasonSubject 261 } 262}