about summary refs log tree commit diff
path: root/src/components/StarterPack/Wizard
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-06-21 21:38:04 -0700
committerGitHub <noreply@github.com>2024-06-21 21:38:04 -0700
commitf089f4578131e83cd177b7809ce0f7b75779dfdc (patch)
tree51978aede2040fb8dc319f0749d3de77c7811fbe /src/components/StarterPack/Wizard
parent35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 (diff)
downloadvoidsky-f089f4578131e83cd177b7809ce0f7b75779dfdc.tar.zst
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>
Diffstat (limited to 'src/components/StarterPack/Wizard')
-rw-r--r--src/components/StarterPack/Wizard/ScreenTransition.tsx31
-rw-r--r--src/components/StarterPack/Wizard/WizardEditListDialog.tsx152
-rw-r--r--src/components/StarterPack/Wizard/WizardListCard.tsx182
3 files changed, 365 insertions, 0 deletions
diff --git a/src/components/StarterPack/Wizard/ScreenTransition.tsx b/src/components/StarterPack/Wizard/ScreenTransition.tsx
new file mode 100644
index 000000000..b7cd4e4c1
--- /dev/null
+++ b/src/components/StarterPack/Wizard/ScreenTransition.tsx
@@ -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>
+  )
+}
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
new file mode 100644
index 000000000..bf250ac35
--- /dev/null
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -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>
+  )
+}
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
new file mode 100644
index 000000000..f1332011d
--- /dev/null
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -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}
+    />
+  )
+}