Implement Web versions of the bottom sheet, toast, and progress circle

zio/stable
Paul Frazee 2022-07-25 23:08:24 -05:00
parent af55a89758
commit 041bfa22a9
19 changed files with 295 additions and 92 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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: {

View File

@ -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>
)
})

View File

@ -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

View File

@ -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(

View File

@ -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,
},
})

View File

@ -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>
)
}

View File

@ -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>
)
})

View File

@ -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

View File

@ -0,0 +1,3 @@
// @ts-ignore no type definition -prf
import ProgressCircle from 'react-native-progress/Circle'
export default ProgressCircle

View File

@ -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

View File

@ -0,0 +1,2 @@
import Toast from 'react-native-root-toast'
export default Toast

View File

@ -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,
}

View File

@ -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,

View File

@ -11,4 +11,6 @@ Paul's todo list
- *
- Linking
- Web linking
- App linking
- App linking
- Housekeeping
- Remove moment.js -- it's too heavy a dependency

View File

@ -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"