about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-11-09 15:57:49 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-11-09 15:57:49 -0600
commite7536289cbb4380dc82dcd70737e165727cbbb92 (patch)
tree867a226b444eb0f04f00af33131088c9f3427f7c
parent93b64cf474574b315bfe48594ed7170b9bd8261e (diff)
downloadvoidsky-e7536289cbb4380dc82dcd70737e165727cbbb92.tar.zst
Add scene creator
-rw-r--r--src/state/models/root-store.ts4
-rw-r--r--src/state/models/shell-ui.ts (renamed from src/state/models/shell.ts)25
-rw-r--r--src/view/com/composer/ComposePost.tsx2
-rw-r--r--src/view/com/modals/CreateScene.tsx210
-rw-r--r--src/view/com/modals/EditProfile.tsx2
-rw-r--r--src/view/com/modals/Modal.tsx6
-rw-r--r--src/view/com/post-thread/PostThread.tsx2
-rw-r--r--src/view/com/posts/FeedItem.tsx2
-rw-r--r--src/view/com/profile/ProfileHeader.tsx2
-rw-r--r--src/view/com/util/DropdownBtn.tsx2
-rw-r--r--src/view/com/util/ErrorMessage.tsx6
-rw-r--r--src/view/com/util/Link.tsx2
-rw-r--r--src/view/lib/strings.ts14
-rw-r--r--src/view/shell/mobile/Composer.tsx2
-rw-r--r--src/view/shell/mobile/MainMenu.tsx15
-rw-r--r--src/view/shell/mobile/TabsSelector.tsx2
-rw-r--r--src/view/shell/mobile/index.tsx2
-rw-r--r--todos.txt4
18 files changed, 281 insertions, 23 deletions
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index da846a3b0..e2a505768 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -9,14 +9,14 @@ import {createContext, useContext} from 'react'
 import {isObj, hasProp} from '../lib/type-guards'
 import {SessionModel} from './session'
 import {NavigationModel} from './navigation'
-import {ShellModel} from './shell'
+import {ShellUiModel} from './shell-ui'
 import {MeModel} from './me'
 import {OnboardModel} from './onboard'
 
 export class RootStoreModel {
   session = new SessionModel(this)
   nav = new NavigationModel()
-  shell = new ShellModel()
+  shell = new ShellUiModel()
   me = new MeModel(this)
   onboard = new OnboardModel()
 
diff --git a/src/state/models/shell.ts b/src/state/models/shell-ui.ts
index bef6ef765..345a6b4a9 100644
--- a/src/state/models/shell.ts
+++ b/src/state/models/shell-ui.ts
@@ -35,14 +35,27 @@ export class EditProfileModel {
   }
 }
 
+export class CreateSceneModel {
+  name = 'create-scene'
+
+  constructor() {
+    makeAutoObservable(this)
+  }
+}
+
 export interface ComposerOpts {
   replyTo?: Post.PostRef
   onPost?: () => void
 }
 
-export class ShellModel {
+export class ShellUiModel {
   isModalActive = false
-  activeModal: LinkActionsModel | SharePostModel | EditProfileModel | undefined
+  activeModal:
+    | LinkActionsModel
+    | SharePostModel
+    | EditProfileModel
+    | CreateSceneModel
+    | undefined
   isComposerActive = false
   composerOpts: ComposerOpts | undefined
 
@@ -50,7 +63,13 @@ export class ShellModel {
     makeAutoObservable(this)
   }
 
-  openModal(modal: LinkActionsModel | SharePostModel | EditProfileModel) {
+  openModal(
+    modal:
+      | LinkActionsModel
+      | SharePostModel
+      | EditProfileModel
+      | CreateSceneModel,
+  ) {
     this.isModalActive = true
     this.activeModal = modal
   }
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index 9d2d6ed14..33c869968 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -8,7 +8,7 @@ import Toast from '../util/Toast'
 import ProgressCircle from '../util/ProgressCircle'
 import {useStores} from '../../../state'
 import * as apilib from '../../../state/lib/api'
-import {ComposerOpts} from '../../../state/models/shell'
+import {ComposerOpts} from '../../../state/models/shell-ui'
 import {s, colors, gradients} from '../../lib/styles'
 
 const MAX_TEXT_LENGTH = 256
diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx
new file mode 100644
index 000000000..16a085d53
--- /dev/null
+++ b/src/view/com/modals/CreateScene.tsx
@@ -0,0 +1,210 @@
+import React, {useState} from 'react'
+import Toast from '../util/Toast'
+import {
+  ActivityIndicator,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {ErrorMessage} from '../util/ErrorMessage'
+import {useStores} from '../../../state'
+import {s, colors, gradients} from '../../lib/styles'
+import {makeValidHandle, createFullHandle} from '../../lib/strings'
+import {AppBskyActorCreateScene} from '../../../third-party/api/index'
+
+export const snapPoints = ['70%']
+
+export function Component({}: {}) {
+  const store = useStores()
+  const [error, setError] = useState<string>('')
+  const [isProcessing, setIsProcessing] = useState<boolean>(false)
+  const [handle, setHandle] = useState<string>('')
+  const [displayName, setDisplayName] = useState<string>('')
+  const [description, setDescription] = useState<string>('')
+  const onPressSave = async () => {
+    setIsProcessing(true)
+    if (error) {
+      setError('')
+    }
+    try {
+      if (!store.me.did) {
+        return
+      }
+      const desc = await store.api.com.atproto.server.getAccountsConfig()
+      const fullHandle = createFullHandle(
+        handle,
+        desc.data.availableUserDomains[0],
+      )
+      // create scene actor
+      const createSceneRes = await store.api.app.bsky.actor.createScene({
+        handle: fullHandle,
+      })
+      // set the scene profile
+      // TODO
+      // follow the scene
+      await store.api.app.bsky.graph.follow
+        .create(
+          {
+            did: store.me.did,
+          },
+          {
+            subject: {
+              did: createSceneRes.data.did,
+              declarationCid: createSceneRes.data.declarationCid,
+            },
+            createdAt: new Date().toISOString(),
+          },
+        )
+        .catch(e => console.error(e)) // an error here is not critical
+      Toast.show('Scene created', {
+        position: Toast.positions.TOP,
+      })
+      store.shell.closeModal()
+      store.nav.navigate(`/profile/${fullHandle}`)
+    } catch (e: any) {
+      if (e instanceof AppBskyActorCreateScene.InvalidHandleError) {
+        setError(
+          'The handle can only contain letters, numbers, and dashes, and must start with a letter.',
+        )
+      } else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
+        setError(`The handle "${handle}" is not available.`)
+      } else {
+        console.error(e)
+        setError(
+          'Failed to create the scene. Check your internet connection and try again.',
+        )
+      }
+      setIsProcessing(false)
+    }
+  }
+
+  return (
+    <View style={s.flex1}>
+      <Text style={styles.title}>Create a scene</Text>
+      <Text style={styles.description}>
+        Scenes are invite-only groups which aggregate what's popular with
+        members.
+      </Text>
+      <View style={styles.inner}>
+        <View style={styles.group}>
+          <Text style={styles.label}>Scene Handle</Text>
+          <TextInput
+            style={styles.textInput}
+            placeholder="e.g. alices-friends"
+            value={handle}
+            onChangeText={str => setHandle(makeValidHandle(str))}
+          />
+        </View>
+        <View style={styles.group}>
+          <Text style={styles.label}>Scene Display Name</Text>
+          <TextInput
+            style={styles.textInput}
+            placeholder="e.g. Alice's Friends"
+            value={displayName}
+            onChangeText={setDisplayName}
+          />
+        </View>
+        <View style={styles.group}>
+          <Text style={styles.label}>Scene Description</Text>
+          <TextInput
+            style={[styles.textArea]}
+            placeholder="e.g. Artists, dog-lovers, and memelords."
+            multiline
+            value={description}
+            onChangeText={setDescription}
+          />
+        </View>
+        <View style={styles.errorContainer}>
+          {error !== '' && (
+            <View style={s.mb10}>
+              <ErrorMessage message={error} numberOfLines={3} />
+            </View>
+          )}
+        </View>
+        {handle.length >= 2 && !isProcessing ? (
+          <TouchableOpacity style={s.mt10} onPress={onPressSave}>
+            <LinearGradient
+              colors={[gradients.primary.start, gradients.primary.end]}
+              start={{x: 0, y: 0}}
+              end={{x: 1, y: 1}}
+              style={[styles.btn]}>
+              <Text style={[s.white, s.bold, s.f18]}>Create Scene</Text>
+            </LinearGradient>
+          </TouchableOpacity>
+        ) : (
+          <View style={s.mt10}>
+            <View style={[styles.btn]}>
+              {isProcessing ? (
+                <ActivityIndicator />
+              ) : (
+                <Text style={[s.gray4, s.bold, s.f18]}>Create Scene</Text>
+              )}
+            </View>
+          </View>
+        )}
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  title: {
+    textAlign: 'center',
+    fontWeight: 'bold',
+    fontSize: 24,
+    marginBottom: 12,
+  },
+  description: {
+    textAlign: 'center',
+    fontSize: 17,
+    paddingHorizontal: 22,
+    color: colors.gray5,
+    marginBottom: 10,
+  },
+  inner: {
+    padding: 14,
+  },
+  group: {
+    marginBottom: 10,
+  },
+  label: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    paddingHorizontal: 4,
+    paddingBottom: 4,
+  },
+  textInput: {
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    borderRadius: 6,
+    paddingHorizontal: 14,
+    paddingVertical: 10,
+    fontSize: 16,
+  },
+  textArea: {
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    borderRadius: 6,
+    paddingHorizontal: 12,
+    paddingTop: 10,
+    fontSize: 16,
+    height: 100,
+    textAlignVertical: 'top',
+  },
+  errorContainer: {
+    height: 80,
+  },
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    width: '100%',
+    borderRadius: 32,
+    padding: 14,
+    marginBottom: 10,
+    backgroundColor: colors.gray1,
+  },
+})
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index ab4d7f563..3049ad5b8 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -68,7 +68,7 @@ export function Component({profileView}: {profileView: ProfileViewModel}) {
           />
         </View>
         <View style={styles.group}>
-          <Text style={styles.label}>Biography</Text>
+          <Text style={styles.label}>Description</Text>
           <TextInput
             style={[styles.textArea]}
             placeholder="e.g. Artist, dog-lover, and memelord."
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 02b65a490..f79a571d4 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -5,11 +5,12 @@ import BottomSheet from '@gorhom/bottom-sheet'
 import {useStores} from '../../../state'
 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 
-import * as models from '../../../state/models/shell'
+import * as models from '../../../state/models/shell-ui'
 
 import * as LinkActionsModal from './LinkActions'
 import * as SharePostModal from './SharePost.native'
 import * as EditProfile from './EditProfile'
+import * as CreateScene from './CreateScene'
 
 const CLOSED_SNAPPOINTS = ['10%']
 
@@ -57,6 +58,9 @@ export const Modal = observer(function Modal() {
         {...(store.shell.activeModal as models.EditProfileModel)}
       />
     )
+  } else if (store.shell.activeModal?.name === 'create-scene') {
+    snapPoints = CreateScene.snapPoints
+    element = <CreateScene.Component />
   } else {
     element = <View />
   }
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 5d0a5ba4b..0349d3428 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -6,7 +6,7 @@ import {
   PostThreadViewPostModel,
 } from '../../../state/models/post-thread-view'
 import {useStores} from '../../../state'
-import {SharePostModel} from '../../../state/models/shell'
+import {SharePostModel} from '../../../state/models/shell-ui'
 import {PostThreadItem} from './PostThreadItem'
 
 export const PostThread = observer(function PostThread({uri}: {uri: string}) {
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 73593166c..43017f7d7 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -5,7 +5,7 @@ import {AtUri} from '../../../third-party/uri'
 import * as PostType from '../../../third-party/api/src/client/types/app/bsky/feed/post'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FeedItemModel} from '../../../state/models/feed-view'
-import {SharePostModel} from '../../../state/models/shell'
+import {SharePostModel} from '../../../state/models/shell-ui'
 import {Link} from '../util/Link'
 import {PostDropdownBtn} from '../util/DropdownBtn'
 import {UserInfoText} from '../util/UserInfoText'
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 536a37cb2..ee4df4fb9 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -11,7 +11,7 @@ import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {ProfileViewModel} from '../../../state/models/profile-view'
 import {useStores} from '../../../state'
-import {EditProfileModel} from '../../../state/models/shell'
+import {EditProfileModel} from '../../../state/models/shell-ui'
 import {pluralize} from '../../lib/strings'
 import {s, colors} from '../../lib/styles'
 import {getGradient} from '../../lib/asset-gen'
diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/DropdownBtn.tsx
index 2e9ca0c15..960293320 100644
--- a/src/view/com/util/DropdownBtn.tsx
+++ b/src/view/com/util/DropdownBtn.tsx
@@ -13,7 +13,7 @@ import RootSiblings from 'react-native-root-siblings'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {colors} from '../../lib/styles'
 import {useStores} from '../../../state'
-import {SharePostModel} from '../../../state/models/shell'
+import {SharePostModel} from '../../../state/models/shell-ui'
 
 export interface DropdownItem {
   icon?: IconProp
diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx
index 3acea1cab..834cd598f 100644
--- a/src/view/com/util/ErrorMessage.tsx
+++ b/src/view/com/util/ErrorMessage.tsx
@@ -5,9 +5,11 @@ import {colors} from '../../lib/styles'
 
 export function ErrorMessage({
   message,
+  numberOfLines,
   onPressTryAgain,
 }: {
   message: string
+  numberOfLines?: number
   onPressTryAgain?: () => void
 }) {
   return (
@@ -19,7 +21,9 @@ export function ErrorMessage({
           size={16}
         />
       </View>
-      <Text style={styles.message}>{message}</Text>
+      <Text style={styles.message} numberOfLines={numberOfLines}>
+        {message}
+      </Text>
       {onPressTryAgain && (
         <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
           <FontAwesomeIcon
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 23d1bd345..08536b0c3 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {StyleProp, Text, TouchableOpacity, ViewStyle} from 'react-native'
 import {useStores} from '../../../state'
-import {LinkActionsModel} from '../../../state/models/shell'
+import {LinkActionsModel} from '../../../state/models/shell-ui'
 
 export const Link = observer(function Link({
   style,
diff --git a/src/view/lib/strings.ts b/src/view/lib/strings.ts
index 8dc63faa4..26e1b04e2 100644
--- a/src/view/lib/strings.ts
+++ b/src/view/lib/strings.ts
@@ -71,3 +71,17 @@ export function extractEntities(text: string): Entity[] | undefined {
   }
   return ents.length > 0 ? ents : undefined
 }
+
+export function makeValidHandle(str: string): string {
+  if (str.length > 20) {
+    str = str.slice(0, 20)
+  }
+  str = str.toLowerCase()
+  return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '')
+}
+
+export function createFullHandle(name: string, domain: string): string {
+  name = name.replace(/[\.]+$/, '')
+  domain = domain.replace(/^[\.]+/, '')
+  return `${name}.${domain}`
+}
diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx
index 96fd50441..7a8d6681b 100644
--- a/src/view/shell/mobile/Composer.tsx
+++ b/src/view/shell/mobile/Composer.tsx
@@ -19,7 +19,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons'
 import {ComposePost} from '../../com/composer/ComposePost'
 import {useStores} from '../../../state'
-import {ComposerOpts} from '../../../state/models/shell'
+import {ComposerOpts} from '../../../state/models/shell-ui'
 import {s, colors} from '../../lib/styles'
 
 export const Composer = observer(
diff --git a/src/view/shell/mobile/MainMenu.tsx b/src/view/shell/mobile/MainMenu.tsx
index 0249714f4..3bc045029 100644
--- a/src/view/shell/mobile/MainMenu.tsx
+++ b/src/view/shell/mobile/MainMenu.tsx
@@ -20,6 +20,7 @@ import _chunk from 'lodash.chunk'
 import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons'
 import {UserAvatar} from '../../com/util/UserAvatar'
 import {useStores} from '../../../state'
+import {CreateSceneModel} from '../../../state/models/shell-ui'
 import {s, colors} from '../../lib/styles'
 
 export const MainMenu = observer(
@@ -54,6 +55,10 @@ export const MainMenu = observer(
       store.nav.navigate(url)
       onClose()
     }
+    const onPressCreateScene = () => {
+      store.shell.openModal(new CreateSceneModel())
+      onClose()
+    }
 
     // rendering
     // =
@@ -65,17 +70,19 @@ export const MainMenu = observer(
     const MenuItem = ({
       icon,
       label,
-      url,
       count,
+      url,
+      onPress,
     }: {
       icon: IconProp
       label: string
-      url: string
       count?: number
+      url?: string
+      onPress?: () => void
     }) => (
       <TouchableOpacity
         style={[styles.menuItem, styles.menuItemMargin]}
-        onPress={() => onNavigate(url)}>
+        onPress={onPress ? onPress : () => onNavigate(url || '/')}>
         <View style={[styles.menuItemIconWrapper]}>
           {icon === 'home' ? (
             <HomeIcon style={styles.menuItemIcon} size="32" />
@@ -209,7 +216,7 @@ export const MainMenu = observer(
                 <MenuItem
                   icon={'user-group'}
                   label="Create Scene"
-                  url="/contacts"
+                  onPress={onPressCreateScene}
                 />
                 {store.me.memberships ? (
                   store.me.memberships.memberships.map((membership, i) => (
diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx
index c0ae2321a..a3da5fa19 100644
--- a/src/view/shell/mobile/TabsSelector.tsx
+++ b/src/view/shell/mobile/TabsSelector.tsx
@@ -19,7 +19,7 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
 import {match} from '../../routes'
-import {LinkActionsModel} from '../../../state/models/shell'
+import {LinkActionsModel} from '../../../state/models/shell-ui'
 
 const TAB_HEIGHT = 42
 
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 49b18a481..9fb17aba2 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -230,11 +230,11 @@ export const MobileShell: React.FC = observer(() => {
         />
         <Btn icon={['far', 'clone']} onPress={onPressTabs} />
       </View>
-      <Modal />
       <MainMenu
         active={isMainMenuActive}
         onClose={() => setMainMenuActive(false)}
       />
+      <Modal />
       <TabsSelector
         active={isTabsSelectorActive}
         onClose={() => setTabsSelectorActive(false)}
diff --git a/todos.txt b/todos.txt
index f4dade89f..6e1933300 100644
--- a/todos.txt
+++ b/todos.txt
@@ -9,8 +9,8 @@ Paul's todo list
   - *
 - Avatars
   - SVG generate
-- Create scene view
-  - *
+- Create scene
+  - Set profile during creation
 - Discover scenes view
   - *
 - User profile