diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index 30b26beadf..25e550b95b 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -11,6 +11,8 @@ import {useLingui} from '@lingui/react'
import {useActorStatus} from '#/lib/actor-status'
import {getModerationCauseKey} from '#/lib/moderation'
import {type LogEvents} from '#/lib/statsig/statsig'
+import {forceLTR} from '#/lib/strings/bidi'
+import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -18,7 +20,7 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, platform, useTheme} from '#/alf'
import {
Button,
ButtonIcon,
@@ -183,14 +185,76 @@ export function AvatarPlaceholder() {
export function NameAndHandle({
profile,
moderationOpts,
+ inline = false,
+}: {
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+ inline?: boolean
+}) {
+ if (inline) {
+ return (
+
+ )
+ } else {
+ return (
+
+
+
+
+ )
+ }
+}
+
+function InlineNameAndHandle({
+ profile,
+ moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
+ const t = useTheme()
+ const verification = useSimpleVerificationState({profile})
+ const moderation = moderateProfile(profile, moderationOpts)
+ const name = sanitizeDisplayName(
+ profile.displayName || sanitizeHandle(profile.handle),
+ moderation.ui('displayName'),
+ )
+ const handle = sanitizeHandle(profile.handle, '@')
return (
-
-
-
+
+
+ {forceLTR(name)}
+
+ {verification.showBadge && (
+
+
+
+ )}
+
+ {NON_BREAKING_SPACE + handle}
+
)
}
@@ -234,7 +298,6 @@ export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) {
return (
{handle}
diff --git a/src/lib/moderation/create-sanitized-display-name.ts b/src/lib/moderation/create-sanitized-display-name.ts
index 4f9584f918..4c62a5c03c 100644
--- a/src/lib/moderation/create-sanitized-display-name.ts
+++ b/src/lib/moderation/create-sanitized-display-name.ts
@@ -1,12 +1,9 @@
-import {AppBskyActorDefs} from '@atproto/api'
-
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
+import type * as bsky from '#/types/bsky'
export function createSanitizedDisplayName(
- profile:
- | AppBskyActorDefs.ProfileViewBasic
- | AppBskyActorDefs.ProfileViewDetailed,
+ profile: bsky.profile.AnyProfileView,
noAt = false,
) {
if (profile.displayName != null && profile.displayName !== '') {
diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx
index 088a4b906e..7f9f3caf53 100644
--- a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx
+++ b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx
@@ -1,21 +1,33 @@
-import {useCallback} from 'react'
-import {Text, View} from 'react-native'
+import {useCallback, useMemo} from 'react'
+import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
+import {type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useInfiniteQuery} from '@tanstack/react-query'
+import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {
type AllNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
+import {useAgent} from '#/state/session'
import {List} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import * as Admonition from '#/components/Admonition'
+import {Button, ButtonText} from '#/components/Button'
import {Bell_Filled_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {ListFooter} from '#/components/Lists'
+import {Loader} from '#/components/Loader'
+import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
import * as SettingsList from '../components/SettingsList'
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
import {PreferenceControls} from './components/PreferenceControls'
@@ -28,13 +40,56 @@ export function ActivityNotificationSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const {data: preferences, isError} = useNotificationSettingsQuery()
+ const agent = useAgent()
+ const moderationOpts = useModerationOpts()
// TODO: Fetch subscriptions
- const items = [] as string[]
+ const {
+ data: subscriptions,
+ isPending,
+ error,
+ isFetchingNextPage,
+ fetchNextPage,
+ hasNextPage,
+ } = useInfiniteQuery({
+ queryKey: ['TEMP'],
+ queryFn: async ({pageParam}) => {
+ const response = await agent.getFollows({
+ actor: agent.assertDid,
+ cursor: pageParam,
+ })
+ return response.data
+ },
+ initialPageParam: undefined as string | undefined,
+ getNextPageParam: prev => prev.cursor,
+ })
- const renderItem = useCallback(() => {
- return null
- }, [])
+ const items = useMemo(() => {
+ if (!subscriptions) return []
+ return subscriptions?.pages.flatMap(page => page.follows)
+ }, [subscriptions])
+
+ const renderItem = useCallback(
+ ({item}: ListRenderItemInfo) => {
+ if (!moderationOpts) return null
+ return (
+
+ )
+ },
+ [moderationOpts],
+ )
+
+ const onEndReached = useCallback(async () => {
+ if (isFetchingNextPage || !hasNextPage || isError) return
+ try {
+ await fetchNextPage()
+ } catch (err) {
+ logger.error('Failed to load more likes', {message: err})
+ }
+ }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
return (
@@ -78,50 +133,118 @@ export function ActivityNotificationSettingsScreen({}: Props) {
}
data={items}
+ keyExtractor={keyExtractor}
renderItem={renderItem}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={4}
ListEmptyComponent={
-
-
-
-
-
-
- Enable notifications for an account by visiting their
- profile and pressing the{' '}
-
- bell icon
- {' '}
- .
-
-
-
-
- By default, only accounts you follow can receive alerts
- from you – this can be changed in{' '}
-
- Settings → Privacy & Security
-
- .
-
-
-
-
-
+ {!isPending ? (
+
+
+
+
+
+
+ Enable notifications for an account by visiting their
+ profile and pressing the{' '}
+
+ bell icon
+ {' '}
+
+ .
+
+
+
+
+ By default, only accounts you follow can receive alerts
+ from you – this can be changed in{' '}
+
+ Settings → Privacy & Security
+
+ .
+
+
+
+
+
+ ) : (
+
+
+
+ )}
}
ListFooterComponent={
}
+ windowSize={11}
/>
)
}
+
+function keyExtractor(item: bsky.profile.AnyProfileView) {
+ return item.did
+}
+
+function ActivitySubscriptionCard({
+ profile,
+ moderationOpts,
+}: {
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+}) {
+ const {_} = useLingui()
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+
+
+ Posts, Replies
+
+
+
+
+ {/* */}
+
+
+ )
+}
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 30ebbf2c21..c1f804203b 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -96,7 +96,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
a.font_bold,
t.atoms.text,
a.leading_tight,
- {maxWidth: '70%', flexShrink: 0},
+ a.flex_shrink_0,
+ {maxWidth: '70%'},
]}>
{forceLTR(
sanitizeDisplayName(