Indicate if someone followed you via a starter pack (#11320)

This commit is contained in:
DS Boyce
2026-07-29 12:11:28 -07:00
committed by GitHub
parent 8e3b252b86
commit b4bad1187a
7 changed files with 304 additions and 216 deletions
-25
View File
@@ -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
View File
@@ -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",
+5 -5
View File
@@ -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'],
])
})
})
+3
View File
@@ -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')
) {
+41 -38
View File
@@ -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}