handle feed item
This commit is contained in:
@@ -258,6 +258,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/feeds", server.WebGeneric)
|
e.GET("/feeds", server.WebGeneric)
|
||||||
e.GET("/notifications", server.WebGeneric)
|
e.GET("/notifications", server.WebGeneric)
|
||||||
e.GET("/notifications/settings", server.WebGeneric)
|
e.GET("/notifications/settings", server.WebGeneric)
|
||||||
|
e.GET("/notifications/activity", server.WebGeneric)
|
||||||
e.GET("/lists", server.WebGeneric)
|
e.GET("/lists", server.WebGeneric)
|
||||||
e.GET("/moderation", server.WebGeneric)
|
e.GET("/moderation", server.WebGeneric)
|
||||||
e.GET("/moderation/modlists", server.WebGeneric)
|
e.GET("/moderation/modlists", server.WebGeneric)
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ import {
|
|||||||
} from '#/components/dialogs/EmailDialog'
|
} from '#/components/dialogs/EmailDialog'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
import {NotificationsActivityListScreen} from './screens/Notifications/ActivityList'
|
||||||
import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings'
|
import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings'
|
||||||
import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings'
|
import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings'
|
||||||
import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings'
|
import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings'
|
||||||
@@ -542,6 +543,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => MessagesInboxScreen}
|
getComponent={() => MessagesInboxScreen}
|
||||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="NotificationsActivityList"
|
||||||
|
getComponent={() => NotificationsActivityListScreen}
|
||||||
|
options={{title: title(msg`Notifications`), requireAuth: true}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="LegacyNotificationSettings"
|
name="LegacyNotificationSettings"
|
||||||
getComponent={() => LegacyNotificationSettingsScreen}
|
getComponent={() => LegacyNotificationSettingsScreen}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
type Agent,
|
||||||
AppBskyFeedGetListFeed as GetListFeed,
|
type AppBskyFeedDefs,
|
||||||
BskyAgent,
|
type AppBskyFeedGetListFeed as GetListFeed,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {type FeedAPI, type FeedAPIResponse} from './types'
|
||||||
|
|
||||||
export class ListFeedAPI implements FeedAPI {
|
export class ListFeedAPI implements FeedAPI {
|
||||||
agent: BskyAgent
|
agent: Agent
|
||||||
params: GetListFeed.QueryParams
|
params: GetListFeed.QueryParams
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
agent,
|
agent,
|
||||||
feedParams,
|
feedParams,
|
||||||
}: {
|
}: {
|
||||||
agent: BskyAgent
|
agent: Agent
|
||||||
feedParams: GetListFeed.QueryParams
|
feedParams: GetListFeed.QueryParams
|
||||||
}) {
|
}) {
|
||||||
this.agent = agent
|
this.agent = agent
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ export type CommonNavigatorParams = {
|
|||||||
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
MessagesInbox: undefined
|
MessagesInbox: undefined
|
||||||
|
NotificationsActivityList: {posts: string}
|
||||||
LegacyNotificationSettings: undefined
|
LegacyNotificationSettings: undefined
|
||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
Start: {name: string; rkey: string}
|
Start: {name: string; rkey: string}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
|||||||
Search: '/search',
|
Search: '/search',
|
||||||
Feeds: '/feeds',
|
Feeds: '/feeds',
|
||||||
Notifications: '/notifications',
|
Notifications: '/notifications',
|
||||||
|
NotificationsActivityList: '/notifications/activity',
|
||||||
LegacyNotificationSettings: '/notifications/settings',
|
LegacyNotificationSettings: '/notifications/settings',
|
||||||
Settings: '/settings',
|
Settings: '/settings',
|
||||||
Lists: '/lists',
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,30 +18,30 @@
|
|||||||
|
|
||||||
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderatePost,
|
moderatePost,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
InfiniteData,
|
type InfiniteData,
|
||||||
QueryClient,
|
type QueryClient,
|
||||||
QueryKey,
|
type QueryKey,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {STALE} from '#/state/queries'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
|
import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
|
||||||
import {useModerationOpts} from '../../preferences/moderation-opts'
|
|
||||||
import {STALE} from '..'
|
|
||||||
import {
|
import {
|
||||||
didOrHandleUriMatches,
|
didOrHandleUriMatches,
|
||||||
embedViewRecordToPostView,
|
embedViewRecordToPostView,
|
||||||
getEmbeddedPost,
|
getEmbeddedPost,
|
||||||
} from '../util'
|
} from '../util'
|
||||||
import {FeedPage} from './types'
|
import {type FeedPage} from './types'
|
||||||
import {useUnreadNotificationsApi} from './unread'
|
import {useUnreadNotificationsApi} from './unread'
|
||||||
import {fetchPage} from './util'
|
import {fetchPage} from './util'
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type OtherNotificationType =
|
|||||||
| 'unverified'
|
| 'unverified'
|
||||||
| 'like-via-repost'
|
| 'like-via-repost'
|
||||||
| 'repost-via-repost'
|
| 'repost-via-repost'
|
||||||
|
| 'subscribed-post'
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
|
|
||||||
type FeedNotificationBase = {
|
type FeedNotificationBase = {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const GROUPABLE_REASONS = [
|
|||||||
'follow',
|
'follow',
|
||||||
'like-via-repost',
|
'like-via-repost',
|
||||||
'repost-via-repost',
|
'repost-via-repost',
|
||||||
|
'subscribed-post',
|
||||||
]
|
]
|
||||||
const MS_1HR = 1e3 * 60 * 60
|
const MS_1HR = 1e3 * 60 * 60
|
||||||
const MS_2DAY = MS_1HR * 48
|
const MS_2DAY = MS_1HR * 48
|
||||||
@@ -252,7 +253,8 @@ function toKnownType(
|
|||||||
notif.reason === 'verified' ||
|
notif.reason === 'verified' ||
|
||||||
notif.reason === 'unverified' ||
|
notif.reason === 'unverified' ||
|
||||||
notif.reason === 'like-via-repost' ||
|
notif.reason === 'like-via-repost' ||
|
||||||
notif.reason === 'repost-via-repost'
|
notif.reason === 'repost-via-repost' ||
|
||||||
|
notif.reason === 'subscribed-post'
|
||||||
) {
|
) {
|
||||||
return notif.reason as NotificationType
|
return notif.reason as NotificationType
|
||||||
}
|
}
|
||||||
@@ -263,7 +265,12 @@ function getSubjectUri(
|
|||||||
type: NotificationType,
|
type: NotificationType,
|
||||||
notif: AppBskyNotificationListNotifications.Notification,
|
notif: AppBskyNotificationListNotifications.Notification,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (type === 'reply' || type === 'quote' || type === 'mention') {
|
if (
|
||||||
|
type === 'reply' ||
|
||||||
|
type === 'quote' ||
|
||||||
|
type === 'mention' ||
|
||||||
|
type === 'subscribed-post'
|
||||||
|
) {
|
||||||
return notif.uri
|
return notif.uri
|
||||||
} else if (
|
} else if (
|
||||||
type === 'post-like' ||
|
type === 'post-like' ||
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {HomeFeedAPI} from '#/lib/api/feed/home'
|
|||||||
import {LikesFeedAPI} from '#/lib/api/feed/likes'
|
import {LikesFeedAPI} from '#/lib/api/feed/likes'
|
||||||
import {ListFeedAPI} from '#/lib/api/feed/list'
|
import {ListFeedAPI} from '#/lib/api/feed/list'
|
||||||
import {MergeFeedAPI} from '#/lib/api/feed/merge'
|
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 {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||||
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
||||||
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
|
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
|
||||||
@@ -53,6 +54,7 @@ export type AuthorFilter =
|
|||||||
| 'posts_with_video'
|
| 'posts_with_video'
|
||||||
type FeedUri = string
|
type FeedUri = string
|
||||||
type ListUri = string
|
type ListUri = string
|
||||||
|
type PostsUriList = string
|
||||||
|
|
||||||
export type FeedDescriptor =
|
export type FeedDescriptor =
|
||||||
| 'following'
|
| 'following'
|
||||||
@@ -60,6 +62,7 @@ export type FeedDescriptor =
|
|||||||
| `feedgen|${FeedUri}`
|
| `feedgen|${FeedUri}`
|
||||||
| `likes|${ActorDid}`
|
| `likes|${ActorDid}`
|
||||||
| `list|${ListUri}`
|
| `list|${ListUri}`
|
||||||
|
| `posts|${PostsUriList}`
|
||||||
| 'demo'
|
| 'demo'
|
||||||
export interface FeedParams {
|
export interface FeedParams {
|
||||||
mergeFeedEnabled?: boolean
|
mergeFeedEnabled?: boolean
|
||||||
@@ -488,6 +491,9 @@ function createApi({
|
|||||||
} else if (feedDesc.startsWith('list')) {
|
} else if (feedDesc.startsWith('list')) {
|
||||||
const [_, list] = feedDesc.split('|')
|
const [_, list] = feedDesc.split('|')
|
||||||
return new ListFeedAPI({agent, feedParams: {list}})
|
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') {
|
} else if (feedDesc === 'demo') {
|
||||||
return new DemoFeedAPI({agent})
|
return new DemoFeedAPI({agent})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
|||||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, platform, useTheme} from '#/alf'
|
import {atoms as a, platform, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||||
import {
|
import {
|
||||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
||||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
||||||
@@ -114,7 +115,9 @@ let NotificationFeedItem = ({
|
|||||||
case 'unverified': {
|
case 'unverified': {
|
||||||
return makeProfileLink(item.notification.author)
|
return makeProfileLink(item.notification.author)
|
||||||
}
|
}
|
||||||
case 'reply': {
|
case 'reply':
|
||||||
|
case 'mention':
|
||||||
|
case 'quote': {
|
||||||
const uripReply = new AtUri(item.notification.uri)
|
const uripReply = new AtUri(item.notification.uri)
|
||||||
return `/profile/${uripReply.host}/post/${uripReply.rkey}`
|
return `/profile/${uripReply.host}/post/${uripReply.rkey}`
|
||||||
}
|
}
|
||||||
@@ -126,6 +129,13 @@ let NotificationFeedItem = ({
|
|||||||
}
|
}
|
||||||
break
|
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 ''
|
return ''
|
||||||
@@ -503,6 +513,42 @@ let NotificationFeedItem = ({
|
|||||||
<Trans>{firstAuthorLink} reposted your repost</Trans>
|
<Trans>{firstAuthorLink} reposted your repost</Trans>
|
||||||
)
|
)
|
||||||
icon = <RepostIcon size="xl" style={{color: t.palette.positive_600}} />
|
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 {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user