diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-07-25 23:08:24 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-07-25 23:08:24 -0500 |
commit | 041bfa22a99d8d6b4b17ad36c983e9e2b2444918 (patch) | |
tree | b03d7934b27af87c76fa62424fa70e6d0e5229a8 | |
parent | af55a89758fc6d44896051b9ddd015a73b92e0f6 (diff) | |
download | voidsky-041bfa22a99d8d6b4b17ad36c983e9e2b2444918.tar.zst |
Implement Web versions of the bottom sheet, toast, and progress circle
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | public/index.html | 39 | ||||
-rw-r--r-- | src/App.web.tsx | 14 | ||||
-rw-r--r-- | src/state/lib/api.ts | 4 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 127 | ||||
-rw-r--r-- | src/view/com/feed/Feed.tsx | 6 | ||||
-rw-r--r-- | src/view/com/feed/FeedItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/modals/SharePost.native.tsx (renamed from src/view/com/sheets/SharePost.tsx) | 11 | ||||
-rw-r--r-- | src/view/com/modals/SharePost.tsx | 57 | ||||
-rw-r--r-- | src/view/com/modals/WebModal.tsx | 20 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 6 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/ProgressCircle.native.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/ProgressCircle.tsx | 20 | ||||
-rw-r--r-- | src/view/com/util/Toast.native.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/Toast.tsx | 62 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | todos.txt | 4 | ||||
-rw-r--r-- | yarn.lock | 5 |
19 files changed, 295 insertions, 92 deletions
diff --git a/package.json b/package.json index 93363c354..add0e37ce 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "mobx-react-lite": "^3.4.0", "moment": "^2.29.4", "react": "17.0.2", + "react-circular-progressbar": "^2.1.0", "react-dom": "17.0.2", "react-native": "0.68.2", "react-native-gesture-handler": "^2.5.0", diff --git a/public/index.html b/public/index.html index f6cb612b6..54fb90eea 100644 --- a/public/index.html +++ b/public/index.html @@ -11,6 +11,45 @@ body { overflow: hidden; } /* These styles make the root element full-height */ #root { display:flex; height:100%; } + + /* These styles are for src/view/com/modals/WebModal */ + div[data-modal-overlay] { + position: fixed; + top: 0; + left: 0; + background: #0004; + width: 100vw; + height: 100vh; + } + div[data-modal-container] { + position: fixed; + top: 20vh; + left: calc(50vw - 300px); + width: 600px; + padding: 20px; + background: #fff; + border-radius: 10px; + box-shadow: 0 5px 10px #0005; + } + + /* These styles are for src/view/com/util/Toast */ + div[data-toast-container] { + position: fixed; + bottom: 5vh; + right: 5vh; + width: 350px; + padding: 20px; + display: flex; + flex-direction: row; + align-items: center; + background: #fff; + border-radius: 10px; + box-shadow: 0 5px 10px #0005; + } + div[data-toast-container] > div { + font-size: 18px; + margin-left: 10px; + } </style> </head> <body> diff --git a/src/App.web.tsx b/src/App.web.tsx index 838b81ee2..9a6fedd5a 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,9 +1,8 @@ import React, {useState, useEffect} from 'react' -import {RootSiblingParent} from 'react-native-root-siblings' -import {GestureHandlerRootView} from 'react-native-gesture-handler' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import * as Routes from './view/routes' +import Toast from './view/com/util/Toast' function App() { const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( @@ -22,13 +21,10 @@ function App() { } return ( - <GestureHandlerRootView style={{flex: 1}}> - <RootSiblingParent> - <RootStoreProvider value={rootStore}> - <Routes.Root /> - </RootStoreProvider> - </RootSiblingParent> - </GestureHandlerRootView> + <RootStoreProvider value={rootStore}> + <Routes.Root /> + <Toast.ToastContainer /> + </RootStoreProvider> ) } diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index bb3ef5d1a..df0047991 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -97,7 +97,7 @@ export async function unrepost(adx: AdxClient, user: string, uri: string) { return numDels > 0 } -type WherePred = (record: GetRecordResponseValidated) => Boolean +type WherePred = (_record: GetRecordResponseValidated) => Boolean async function deleteWhere( coll: AdxRepoCollectionClient, schema: SchemaOpt, @@ -115,7 +115,7 @@ async function deleteWhere( return toDelete.length } -type IterateAllCb = (record: GetRecordResponseValidated) => void +type IterateAllCb = (_record: GetRecordResponseValidated) => void async function iterateAll( coll: AdxRepoCollectionClient, schema: SchemaOpt, diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c7ce3f4c7..57dc0ef86 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1,9 +1,8 @@ import React, {useState, forwardRef, useImperativeHandle} from 'react' import {observer} from 'mobx-react-lite' import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native' -import Toast from 'react-native-root-toast' -// @ts-ignore no type definition -prf -import ProgressCircle from 'react-native-progress/Circle' +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' @@ -12,75 +11,71 @@ const MAX_TEXT_LENGTH = 256 const WARNING_TEXT_LENGTH = 200 const DANGER_TEXT_LENGTH = 255 -export const Composer = observer( - forwardRef(function Composer( - { - replyTo, - }: { - replyTo: string | undefined - }, - ref, - ) { - const store = useStores() - const [text, setText] = useState('') +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) - } + 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 - }, - })) + 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 + 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> + 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> - </KeyboardAvoidingView> - ) - }), -) + </View> + </KeyboardAvoidingView> + ) +}) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/feed/Feed.tsx b/src/view/com/feed/Feed.tsx index 8283e275e..6787b51ae 100644 --- a/src/view/com/feed/Feed.tsx +++ b/src/view/com/feed/Feed.tsx @@ -4,7 +4,7 @@ import {Text, View, FlatList} from 'react-native' import {OnNavigateContent} from '../../routes/types' import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view' import {FeedItem} from './FeedItem' -import {ShareBottomSheet} from '../sheets/SharePost' +import {ShareModal} from '../modals/SharePost' export const Feed = observer(function Feed({ feed, @@ -13,7 +13,7 @@ export const Feed = observer(function Feed({ feed: FeedViewModel onNavigateContent: OnNavigateContent }) { - const shareSheetRef = useRef<{open: (uri: string) => void}>() + const shareSheetRef = useRef<{open: (_uri: string) => void}>() const onPressShare = (uri: string) => { shareSheetRef.current?.open(uri) @@ -52,7 +52,7 @@ export const Feed = observer(function Feed({ /> )} {feed.isEmpty && <Text>This feed is empty!</Text>} - <ShareBottomSheet ref={shareSheetRef} /> + <ShareModal ref={shareSheetRef} /> </View> ) }) diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx index 018b58179..616fb0aca 100644 --- a/src/view/com/feed/FeedItem.tsx +++ b/src/view/com/feed/FeedItem.tsx @@ -16,7 +16,7 @@ export const FeedItem = observer(function FeedItem({ }: { item: FeedViewItemModel onNavigateContent: OnNavigateContent - onPressShare: (uri: string) => void + onPressShare: (_uri: string) => void }) { const record = item.record as unknown as bsky.Post.Record diff --git a/src/view/com/sheets/SharePost.tsx b/src/view/com/modals/SharePost.native.tsx index b0f22c54e..0e99bd4d1 100644 --- a/src/view/com/sheets/SharePost.tsx +++ b/src/view/com/modals/SharePost.native.tsx @@ -19,14 +19,11 @@ import Animated, { interpolate, useAnimatedStyle, } from 'react-native-reanimated' -import Toast from 'react-native-root-toast' +import Toast from '../util/Toast' import Clipboard from '@react-native-clipboard/clipboard' import {s} from '../../lib/styles' -export const ShareBottomSheet = forwardRef(function ShareBottomSheet( - {}: {}, - ref, -) { +export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { const [isOpen, setIsOpen] = useState<boolean>(false) const [uri, setUri] = useState<string>('') const bottomSheetRef = useRef<BottomSheet>(null) @@ -41,6 +38,9 @@ export const ShareBottomSheet = forwardRef(function ShareBottomSheet( const onPressCopy = () => { Clipboard.setString(uri) + console.log('showing') + console.log(Toast) + console.log(Toast.show) Toast.show('Link copied', { position: Toast.positions.TOP, }) @@ -56,7 +56,6 @@ export const ShareBottomSheet = forwardRef(function ShareBottomSheet( } const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { - console.log('hit!', animatedIndex.value) // animated variables const opacity = useAnimatedStyle(() => ({ opacity: interpolate( diff --git a/src/view/com/modals/SharePost.tsx b/src/view/com/modals/SharePost.tsx new file mode 100644 index 000000000..d6d4bb5c1 --- /dev/null +++ b/src/view/com/modals/SharePost.tsx @@ -0,0 +1,57 @@ +import React, {forwardRef, useState, useImperativeHandle} from 'react' +import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import {Modal} from './WebModal' +import Toast from '../util/Toast' +import {s} from '../../lib/styles' + +export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { + const [isOpen, setIsOpen] = useState<boolean>(false) + const [uri, setUri] = useState<string>('') + + useImperativeHandle(ref, () => ({ + open(uri: string) { + console.log('sharing', uri) + setUri(uri) + setIsOpen(true) + }, + })) + + const onPressCopy = () => { + // TODO + Toast.show('Link copied', { + position: Toast.positions.TOP, + }) + } + const onClose = () => { + setIsOpen(false) + } + + return ( + <> + {isOpen && ( + <Modal onClose={onClose}> + <View> + <Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text> + <Text style={[s.textCenter, s.mb10]}>{uri}</Text> + <Button title="Copy to clipboard" onPress={onPressCopy} /> + <View style={s.p10}> + <TouchableOpacity onPress={onClose} style={styles.closeBtn}> + <Text style={s.textCenter}>Close</Text> + </TouchableOpacity> + </View> + </View> + </Modal> + )} + </> + ) +}) + +const styles = StyleSheet.create({ + closeBtn: { + width: '100%', + borderColor: '#000', + borderWidth: 1, + borderRadius: 4, + padding: 10, + }, +}) diff --git a/src/view/com/modals/WebModal.tsx b/src/view/com/modals/WebModal.tsx new file mode 100644 index 000000000..fed2fae91 --- /dev/null +++ b/src/view/com/modals/WebModal.tsx @@ -0,0 +1,20 @@ +/** + * Use this for the Web build only. + * It's intended to replace the BottomSheet. + * + * Note: the dataSet properties are used to leverage custom CSS in public/index.html + */ +import React from 'react' +// @ts-ignore no declarations available -prf +import {TouchableWithoutFeedback, View} from 'react-native-web' + +type Props = {onClose: () => void} +export const Modal: React.FC<Props> = ({onClose, children}) => { + return ( + <TouchableWithoutFeedback onPress={onClose}> + <View dataSet={{'modal-overlay': 1}}> + <View dataSet={{'modal-container': 1}}>{children}</View> + </View> + </TouchableWithoutFeedback> + ) +} diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 784cc39d2..6191875c7 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -9,7 +9,7 @@ import { } from '../../../state/models/post-thread-view' import {useStores} from '../../../state' import {PostThreadItem} from './PostThreadItem' -import {ShareBottomSheet} from '../sheets/SharePost' +import {ShareModal} from '../modals/SharePost' import {s} from '../../lib/styles' const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates @@ -24,7 +24,7 @@ export const PostThread = observer(function PostThread({ const store = useStores() const [view, setView] = useState<PostThreadViewModel | undefined>() const [lastUpdate, setLastUpdate] = useState<number>(Date.now()) - const shareSheetRef = useRef<{open: (uri: string) => void}>() + const shareSheetRef = useRef<{open: (_uri: string) => void}>() useEffect(() => { if (view?.params.uri === uri) { @@ -94,7 +94,7 @@ export const PostThread = observer(function PostThread({ refreshing={view.isRefreshing} onRefresh={onRefresh} /> - <ShareBottomSheet ref={shareSheetRef} /> + <ShareModal ref={shareSheetRef} /> </View> ) }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 30a64bc0e..7ed00403b 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -25,7 +25,7 @@ export const PostThreadItem = observer(function PostThreadItem({ }: { item: PostThreadViewPostModel onNavigateContent: OnNavigateContent - onPressShare: (uri: string) => void + onPressShare: (_uri: string) => void }) { const record = item.record as unknown as bsky.Post.Record const hasEngagement = item.likeCount || item.repostCount diff --git a/src/view/com/util/ProgressCircle.native.tsx b/src/view/com/util/ProgressCircle.native.tsx new file mode 100644 index 000000000..a09232b4b --- /dev/null +++ b/src/view/com/util/ProgressCircle.native.tsx @@ -0,0 +1,3 @@ +// @ts-ignore no type definition -prf +import ProgressCircle from 'react-native-progress/Circle' +export default ProgressCircle diff --git a/src/view/com/util/ProgressCircle.tsx b/src/view/com/util/ProgressCircle.tsx new file mode 100644 index 000000000..0e425a6e6 --- /dev/null +++ b/src/view/com/util/ProgressCircle.tsx @@ -0,0 +1,20 @@ +import {View} from 'react-native' +import {CircularProgressbar, buildStyles} from 'react-circular-progressbar' + +const ProgressCircle = ({ + color, + progress, +}: { + color?: string + progress: number +}) => { + return ( + <View style={{width: 20, height: 20}}> + <CircularProgressbar + value={progress * 100} + styles={buildStyles({pathColor: color || '#00f'})} + /> + </View> + ) +} +export default ProgressCircle diff --git a/src/view/com/util/Toast.native.tsx b/src/view/com/util/Toast.native.tsx new file mode 100644 index 000000000..4b9fd7f80 --- /dev/null +++ b/src/view/com/util/Toast.native.tsx @@ -0,0 +1,2 @@ +import Toast from 'react-native-root-toast' +export default Toast diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx new file mode 100644 index 000000000..1726b71b3 --- /dev/null +++ b/src/view/com/util/Toast.tsx @@ -0,0 +1,62 @@ +/* + * Note: the dataSet properties are used to leverage custom CSS in public/index.html + */ + +import React, {useState, useEffect} from 'react' +// @ts-ignore no declarations available -prf +import {Text, View} from 'react-native-web' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' + +interface ActiveToast { + text: string +} +type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void + +// globals +// = +let globalSetActiveToast: GlobalSetActiveToast | undefined +let toastTimeout: NodeJS.Timeout | undefined + +// components +// = +type ToastContainerProps = {} +const ToastContainer: React.FC<ToastContainerProps> = ({}) => { + const [activeToast, setActiveToast] = useState<ActiveToast | undefined>() + useEffect(() => { + globalSetActiveToast = (t: ActiveToast | undefined) => { + setActiveToast(t) + } + }) + return ( + <> + {activeToast && ( + <View dataSet={{'toast-container': 1}}> + <FontAwesomeIcon icon="check" size={24} /> + <Text>{activeToast.text}</Text> + </View> + )} + </> + ) +} + +// exports +// = +export default { + show(text: string, _opts: any) { + console.log('TODO: toast', text) + if (toastTimeout) { + clearTimeout(toastTimeout) + } + globalSetActiveToast?.({text}) + toastTimeout = setTimeout(() => { + globalSetActiveToast?.(undefined) + }, 2e3) + }, + positions: { + TOP: 0, + }, + durations: { + LONG: 0, + }, + ToastContainer, +} diff --git a/src/view/index.ts b/src/view/index.ts index 4a469d263..645aa425b 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -4,6 +4,7 @@ import {library} from '@fortawesome/fontawesome-svg-core' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' +import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' @@ -39,6 +40,7 @@ export function setup() { faArrowLeft, faBars, faBell, + faCheck, faComment, faHeart, fasHeart, diff --git a/todos.txt b/todos.txt index 70807cfe2..221de14f3 100644 --- a/todos.txt +++ b/todos.txt @@ -11,4 +11,6 @@ Paul's todo list - * - Linking - Web linking - - App linking \ No newline at end of file + - App linking +- Housekeeping + - Remove moment.js -- it's too heavy a dependency \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 231bea511..082f054ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11300,6 +11300,11 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-circular-progressbar@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz#99e5ae499c21de82223b498289e96f66adb8fa3a" + integrity sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g== + react-dev-utils@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" |