add list cards, add inline variant to NameAndHandle
This commit is contained in:
@@ -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 (
|
||||
<InlineNameAndHandle profile={profile} moderationOpts={moderationOpts} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<Handle profile={profile} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={[a.flex_1]}>
|
||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<Handle profile={profile} />
|
||||
<View style={[a.flex_row, a.align_end, a.flex_shrink]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{forceLTR(name)}
|
||||
</Text>
|
||||
{verification.showBadge && (
|
||||
<View
|
||||
style={[
|
||||
a.pl_2xs,
|
||||
a.self_center,
|
||||
{marginTop: platform({default: 0, android: -1})},
|
||||
]}>
|
||||
<VerificationCheck
|
||||
width={platform({android: 13, default: 12})}
|
||||
verifier={verification.role === 'verifier'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
{flexShrink: 10},
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{NON_BREAKING_SPACE + handle}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -234,7 +298,6 @@ export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
|
||||
return (
|
||||
<Text
|
||||
emoji
|
||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{handle}
|
||||
|
||||
@@ -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 !== '') {
|
||||
|
||||
@@ -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<bsky.profile.AnyProfileView>) => {
|
||||
if (!moderationOpts) return null
|
||||
return (
|
||||
<ActivitySubscriptionCard
|
||||
profile={item}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[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 (
|
||||
<Layout.Screen>
|
||||
@@ -78,50 +133,118 @@ export function ActivityNotificationSettingsScreen({}: Props) {
|
||||
</SettingsList.Container>
|
||||
}
|
||||
data={items}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={4}
|
||||
ListEmptyComponent={
|
||||
<View style={[a.px_xl, a.py_md]}>
|
||||
<Admonition.Outer type="tip">
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
Enable notifications for an account by visiting their
|
||||
profile and pressing the{' '}
|
||||
<Text style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||
bell icon
|
||||
</Text>{' '}
|
||||
<BellIcon size="xs" style={t.atoms.text_contrast_high} />.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
By default, only accounts you follow can receive alerts
|
||||
from you – this can be changed in{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Privacy & Security settings`)}
|
||||
to={{screen: 'PrivacyAndSecuritySettings'}}
|
||||
style={[a.font_bold]}>
|
||||
Settings → Privacy & Security
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
</View>
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
{!isPending ? (
|
||||
<Admonition.Outer type="tip">
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
Enable notifications for an account by visiting their
|
||||
profile and pressing the{' '}
|
||||
<RNText
|
||||
style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||
bell icon
|
||||
</RNText>{' '}
|
||||
<BellIcon
|
||||
size="xs"
|
||||
style={t.atoms.text_contrast_high}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
By default, only accounts you follow can receive alerts
|
||||
from you – this can be changed in{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Privacy & Security settings`)}
|
||||
to={{screen: 'PrivacyAndSecuritySettings'}}
|
||||
style={[a.font_bold]}>
|
||||
Settings → Privacy & Security
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
</View>
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.align_center, a.pt_xl]}>
|
||||
<Loader size="lg" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
style={[items.length === 0 && a.border_transparent]}
|
||||
// isFetchingNextPage={isFetchingNextPage}
|
||||
// error={cleanError(error)}
|
||||
// onRetry={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
windowSize={11}
|
||||
/>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
inline
|
||||
/>
|
||||
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Posts, Replies</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={_(
|
||||
msg`Edit notifications from ${createSanitizedDisplayName(
|
||||
profile,
|
||||
)}`,
|
||||
)}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Edit</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</ProfileCard.Header>
|
||||
{/* <ProfileCard.Labels profile={profile} moderationOpts={moderationOpts} /> */}
|
||||
</ProfileCard.Outer>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user