about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/shell.ts12
-rw-r--r--src/view/com/composer/Composer.tsx90
-rw-r--r--src/view/com/feed/FeedItem.tsx3
-rw-r--r--src/view/com/modals/ComposePost.tsx165
-rw-r--r--src/view/com/modals/Modal.tsx23
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx3
-rw-r--r--src/view/com/post/Post.tsx3
-rw-r--r--src/view/index.ts2
-rw-r--r--src/view/routes.ts2
-rw-r--r--src/view/screens/Composer.tsx43
-rw-r--r--src/view/screens/Home.tsx3
-rw-r--r--todos.txt2
12 files changed, 207 insertions, 144 deletions
diff --git a/src/state/models/shell.ts b/src/state/models/shell.ts
index a2e83b5e3..c67b474b7 100644
--- a/src/state/models/shell.ts
+++ b/src/state/models/shell.ts
@@ -16,15 +16,23 @@ export class SharePostModel {
   }
 }
 
+export class ComposePostModel {
+  name = 'compose-post'
+
+  constructor(public replyTo?: string) {
+    makeAutoObservable(this)
+  }
+}
+
 export class ShellModel {
   isModalActive = false
-  activeModal: LinkActionsModel | SharePostModel | undefined
+  activeModal: LinkActionsModel | SharePostModel | ComposePostModel | undefined
 
   constructor() {
     makeAutoObservable(this)
   }
 
-  openModal(modal: LinkActionsModel | SharePostModel) {
+  openModal(modal: LinkActionsModel | SharePostModel | ComposePostModel) {
     this.isModalActive = true
     this.activeModal = modal
   }
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
deleted file mode 100644
index 6a15599d8..000000000
--- a/src/view/com/composer/Composer.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React, {useState, forwardRef, useImperativeHandle} from 'react'
-import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native'
-import Toast from '../util/Toast'
-import ProgressCircle from '../util/ProgressCircle'
-import {useStores} from '../../../state'
-import {s} from '../../lib/styles'
-import * as apilib from '../../../state/lib/api'
-
-const MAX_TEXT_LENGTH = 256
-const WARNING_TEXT_LENGTH = 200
-const DANGER_TEXT_LENGTH = 255
-
-export const Composer = forwardRef(function Composer(
-  {
-    replyTo,
-  }: {
-    replyTo: string | undefined
-  },
-  ref,
-) {
-  const store = useStores()
-  const [text, setText] = useState('')
-
-  const onChangeText = (newText: string) => {
-    if (newText.length > MAX_TEXT_LENGTH) {
-      setText(newText.slice(0, MAX_TEXT_LENGTH))
-    } else {
-      setText(newText)
-    }
-  }
-
-  useImperativeHandle(ref, () => ({
-    async publish() {
-      if (text.trim().length === 0) {
-        return false
-      }
-      await apilib.post(store.api, 'alice.com', text, replyTo)
-      Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been created`, {
-        duration: Toast.durations.LONG,
-        position: Toast.positions.TOP,
-        shadow: true,
-        animation: true,
-        hideOnPress: true,
-      })
-      return true
-    },
-  }))
-
-  const progressColor =
-    text.length > DANGER_TEXT_LENGTH
-      ? '#e60000'
-      : text.length > WARNING_TEXT_LENGTH
-      ? '#f7c600'
-      : undefined
-
-  return (
-    <KeyboardAvoidingView style={styles.outer} behavior="padding">
-      <TextInput
-        multiline
-        scrollEnabled
-        onChangeText={text => onChangeText(text)}
-        value={text}
-        placeholder={replyTo ? 'Write your reply' : "What's new in the scene?"}
-        style={styles.textInput}
-      />
-      <View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}>
-        <View style={s.flex1} />
-        <View>
-          <ProgressCircle
-            color={progressColor}
-            progress={text.length / MAX_TEXT_LENGTH}
-          />
-        </View>
-      </View>
-    </KeyboardAvoidingView>
-  )
-})
-
-const styles = StyleSheet.create({
-  outer: {
-    flexDirection: 'column',
-    backgroundColor: '#fff',
-    padding: 10,
-    height: '100%',
-  },
-  textInput: {
-    flex: 1,
-    padding: 10,
-  },
-})
diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx
index 52d162a62..e9cf83346 100644
--- a/src/view/com/feed/FeedItem.tsx
+++ b/src/view/com/feed/FeedItem.tsx
@@ -4,6 +4,7 @@ import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {bsky, AdxUri} from '@adxp/mock-api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FeedViewItemModel} from '../../../state/models/feed-view'
+import {ComposePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {PostDropdownBtn} from '../util/DropdownBtn'
 import {s, colors} from '../../lib/styles'
@@ -28,7 +29,7 @@ export const FeedItem = observer(function FeedItem({
   const authorHref = `/profile/${item.author.name}`
 
   const onPressReply = () => {
-    store.nav.navigate('/composer')
+    store.shell.openModal(new ComposePostModel(item.uri))
   }
   const onPressToggleRepost = () => {
     item
diff --git a/src/view/com/modals/ComposePost.tsx b/src/view/com/modals/ComposePost.tsx
new file mode 100644
index 000000000..253db3771
--- /dev/null
+++ b/src/view/com/modals/ComposePost.tsx
@@ -0,0 +1,165 @@
+import React, {useState} from 'react'
+import {
+  KeyboardAvoidingView,
+  StyleSheet,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {BottomSheetTextInput} from '@gorhom/bottom-sheet'
+import LinearGradient from 'react-native-linear-gradient'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import Toast from '../util/Toast'
+import ProgressCircle from '../util/ProgressCircle'
+import {useStores} from '../../../state'
+import * as apilib from '../../../state/lib/api'
+import {s, colors, gradients} from '../../lib/styles'
+
+const MAX_TEXT_LENGTH = 256
+const WARNING_TEXT_LENGTH = 200
+const DANGER_TEXT_LENGTH = 255
+export const snapPoints = ['100%']
+
+export function Component({replyTo}: {replyTo?: string}) {
+  const store = useStores()
+  const [text, setText] = useState('')
+  const [error, setError] = useState('')
+
+  const onChangeText = (newText: string) => {
+    if (newText.length > MAX_TEXT_LENGTH) {
+      setText(newText.slice(0, MAX_TEXT_LENGTH))
+    } else {
+      setText(newText)
+    }
+  }
+  const onPressCancel = () => {
+    store.shell.closeModal()
+  }
+  const onPressPublish = async () => {
+    setError('')
+    if (text.trim().length === 0) {
+      setError('Did you want to say anything?')
+      return false
+    }
+    try {
+      await apilib.post(store.api, 'alice.com', text, replyTo)
+    } catch (e: any) {
+      console.error(`Failed to create post: ${e.toString()}`)
+      setError(
+        'Post failed to upload. Please check your Internet connection and try again.',
+      )
+      return
+    }
+    store.shell.closeModal()
+    Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`, {
+      duration: Toast.durations.LONG,
+      position: Toast.positions.TOP,
+      shadow: true,
+      animation: true,
+      hideOnPress: true,
+    })
+  }
+
+  const progressColor =
+    text.length > DANGER_TEXT_LENGTH
+      ? '#e60000'
+      : text.length > WARNING_TEXT_LENGTH
+      ? '#f7c600'
+      : undefined
+
+  return (
+    <View style={styles.outer}>
+      <View style={styles.topbar}>
+        <TouchableOpacity onPress={onPressCancel}>
+          <Text style={[s.blue3, s.f16]}>Cancel</Text>
+        </TouchableOpacity>
+        <View style={s.flex1} />
+        <TouchableOpacity onPress={onPressPublish}>
+          <LinearGradient
+            colors={[gradients.primary.start, gradients.primary.end]}
+            start={{x: 0, y: 0}}
+            end={{x: 1, y: 1}}
+            style={styles.postBtn}>
+            <Text style={[s.white, s.f16, s.semiBold]}>Post</Text>
+          </LinearGradient>
+        </TouchableOpacity>
+      </View>
+      {error !== '' && (
+        <View style={styles.errorLine}>
+          <View style={styles.errorIcon}>
+            <FontAwesomeIcon
+              icon="exclamation"
+              style={{color: colors.red4}}
+              size={10}
+            />
+          </View>
+          <Text style={s.red4}>{error}</Text>
+        </View>
+      )}
+      <BottomSheetTextInput
+        multiline
+        scrollEnabled
+        autoFocus
+        onChangeText={(text: string) => onChangeText(text)}
+        value={text}
+        placeholder={replyTo ? 'Write your reply' : "What's new?"}
+        style={styles.textInput}
+      />
+      <View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}>
+        <View style={s.flex1} />
+        <View>
+          <ProgressCircle
+            color={progressColor}
+            progress={text.length / MAX_TEXT_LENGTH}
+          />
+        </View>
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    flexDirection: 'column',
+    backgroundColor: '#fff',
+    padding: 15,
+    height: '100%',
+  },
+  topbar: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingTop: 10,
+    paddingBottom: 5,
+    paddingHorizontal: 5,
+  },
+  postBtn: {
+    borderRadius: 20,
+    paddingHorizontal: 20,
+    paddingVertical: 6,
+  },
+  errorLine: {
+    flexDirection: 'row',
+    backgroundColor: colors.red1,
+    borderRadius: 6,
+    paddingHorizontal: 8,
+    paddingVertical: 6,
+    marginVertical: 6,
+  },
+  errorIcon: {
+    borderWidth: 1,
+    borderColor: colors.red4,
+    color: colors.red4,
+    borderRadius: 30,
+    width: 16,
+    height: 16,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginRight: 5,
+  },
+  textInput: {
+    flex: 1,
+    padding: 5,
+    fontSize: 18,
+  },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index dc5b719bc..6e0846000 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -5,8 +5,11 @@ import BottomSheet from '@gorhom/bottom-sheet'
 import {useStores} from '../../../state'
 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 
+import * as models from '../../../state/models/shell'
+
 import * as LinkActionsModal from './LinkActions'
 import * as SharePostModal from './SharePost.native'
+import * as ComposePostModal from './ComposePost'
 
 export const Modal = observer(function Modal() {
   const store = useStores()
@@ -28,10 +31,25 @@ export const Modal = observer(function Modal() {
   let snapPoints, element
   if (store.shell.activeModal?.name === 'link-actions') {
     snapPoints = LinkActionsModal.snapPoints
-    element = <LinkActionsModal.Component {...store.shell.activeModal} />
+    element = (
+      <LinkActionsModal.Component
+        {...(store.shell.activeModal as models.LinkActionsModel)}
+      />
+    )
   } else if (store.shell.activeModal?.name === 'share-post') {
     snapPoints = SharePostModal.snapPoints
-    element = <SharePostModal.Component {...store.shell.activeModal} />
+    element = (
+      <SharePostModal.Component
+        {...(store.shell.activeModal as models.SharePostModel)}
+      />
+    )
+  } else if (store.shell.activeModal?.name === 'compose-post') {
+    snapPoints = ComposePostModal.snapPoints
+    element = (
+      <ComposePostModal.Component
+        {...(store.shell.activeModal as models.ComposePostModel)}
+      />
+    )
   } else {
     return <View />
   }
@@ -41,6 +59,7 @@ export const Modal = observer(function Modal() {
       ref={bottomSheetRef}
       snapPoints={snapPoints}
       enablePanDownToClose
+      keyboardBehavior="fillParent"
       backdropComponent={createCustomBackdrop(onClose)}
       onChange={onShareBottomSheetChange}>
       {element}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 53ae8e548..d500514ef 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -4,6 +4,7 @@ import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {bsky, AdxUri} from '@adxp/mock-api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
+import {ComposePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {PostDropdownBtn} from '../util/DropdownBtn'
 import {s, colors} from '../../lib/styles'
@@ -41,7 +42,7 @@ export const PostThreadItem = observer(function PostThreadItem({
   const repostsTitle = 'Reposts of this post'
 
   const onPressReply = () => {
-    store.nav.navigate(`/composer?replyTo=${item.uri}`)
+    store.shell.openModal(new ComposePostModel(item.uri))
   }
   const onPressToggleRepost = () => {
     item
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 2de7432bd..a6580fa5a 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -11,6 +11,7 @@ import {
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {PostThreadViewModel} from '../../../state/models/post-thread-view'
+import {ComposePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
@@ -63,7 +64,7 @@ export const Post = observer(function Post({uri}: {uri: string}) {
   const authorHref = `/profile/${item.author.name}`
   const authorTitle = item.author.name
   const onPressReply = () => {
-    store.nav.navigate(`/composer?replyTo=${item.uri}`)
+    store.shell.openModal(new ComposePostModel(item.uri))
   }
   const onPressToggleRepost = () => {
     item
diff --git a/src/view/index.ts b/src/view/index.ts
index 3dcadf676..af24030c8 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -14,6 +14,7 @@ import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
 import {faClone} from '@fortawesome/free-regular-svg-icons/faClone'
 import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
 import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
+import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation'
 import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
 import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
 import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
@@ -46,6 +47,7 @@ export function setup() {
     faClone,
     faComment,
     faEllipsis,
+    faExclamation,
     faGear,
     faHeart,
     fasHeart,
diff --git a/src/view/routes.ts b/src/view/routes.ts
index 293d53e30..d31dbae35 100644
--- a/src/view/routes.ts
+++ b/src/view/routes.ts
@@ -6,7 +6,6 @@ import {Notifications} from './screens/Notifications'
 import {Login} from './screens/Login'
 import {Signup} from './screens/Signup'
 import {NotFound} from './screens/NotFound'
-import {Composer} from './screens/Composer'
 import {PostThread} from './screens/PostThread'
 import {PostLikedBy} from './screens/PostLikedBy'
 import {PostRepostedBy} from './screens/PostRepostedBy'
@@ -48,7 +47,6 @@ export const routes: Route[] = [
     'retweet',
     r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/reposted-by'),
   ],
-  [Composer, 'pen-nib', r('/compose')],
   [Login, ['far', 'user'], r('/login')],
   [Signup, ['far', 'user'], r('/signup')],
 ]
diff --git a/src/view/screens/Composer.tsx b/src/view/screens/Composer.tsx
deleted file mode 100644
index 2de84583f..000000000
--- a/src/view/screens/Composer.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React, {useLayoutEffect, useRef} from 'react'
-// import {Text, TouchableOpacity} from 'react-native'
-// import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Composer as ComposerComponent} from '../com/composer/Composer'
-import {ScreenParams} from '../routes'
-
-export const Composer = ({params}: ScreenParams) => {
-  const {replyTo} = params
-  const ref = useRef<{publish: () => Promise<boolean>}>()
-
-  // TODO
-  // useLayoutEffect(() => {
-  //   navigation.setOptions({
-  //     headerShown: true,
-  //     headerTitle: replyTo ? 'Reply' : 'New Post',
-  //     headerLeft: () => (
-  //       <TouchableOpacity onPress={() => navigation.goBack()}>
-  //         <FontAwesomeIcon icon="x" />
-  //       </TouchableOpacity>
-  //     ),
-  //     headerRight: () => (
-  //       <TouchableOpacity
-  //         onPress={() => {
-  //           if (!ref.current) {
-  //             return
-  //           }
-  //           ref.current.publish().then(
-  //             posted => {
-  //               if (posted) {
-  //                 navigation.goBack()
-  //               }
-  //             },
-  //             err => console.error('Failed to create post', err),
-  //           )
-  //         }}>
-  //         <Text>Post</Text>
-  //       </TouchableOpacity>
-  //     ),
-  //   })
-  // }, [navigation, replyTo, ref])
-
-  return <ComposerComponent ref={ref} replyTo={replyTo} />
-}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index b29e042bd..27a17d0e9 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -5,6 +5,7 @@ import {Feed} from '../com/feed/Feed'
 import {FAB} from '../com/util/FloatingActionButton'
 import {useStores} from '../../state'
 import {FeedViewModel} from '../../state/models/feed-view'
+import {ComposePostModel} from '../../state/models/shell'
 import {ScreenParams} from '../routes'
 import {s} from '../lib/styles'
 
@@ -30,7 +31,7 @@ export const Home = observer(function Home({visible}: ScreenParams) {
   }, [visible, store])
 
   const onComposePress = () => {
-    store.nav.navigate('/compose')
+    store.shell.openModal(new ComposePostModel())
   }
 
   return (
diff --git a/todos.txt b/todos.txt
index fd68f690a..e9c96c808 100644
--- a/todos.txt
+++ b/todos.txt
@@ -1,7 +1,7 @@
 Paul's todo list
 
 - Composer
-  - Check on navigation stack during a bunch of replies
+  - Update the view after creating a post
 - Search view
   - *
 - Linking