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", "mobx-react-lite": "^3.4.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"react": "17.0.2", "react": "17.0.2",
"react-circular-progressbar": "^2.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-native": "0.68.2", "react-native": "0.68.2",
"react-native-gesture-handler": "^2.5.0", "react-native-gesture-handler": "^2.5.0",

View File

@ -11,6 +11,45 @@
body { overflow: hidden; } body { overflow: hidden; }
/* These styles make the root element full-height */ /* These styles make the root element full-height */
#root { display:flex; height:100%; } #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> </style>
</head> </head>
<body> <body>

View File

@ -1,9 +1,8 @@
import React, {useState, useEffect} from 'react' 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 * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state' import {RootStoreModel, setupState, RootStoreProvider} from './state'
import * as Routes from './view/routes' import * as Routes from './view/routes'
import Toast from './view/com/util/Toast'
function App() { function App() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
@ -22,13 +21,10 @@ function App() {
} }
return ( return (
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}> <RootStoreProvider value={rootStore}>
<Routes.Root /> <Routes.Root />
<Toast.ToastContainer />
</RootStoreProvider> </RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>
) )
} }

View File

@ -97,7 +97,7 @@ export async function unrepost(adx: AdxClient, user: string, uri: string) {
return numDels > 0 return numDels > 0
} }
type WherePred = (record: GetRecordResponseValidated) => Boolean type WherePred = (_record: GetRecordResponseValidated) => Boolean
async function deleteWhere( async function deleteWhere(
coll: AdxRepoCollectionClient, coll: AdxRepoCollectionClient,
schema: SchemaOpt, schema: SchemaOpt,
@ -115,7 +115,7 @@ async function deleteWhere(
return toDelete.length return toDelete.length
} }
type IterateAllCb = (record: GetRecordResponseValidated) => void type IterateAllCb = (_record: GetRecordResponseValidated) => void
async function iterateAll( async function iterateAll(
coll: AdxRepoCollectionClient, coll: AdxRepoCollectionClient,
schema: SchemaOpt, schema: SchemaOpt,

View File

@ -1,9 +1,8 @@
import React, {useState, forwardRef, useImperativeHandle} from 'react' import React, {useState, forwardRef, useImperativeHandle} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native' import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native'
import Toast from 'react-native-root-toast' import Toast from '../util/Toast'
// @ts-ignore no type definition -prf import ProgressCircle from '../util/ProgressCircle'
import ProgressCircle from 'react-native-progress/Circle'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s} from '../../lib/styles' import {s} from '../../lib/styles'
import * as apilib from '../../../state/lib/api' import * as apilib from '../../../state/lib/api'
@ -12,8 +11,7 @@ const MAX_TEXT_LENGTH = 256
const WARNING_TEXT_LENGTH = 200 const WARNING_TEXT_LENGTH = 200
const DANGER_TEXT_LENGTH = 255 const DANGER_TEXT_LENGTH = 255
export const Composer = observer( export const Composer = forwardRef(function Composer(
forwardRef(function Composer(
{ {
replyTo, replyTo,
}: { }: {
@ -63,9 +61,7 @@ export const Composer = observer(
scrollEnabled scrollEnabled
onChangeText={text => onChangeText(text)} onChangeText={text => onChangeText(text)}
value={text} value={text}
placeholder={ placeholder={replyTo ? 'Write your reply' : "What's new in the scene?"}
replyTo ? 'Write your reply' : "What's new in the scene?"
}
style={styles.textInput} style={styles.textInput}
/> />
<View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}> <View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}>
@ -79,8 +75,7 @@ export const Composer = observer(
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
}), })
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {

View File

@ -4,7 +4,7 @@ import {Text, View, FlatList} from 'react-native'
import {OnNavigateContent} from '../../routes/types' import {OnNavigateContent} from '../../routes/types'
import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view' import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {ShareBottomSheet} from '../sheets/SharePost' import {ShareModal} from '../modals/SharePost'
export const Feed = observer(function Feed({ export const Feed = observer(function Feed({
feed, feed,
@ -13,7 +13,7 @@ export const Feed = observer(function Feed({
feed: FeedViewModel feed: FeedViewModel
onNavigateContent: OnNavigateContent onNavigateContent: OnNavigateContent
}) { }) {
const shareSheetRef = useRef<{open: (uri: string) => void}>() const shareSheetRef = useRef<{open: (_uri: string) => void}>()
const onPressShare = (uri: string) => { const onPressShare = (uri: string) => {
shareSheetRef.current?.open(uri) shareSheetRef.current?.open(uri)
@ -52,7 +52,7 @@ export const Feed = observer(function Feed({
/> />
)} )}
{feed.isEmpty && <Text>This feed is empty!</Text>} {feed.isEmpty && <Text>This feed is empty!</Text>}
<ShareBottomSheet ref={shareSheetRef} /> <ShareModal ref={shareSheetRef} />
</View> </View>
) )
}) })

View File

@ -16,7 +16,7 @@ export const FeedItem = observer(function FeedItem({
}: { }: {
item: FeedViewItemModel item: FeedViewItemModel
onNavigateContent: OnNavigateContent onNavigateContent: OnNavigateContent
onPressShare: (uri: string) => void onPressShare: (_uri: string) => void
}) { }) {
const record = item.record as unknown as bsky.Post.Record const record = item.record as unknown as bsky.Post.Record

View File

@ -19,14 +19,11 @@ import Animated, {
interpolate, interpolate,
useAnimatedStyle, useAnimatedStyle,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import Toast from 'react-native-root-toast' import Toast from '../util/Toast'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import {s} from '../../lib/styles' import {s} from '../../lib/styles'
export const ShareBottomSheet = forwardRef(function ShareBottomSheet( export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
{}: {},
ref,
) {
const [isOpen, setIsOpen] = useState<boolean>(false) const [isOpen, setIsOpen] = useState<boolean>(false)
const [uri, setUri] = useState<string>('') const [uri, setUri] = useState<string>('')
const bottomSheetRef = useRef<BottomSheet>(null) const bottomSheetRef = useRef<BottomSheet>(null)
@ -41,6 +38,9 @@ export const ShareBottomSheet = forwardRef(function ShareBottomSheet(
const onPressCopy = () => { const onPressCopy = () => {
Clipboard.setString(uri) Clipboard.setString(uri)
console.log('showing')
console.log(Toast)
console.log(Toast.show)
Toast.show('Link copied', { Toast.show('Link copied', {
position: Toast.positions.TOP, position: Toast.positions.TOP,
}) })
@ -56,7 +56,6 @@ export const ShareBottomSheet = forwardRef(function ShareBottomSheet(
} }
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
console.log('hit!', animatedIndex.value)
// animated variables // animated variables
const opacity = useAnimatedStyle(() => ({ const opacity = useAnimatedStyle(() => ({
opacity: interpolate( 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' } from '../../../state/models/post-thread-view'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {ShareBottomSheet} from '../sheets/SharePost' import {ShareModal} from '../modals/SharePost'
import {s} from '../../lib/styles' import {s} from '../../lib/styles'
const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates 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 store = useStores()
const [view, setView] = useState<PostThreadViewModel | undefined>() const [view, setView] = useState<PostThreadViewModel | undefined>()
const [lastUpdate, setLastUpdate] = useState<number>(Date.now()) const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
const shareSheetRef = useRef<{open: (uri: string) => void}>() const shareSheetRef = useRef<{open: (_uri: string) => void}>()
useEffect(() => { useEffect(() => {
if (view?.params.uri === uri) { if (view?.params.uri === uri) {
@ -94,7 +94,7 @@ export const PostThread = observer(function PostThread({
refreshing={view.isRefreshing} refreshing={view.isRefreshing}
onRefresh={onRefresh} onRefresh={onRefresh}
/> />
<ShareBottomSheet ref={shareSheetRef} /> <ShareModal ref={shareSheetRef} />
</View> </View>
) )
}) })

View File

@ -25,7 +25,7 @@ export const PostThreadItem = observer(function PostThreadItem({
}: { }: {
item: PostThreadViewPostModel item: PostThreadViewPostModel
onNavigateContent: OnNavigateContent onNavigateContent: OnNavigateContent
onPressShare: (uri: string) => void onPressShare: (_uri: string) => void
}) { }) {
const record = item.record as unknown as bsky.Post.Record const record = item.record as unknown as bsky.Post.Record
const hasEngagement = item.likeCount || item.repostCount 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 {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' 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 {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
@ -39,6 +40,7 @@ export function setup() {
faArrowLeft, faArrowLeft,
faBars, faBars,
faBell, faBell,
faCheck,
faComment, faComment,
faHeart, faHeart,
fasHeart, fasHeart,

View File

@ -12,3 +12,5 @@ Paul's todo list
- Linking - Linking
- Web 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" regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2" 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: react-dev-utils@^12.0.1:
version "12.0.1" version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"