refactor list card items, apply moderation

This commit is contained in:
Hailey
2024-06-17 20:16:44 -07:00
parent 6e470fbe80
commit 6ecb0d2342
7 changed files with 261 additions and 213 deletions
@@ -1,7 +1,7 @@
import React, {useRef} from 'react'
import type {ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
@@ -13,8 +13,10 @@ import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard'
import {
WizardFeedCard,
WizardProfileCard,
} from '#/components/StarterPack/Wizard/WizardListCard'
import {Text} from '#/components/Typography'
function keyExtractor(
@@ -28,10 +30,12 @@ export function WizardEditListDialog({
control,
state,
dispatch,
moderationOpts,
}: {
control: Dialog.DialogControlProps
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
}) {
const {_} = useLingui()
const t = useTheme()
@@ -53,9 +57,19 @@ export function WizardEditListDialog({
const renderItem = ({item}: ListRenderItemInfo<any>) =>
state.currentStep === 'Profiles' ? (
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
<WizardProfileCard
profile={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
) : (
<WizardFeedCard generator={item} state={state} dispatch={dispatch} />
<WizardFeedCard
generator={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
)
return (
@@ -1,86 +0,0 @@
import React from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DISCOVER_FEED_URI} from 'lib/constants'
import {isWeb} from 'platform/detection'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Checkbox} from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
export function WizardFeedCard({
generator,
state,
dispatch,
}: {
generator: GeneratorView
state: WizardState
dispatch: (action: WizardAction) => void
}) {
const {_} = useLingui()
const t = useTheme()
const isDiscover = generator.uri === DISCOVER_FEED_URI
const includesFeed =
isDiscover || state.feeds.some(f => f.uri === generator.uri)
const isDisabled = isDiscover || (!includesFeed && state.feeds.length >= 3)
const onPressAddRemove = () => {
if (isDisabled) return
Keyboard.dismiss()
if (includesFeed) {
dispatch({type: 'RemoveFeed', feedUri: generator.uri})
} else {
dispatch({type: 'AddFeed', feed: generator})
}
}
return (
<Pressable
accessibilityRole="button"
style={[
a.flex_row,
a.align_center,
a.px_lg,
a.py_sm,
a.gap_md,
a.border_b,
t.atoms.border_contrast_low,
// @ts-expect-error web only
isWeb && {
cursor: 'default',
},
]}
onPress={onPressAddRemove}>
<UserAvatar type="algo" size={45} avatar={generator.avatar} />
<View style={[a.flex_1]}>
<Text style={[a.flex_1, a.font_bold, a.text_md]} numberOfLines={1}>
{generator.displayName}
</Text>
<Text
style={[a.flex_1, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{_(msg`Feed by @${generator.creator.handle}`)}
</Text>
</View>
<Toggle.Item
name={_(msg`Person toggle`)}
label={
includesFeed
? _(msg`Remove ${generator.displayName} from starter pack`)
: _(msg`Add ${generator.displayName} to starter pack`)
}
value={includesFeed}
disabled={isDisabled}
onChange={onPressAddRemove}>
<Checkbox />
</Toggle.Item>
</Pressable>
)
}
@@ -0,0 +1,189 @@
import React from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
moderateFeedGenerator,
moderateProfile,
ModerationOpts,
ModerationUI,
} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DISCOVER_FEED_URI} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Checkbox} from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
function WizardListCard({
type,
displayName,
subtitle,
onPress,
avatar,
included,
disabled,
moderationUi,
}: {
type: 'user' | 'algo'
profile?: AppBskyActorDefs.ProfileViewBasic
feed?: AppBskyFeedDefs.GeneratorView
displayName: string
subtitle: string
onPress: () => void
avatar?: string
included?: boolean
disabled?: boolean
moderationUi: ModerationUI
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Pressable
accessibilityRole="button"
style={[
a.flex_row,
a.align_center,
a.px_lg,
a.py_sm,
a.gap_md,
a.border_b,
t.atoms.border_contrast_low,
// @ts-expect-error web only
isWeb && {
cursor: 'default',
},
]}
onPress={onPress}>
<UserAvatar
size={45}
avatar={avatar}
moderation={moderationUi}
type={type}
/>
<View style={[a.flex_1]}>
<Text style={[a.flex_1, a.font_bold, a.text_md]} numberOfLines={1}>
{displayName}
</Text>
<Text
style={[a.flex_1, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{subtitle}
</Text>
</View>
<Toggle.Item
name={type === 'user' ? _(msg`Person toggle`) : _(msg`Feed toggle`)}
label={
included
? _(msg`Remove ${displayName} from starter pack`)
: _(msg`Add ${displayName} to starter pack`)
}
value={included}
disabled={disabled}
onChange={onPress}>
<Checkbox />
</Toggle.Item>
</Pressable>
)
}
export function WizardProfileCard({
state,
dispatch,
profile,
moderationOpts,
}: {
state: WizardState
dispatch: (action: WizardAction) => void
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
}) {
const {currentAccount} = useSession()
const included = state.profiles.some(p => p.did === profile.did)
const isMe = profile.did === currentAccount?.did
const disabled = isMe || state.profiles.length >= 50
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName)
: `@${sanitizeHandle(profile.handle)}`
const onPress = () => {
if (disabled) return
Keyboard.dismiss()
if (profile.did === currentAccount?.did) return
if (!included) {
dispatch({type: 'AddProfile', profile})
} else {
dispatch({type: 'RemoveProfile', profileDid: profile.did})
}
}
return (
<WizardListCard
type="user"
displayName={displayName}
subtitle={`@${sanitizeHandle(profile.handle)}`}
onPress={onPress}
avatar={profile.avatar}
included={included}
disabled={disabled}
moderationUi={moderationUi}
/>
)
}
export function WizardFeedCard({
generator,
state,
dispatch,
moderationOpts,
}: {
generator: GeneratorView
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
}) {
const isDiscover = generator.uri === DISCOVER_FEED_URI
const included = isDiscover || state.feeds.some(f => f.uri === generator.uri)
const disabled = isDiscover || (!included && state.feeds.length >= 3)
const moderationUi = moderateFeedGenerator(generator, moderationOpts).ui(
'avatar',
)
const onPress = () => {
if (disabled) return
Keyboard.dismiss()
if (included) {
dispatch({type: 'RemoveFeed', feedUri: generator.uri})
} else {
dispatch({type: 'AddFeed', feed: generator})
}
}
return (
<WizardListCard
type="algo"
displayName={sanitizeDisplayName(generator.displayName)}
subtitle={`Feed by @${sanitizeHandle(generator.creator.handle)}`}
onPress={onPress}
avatar={generator.avatar}
included={included}
disabled={disabled}
moderationUi={moderationUi}
/>
)
}
@@ -1,96 +0,0 @@
import React from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Checkbox} from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
export function WizardProfileCard({
state,
dispatch,
profile,
}: {
state: WizardState
dispatch: (action: WizardAction) => void
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const includesProfile = state.profiles.some(p => p.did === profile.did)
const isMe = profile.did === currentAccount?.did
const isDisabled = isMe || state.profiles.length >= 50
const onPressAddRemove = () => {
if (isDisabled) return
Keyboard.dismiss()
if (profile.did === currentAccount?.did) return
if (!includesProfile) {
dispatch({type: 'AddProfile', profile})
} else {
dispatch({type: 'RemoveProfile', profileDid: profile.did})
}
}
return (
<Pressable
accessibilityRole="button"
style={[
a.flex_row,
a.align_center,
a.px_lg,
a.py_sm,
a.gap_md,
a.border_b,
t.atoms.border_contrast_low,
// @ts-expect-error web only
isWeb && {
cursor: 'default',
},
]}
onPress={onPressAddRemove}>
<UserAvatar size={45} avatar={profile?.avatar} />
<View style={[a.flex_1]}>
<Text style={[a.flex_1, a.font_bold, a.text_md]} numberOfLines={1}>
{profile?.displayName || profile?.handle}
</Text>
<Text
style={[a.flex_1, t.atoms.text_contrast_medium]}
numberOfLines={1}>
@{profile?.handle}
</Text>
</View>
<Toggle.Item
name={_(msg`Person toggle`)}
label={
includesProfile
? _(
msg`Remove ${
profile.displayName || profile.handle
} from starter pack`,
)
: _(
msg`Add ${
profile.displayName || profile.handle
} to starter pack`,
)
}
value={includesProfile}
disabled={isDisabled}
onChange={onPressAddRemove}>
<Checkbox />
</Toggle.Item>
</Pressable>
)
}
+11 -3
View File
@@ -1,6 +1,7 @@
import React, {useState} from 'react'
import {ListRenderItemInfo, View} from 'react-native'
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
import {ModerationOpts} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import debounce from 'lodash.debounce'
@@ -14,13 +15,13 @@ import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard'
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard'
function keyExtractor(item: GeneratorView) {
return item.uri
}
export function StepFeeds() {
export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
const t = useTheme()
const [state, dispatch] = useWizardState()
const [query, setQuery] = useState('')
@@ -52,7 +53,14 @@ export function StepFeeds() {
}
const renderItem = ({item}: ListRenderItemInfo<GeneratorView>) => {
return <WizardFeedCard generator={item} state={state} dispatch={dispatch} />
return (
<WizardFeedCard
generator={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
)
}
return (
@@ -1,7 +1,7 @@
import React, {useState} from 'react'
import {ListRenderItemInfo, View} from 'react-native'
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
import {AppBskyActorDefs} from '@atproto/api'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {useActorSearch} from 'state/queries/actor-search'
@@ -11,13 +11,17 @@ import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
return item.did
}
export function StepProfiles() {
export function StepProfiles({
moderationOpts,
}: {
moderationOpts: ModerationOpts
}) {
const t = useTheme()
const [state, dispatch] = useWizardState()
const [query, setQuery] = useState('')
@@ -32,7 +36,12 @@ export function StepProfiles() {
item,
}: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
return (
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
<WizardProfileCard
profile={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
)
}
+29 -19
View File
@@ -10,6 +10,7 @@ import {
AppBskyGraphDefs,
AppBskyGraphStarterpack,
AtUri,
ModerationOpts,
} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -24,8 +25,11 @@ import {HITSLOP_10} from 'lib/constants'
import {createStarterPackList} from 'lib/generate-starterpack'
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
import {logEvent} from 'lib/statsig/statsig'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers'
import {isAndroid, isNative, isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {invalidateActorStarterPacksQuery} from 'state/queries/actor-starter-packs'
import {
invalidateListMembersQuery,
@@ -61,6 +65,7 @@ export function Wizard({
}: NativeStackScreenProps<CommonNavigatorParams, 'StarterPackWizard'>) {
const {name, rkey} = route.params ?? {}
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const {_} = useLingui()
@@ -91,7 +96,10 @@ export function Wizard({
} = useProfileQuery({did: currentAccount?.did})
const isEdit = Boolean(name && rkey)
const isReady = (!isEdit || (isEdit && starterPack && listItems)) && profile
const isReady =
(!isEdit || (isEdit && starterPack && listItems)) &&
profile &&
moderationOpts
if (!isReady) {
return (
@@ -130,6 +138,7 @@ export function Wizard({
}
listItems={listItems}
listUri={listUri}
moderationOpts={moderationOpts}
/>
</Provider>
)
@@ -141,12 +150,14 @@ function WizardInner({
createdAt: initialCreatedAt,
listUri: initialListUri,
listItems: initialListItems,
moderationOpts,
}: {
did?: string
rkey?: string
createdAt?: string
listUri?: string
listItems?: AppBskyGraphDefs.ListItemView[]
moderationOpts: ModerationOpts
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
@@ -445,11 +456,21 @@ function WizardInner({
<Container
showDeleteBtn={Boolean(did && rkey)}
deleteStarterPack={deleteStarterPack}>
<StepView />
{state.currentStep === 'Details' ? (
<StepDetails />
) : state.currentStep === 'Profiles' ? (
<StepProfiles moderationOpts={moderationOpts} />
) : state.currentStep === 'Feeds' ? (
<StepFeeds moderationOpts={moderationOpts} />
) : null}
</Container>
{state.currentStep !== 'Details' && (
<Footer onNext={onNext} nextBtnText={currUiStrings.nextBtn} />
<Footer
onNext={onNext}
nextBtnText={currUiStrings.nextBtn}
moderationOpts={moderationOpts}
/>
)}
</CenteredView>
)
@@ -530,9 +551,11 @@ function Container({
function Footer({
onNext,
nextBtnText,
moderationOpts,
}: {
onNext: () => void
nextBtnText: string
moderationOpts: ModerationOpts
}) {
const {_} = useLingui()
const t = useTheme()
@@ -694,6 +717,7 @@ function Footer({
control={editDialogControl}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
</View>
)
@@ -701,23 +725,9 @@ function Footer({
function getName(item: AppBskyActorDefs.ProfileViewBasic | GeneratorView) {
if (typeof item.displayName === 'string') {
return enforceLen(item.displayName, 16, true)
return enforceLen(sanitizeDisplayName(item.displayName), 16, true)
} else if (typeof item.handle === 'string') {
return enforceLen(item.handle, 16, true)
return enforceLen(sanitizeHandle(item.handle), 16, true)
}
return ''
}
function StepView() {
const [state] = useWizardState()
if (state.currentStep === 'Details') {
return <StepDetails />
}
if (state.currentStep === 'Profiles') {
return <StepProfiles />
}
if (state.currentStep === 'Feeds') {
return <StepFeeds />
}
}