From 9180aa10befce9ab8e9195b3c7aa816bfe682383 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 19 Jun 2025 19:32:24 +0300 Subject: [PATCH] add list cards, add inline variant to NameAndHandle --- src/components/ProfileCard.tsx | 73 ++++++- .../create-sanitized-display-name.ts | 7 +- .../ActivityNotificationSettings.tsx | 201 ++++++++++++++---- src/view/com/util/PostMeta.tsx | 3 +- 4 files changed, 234 insertions(+), 50 deletions(-) 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(