From c802030a94ad9a80c9b785555830cd068d3b9635 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 30 Jun 2025 21:46:15 +0300 Subject: [PATCH] handle feed item --- bskyweb/cmd/bskyweb/server.go | 1 + src/Navigation.tsx | 6 +++ src/lib/api/feed/list.ts | 12 ++--- src/lib/api/feed/posts.ts | 44 +++++++++++++++++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/screens/Notifications/ActivityList.tsx | 42 ++++++++++++++++ src/state/queries/notifications/feed.ts | 14 +++--- src/state/queries/notifications/types.ts | 1 + src/state/queries/notifications/util.ts | 11 ++++- src/state/queries/post-feed.ts | 6 +++ .../notifications/NotificationFeedItem.tsx | 48 ++++++++++++++++++- 12 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 src/lib/api/feed/posts.ts create mode 100644 src/screens/Notifications/ActivityList.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 1654b3e498..755f2fa3f6 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -258,6 +258,7 @@ func serve(cctx *cli.Context) error { e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/notifications/settings", server.WebGeneric) + e.GET("/notifications/activity", server.WebGeneric) e.GET("/lists", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/modlists", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 5f82772281..26a2b2a2ae 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -110,6 +110,7 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {NotificationsActivityListScreen} from './screens/Notifications/ActivityList' import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings' @@ -542,6 +543,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => MessagesInboxScreen} options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> + NotificationsActivityListScreen} + options={{title: title(msg`Notifications`), requireAuth: true}} + /> LegacyNotificationSettingsScreen} diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index 9744e3d4ce..9697b0aaf3 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -1,20 +1,20 @@ import { - AppBskyFeedDefs, - AppBskyFeedGetListFeed as GetListFeed, - BskyAgent, + type Agent, + type AppBskyFeedDefs, + type AppBskyFeedGetListFeed as GetListFeed, } from '@atproto/api' -import {FeedAPI, FeedAPIResponse} from './types' +import {type FeedAPI, type FeedAPIResponse} from './types' export class ListFeedAPI implements FeedAPI { - agent: BskyAgent + agent: Agent params: GetListFeed.QueryParams constructor({ agent, feedParams, }: { - agent: BskyAgent + agent: Agent feedParams: GetListFeed.QueryParams }) { this.agent = agent diff --git a/src/lib/api/feed/posts.ts b/src/lib/api/feed/posts.ts new file mode 100644 index 0000000000..36a3e6dd3d --- /dev/null +++ b/src/lib/api/feed/posts.ts @@ -0,0 +1,44 @@ +import { + type Agent, + type AppBskyFeedDefs, + type AppBskyFeedGetPosts, +} from '@atproto/api' + +import {type FeedAPI, type FeedAPIResponse} from './types' + +export class PostListFeedAPI implements FeedAPI { + agent: Agent + params: AppBskyFeedGetPosts.QueryParams + peek: AppBskyFeedDefs.FeedViewPost | null = null + + constructor({ + agent, + feedParams, + }: { + agent: Agent + feedParams: AppBskyFeedGetPosts.QueryParams + }) { + this.agent = agent + this.params = feedParams + } + + async peekLatest(): Promise { + if (this.peek) return this.peek + throw new Error('Has not fetched yet') + } + + async fetch({}: {}): Promise { + const res = await this.agent.app.bsky.feed.getPosts({ + ...this.params, + }) + if (res.success) { + this.peek = {post: res.data.posts[0]} + return { + feed: res.data.posts.map(post => ({post})), + } + } + return { + feed: [], + } + } +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index abc1dc4f40..b1db5caa64 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -73,6 +73,7 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesSettings: undefined MessagesInbox: undefined + NotificationsActivityList: {posts: string} LegacyNotificationSettings: undefined Feeds: undefined Start: {name: string; rkey: string} diff --git a/src/routes.ts b/src/routes.ts index 763f060a1c..7fc673e2be 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -11,6 +11,7 @@ export const router = new Router({ Search: '/search', Feeds: '/feeds', Notifications: '/notifications', + NotificationsActivityList: '/notifications/activity', LegacyNotificationSettings: '/notifications/settings', Settings: '/settings', Lists: '/lists', diff --git a/src/screens/Notifications/ActivityList.tsx b/src/screens/Notifications/ActivityList.tsx new file mode 100644 index 0000000000..560f860972 --- /dev/null +++ b/src/screens/Notifications/ActivityList.tsx @@ -0,0 +1,42 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type AllNavigatorParams} from '#/lib/routes/types' +import {PostFeed} from '#/view/com/posts/PostFeed' +import {EmptyState} from '#/view/com/util/EmptyState' +import * as Layout from '#/components/Layout' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'NotificationsActivityList' +> +export function NotificationsActivityListScreen({ + route: { + params: {posts}, + }, +}: Props) { + const uris = decodeURIComponent(posts) + const {_} = useLingui() + + return ( + + + + + + Notifications + + + + + ( + + )} + disablePoll + /> + + ) +} diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index fce7802bc3..6010f11b40 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -18,30 +18,30 @@ import {useCallback, useEffect, useMemo, useRef} from 'react' import { - AppBskyActorDefs, + type AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost, AtUri, moderatePost, } from '@atproto/api' import { - InfiniteData, - QueryClient, - QueryKey, + type InfiniteData, + type QueryClient, + type QueryKey, useInfiniteQuery, useQueryClient, } from '@tanstack/react-query' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {STALE} from '#/state/queries' import {useAgent} from '#/state/session' import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' -import {useModerationOpts} from '../../preferences/moderation-opts' -import {STALE} from '..' import { didOrHandleUriMatches, embedViewRecordToPostView, getEmbeddedPost, } from '../util' -import {FeedPage} from './types' +import {type FeedPage} from './types' import {useUnreadNotificationsApi} from './unread' import {fetchPage} from './util' diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index e05715f77d..a7b837086b 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -48,6 +48,7 @@ type OtherNotificationType = | 'unverified' | 'like-via-repost' | 'repost-via-repost' + | 'subscribed-post' | 'unknown' type FeedNotificationBase = { diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 007f65cc73..4cb023ebca 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -28,6 +28,7 @@ const GROUPABLE_REASONS = [ 'follow', 'like-via-repost', 'repost-via-repost', + 'subscribed-post', ] const MS_1HR = 1e3 * 60 * 60 const MS_2DAY = MS_1HR * 48 @@ -252,7 +253,8 @@ function toKnownType( notif.reason === 'verified' || notif.reason === 'unverified' || notif.reason === 'like-via-repost' || - notif.reason === 'repost-via-repost' + notif.reason === 'repost-via-repost' || + notif.reason === 'subscribed-post' ) { return notif.reason as NotificationType } @@ -263,7 +265,12 @@ function getSubjectUri( type: NotificationType, notif: AppBskyNotificationListNotifications.Notification, ): string | undefined { - if (type === 'reply' || type === 'quote' || type === 'mention') { + if ( + type === 'reply' || + type === 'quote' || + type === 'mention' || + type === 'subscribed-post' + ) { return notif.uri } else if ( type === 'post-like' || diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 920892924e..361081e67e 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -24,6 +24,7 @@ import {HomeFeedAPI} from '#/lib/api/feed/home' import {LikesFeedAPI} from '#/lib/api/feed/likes' import {ListFeedAPI} from '#/lib/api/feed/list' import {MergeFeedAPI} from '#/lib/api/feed/merge' +import {PostListFeedAPI} from '#/lib/api/feed/posts' import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types' import {aggregateUserInterests} from '#/lib/api/feed/utils' import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip' @@ -53,6 +54,7 @@ export type AuthorFilter = | 'posts_with_video' type FeedUri = string type ListUri = string +type PostsUriList = string export type FeedDescriptor = | 'following' @@ -60,6 +62,7 @@ export type FeedDescriptor = | `feedgen|${FeedUri}` | `likes|${ActorDid}` | `list|${ListUri}` + | `posts|${PostsUriList}` | 'demo' export interface FeedParams { mergeFeedEnabled?: boolean @@ -488,6 +491,9 @@ function createApi({ } else if (feedDesc.startsWith('list')) { const [_, list] = feedDesc.split('|') return new ListFeedAPI({agent, feedParams: {list}}) + } else if (feedDesc.startsWith('posts')) { + const [_, uriList] = feedDesc.split('|') + return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}}) } else if (feedDesc === 'demo') { return new DemoFeedAPI({agent}) } else { diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 85f67919ac..111054446e 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -52,6 +52,7 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, platform, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' +import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, @@ -114,7 +115,9 @@ let NotificationFeedItem = ({ case 'unverified': { return makeProfileLink(item.notification.author) } - case 'reply': { + case 'reply': + case 'mention': + case 'quote': { const uripReply = new AtUri(item.notification.uri) return `/profile/${uripReply.host}/post/${uripReply.rkey}` } @@ -126,6 +129,13 @@ let NotificationFeedItem = ({ } break } + case 'subscribed-post': { + const posts: string[] = [] + for (const post of [item.notification, ...(item.additional ?? [])]) { + posts.push(post.uri) + } + return `/notifications/activity?posts=${encodeURIComponent(posts.join(','))}` + } } return '' @@ -503,6 +513,42 @@ let NotificationFeedItem = ({ {firstAuthorLink} reposted your repost ) icon = + } else if (item.type === 'subscribed-post') { + const postsCount = 1 + (item.additional?.length || 0) + a11yLabel = hasMultipleAuthors + ? _( + msg`New posts from ${firstAuthorName} and ${plural( + additionalAuthorsCount, + { + one: `${formattedAuthorsCount} other`, + other: `${formattedAuthorsCount} others`, + }, + )}`, + ) + : _( + msg`New ${plural(postsCount, { + one: 'post', + other: 'posts', + })} from ${firstAuthorName}`, + ) + notificationContent = hasMultipleAuthors ? ( + + New posts from {firstAuthorLink} and{' '} + + + {' '} + + ) : ( + + New from{' '} + {firstAuthorLink} + + ) + icon = } else { return null }