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 {useActorStatus} from '#/lib/actor-status'
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey} from '#/lib/moderation'
|
||||||
import {type LogEvents} from '#/lib/statsig/statsig'
|
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 {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
@@ -18,7 +20,7 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, platform, useTheme} from '#/alf'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
@@ -183,14 +185,76 @@ export function AvatarPlaceholder() {
|
|||||||
export function NameAndHandle({
|
export function NameAndHandle({
|
||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
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
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
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 (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_row, a.align_end, a.flex_shrink]}>
|
||||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
<Text
|
||||||
<Handle profile={profile} />
|
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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -234,7 +298,6 @@ export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
emoji
|
|
||||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{handle}
|
{handle}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function createSanitizedDisplayName(
|
export function createSanitizedDisplayName(
|
||||||
profile:
|
profile: bsky.profile.AnyProfileView,
|
||||||
| AppBskyActorDefs.ProfileViewBasic
|
|
||||||
| AppBskyActorDefs.ProfileViewDetailed,
|
|
||||||
noAt = false,
|
noAt = false,
|
||||||
) {
|
) {
|
||||||
if (profile.displayName != null && profile.displayName !== '') {
|
if (profile.displayName != null && profile.displayName !== '') {
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback, useMemo} from 'react'
|
||||||
import {Text, View} from 'react-native'
|
import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
|
||||||
|
import {type ModerationOpts} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
import {
|
import {
|
||||||
type AllNavigatorParams,
|
type AllNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} 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 {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as Admonition from '#/components/Admonition'
|
import * as Admonition from '#/components/Admonition'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Bell_Filled_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
import {Bell_Filled_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
||||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {ListFooter} from '#/components/Lists'
|
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 * as SettingsList from '../components/SettingsList'
|
||||||
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
||||||
import {PreferenceControls} from './components/PreferenceControls'
|
import {PreferenceControls} from './components/PreferenceControls'
|
||||||
@@ -28,13 +40,56 @@ export function ActivityNotificationSettingsScreen({}: Props) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {data: preferences, isError} = useNotificationSettingsQuery()
|
const {data: preferences, isError} = useNotificationSettingsQuery()
|
||||||
|
const agent = useAgent()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
// TODO: Fetch subscriptions
|
// 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(() => {
|
const items = useMemo(() => {
|
||||||
return null
|
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 (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -78,50 +133,118 @@ export function ActivityNotificationSettingsScreen({}: Props) {
|
|||||||
</SettingsList.Container>
|
</SettingsList.Container>
|
||||||
}
|
}
|
||||||
data={items}
|
data={items}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onEndReachedThreshold={4}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={[a.px_xl, a.py_md]}>
|
<View style={[a.px_xl, a.py_md]}>
|
||||||
<Admonition.Outer type="tip">
|
{!isPending ? (
|
||||||
<Admonition.Row>
|
<Admonition.Outer type="tip">
|
||||||
<Admonition.Icon />
|
<Admonition.Row>
|
||||||
<View style={[a.flex_1, a.gap_sm]}>
|
<Admonition.Icon />
|
||||||
<Admonition.Text>
|
<View style={[a.flex_1, a.gap_sm]}>
|
||||||
<Trans>
|
<Admonition.Text>
|
||||||
Enable notifications for an account by visiting their
|
<Trans>
|
||||||
profile and pressing the{' '}
|
Enable notifications for an account by visiting their
|
||||||
<Text style={[a.font_bold, t.atoms.text_contrast_high]}>
|
profile and pressing the{' '}
|
||||||
bell icon
|
<RNText
|
||||||
</Text>{' '}
|
style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||||
<BellIcon size="xs" style={t.atoms.text_contrast_high} />.
|
bell icon
|
||||||
</Trans>
|
</RNText>{' '}
|
||||||
</Admonition.Text>
|
<BellIcon
|
||||||
<Admonition.Text>
|
size="xs"
|
||||||
<Trans>
|
style={t.atoms.text_contrast_high}
|
||||||
By default, only accounts you follow can receive alerts
|
/>
|
||||||
from you – this can be changed in{' '}
|
.
|
||||||
<InlineLinkText
|
</Trans>
|
||||||
label={_(msg`Privacy & Security settings`)}
|
</Admonition.Text>
|
||||||
to={{screen: 'PrivacyAndSecuritySettings'}}
|
<Admonition.Text>
|
||||||
style={[a.font_bold]}>
|
<Trans>
|
||||||
Settings → Privacy & Security
|
By default, only accounts you follow can receive alerts
|
||||||
</InlineLinkText>
|
from you – this can be changed in{' '}
|
||||||
.
|
<InlineLinkText
|
||||||
</Trans>
|
label={_(msg`Privacy & Security settings`)}
|
||||||
</Admonition.Text>
|
to={{screen: 'PrivacyAndSecuritySettings'}}
|
||||||
</View>
|
style={[a.font_bold]}>
|
||||||
</Admonition.Row>
|
Settings → Privacy & Security
|
||||||
</Admonition.Outer>
|
</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>
|
</View>
|
||||||
}
|
}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
style={[items.length === 0 && a.border_transparent]}
|
style={[items.length === 0 && a.border_transparent]}
|
||||||
// isFetchingNextPage={isFetchingNextPage}
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
// error={cleanError(error)}
|
error={cleanError(error)}
|
||||||
// onRetry={fetchNextPage}
|
onRetry={fetchNextPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
windowSize={11}
|
||||||
/>
|
/>
|
||||||
</Layout.Screen>
|
</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,
|
a.font_bold,
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
{maxWidth: '70%', flexShrink: 0},
|
a.flex_shrink_0,
|
||||||
|
{maxWidth: '70%'},
|
||||||
]}>
|
]}>
|
||||||
{forceLTR(
|
{forceLTR(
|
||||||
sanitizeDisplayName(
|
sanitizeDisplayName(
|
||||||
|
|||||||
Reference in New Issue
Block a user