Implement Web versions of the bottom sheet, toast, and progress circle
parent
af55a89758
commit
041bfa22a9
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// @ts-ignore no type definition -prf
|
||||
import ProgressCircle from 'react-native-progress/Circle'
|
||||
export default ProgressCircle
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
import Toast from 'react-native-root-toast'
|
||||
export default Toast
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue