handle feed item

This commit is contained in:
Samuel Newman
2025-06-30 21:46:15 +03:00
parent a0a8a4d50c
commit c802030a94
12 changed files with 171 additions and 16 deletions
+1
View File
@@ -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)
+6
View File
@@ -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}}
/>
<Stack.Screen
name="NotificationsActivityList"
getComponent={() => NotificationsActivityListScreen}
options={{title: title(msg`Notifications`), requireAuth: true}}
/>
<Stack.Screen
name="LegacyNotificationSettings"
getComponent={() => LegacyNotificationSettingsScreen}
+6 -6
View File
@@ -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
+44
View File
@@ -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<AppBskyFeedDefs.FeedViewPost> {
if (this.peek) return this.peek
throw new Error('Has not fetched yet')
}
async fetch({}: {}): Promise<FeedAPIResponse> {
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: [],
}
}
}
+1
View File
@@ -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}
+1
View File
@@ -11,6 +11,7 @@ export const router = new Router<AllNavigatableRoutes>({
Search: '/search',
Feeds: '/feeds',
Notifications: '/notifications',
NotificationsActivityList: '/notifications/activity',
LegacyNotificationSettings: '/notifications/settings',
Settings: '/settings',
Lists: '/lists',
@@ -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 (
<Layout.Screen testID="NotificationsActivityListScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Notifications</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostFeed
feed={`posts|${uris}`}
renderEmptyState={() => (
<EmptyState icon="growth" message={_(msg`No posts here`)} />
)}
disablePoll
/>
</Layout.Screen>
)
}
+7 -7
View File
@@ -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'
+1
View File
@@ -48,6 +48,7 @@ type OtherNotificationType =
| 'unverified'
| 'like-via-repost'
| 'repost-via-repost'
| 'subscribed-post'
| 'unknown'
type FeedNotificationBase = {
+9 -2
View File
@@ -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' ||
+6
View File
@@ -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 {
@@ -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 = ({
<Trans>{firstAuthorLink} reposted your repost</Trans>
)
icon = <RepostIcon size="xl" style={{color: t.palette.positive_600}} />
} 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 ? (
<Trans>
New posts from {firstAuthorLink} and{' '}
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
<Plural
value={additionalAuthorsCount}
one={`${formattedAuthorsCount} other`}
other={`${formattedAuthorsCount} others`}
/>
</Text>{' '}
</Trans>
) : (
<Trans>
New <Plural value={postsCount} one="post" other="posts" /> from{' '}
{firstAuthorLink}
</Trans>
)
icon = <BellRingingIcon size="xl" style={{color: t.palette.primary_500}} />
} else {
return null
}