Starter Packs (#4332)

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Hailey 2024-06-21 21:38:04 -07:00 committed by GitHub
parent 35f64535cb
commit f089f45781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 6336 additions and 237 deletions

View file

@ -0,0 +1,31 @@
import React from 'react'
import {StyleProp, ViewStyle} from 'react-native'
import Animated, {
FadeIn,
FadeOut,
SlideInLeft,
SlideInRight,
} from 'react-native-reanimated'
import {isWeb} from 'platform/detection'
export function ScreenTransition({
direction,
style,
children,
}: {
direction: 'Backward' | 'Forward'
style?: StyleProp<ViewStyle>
children: React.ReactNode
}) {
const entering = direction === 'Forward' ? SlideInRight : SlideInLeft
return (
<Animated.View
entering={isWeb ? FadeIn.duration(90) : entering}
exiting={FadeOut.duration(90)} // Totally vibes based
style={style}>
{children}
</Animated.View>
)
}

View file

@ -0,0 +1,152 @@
import React, {useRef} from 'react'
import type {ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
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'
import {useLingui} from '@lingui/react'
import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
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,
WizardProfileCard,
} from '#/components/StarterPack/Wizard/WizardListCard'
import {Text} from '#/components/Typography'
function keyExtractor(
item: AppBskyActorDefs.ProfileViewBasic | GeneratorView,
index: number,
) {
return `${item.did}-${index}`
}
export function WizardEditListDialog({
control,
state,
dispatch,
moderationOpts,
profile,
}: {
control: Dialog.DialogControlProps
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds
return [
profile,
...state.profiles.filter(p => p.did !== currentAccount?.did),
]
}
const renderItem = ({item}: ListRenderItemInfo<any>) =>
state.currentStep === 'Profiles' ? (
<WizardProfileCard
profile={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
) : (
<WizardFeedCard
generator={item}
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
/>
)
return (
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
<Dialog.Handle />
<Dialog.InnerFlatList
ref={listRef}
data={getData()}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListHeaderComponent={
<View
style={[
a.flex_row,
a.justify_between,
a.border_b,
a.px_sm,
a.mb_sm,
t.atoms.bg,
t.atoms.border_contrast_medium,
isWeb
? [
a.align_center,
{
height: 48,
},
]
: [
a.pb_sm,
a.align_end,
{
height: 68,
},
],
]}>
<View style={{width: 60}} />
<Text style={[a.font_bold, a.text_xl]}>
{state.currentStep === 'Profiles' ? (
<Trans>Edit People</Trans>
) : (
<Trans>Edit Feeds</Trans>
)}
</Text>
<View style={{width: 60}}>
{isWeb && (
<Button
label={_(msg`Close`)}
variant="ghost"
color="primary"
size="xsmall"
onPress={() => control.close()}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
)}
</View>
</View>
}
stickyHeaderIndices={[0]}
style={[
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({
height: '100%',
paddingHorizontal: 0,
marginTop: 0,
paddingTop: 0,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
}),
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
removeClippedSubviews={true}
/>
</Dialog.Outer>
)
}

View file

@ -0,0 +1,182 @@
import React from 'react'
import {Keyboard, 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 {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 (
<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}
style={[
a.flex_row,
a.align_center,
a.px_lg,
a.py_md,
a.gap_md,
a.border_b,
t.atoms.border_contrast_low,
]}>
<UserAvatar
size={45}
avatar={avatar}
moderation={moderationUi}
type={type}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
style={[a.flex_1, a.font_bold, a.text_md, a.leading_tight]}
numberOfLines={1}>
{displayName}
</Text>
<Text
style={[a.flex_1, a.leading_tight, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{subtitle}
</Text>
</View>
<Checkbox />
</Toggle.Item>
)
}
export function WizardProfileCard({
state,
dispatch,
profile,
moderationOpts,
}: {
state: WizardState
dispatch: (action: WizardAction) => void
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
}) {
const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did
const included = isMe || state.profiles.some(p => p.did === profile.did)
const disabled = isMe || (!included && state.profiles.length >= 49)
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}
/>
)
}