Indicate if someone followed you via a starter pack (#11320)
This commit is contained in:
@@ -1638,31 +1638,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"typescript/no-floating-promises": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-misused-promises": {
|
||||
"count": 2
|
||||
},
|
||||
"typescript/no-unsafe-member-access": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"src/view/com/notifications/NotificationFeedItem.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 3
|
||||
},
|
||||
"typescript/no-misused-promises": {
|
||||
"count": 3
|
||||
},
|
||||
"typescript/no-unsafe-member-access": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/view/com/pager/Pager.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 4
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@
|
||||
"prettier": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.33",
|
||||
"@atproto/api": "0.20.34",
|
||||
"@atproto/common-web": "0.5.6",
|
||||
"@atproto/syntax": "0.7.2",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
|
||||
Generated
+5
-5
@@ -242,8 +242,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.33
|
||||
version: 0.20.33
|
||||
specifier: 0.20.34
|
||||
version: 0.20.34
|
||||
'@atproto/common-web':
|
||||
specifier: 0.5.6
|
||||
version: 0.5.6
|
||||
@@ -871,8 +871,8 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@atproto/api@0.20.33':
|
||||
resolution: {integrity: sha512-3YpnBVMieQFWetLvqibn2yG6vhNZ7ozSi/EUujuGcvYXt9g/NrvmHufaUtrXCasp2mHDI7t7UG5sLEBg6YNEJw==}
|
||||
'@atproto/api@0.20.34':
|
||||
resolution: {integrity: sha512-nKfCLkH2Al58YupLGtoVIucZ5XjtN9sU5oOTI70lp8J4nxl52C/Tk7AktcWe+Nx7st3I0fUgf+C5cHePgvwT0w==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.6':
|
||||
@@ -9389,7 +9389,7 @@ snapshots:
|
||||
|
||||
'@0no-co/graphql.web@1.2.0': {}
|
||||
|
||||
'@atproto/api@0.20.33':
|
||||
'@atproto/api@0.20.34':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.6
|
||||
'@atproto/lexicon': 0.7.7
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {type AppBskyNotificationListNotifications} from '@atproto/api'
|
||||
import {describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
import {groupNotifications} from '../util'
|
||||
|
||||
jest.mock('#/state/queries/profile', () => ({precacheProfile: jest.fn()}))
|
||||
|
||||
type Notification = AppBskyNotificationListNotifications.Notification
|
||||
|
||||
function makeFollowNotification(
|
||||
did: string,
|
||||
starterPackUri?: string,
|
||||
): Notification {
|
||||
return {
|
||||
uri: `at://${did}/app.bsky.graph.follow/follow`,
|
||||
cid: `cid-${did}`,
|
||||
author: {
|
||||
did,
|
||||
handle: `${did}.test`,
|
||||
displayName: did,
|
||||
avatar: undefined,
|
||||
associated: undefined,
|
||||
viewer: {},
|
||||
labels: [],
|
||||
createdAt: '2026-07-28T12:00:00.000Z',
|
||||
},
|
||||
reason: 'follow',
|
||||
record: {},
|
||||
starterPack: starterPackUri
|
||||
? ({uri: starterPackUri} as Notification['starterPack'])
|
||||
: undefined,
|
||||
isRead: false,
|
||||
indexedAt: '2026-07-28T12:00:00.000Z',
|
||||
}
|
||||
}
|
||||
|
||||
describe('groupNotifications', () => {
|
||||
it('groups follows by starter pack', () => {
|
||||
const packA = 'at://did:plc:alice/app.bsky.graph.starterpack/a'
|
||||
const packB = 'at://did:plc:bob/app.bsky.graph.starterpack/b'
|
||||
|
||||
const grouped = groupNotifications([
|
||||
makeFollowNotification('did:plc:a', packA),
|
||||
makeFollowNotification('did:plc:b', packB),
|
||||
makeFollowNotification('did:plc:c', packA),
|
||||
makeFollowNotification('did:plc:d'),
|
||||
makeFollowNotification('did:plc:e', packB),
|
||||
makeFollowNotification('did:plc:f'),
|
||||
])
|
||||
|
||||
expect(
|
||||
grouped.map(item => [
|
||||
item.notification.author.did,
|
||||
...(item.additional ?? []).map(notification => notification.author.did),
|
||||
]),
|
||||
).toEqual([
|
||||
['did:plc:a', 'did:plc:c'],
|
||||
['did:plc:b', 'did:plc:e'],
|
||||
['did:plc:d', 'did:plc:f'],
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -163,6 +163,9 @@ export function groupNotifications(
|
||||
Math.abs(ts2 - ts) < MS_2DAY &&
|
||||
notif.reason === groupedNotif.notification.reason &&
|
||||
notif.reasonSubject === groupedNotif.notification.reasonSubject &&
|
||||
(notif.reason !== 'follow' ||
|
||||
notif.starterPack?.uri ===
|
||||
groupedNotif.notification.starterPack?.uri) &&
|
||||
(notif.author.did !== groupedNotif.notification.author.did ||
|
||||
notif.reason === 'subscribed-post')
|
||||
) {
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
type ListRenderItemInfo,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {ActivityIndicator, type ListRenderItemInfo, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||
import {
|
||||
type FeedNotification,
|
||||
useNotificationFeedQuery,
|
||||
} from '#/state/queries/notifications/feed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {List, type ListProps, type ListRef} from '#/view/com/util/List'
|
||||
import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {atoms as a, platform} from '#/alf'
|
||||
import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
||||
import {NotificationFeedItem} from './NotificationFeedItem'
|
||||
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_FEED_ITEM = {type: 'empty', _reactKey: '__empty__'} as const
|
||||
const LOAD_MORE_ERROR_ITEM = {
|
||||
type: 'load-more-error',
|
||||
_reactKey: '__load_more_error__',
|
||||
} as const
|
||||
const LOADING_ITEM = {type: 'loading', _reactKey: '__loading__'} as const
|
||||
|
||||
type NotificationFeedListItem =
|
||||
| FeedNotification
|
||||
| typeof EMPTY_FEED_ITEM
|
||||
| typeof LOAD_MORE_ERROR_ITEM
|
||||
| typeof LOADING_ITEM
|
||||
|
||||
export function NotificationFeed({
|
||||
filter,
|
||||
@@ -46,7 +52,7 @@ export function NotificationFeed({
|
||||
}) {
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const trackPostView = usePostViewTracking('Notifications')
|
||||
const {
|
||||
@@ -71,7 +77,7 @@ export function NotificationFeed({
|
||||
!isFetching && !data?.pages.find(page => page.items.length > 0)
|
||||
|
||||
const items = useMemo(() => {
|
||||
let arr: any[] = []
|
||||
let arr: NotificationFeedListItem[] = []
|
||||
if (isFetched) {
|
||||
if (isEmpty) {
|
||||
arr = arr.concat([EMPTY_FEED_ITEM])
|
||||
@@ -113,29 +119,27 @@ export function NotificationFeed({
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const onPressRetryLoadMore = useCallback(() => {
|
||||
fetchNextPage()
|
||||
void fetchNextPage()
|
||||
}, [fetchNextPage])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item, index}: ListRenderItemInfo<any>) => {
|
||||
if (item === EMPTY_FEED_ITEM) {
|
||||
({item, index}: ListRenderItemInfo<NotificationFeedListItem>) => {
|
||||
if (item.type === 'empty') {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={BellIcon}
|
||||
message={_(msg`No notifications yet!`)}
|
||||
style={styles.emptyState}
|
||||
message={l`No notifications yet!`}
|
||||
style={[a.py_5xl]}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
} else if (item.type === 'load-more-error') {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label={_(
|
||||
msg`There was an issue fetching notifications. Tap here to try again.`,
|
||||
)}
|
||||
label={l`There was an issue fetching notifications. Tap here to try again.`}
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOADING_ITEM) {
|
||||
} else if (item.type === 'loading') {
|
||||
return <NotificationFeedLoadingPlaceholder />
|
||||
}
|
||||
return (
|
||||
@@ -147,13 +151,13 @@ export function NotificationFeed({
|
||||
/>
|
||||
)
|
||||
},
|
||||
[moderationOpts, _, onPressRetryLoadMore, filter],
|
||||
[moderationOpts, l, onPressRetryLoadMore, filter],
|
||||
)
|
||||
|
||||
const FeedFooter = useCallback(
|
||||
() =>
|
||||
isFetchingNextPage ? (
|
||||
<View style={styles.feedFooter}>
|
||||
<View style={[a.pt_xl]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
@@ -169,7 +173,11 @@ export function NotificationFeed({
|
||||
}, [enabled])
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
<View
|
||||
style={platform({
|
||||
web: {minHeight: '100%'},
|
||||
default: {height: '100%'},
|
||||
})}>
|
||||
{error && (
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
@@ -180,16 +188,16 @@ export function NotificationFeed({
|
||||
testID="notifsFeed"
|
||||
ref={scrollElRef}
|
||||
data={items}
|
||||
keyExtractor={item => item._reactKey}
|
||||
keyExtractor={(item: NotificationFeedListItem) => item._reactKey}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListFooterComponent={FeedFooter}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
onRefresh={() => void onRefresh()}
|
||||
onEndReached={() => void onEndReached()}
|
||||
onEndReachedThreshold={2}
|
||||
onScrolledDownChange={onScrolledDownChange}
|
||||
onItemSeen={item => {
|
||||
onItemSeen={(item: NotificationFeedListItem) => {
|
||||
if (
|
||||
(item.type === 'reply' ||
|
||||
item.type === 'mention' ||
|
||||
@@ -199,7 +207,7 @@ export function NotificationFeed({
|
||||
trackPostView(item.subject)
|
||||
}
|
||||
}}
|
||||
contentContainerStyle={s.contentContainer}
|
||||
contentContainerStyle={{paddingBottom: 200}}
|
||||
desktopFixedHeight
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
@@ -209,8 +217,3 @@ export function NotificationFeed({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
feedFooter: {paddingTop: 20},
|
||||
emptyState: {paddingVertical: 40},
|
||||
})
|
||||
|
||||
@@ -12,16 +12,17 @@ import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
type AppBskyGraphDefs,
|
||||
AppBskyGraphFollow,
|
||||
AppBskyGraphStarterpack,
|
||||
AtUri,
|
||||
moderateProfile,
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {TID} from '@atproto/common-web'
|
||||
import {msg, plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -32,7 +33,6 @@ import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {forceLTR} from '#/lib/strings/bidi'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {type FeedNotification} from '#/state/queries/notifications/feed'
|
||||
@@ -44,7 +44,7 @@ import {Post} from '#/view/com/post/Post'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||
import {atoms as a, native, platform, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
@@ -64,7 +64,10 @@ import * as MediaPreview from '#/components/MediaPreview'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {
|
||||
Notification as StarterPackCard,
|
||||
useStarterPackLink,
|
||||
} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -93,9 +96,9 @@ let NotificationFeedItem = ({
|
||||
}): React.ReactNode => {
|
||||
const queryClient = useQueryClient()
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {t: l, i18n} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const [isAuthorsExpanded, setIsAuthorsExpanded] = useState<boolean>(false)
|
||||
const [isAuthorsExpanded, setIsAuthorsExpanded] = useState(false)
|
||||
const [isHoveringAuthorsList, setIsHoveringAuthorsList] = useState(false)
|
||||
const itemHref = useMemo(() => {
|
||||
switch (item.type) {
|
||||
@@ -253,7 +256,7 @@ let NotificationFeedItem = ({
|
||||
to={firstAuthor.href}
|
||||
disableMismatchWarning
|
||||
emoji
|
||||
label={_(msg`Go to ${firstAuthorName}'s profile`)}>
|
||||
label={l`Go to ${firstAuthorName}'s profile`}>
|
||||
{forceLTR(firstAuthorName)}
|
||||
<ProfileBadges
|
||||
profile={firstAuthor.profile}
|
||||
@@ -275,12 +278,23 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
const additionalAuthorsCount = authors.length - 1
|
||||
const hasMultipleAuthors = additionalAuthorsCount > 0
|
||||
const starterPack = item.notification.starterPack
|
||||
const allFollowedViaSameStarterPack =
|
||||
item.type === 'follow' &&
|
||||
starterPack !== undefined &&
|
||||
(item.additional ?? []).every(
|
||||
notification => notification.starterPack?.uri === starterPack.uri,
|
||||
)
|
||||
const starterPackName =
|
||||
allFollowedViaSameStarterPack && starterPack
|
||||
? getStarterPackName(starterPack)
|
||||
: undefined
|
||||
const formattedAuthorsCount = hasMultipleAuthors
|
||||
? formatCount(i18n, additionalAuthorsCount)
|
||||
: ''
|
||||
|
||||
let a11yLabel = ''
|
||||
let notificationContent: React.ReactElement<any>
|
||||
let notificationContent: React.ReactElement
|
||||
let icon = (
|
||||
<HeartIconFilled
|
||||
size="xl"
|
||||
@@ -293,13 +307,11 @@ let NotificationFeedItem = ({
|
||||
|
||||
if (item.type === 'post-like') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your post`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} liked your post`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your post`
|
||||
: l`${firstAuthorName} liked your post`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -317,13 +329,11 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
} else if (item.type === 'repost') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} reposted your post`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} reposted your post`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} reposted your post`
|
||||
: l`${firstAuthorName} reposted your post`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -346,17 +356,24 @@ let NotificationFeedItem = ({
|
||||
* Follow-backs are ungrouped, grouped follow-backs not supported atm,
|
||||
* see `src/state/queries/notifications/util.ts`
|
||||
*/
|
||||
a11yLabel = _(msg`${firstAuthorName} followed you back`)
|
||||
a11yLabel = starterPackName
|
||||
? l`${firstAuthorName} followed you back via starter pack ${starterPackName}`
|
||||
: l`${firstAuthorName} followed you back`
|
||||
notificationContent = <Trans>{firstAuthorLink} followed you back</Trans>
|
||||
} else {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
a11yLabel = starterPackName
|
||||
? hasMultipleAuthors
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} followed you`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} followed you`)
|
||||
})} followed you via starter pack ${starterPackName}`
|
||||
: l`${firstAuthorName} followed you via starter pack ${starterPackName}`
|
||||
: hasMultipleAuthors
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} followed you`
|
||||
: l`${firstAuthorName} followed you`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -375,7 +392,7 @@ let NotificationFeedItem = ({
|
||||
}
|
||||
icon = <PersonPlusIcon size="xl" style={{color: t.palette.primary_500}} />
|
||||
} else if (item.type === 'contact-match') {
|
||||
a11yLabel = _(msg`Your contact ${firstAuthorName} is on Bluesky`)
|
||||
a11yLabel = l`Your contact ${firstAuthorName} is on Bluesky`
|
||||
notificationContent = (
|
||||
<Trans>Your contact {firstAuthorLink} is on Bluesky</Trans>
|
||||
)
|
||||
@@ -384,13 +401,11 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
} else if (item.type === 'feedgen-like') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your custom feed`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} liked your custom feed`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your custom feed`
|
||||
: l`${firstAuthorName} liked your custom feed`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -408,13 +423,11 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
} else if (item.type === 'starterpack-joined') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} signed up with your starter pack`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} signed up with your starter pack`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} signed up with your starter pack`
|
||||
: l`${firstAuthorName} signed up with your starter pack`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -437,13 +450,11 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
} else if (item.type === 'verified') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} verified you`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} verified you`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} verified you`
|
||||
: l`${firstAuthorName} verified you`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -462,13 +473,11 @@ let NotificationFeedItem = ({
|
||||
icon = <VerifiedCheck size="xl" />
|
||||
} else if (item.type === 'unverified') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} removed their verifications from your account`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} removed their verification from your account`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} removed their verifications from your account`
|
||||
: l`${firstAuthorName} removed their verification from your account`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -489,13 +498,11 @@ let NotificationFeedItem = ({
|
||||
icon = <VerifiedCheck size="xl" fill={t.palette.contrast_500} />
|
||||
} else if (item.type === 'like-via-repost') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your repost`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} liked your repost`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} liked your repost`
|
||||
: l`${firstAuthorName} liked your repost`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -513,13 +520,11 @@ let NotificationFeedItem = ({
|
||||
)
|
||||
} else if (item.type === 'repost-via-repost') {
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} reposted your repost`,
|
||||
)
|
||||
: _(msg`${firstAuthorName} reposted your repost`)
|
||||
? l`${firstAuthorName} and ${plural(additionalAuthorsCount, {
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
})} reposted your repost`
|
||||
: l`${firstAuthorName} reposted your repost`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
{firstAuthorLink} and{' '}
|
||||
@@ -539,21 +544,17 @@ let NotificationFeedItem = ({
|
||||
} 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}`,
|
||||
)
|
||||
? l`New posts from ${firstAuthorName} and ${plural(
|
||||
additionalAuthorsCount,
|
||||
{
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
},
|
||||
)}`
|
||||
: l`New ${plural(postsCount, {
|
||||
one: 'post',
|
||||
other: 'posts',
|
||||
})} from ${firstAuthorName}`
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
New posts from {firstAuthorLink} and{' '}
|
||||
@@ -608,18 +609,16 @@ let NotificationFeedItem = ({
|
||||
{
|
||||
name: 'toggleAuthorsExpanded',
|
||||
label: isAuthorsExpanded
|
||||
? _(msg`Collapse list of users`)
|
||||
: _(msg`Expand list of users`),
|
||||
? l`Collapse list of users`
|
||||
: l`Expand list of users`,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'viewProfile',
|
||||
label: _(
|
||||
msg`View ${
|
||||
authors[0].profile.displayName || authors[0].profile.handle
|
||||
}'s profile`,
|
||||
),
|
||||
label: l`View ${
|
||||
authors[0].profile.displayName || authors[0].profile.handle
|
||||
}'s profile`,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -686,6 +685,9 @@ let NotificationFeedItem = ({
|
||||
</TimeElapsed>
|
||||
</Text>
|
||||
</ExpandListPressable>
|
||||
{allFollowedViaSameStarterPack && starterPack ? (
|
||||
<FollowedViaStarterPack starterPack={starterPack} />
|
||||
) : null}
|
||||
{(item.type === 'follow' && !hasMultipleAuthors && !isFollowBack) ||
|
||||
(item.type === 'contact-match' &&
|
||||
!item.notification.author.viewer?.following) ? (
|
||||
@@ -737,6 +739,53 @@ let NotificationFeedItem = ({
|
||||
NotificationFeedItem = memo(NotificationFeedItem)
|
||||
export {NotificationFeedItem}
|
||||
|
||||
function FollowedViaStarterPack({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const link = useStarterPackLink({view: starterPack})
|
||||
|
||||
const starterPackName = getStarterPackName(starterPack)
|
||||
|
||||
if (!starterPackName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={[native(a.pt_xs), t.atoms.text_contrast_medium]}>
|
||||
<Trans comment="When the source of a follow is a starter pack, i.e., 'via starter pack {starterPackName}'.">
|
||||
via starter pack{' '}
|
||||
<StarterPack
|
||||
size="sm"
|
||||
gradient="sky"
|
||||
style={[native(a.mr_2xs), {transform: [{translateY: 4}]}]}
|
||||
/>
|
||||
<InlineLinkText
|
||||
to={link.to}
|
||||
label={link.label}
|
||||
onPress={link.precache}
|
||||
onMouseEnter={link.precache}
|
||||
style={[a.font_semi_bold, t.atoms.text]}>
|
||||
{starterPackName}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function getStarterPackName(
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
) {
|
||||
return bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
starterPack.record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
? starterPack.record.name
|
||||
: undefined
|
||||
}
|
||||
|
||||
function ExpandListPressable({
|
||||
hasMultipleAuthors,
|
||||
children,
|
||||
@@ -767,7 +816,7 @@ function ExpandListPressable({
|
||||
}
|
||||
|
||||
function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const profileShadow = useProfileShadow(profile)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||
@@ -787,15 +836,14 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
try {
|
||||
await queueFollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
),
|
||||
l`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (err: any) {
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), {
|
||||
Toast.show(l`An issue occurred, please try again.`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -809,15 +857,14 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
try {
|
||||
await queueUnfollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
),
|
||||
l`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (err: any) {
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), {
|
||||
Toast.show(l`An issue occurred, please try again.`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -838,12 +885,10 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
|
||||
const isFollowing = profileShadow.viewer.following
|
||||
const isFollowedBy = profileShadow.viewer.followedBy
|
||||
const followingLabel = _(
|
||||
msg({
|
||||
message: 'Following',
|
||||
comment: 'User is following this account, click to unfollow',
|
||||
}),
|
||||
)
|
||||
const followingLabel = l({
|
||||
message: 'Following',
|
||||
comment: 'User is following this account, click to unfollow',
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={[a.pt_sm]}>
|
||||
@@ -853,7 +898,7 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.self_start]}
|
||||
onPress={onPressUnfollow}>
|
||||
onPress={(e: GestureResponderEvent) => void onPressUnfollow(e)}>
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Following</Trans>
|
||||
@@ -861,11 +906,11 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
label={isFollowedBy ? _(msg`Follow back`) : _(msg`Follow`)}
|
||||
label={isFollowedBy ? l`Follow back` : l`Follow`}
|
||||
color="primary"
|
||||
size="small"
|
||||
style={[a.self_start]}
|
||||
onPress={onPressFollow}>
|
||||
onPress={(e: GestureResponderEvent) => void onPressFollow(e)}>
|
||||
<ButtonIcon icon={PlusIcon} />
|
||||
<ButtonText>
|
||||
{isFollowedBy ? <Trans>Follow back</Trans> : <Trans>Follow</Trans>}
|
||||
@@ -877,11 +922,30 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
}
|
||||
|
||||
function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const agent = useAgent()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const onPress = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await agent.api.chat.bsky.convo.getConvoForMembers(
|
||||
{
|
||||
members: [profile.did, agent.session!.did],
|
||||
},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: res.data.convo.id,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Failed to get conversation', {safeMessage: e})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
profile.associated?.chat?.allowIncoming === 'none' ||
|
||||
(profile.associated?.chat?.allowIncoming === 'following' &&
|
||||
@@ -892,30 +956,13 @@ function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) {
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={_(msg`Say hello!`)}
|
||||
label={l`Say hello!`}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
size="small"
|
||||
style={[a.self_center, {marginLeft: 'auto'}]}
|
||||
disabled={isLoading}
|
||||
onPress={async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await agent.api.chat.bsky.convo.getConvoForMembers(
|
||||
{
|
||||
members: [profile.did, agent.session!.did],
|
||||
},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: res.data.convo.id,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Failed to get conversation', {safeMessage: e})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}}>
|
||||
onPress={() => void onPress()}>
|
||||
<ButtonText>
|
||||
<Trans>Say hello!</Trans>
|
||||
</ButtonText>
|
||||
@@ -935,7 +982,7 @@ function CondensedAuthorsList({
|
||||
showDmButton?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
if (!visible) {
|
||||
return (
|
||||
@@ -944,10 +991,8 @@ function CondensedAuthorsList({
|
||||
style={styles.expandedAuthorsCloseBtn}
|
||||
onPress={onToggleAuthorsExpanded}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Hide user list`)}
|
||||
accessibilityHint={_(
|
||||
msg`Collapses list of users for a given notification`,
|
||||
)}>
|
||||
accessibilityLabel={l`Hide user list`}
|
||||
accessibilityHint={l`Collapses list of users for a given notification`}>
|
||||
<ChevronUpIcon
|
||||
size="md"
|
||||
style={[a.ml_xs, a.mr_md, t.atoms.text_contrast_high]}
|
||||
@@ -978,7 +1023,7 @@ function CondensedAuthorsList({
|
||||
onPress={onToggleAuthorsExpanded}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
{authors.slice(0, MAX_AUTHORS).map(author => (
|
||||
<View key={author.href} style={s.mr5}>
|
||||
<View key={author.href} style={{marginRight: 5}}>
|
||||
<PreviewableUserAvatar
|
||||
size={35}
|
||||
profile={author.profile}
|
||||
|
||||
Reference in New Issue
Block a user