diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-09-05 14:16:48 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-09-05 14:16:48 -0500 |
commit | cb310ab1c1e4d2beb6f39b39e4972b9ae21f9849 (patch) | |
tree | 11c2c4c531c65dbf0f48293fd2eb35dbe5a20323 | |
parent | 41bbe2b60bf37f52ab8404a275a523c4b4b97a15 (diff) | |
download | voidsky-cb310ab1c1e4d2beb6f39b39e4972b9ae21f9849.tar.zst |
Rewrite the post composer as a modal
-rw-r--r-- | src/state/models/shell.ts | 12 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 90 | ||||
-rw-r--r-- | src/view/com/feed/FeedItem.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/ComposePost.tsx | 165 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 23 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 3 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 3 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/routes.ts | 2 | ||||
-rw-r--r-- | src/view/screens/Composer.tsx | 43 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 3 | ||||
-rw-r--r-- | todos.txt | 2 |
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 |