From cb310ab1c1e4d2beb6f39b39e4972b9ae21f9849 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 5 Sep 2022 14:16:48 -0500 Subject: Rewrite the post composer as a modal --- src/state/models/shell.ts | 12 +- src/view/com/composer/Composer.tsx | 90 --------------- src/view/com/feed/FeedItem.tsx | 3 +- src/view/com/modals/ComposePost.tsx | 165 ++++++++++++++++++++++++++++ src/view/com/modals/Modal.tsx | 23 +++- src/view/com/post-thread/PostThreadItem.tsx | 3 +- src/view/com/post/Post.tsx | 3 +- src/view/index.ts | 2 + src/view/routes.ts | 2 - src/view/screens/Composer.tsx | 43 -------- src/view/screens/Home.tsx | 3 +- 11 files changed, 206 insertions(+), 143 deletions(-) delete mode 100644 src/view/com/composer/Composer.tsx create mode 100644 src/view/com/modals/ComposePost.tsx delete mode 100644 src/view/screens/Composer.tsx (limited to 'src') 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 ( - - onChangeText(text)} - value={text} - placeholder={replyTo ? 'Write your reply' : "What's new in the scene?"} - style={styles.textInput} - /> - - - - - - - - ) -}) - -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 ( + + + + Cancel + + + + + Post + + + + {error !== '' && ( + + + + + {error} + + )} + onChangeText(text)} + value={text} + placeholder={replyTo ? 'Write your reply' : "What's new?"} + style={styles.textInput} + /> + + + + + + + + ) +} + +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 = + element = ( + + ) } else if (store.shell.activeModal?.name === 'share-post') { snapPoints = SharePostModal.snapPoints - element = + element = ( + + ) + } else if (store.shell.activeModal?.name === 'compose-post') { + snapPoints = ComposePostModal.snapPoints + element = ( + + ) } else { return } @@ -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/(?[^/]+)/post/(?[^/]+)/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}>() - - // TODO - // useLayoutEffect(() => { - // navigation.setOptions({ - // headerShown: true, - // headerTitle: replyTo ? 'Reply' : 'New Post', - // headerLeft: () => ( - // navigation.goBack()}> - // - // - // ), - // headerRight: () => ( - // { - // if (!ref.current) { - // return - // } - // ref.current.publish().then( - // posted => { - // if (posted) { - // navigation.goBack() - // } - // }, - // err => console.error('Failed to create post', err), - // ) - // }}> - // Post - // - // ), - // }) - // }, [navigation, replyTo, ref]) - - return -} 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 ( -- cgit 1.4.1