New navigation model (#1)
* Flatten all routing into a single stack * Replace router with custom implementation * Add shell header and titles * Add tab selector * Add back/forward history menus on longpress * Fix: don't modify state during render * Add refresh() to navigation and reroute navigations to the current location to refresh instead of add to history * Cache screens during navigation to maintain scroll position and improve load-time for renderszio/stable
parent
d1470bad66
commit
97f52b6a03
|
@ -23,10 +23,6 @@
|
|||
"@gorhom/bottom-sheet": "^4",
|
||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
||||
"@react-navigation/bottom-tabs": "^6.3.1",
|
||||
"@react-navigation/native": "^6.0.10",
|
||||
"@react-navigation/native-stack": "^6.6.2",
|
||||
"@react-navigation/stack": "^6.2.1",
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"lodash.omit": "^4.5.0",
|
||||
|
|
|
@ -5,7 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
|||
import {whenWebCrypto} from './platform/polyfills.native'
|
||||
import * as view from './view/index'
|
||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||
import * as Routes from './view/routes'
|
||||
import {MobileShell} from './view/shell/mobile'
|
||||
|
||||
function App() {
|
||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||
|
@ -31,7 +31,7 @@ function App() {
|
|||
<GestureHandlerRootView style={{flex: 1}}>
|
||||
<RootSiblingParent>
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<Routes.Root />
|
||||
<MobileShell />
|
||||
</RootStoreProvider>
|
||||
</RootSiblingParent>
|
||||
</GestureHandlerRootView>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {useState, useEffect} from 'react'
|
||||
import * as view from './view/index'
|
||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||
import * as Routes from './view/routes'
|
||||
import {DesktopWebShell} from './view/shell/desktop-web'
|
||||
import Toast from './view/com/util/Toast'
|
||||
|
||||
function App() {
|
||||
|
@ -22,7 +22,7 @@ function App() {
|
|||
|
||||
return (
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<Routes.Root />
|
||||
<DesktopWebShell />
|
||||
<Toast.ToastContainer />
|
||||
</RootStoreProvider>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
let __tabId = 0
|
||||
function genTabId() {
|
||||
return ++__tabId
|
||||
}
|
||||
|
||||
interface HistoryItem {
|
||||
url: string
|
||||
ts: number
|
||||
title?: string
|
||||
}
|
||||
|
||||
export class NavigationTabModel {
|
||||
id = genTabId()
|
||||
history: HistoryItem[] = [{url: '/', ts: Date.now()}]
|
||||
index = 0
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
serialize: false,
|
||||
hydrate: false,
|
||||
})
|
||||
}
|
||||
|
||||
// accessors
|
||||
// =
|
||||
|
||||
get current() {
|
||||
return this.history[this.index]
|
||||
}
|
||||
|
||||
get canGoBack() {
|
||||
return this.index > 0
|
||||
}
|
||||
|
||||
get canGoForward() {
|
||||
return this.index < this.history.length - 1
|
||||
}
|
||||
|
||||
getBackList(n: number) {
|
||||
const start = Math.max(this.index - n, 0)
|
||||
const end = Math.min(this.index, n)
|
||||
return this.history.slice(start, end).map((item, i) => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
index: start + i,
|
||||
}))
|
||||
}
|
||||
|
||||
get backTen() {
|
||||
return this.getBackList(10)
|
||||
}
|
||||
|
||||
getForwardList(n: number) {
|
||||
const start = Math.min(this.index + 1, this.history.length)
|
||||
const end = Math.min(this.index + n, this.history.length)
|
||||
return this.history.slice(start, end).map((item, i) => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
index: start + i,
|
||||
}))
|
||||
}
|
||||
|
||||
get forwardTen() {
|
||||
return this.getForwardList(10)
|
||||
}
|
||||
|
||||
// navigation
|
||||
// =
|
||||
|
||||
navigate(url: string, title?: string) {
|
||||
if (this.current?.url === url) {
|
||||
this.refresh()
|
||||
} else {
|
||||
if (this.index < this.history.length - 1) {
|
||||
this.history.length = this.index + 1
|
||||
}
|
||||
this.history.push({url, title, ts: Date.now()})
|
||||
this.index = this.history.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.history = [
|
||||
...this.history.slice(0, this.index),
|
||||
{url: this.current.url, title: this.current.title, ts: Date.now()},
|
||||
...this.history.slice(this.index + 1),
|
||||
]
|
||||
}
|
||||
|
||||
goBack() {
|
||||
if (this.canGoBack) {
|
||||
this.index--
|
||||
}
|
||||
}
|
||||
|
||||
goForward() {
|
||||
if (this.canGoForward) {
|
||||
this.index++
|
||||
}
|
||||
}
|
||||
|
||||
goToIndex(index: number) {
|
||||
if (index >= 0 && index <= this.history.length - 1) {
|
||||
this.index = index
|
||||
}
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.current.title = title
|
||||
}
|
||||
|
||||
// persistence
|
||||
// =
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
history: this.history,
|
||||
index: this.index,
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
this.history = []
|
||||
this.index = 0
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'history') && Array.isArray(v.history)) {
|
||||
for (const item of v.history) {
|
||||
if (
|
||||
isObj(item) &&
|
||||
hasProp(item, 'url') &&
|
||||
typeof item.url === 'string'
|
||||
) {
|
||||
let copy: HistoryItem = {
|
||||
url: item.url,
|
||||
ts:
|
||||
hasProp(item, 'ts') && typeof item.ts === 'number'
|
||||
? item.ts
|
||||
: Date.now(),
|
||||
}
|
||||
if (hasProp(item, 'title') && typeof item.title === 'string') {
|
||||
copy.title = item.title
|
||||
}
|
||||
this.history.push(copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasProp(v, 'index') && typeof v.index === 'number') {
|
||||
this.index = v.index
|
||||
}
|
||||
if (this.index >= this.history.length - 1) {
|
||||
this.index = this.history.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigationModel {
|
||||
tabs: NavigationTabModel[] = [new NavigationTabModel()]
|
||||
tabIndex = 0
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
serialize: false,
|
||||
hydrate: false,
|
||||
})
|
||||
}
|
||||
|
||||
// accessors
|
||||
// =
|
||||
|
||||
get tab() {
|
||||
return this.tabs[this.tabIndex]
|
||||
}
|
||||
|
||||
isCurrentScreen(tabId: number, index: number) {
|
||||
return this.tab.id === tabId && this.tab.index === index
|
||||
}
|
||||
|
||||
// navigation
|
||||
// =
|
||||
|
||||
navigate(url: string, title?: string) {
|
||||
this.tab.navigate(url, title)
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.tab.refresh()
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.tab.setTitle(title)
|
||||
}
|
||||
|
||||
// tab management
|
||||
// =
|
||||
|
||||
newTab(url: string, title?: string) {
|
||||
const tab = new NavigationTabModel()
|
||||
tab.navigate(url, title)
|
||||
this.tabs.push(tab)
|
||||
this.tabIndex = this.tabs.length - 1
|
||||
}
|
||||
|
||||
setActiveTab(tabIndex: number) {
|
||||
this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0)
|
||||
}
|
||||
|
||||
closeTab(tabIndex: number) {
|
||||
this.tabs = [
|
||||
...this.tabs.slice(0, tabIndex),
|
||||
...this.tabs.slice(tabIndex + 1),
|
||||
]
|
||||
if (this.tabs.length === 0) {
|
||||
this.newTab('/')
|
||||
} else if (this.tabIndex >= this.tabs.length) {
|
||||
this.tabIndex = this.tabs.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// persistence
|
||||
// =
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
tabs: this.tabs.map(t => t.serialize()),
|
||||
tabIndex: this.tabIndex,
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
this.tabs.length = 0
|
||||
this.tabIndex = 0
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) {
|
||||
for (const tab of v.tabs) {
|
||||
const copy = new NavigationTabModel()
|
||||
copy.hydrate(tab)
|
||||
if (copy.history.length) {
|
||||
this.tabs.push(copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasProp(v, 'tabIndex') && typeof v.tabIndex === 'number') {
|
||||
this.tabIndex = v.tabIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,12 +7,14 @@ import {adx, AdxClient} from '@adxp/mock-api'
|
|||
import {createContext, useContext} from 'react'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {SessionModel} from './session'
|
||||
import {NavigationModel} from './navigation'
|
||||
import {MeModel} from './me'
|
||||
import {FeedViewModel} from './feed-view'
|
||||
import {NotificationsViewModel} from './notifications-view'
|
||||
|
||||
export class RootStoreModel {
|
||||
session = new SessionModel()
|
||||
nav = new NavigationModel()
|
||||
me = new MeModel(this)
|
||||
homeFeed = new FeedViewModel(this, {})
|
||||
notesFeed = new NotificationsViewModel(this, {})
|
||||
|
@ -35,6 +37,7 @@ export class RootStoreModel {
|
|||
serialize(): unknown {
|
||||
return {
|
||||
session: this.session.serialize(),
|
||||
nav: this.nav.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,6 +46,9 @@ export class RootStoreModel {
|
|||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
}
|
||||
if (hasProp(v, 'nav')) {
|
||||
this.nav.hydrate(v.nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import React, {useRef} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
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 {ShareModal} from '../modals/SharePost'
|
||||
|
||||
export const Feed = observer(function Feed({
|
||||
feed,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
feed: FeedViewModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
|
||||
const shareSheetRef = useRef<{open: (_uri: string) => void}>()
|
||||
|
||||
const onPressShare = (uri: string) => {
|
||||
|
@ -23,11 +16,7 @@ export const Feed = observer(function Feed({
|
|||
// renderItem function renders components that follow React performance best practices
|
||||
// like PureComponent, shouldComponentUpdate, etc
|
||||
const renderItem = ({item}: {item: FeedViewItemModel}) => (
|
||||
<FeedItem
|
||||
item={item}
|
||||
onNavigateContent={onNavigateContent}
|
||||
onPressShare={onPressShare}
|
||||
/>
|
||||
<FeedItem item={item} onPressShare={onPressShare} />
|
||||
)
|
||||
const onRefresh = () => {
|
||||
feed.refresh().catch(err => console.error('Failed to refresh', err))
|
||||
|
|
|
@ -3,39 +3,31 @@ import {observer} from 'mobx-react-lite'
|
|||
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {bsky, AdxUri} from '@adxp/mock-api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {FeedViewItemModel} from '../../../state/models/feed-view'
|
||||
import {s} from '../../lib/styles'
|
||||
import {ago} from '../../lib/strings'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
import {useStores} from '../../../state'
|
||||
|
||||
export const FeedItem = observer(function FeedItem({
|
||||
item,
|
||||
onNavigateContent,
|
||||
onPressShare,
|
||||
}: {
|
||||
item: FeedViewItemModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
onPressShare: (_uri: string) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const record = item.record as unknown as bsky.Post.Record
|
||||
|
||||
const onPressOuter = () => {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostThread', {
|
||||
name: item.author.name,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||
}
|
||||
const onPressAuthor = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.author.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}`)
|
||||
}
|
||||
const onPressReply = () => {
|
||||
onNavigateContent('Composer', {
|
||||
replyTo: item.uri,
|
||||
})
|
||||
store.nav.navigate('/composer')
|
||||
}
|
||||
const onPressToggleRepost = () => {
|
||||
item
|
||||
|
@ -137,8 +129,11 @@ export const FeedItem = observer(function FeedItem({
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e8e8e8',
|
||||
// borderWidth: 1,
|
||||
// borderColor: '#e8e8e8',
|
||||
borderRadius: 10,
|
||||
margin: 2,
|
||||
marginBottom: 0,
|
||||
backgroundColor: '#fff',
|
||||
padding: 10,
|
||||
},
|
||||
|
@ -175,6 +170,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
postText: {
|
||||
paddingBottom: 5,
|
||||
fontFamily: 'Helvetica Neue',
|
||||
},
|
||||
ctrls: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
@ -1,27 +1,10 @@
|
|||
import React, {
|
||||
forwardRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
Button,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import BottomSheet, {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react'
|
||||
import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import BottomSheet from '@gorhom/bottom-sheet'
|
||||
import Toast from '../util/Toast'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {s} from '../../lib/styles'
|
||||
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||
|
||||
export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
|
@ -33,14 +16,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
|||
console.log('sharing', uri)
|
||||
setUri(uri)
|
||||
setIsOpen(true)
|
||||
bottomSheetRef.current?.expand()
|
||||
},
|
||||
}))
|
||||
|
||||
const onPressCopy = () => {
|
||||
Clipboard.setString(uri)
|
||||
console.log('showing')
|
||||
console.log(Toast)
|
||||
console.log(Toast.show)
|
||||
Toast.show('Link copied', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
|
@ -55,36 +37,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
|||
bottomSheetRef.current?.close()
|
||||
}
|
||||
|
||||
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
|
||||
// animated variables
|
||||
const opacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
animatedIndex.value, // current snap index
|
||||
[-1, 0], // input range
|
||||
[0, 0.5], // output range
|
||||
Extrapolate.CLAMP,
|
||||
),
|
||||
}))
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [style, {backgroundColor: '#000'}, opacity],
|
||||
[style, opacity],
|
||||
)
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<Animated.View style={containerStyle} />
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
index={-1}
|
||||
snapPoints={['50%']}
|
||||
enablePanDownToClose
|
||||
backdropComponent={CustomBackdrop}
|
||||
backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined}
|
||||
onChange={onShareBottomSheetChange}>
|
||||
<View>
|
||||
<Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
|
||||
|
@ -97,8 +56,6 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
|||
</View>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Text, View, FlatList} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
NotificationsViewModel,
|
||||
NotificationsViewItemModel,
|
||||
|
@ -10,17 +9,15 @@ import {FeedItem} from './FeedItem'
|
|||
|
||||
export const Feed = observer(function Feed({
|
||||
view,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
view: NotificationsViewModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
|
||||
// VirtualizedList: You have a large list that is slow to update - make sure your
|
||||
// renderItem function renders components that follow React performance best practices
|
||||
// like PureComponent, shouldComponentUpdate, etc
|
||||
const renderItem = ({item}: {item: NotificationsViewItemModel}) => (
|
||||
<FeedItem item={item} onNavigateContent={onNavigateContent} />
|
||||
<FeedItem item={item} />
|
||||
)
|
||||
const onRefresh = () => {
|
||||
view.refresh().catch(err => console.error('Failed to refresh', err))
|
||||
|
|
|
@ -3,44 +3,34 @@ import {observer} from 'mobx-react-lite'
|
|||
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {AdxUri} from '@adxp/mock-api'
|
||||
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
|
||||
import {s} from '../../lib/styles'
|
||||
import {ago} from '../../lib/strings'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
import {PostText} from '../post/PostText'
|
||||
import {Post} from '../post/Post'
|
||||
import {useStores} from '../../../state'
|
||||
|
||||
export const FeedItem = observer(function FeedItem({
|
||||
item,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
item: NotificationsViewItemModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
const store = useStores()
|
||||
|
||||
const onPressOuter = () => {
|
||||
if (item.isLike || item.isRepost) {
|
||||
const urip = new AdxUri(item.subjectUri)
|
||||
onNavigateContent('PostThread', {
|
||||
name: urip.host,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
|
||||
} else if (item.isFollow) {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.author.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}`)
|
||||
} else if (item.isReply) {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostThread', {
|
||||
name: urip.host,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
|
||||
}
|
||||
}
|
||||
const onPressAuthor = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.author.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}`)
|
||||
}
|
||||
|
||||
let action = ''
|
||||
|
@ -92,7 +82,7 @@ export const FeedItem = observer(function FeedItem({
|
|||
</View>
|
||||
{item.isReply ? (
|
||||
<View style={s.pt5}>
|
||||
<Post uri={item.uri} onNavigateContent={onNavigateContent} />
|
||||
<Post uri={item.uri} />
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
LikedByViewModel,
|
||||
LikedByViewItemModel,
|
||||
|
@ -18,13 +17,7 @@ import {useStores} from '../../../state'
|
|||
import {s} from '../../lib/styles'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
|
||||
export const PostLikedBy = observer(function PostLikedBy({
|
||||
uri,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
uri: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<LikedByViewModel | undefined>()
|
||||
|
||||
|
@ -66,7 +59,7 @@ export const PostLikedBy = observer(function PostLikedBy({
|
|||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: LikedByViewItemModel}) => (
|
||||
<LikedByItem item={item} onNavigateContent={onNavigateContent} />
|
||||
<LikedByItem item={item} />
|
||||
)
|
||||
return (
|
||||
<View>
|
||||
|
@ -79,17 +72,10 @@ export const PostLikedBy = observer(function PostLikedBy({
|
|||
)
|
||||
})
|
||||
|
||||
const LikedByItem = ({
|
||||
item,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
item: LikedByViewItemModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) => {
|
||||
const LikedByItem = ({item}: {item: LikedByViewItemModel}) => {
|
||||
const store = useStores()
|
||||
const onPressOuter = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.name}`)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
RepostedByViewModel,
|
||||
RepostedByViewItemModel,
|
||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
|||
|
||||
export const PostRepostedBy = observer(function PostRepostedBy({
|
||||
uri,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
uri: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<RepostedByViewModel | undefined>()
|
||||
|
@ -68,7 +65,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
|||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
|
||||
<RepostedByItem item={item} onNavigateContent={onNavigateContent} />
|
||||
<RepostedByItem item={item} />
|
||||
)
|
||||
return (
|
||||
<View>
|
||||
|
@ -81,17 +78,10 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
|||
)
|
||||
})
|
||||
|
||||
const RepostedByItem = ({
|
||||
item,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
item: RepostedByViewItemModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) => {
|
||||
const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
|
||||
const store = useStores()
|
||||
const onPressOuter = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.name}`)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ActivityIndicator, FlatList, Text, View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
PostThreadViewModel,
|
||||
PostThreadViewPostModel,
|
||||
|
@ -14,13 +12,7 @@ import {s} from '../../lib/styles'
|
|||
|
||||
const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates
|
||||
|
||||
export const PostThread = observer(function PostThread({
|
||||
uri,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
uri: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
export const PostThread = observer(function PostThread({uri}: {uri: string}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
||||
const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
|
||||
|
@ -37,12 +29,13 @@ export const PostThread = observer(function PostThread({
|
|||
newView.setup().catch(err => console.error('Failed to fetch thread', err))
|
||||
}, [uri, view?.params.uri, store])
|
||||
|
||||
useFocusEffect(() => {
|
||||
if (Date.now() - lastUpdate > UPDATE_DELAY) {
|
||||
view?.update()
|
||||
setLastUpdate(Date.now())
|
||||
}
|
||||
})
|
||||
// TODO
|
||||
// useFocusEffect(() => {
|
||||
// if (Date.now() - lastUpdate > UPDATE_DELAY) {
|
||||
// view?.update()
|
||||
// setLastUpdate(Date.now())
|
||||
// }
|
||||
// })
|
||||
|
||||
const onPressShare = (uri: string) => {
|
||||
shareSheetRef.current?.open(uri)
|
||||
|
@ -79,11 +72,7 @@ export const PostThread = observer(function PostThread({
|
|||
// =
|
||||
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
|
||||
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
|
||||
<PostThreadItem
|
||||
item={item}
|
||||
onNavigateContent={onNavigateContent}
|
||||
onPressShare={onPressShare}
|
||||
/>
|
||||
<PostThreadItem item={item} onPressShare={onPressShare} />
|
||||
)
|
||||
return (
|
||||
<View style={s.h100pct}>
|
||||
|
|
|
@ -3,11 +3,11 @@ import {observer} from 'mobx-react-lite'
|
|||
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {bsky, AdxUri} from '@adxp/mock-api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
|
||||
import {s} from '../../lib/styles'
|
||||
import {ago, pluralize} from '../../lib/strings'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
import {useStores} from '../../../state'
|
||||
|
||||
function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
|
||||
const arr: T[] = []
|
||||
|
@ -19,46 +19,36 @@ function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
|
|||
|
||||
export const PostThreadItem = observer(function PostThreadItem({
|
||||
item,
|
||||
onNavigateContent,
|
||||
onPressShare,
|
||||
}: {
|
||||
item: PostThreadViewPostModel
|
||||
onNavigateContent: OnNavigateContent
|
||||
onPressShare: (_uri: string) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const record = item.record as unknown as bsky.Post.Record
|
||||
const hasEngagement = item.likeCount || item.repostCount
|
||||
|
||||
const onPressOuter = () => {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostThread', {
|
||||
name: item.author.name,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||
}
|
||||
const onPressAuthor = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.author.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}`)
|
||||
}
|
||||
const onPressLikes = () => {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostLikedBy', {
|
||||
name: item.author.name,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(
|
||||
`/profile/${item.author.name}/post/${urip.recordKey}/liked-by`,
|
||||
)
|
||||
}
|
||||
const onPressReposts = () => {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostRepostedBy', {
|
||||
name: item.author.name,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(
|
||||
`/profile/${item.author.name}/post/${urip.recordKey}/reposted-by`,
|
||||
)
|
||||
}
|
||||
const onPressReply = () => {
|
||||
onNavigateContent('Composer', {
|
||||
replyTo: item.uri,
|
||||
})
|
||||
store.nav.navigate(`/composer?replyTo=${item.uri}`)
|
||||
}
|
||||
const onPressToggleRepost = () => {
|
||||
item
|
||||
|
@ -227,6 +217,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
postText: {
|
||||
paddingBottom: 5,
|
||||
fontFamily: 'Helvetica Neue',
|
||||
},
|
||||
expandedInfo: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
@ -10,20 +10,13 @@ import {
|
|||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {PostThreadViewModel} from '../../../state/models/post-thread-view'
|
||||
import {useStores} from '../../../state'
|
||||
import {s} from '../../lib/styles'
|
||||
import {ago} from '../../lib/strings'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
|
||||
export const Post = observer(function Post({
|
||||
uri,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
uri: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
export const Post = observer(function Post({uri}: {uri: string}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
||||
|
||||
|
@ -63,20 +56,13 @@ export const Post = observer(function Post({
|
|||
|
||||
const onPressOuter = () => {
|
||||
const urip = new AdxUri(item.uri)
|
||||
onNavigateContent('PostThread', {
|
||||
name: item.author.name,
|
||||
recordKey: urip.recordKey,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||
}
|
||||
const onPressAuthor = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.author.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.author.name}`)
|
||||
}
|
||||
const onPressReply = () => {
|
||||
onNavigateContent('Composer', {
|
||||
replyTo: item.uri,
|
||||
})
|
||||
store.nav.navigate(`/composer?replyTo=${item.uri}`)
|
||||
}
|
||||
const onPressToggleRepost = () => {
|
||||
item
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
UserFollowersViewModel,
|
||||
FollowerItem,
|
||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
|||
|
||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
||||
name,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
name: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<UserFollowersViewModel | undefined>()
|
||||
|
@ -67,9 +64,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
|||
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: FollowerItem}) => (
|
||||
<User item={item} onNavigateContent={onNavigateContent} />
|
||||
)
|
||||
const renderItem = ({item}: {item: FollowerItem}) => <User item={item} />
|
||||
return (
|
||||
<View>
|
||||
<FlatList
|
||||
|
@ -81,17 +76,10 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
|||
)
|
||||
})
|
||||
|
||||
const User = ({
|
||||
item,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
item: FollowerItem
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) => {
|
||||
const User = ({item}: {item: FollowerItem}) => {
|
||||
const store = useStores()
|
||||
const onPressOuter = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.name}`)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {
|
||||
UserFollowsViewModel,
|
||||
FollowItem,
|
||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
|||
|
||||
export const ProfileFollows = observer(function ProfileFollows({
|
||||
name,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
name: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<UserFollowsViewModel | undefined>()
|
||||
|
@ -67,9 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({
|
|||
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: FollowItem}) => (
|
||||
<User item={item} onNavigateContent={onNavigateContent} />
|
||||
)
|
||||
const renderItem = ({item}: {item: FollowItem}) => <User item={item} />
|
||||
return (
|
||||
<View>
|
||||
<FlatList
|
||||
|
@ -81,17 +76,10 @@ export const ProfileFollows = observer(function ProfileFollows({
|
|||
)
|
||||
})
|
||||
|
||||
const User = ({
|
||||
item,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
item: FollowItem
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) => {
|
||||
const User = ({item}: {item: FollowItem}) => {
|
||||
const store = useStores()
|
||||
const onPressOuter = () => {
|
||||
onNavigateContent('Profile', {
|
||||
name: item.name,
|
||||
})
|
||||
store.nav.navigate(`/profile/${item.name}`)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {OnNavigateContent} from '../../routes/types'
|
||||
import {ProfileViewModel} from '../../../state/models/profile-view'
|
||||
import {useStores} from '../../../state'
|
||||
import {pluralize} from '../../lib/strings'
|
||||
|
@ -19,10 +18,8 @@ import Toast from '../util/Toast'
|
|||
|
||||
export const ProfileHeader = observer(function ProfileHeader({
|
||||
user,
|
||||
onNavigateContent,
|
||||
}: {
|
||||
user: string
|
||||
onNavigateContent: OnNavigateContent
|
||||
}) {
|
||||
const store = useStores()
|
||||
const [view, setView] = useState<ProfileViewModel | undefined>()
|
||||
|
@ -55,10 +52,10 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
)
|
||||
}
|
||||
const onPressFollowers = () => {
|
||||
onNavigateContent('ProfileFollowers', {name: user})
|
||||
store.nav.navigate(`/profile/${user}/followers`)
|
||||
}
|
||||
const onPressFollows = () => {
|
||||
onNavigateContent('ProfileFollows', {name: user})
|
||||
store.nav.navigate(`/profile/${user}/follows`)
|
||||
}
|
||||
|
||||
// loading
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native'
|
||||
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
export function createCustomBackdrop(
|
||||
onClose?: ((event: GestureResponderEvent) => void) | undefined,
|
||||
): React.FC<BottomSheetBackdropProps> {
|
||||
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
|
||||
// animated variables
|
||||
const opacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
animatedIndex.value, // current snap index
|
||||
[-1, 0], // input range
|
||||
[0, 0.5], // output range
|
||||
Extrapolate.CLAMP,
|
||||
),
|
||||
}))
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [style, {backgroundColor: '#000'}, opacity],
|
||||
[style, opacity],
|
||||
)
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<Animated.View style={containerStyle} />
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
return CustomBackdrop
|
||||
}
|
|
@ -1,33 +1,55 @@
|
|||
import {library} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
|
||||
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
|
||||
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 {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
|
||||
import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark'
|
||||
import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
|
||||
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 {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
|
||||
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
|
||||
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
|
||||
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
|
||||
import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
|
||||
import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
|
||||
import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
|
||||
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
|
||||
import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
|
||||
import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
|
||||
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
|
||||
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
|
||||
|
||||
export function setup() {
|
||||
library.add(
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faArrowLeft,
|
||||
faBars,
|
||||
faBell,
|
||||
farBell,
|
||||
faBookmark,
|
||||
farBookmark,
|
||||
faCheck,
|
||||
faClone,
|
||||
faComment,
|
||||
faEllipsis,
|
||||
faHeart,
|
||||
fasHeart,
|
||||
faHouse,
|
||||
faPlus,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faPenNib,
|
||||
faPlus,
|
||||
faRetweet,
|
||||
faShareFromSquare,
|
||||
faUser,
|
||||
faUsers,
|
||||
faX,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import {useEffect} from 'react'
|
||||
import {useStores} from '../../state'
|
||||
|
||||
type CB = () => void
|
||||
/**
|
||||
* This custom effect hook will trigger on every "navigation"
|
||||
* Use this in screens to handle any loading behaviors needed
|
||||
*/
|
||||
export function useLoadEffect(cb: CB, deps: any[] = []) {
|
||||
const store = useStores()
|
||||
useEffect(cb, [store.nav.tab, ...deps])
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {Home} from './screens/Home'
|
||||
import {Search} from './screens/Search'
|
||||
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'
|
||||
import {Profile} from './screens/Profile'
|
||||
import {ProfileFollowers} from './screens/ProfileFollowers'
|
||||
import {ProfileFollows} from './screens/ProfileFollows'
|
||||
|
||||
export type ScreenParams = {
|
||||
params: Record<string, any>
|
||||
}
|
||||
export type Route = [React.FC<ScreenParams>, IconProp, RegExp]
|
||||
export type MatchResult = {
|
||||
Com: React.FC<ScreenParams>
|
||||
icon: IconProp
|
||||
params: Record<string, any>
|
||||
}
|
||||
|
||||
const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i')
|
||||
export const routes: Route[] = [
|
||||
[Home, 'house', r('/')],
|
||||
[Search, 'magnifying-glass', r('/search')],
|
||||
[Notifications, 'bell', r('/notifications')],
|
||||
[Profile, ['far', 'user'], r('/profile/(?<name>[^/]+)')],
|
||||
[ProfileFollowers, 'users', r('/profile/(?<name>[^/]+)/followers')],
|
||||
[ProfileFollows, 'users', r('/profile/(?<name>[^/]+)/follows')],
|
||||
[
|
||||
PostThread,
|
||||
['far', 'message'],
|
||||
r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)'),
|
||||
],
|
||||
[
|
||||
PostLikedBy,
|
||||
'heart',
|
||||
r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/liked-by'),
|
||||
],
|
||||
[
|
||||
PostRepostedBy,
|
||||
'retweet',
|
||||
r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/reposted-by'),
|
||||
],
|
||||
[Composer, 'pen-nib', r('/compose')],
|
||||
[Login, ['far', 'user'], r('/login')],
|
||||
[Signup, ['far', 'user'], r('/signup')],
|
||||
]
|
||||
|
||||
export function match(url: string): MatchResult {
|
||||
for (const [Com, icon, pattern] of routes) {
|
||||
const res = pattern.exec(url)
|
||||
if (res) {
|
||||
// TODO: query params
|
||||
return {Com, icon, params: res.groups || {}}
|
||||
}
|
||||
}
|
||||
return {Com: NotFound, icon: 'magnifying-glass', params: {}}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import {Linking, Text} from 'react-native'
|
||||
import {
|
||||
NavigationContainer,
|
||||
LinkingOptions,
|
||||
RouteProp,
|
||||
ParamListBase,
|
||||
} from '@react-navigation/native'
|
||||
import {createNativeStackNavigator} from '@react-navigation/native-stack'
|
||||
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import type {RootTabsParamList} from './types'
|
||||
import {useStores} from '../../state'
|
||||
import * as platform from '../../platform/detection'
|
||||
import {Home} from '../screens/tabroots/Home'
|
||||
import {Search} from '../screens/tabroots/Search'
|
||||
import {Notifications} from '../screens/tabroots/Notifications'
|
||||
import {Menu} from '../screens/tabroots/Menu'
|
||||
import {Login} from '../screens/tabroots/Login'
|
||||
import {Signup} from '../screens/tabroots/Signup'
|
||||
import {NotFound} from '../screens/tabroots/NotFound'
|
||||
import {Composer} from '../screens/stacks/Composer'
|
||||
import {PostThread} from '../screens/stacks/PostThread'
|
||||
import {PostLikedBy} from '../screens/stacks/PostLikedBy'
|
||||
import {PostRepostedBy} from '../screens/stacks/PostRepostedBy'
|
||||
import {Profile} from '../screens/stacks/Profile'
|
||||
import {ProfileFollowers} from '../screens/stacks/ProfileFollowers'
|
||||
import {ProfileFollows} from '../screens/stacks/ProfileFollows'
|
||||
|
||||
const linking: LinkingOptions<RootTabsParamList> = {
|
||||
prefixes: [
|
||||
'http://localhost:3000', // local dev
|
||||
'https://pubsq.pfrazee.com', // test server (universal links only)
|
||||
'pubsqapp://', // custom protocol (ios)
|
||||
'pubsq://app', // custom protocol (android)
|
||||
],
|
||||
config: {
|
||||
screens: {
|
||||
HomeTab: '',
|
||||
SearchTab: 'search',
|
||||
NotificationsTab: 'notifications',
|
||||
MenuTab: 'menu',
|
||||
Profile: 'profile/:name',
|
||||
ProfileFollowers: 'profile/:name/followers',
|
||||
ProfileFollows: 'profile/:name/follows',
|
||||
PostThread: 'profile/:name/post/:recordKey',
|
||||
PostLikedBy: 'profile/:name/post/:recordKey/liked-by',
|
||||
PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by',
|
||||
Composer: 'compose',
|
||||
Login: 'login',
|
||||
Signup: 'signup',
|
||||
NotFound: '*',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const RootTabs = createBottomTabNavigator<RootTabsParamList>()
|
||||
export const HomeTabStack = createNativeStackNavigator()
|
||||
export const SearchTabStack = createNativeStackNavigator()
|
||||
export const NotificationsTabStack = createNativeStackNavigator()
|
||||
|
||||
const tabBarScreenOptions = ({
|
||||
route,
|
||||
}: {
|
||||
route: RouteProp<ParamListBase, string>
|
||||
}) => ({
|
||||
headerShown: false,
|
||||
tabBarShowLabel: false,
|
||||
tabBarIcon: (state: {focused: boolean; color: string; size: number}) => {
|
||||
switch (route.name) {
|
||||
case 'HomeTab':
|
||||
return <FontAwesomeIcon icon="house" style={{color: state.color}} />
|
||||
case 'SearchTab':
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
icon="magnifying-glass"
|
||||
style={{color: state.color}}
|
||||
/>
|
||||
)
|
||||
case 'NotificationsTab':
|
||||
return <FontAwesomeIcon icon="bell" style={{color: state.color}} />
|
||||
case 'MenuTab':
|
||||
return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
|
||||
default:
|
||||
return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const HIDE_HEADER = {headerShown: false}
|
||||
const HIDE_TAB = {tabBarButton: () => null}
|
||||
|
||||
function HomeStackCom() {
|
||||
return (
|
||||
<HomeTabStack.Navigator>
|
||||
<HomeTabStack.Screen name="Home" component={Home} />
|
||||
<HomeTabStack.Screen name="Composer" component={Composer} />
|
||||
<HomeTabStack.Screen name="Profile" component={Profile} />
|
||||
<HomeTabStack.Screen
|
||||
name="ProfileFollowers"
|
||||
component={ProfileFollowers}
|
||||
/>
|
||||
<HomeTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
|
||||
<HomeTabStack.Screen name="PostThread" component={PostThread} />
|
||||
<HomeTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
|
||||
<HomeTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
|
||||
</HomeTabStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchStackCom() {
|
||||
return (
|
||||
<SearchTabStack.Navigator>
|
||||
<SearchTabStack.Screen
|
||||
name="Search"
|
||||
component={Search}
|
||||
options={HIDE_HEADER}
|
||||
/>
|
||||
<SearchTabStack.Screen name="Profile" component={Profile} />
|
||||
<SearchTabStack.Screen
|
||||
name="ProfileFollowers"
|
||||
component={ProfileFollowers}
|
||||
/>
|
||||
<SearchTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
|
||||
<SearchTabStack.Screen name="PostThread" component={PostThread} />
|
||||
<SearchTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
|
||||
<SearchTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
|
||||
</SearchTabStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationsStackCom() {
|
||||
return (
|
||||
<NotificationsTabStack.Navigator>
|
||||
<NotificationsTabStack.Screen
|
||||
name="Notifications"
|
||||
component={Notifications}
|
||||
/>
|
||||
<NotificationsTabStack.Screen name="Profile" component={Profile} />
|
||||
<NotificationsTabStack.Screen
|
||||
name="ProfileFollowers"
|
||||
component={ProfileFollowers}
|
||||
/>
|
||||
<NotificationsTabStack.Screen
|
||||
name="ProfileFollows"
|
||||
component={ProfileFollows}
|
||||
/>
|
||||
<NotificationsTabStack.Screen name="PostThread" component={PostThread} />
|
||||
<NotificationsTabStack.Screen
|
||||
name="PostLikedBy"
|
||||
component={PostLikedBy}
|
||||
/>
|
||||
<NotificationsTabStack.Screen
|
||||
name="PostRepostedBy"
|
||||
component={PostRepostedBy}
|
||||
/>
|
||||
</NotificationsTabStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
export const Root = observer(() => {
|
||||
const store = useStores()
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Initial link setup')
|
||||
Linking.getInitialURL().then((url: string | null) => {
|
||||
console.log('Initial url', url)
|
||||
})
|
||||
Linking.addEventListener('url', ({url}) => {
|
||||
console.log('Deep link opened with', url)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// hide the tabbar on desktop web
|
||||
const tabBar = platform.isDesktopWeb ? () => null : undefined
|
||||
|
||||
return (
|
||||
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
|
||||
<RootTabs.Navigator
|
||||
initialRouteName={store.session.isAuthed ? 'HomeTab' : 'Login'}
|
||||
screenOptions={tabBarScreenOptions}
|
||||
tabBar={tabBar}>
|
||||
{store.session.isAuthed ? (
|
||||
<>
|
||||
<RootTabs.Screen name="HomeTab" component={HomeStackCom} />
|
||||
<RootTabs.Screen name="SearchTab" component={SearchStackCom} />
|
||||
<RootTabs.Screen
|
||||
name="NotificationsTab"
|
||||
component={NotificationsStackCom}
|
||||
/>
|
||||
<RootTabs.Screen name="MenuTab" component={Menu} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RootTabs.Screen name="Login" component={Login} />
|
||||
<RootTabs.Screen name="Signup" component={Signup} />
|
||||
</>
|
||||
)}
|
||||
<RootTabs.Screen
|
||||
name="NotFound"
|
||||
component={NotFound}
|
||||
options={HIDE_TAB}
|
||||
/>
|
||||
</RootTabs.Navigator>
|
||||
</NavigationContainer>
|
||||
)
|
||||
})
|
|
@ -1,47 +0,0 @@
|
|||
import type {StackScreenProps} from '@react-navigation/stack'
|
||||
|
||||
export type RootTabsParamList = {
|
||||
HomeTab: undefined
|
||||
SearchTab: undefined
|
||||
NotificationsTab: undefined
|
||||
MenuTab: undefined
|
||||
Profile: {name: string}
|
||||
ProfileFollowers: {name: string}
|
||||
ProfileFollows: {name: string}
|
||||
PostThread: {name: string; recordKey: string}
|
||||
PostLikedBy: {name: string; recordKey: string}
|
||||
PostRepostedBy: {name: string; recordKey: string}
|
||||
Composer: {replyTo?: string}
|
||||
Login: undefined
|
||||
Signup: undefined
|
||||
NotFound: undefined
|
||||
}
|
||||
export type RootTabsScreenProps<T extends keyof RootTabsParamList> =
|
||||
StackScreenProps<RootTabsParamList, T>
|
||||
|
||||
export type OnNavigateContent = (
|
||||
screen: string,
|
||||
params: Record<string, string>,
|
||||
) => void
|
||||
|
||||
/*
|
||||
NOTE
|
||||
this is leftover from a nested nav implementation
|
||||
keeping it around for future reference
|
||||
-prf
|
||||
|
||||
import type {NavigatorScreenParams} from '@react-navigation/native'
|
||||
import type {CompositeScreenProps} from '@react-navigation/native'
|
||||
import type {BottomTabScreenProps} from '@react-navigation/bottom-tabs'
|
||||
|
||||
Container: NavigatorScreenParams<PrimaryStacksParamList>
|
||||
export type PrimaryStacksParamList = {
|
||||
Home: undefined
|
||||
Profile: {name: string}
|
||||
}
|
||||
export type PrimaryStacksScreenProps<T extends keyof PrimaryStacksParamList> =
|
||||
CompositeScreenProps<
|
||||
BottomTabScreenProps<PrimaryStacksParamList, T>,
|
||||
RootTabsScreenProps<keyof RootTabsParamList>
|
||||
>
|
||||
*/
|
|
@ -0,0 +1,43 @@
|
|||
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<boolean>}>()
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: replyTo ? 'Reply' : 'New Post',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="x" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// headerRight: () => (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => {
|
||||
// if (!ref.current) {
|
||||
// return
|
||||
// }
|
||||
// ref.current.publish().then(
|
||||
// posted => {
|
||||
// if (posted) {
|
||||
// navigation.goBack()
|
||||
// }
|
||||
// },
|
||||
// err => console.error('Failed to create post', err),
|
||||
// )
|
||||
// }}>
|
||||
// <Text>Post</Text>
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation, replyTo, ref])
|
||||
|
||||
return <ComposerComponent ref={ref} replyTo={replyTo} />
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import React, {useState, useEffect, useLayoutEffect} from 'react'
|
||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Feed} from '../com/feed/Feed'
|
||||
import {useStores} from '../../state'
|
||||
import {useLoadEffect} from '../lib/navigation'
|
||||
import {AVIS} from '../lib/assets'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export function Home({params}: ScreenParams) {
|
||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||
const store = useStores()
|
||||
useLoadEffect(() => {
|
||||
store.nav.setTitle('Home')
|
||||
console.log('Fetching home feed')
|
||||
store.homeFeed.setup().then(() => setHasSetup(true))
|
||||
}, [store.nav, store.homeFeed])
|
||||
|
||||
// TODO
|
||||
// useEffect(() => {
|
||||
// return navigation.addListener('focus', () => {
|
||||
// if (hasSetup) {
|
||||
// console.log('Updating home feed')
|
||||
// store.homeFeed.update()
|
||||
// }
|
||||
// })
|
||||
// }, [navigation, store.homeFeed, hasSetup])
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'V I B E',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
|
||||
// <Image source={AVIS['alice.com']} style={styles.avi} />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// headerRight: () => (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => {
|
||||
// navigation.push('Composer', {})
|
||||
// }}>
|
||||
// <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Feed feed={store.homeFeed} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
})
|
|
@ -1,15 +1,12 @@
|
|||
import React from 'react'
|
||||
import {Text, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Shell} from '../../shell'
|
||||
// import type {RootTabsScreenProps} from '../routes/types'
|
||||
// import {useStores} from '../../state'
|
||||
|
||||
export const Login = observer(
|
||||
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
||||
// const store = useStores()
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||
{/*store.session.uiError && <Text>{store.session.uiError}</Text>}
|
||||
|
@ -25,7 +22,6 @@ export const Login = observer(
|
|||
<ActivityIndicator />
|
||||
)*/}
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import {useStores} from '../../state'
|
||||
|
||||
export const NotFound = () => {
|
||||
const stores = useStores()
|
||||
return (
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
|
||||
<Button title="Home" onPress={() => stores.nav.navigate('/')} />
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import React, {useState, useEffect, useLayoutEffect} from 'react'
|
||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Feed} from '../com/notifications/Feed'
|
||||
import {useStores} from '../../state'
|
||||
import {AVIS} from '../lib/assets'
|
||||
import {ScreenParams} from '../routes'
|
||||
import {useLoadEffect} from '../lib/navigation'
|
||||
|
||||
export const Notifications = ({params}: ScreenParams) => {
|
||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||
const store = useStores()
|
||||
useLoadEffect(() => {
|
||||
store.nav.setTitle('Notifications')
|
||||
console.log('Fetching notifications feed')
|
||||
store.notesFeed.setup().then(() => setHasSetup(true))
|
||||
}, [store.notesFeed])
|
||||
|
||||
// TODO
|
||||
// useEffect(() => {
|
||||
// return navigation.addListener('focus', () => {
|
||||
// if (hasSetup) {
|
||||
// console.log('Updating notifications feed')
|
||||
// store.notesFeed.update()
|
||||
// }
|
||||
// })
|
||||
// }, [navigation, store.notesFeed, hasSetup])
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Notifications',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
|
||||
// <Image source={AVIS['alice.com']} style={styles.avi} />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// headerRight: () => (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => {
|
||||
// navigation.push('Composer', {})
|
||||
// }}>
|
||||
// <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Feed view={store.notesFeed} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../lib/strings'
|
||||
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export const PostLikedBy = ({params}: ScreenParams) => {
|
||||
const {name, recordKey} = params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Liked By',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="arrow-left" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return <PostLikedByComponent uri={uri} />
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../lib/strings'
|
||||
import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export const PostRepostedBy = ({params}: ScreenParams) => {
|
||||
const {name, recordKey} = params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Reposted By',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="arrow-left" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return <PostRepostedByComponent uri={uri} />
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import React, {useEffect, useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../lib/strings'
|
||||
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
|
||||
import {ScreenParams} from '../routes'
|
||||
import {useStores} from '../../state'
|
||||
import {useLoadEffect} from '../lib/navigation'
|
||||
|
||||
export const PostThread = ({params}: ScreenParams) => {
|
||||
const store = useStores()
|
||||
const {name, recordKey} = params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
useLoadEffect(() => {
|
||||
store.nav.setTitle(`Post by ${name}`)
|
||||
}, [store.nav, name])
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Thread',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="arrow-left" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return <PostThreadComponent uri={uri} />
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React, {useState, useEffect} from 'react'
|
||||
import {View, StyleSheet} from 'react-native'
|
||||
import {FeedViewModel} from '../../state/models/feed-view'
|
||||
import {useStores} from '../../state'
|
||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||
import {Feed} from '../com/feed/Feed'
|
||||
import {ScreenParams} from '../routes'
|
||||
import {useLoadEffect} from '../lib/navigation'
|
||||
|
||||
export const Profile = ({params}: ScreenParams) => {
|
||||
const store = useStores()
|
||||
const [hasSetup, setHasSetup] = useState<string>('')
|
||||
const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
|
||||
|
||||
useLoadEffect(() => {
|
||||
const author = params.name
|
||||
if (feedView?.params.author === author) {
|
||||
return // no change needed? or trigger refresh?
|
||||
}
|
||||
console.log('Fetching profile feed', author)
|
||||
const newFeedView = new FeedViewModel(store, {author})
|
||||
setFeedView(newFeedView)
|
||||
newFeedView
|
||||
.setup()
|
||||
.catch(err => console.error('Failed to fetch feed', err))
|
||||
.then(() => {
|
||||
setHasSetup(author)
|
||||
store.nav.setTitle(author)
|
||||
})
|
||||
}, [params.name, feedView?.params.author, store])
|
||||
|
||||
// TODO
|
||||
// useEffect(() => {
|
||||
// return navigation.addListener('focus', () => {
|
||||
// if (hasSetup === feedView?.params.author) {
|
||||
// console.log('Updating profile feed', hasSetup)
|
||||
// feedView?.update()
|
||||
// }
|
||||
// })
|
||||
// }, [navigation, feedView, hasSetup])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ProfileHeader user={params.name} />
|
||||
<View style={styles.feed}>{feedView && <Feed feed={feedView} />}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
feed: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export const ProfileFollowers = ({params}: ScreenParams) => {
|
||||
const {name} = params
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Followers',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="arrow-left" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return <ProfileFollowersComponent name={name} />
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export const ProfileFollows = ({params}: ScreenParams) => {
|
||||
const {name} = params
|
||||
|
||||
// TODO
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerShown: true,
|
||||
// headerTitle: 'Following',
|
||||
// headerLeft: () => (
|
||||
// <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
// <FontAwesomeIcon icon="arrow-left" />
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// })
|
||||
// }, [navigation])
|
||||
|
||||
return <ProfileFollowsComponent name={name} />
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import {Text, View} from 'react-native'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
||||
export const Search = ({params}: ScreenParams) => {
|
||||
return (
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
import React from 'react'
|
||||
import {Text, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Shell} from '../../shell'
|
||||
// import type {RootTabsScreenProps} from '../routes/types'
|
||||
// import {useStores} from '../../state'
|
||||
|
||||
export const Signup = observer(
|
||||
(/*{navigation}: RootTabsScreenProps<'Signup'>*/) => {
|
||||
// const store = useStores()
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||
{/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||
|
@ -28,7 +25,6 @@ export const Signup = observer(
|
|||
<ActivityIndicator />
|
||||
)*/}
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -1,50 +0,0 @@
|
|||
import React, {useLayoutEffect, useRef} from 'react'
|
||||
import {Text, TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {Composer as ComposerComponent} from '../../com/composer/Composer'
|
||||
|
||||
export const Composer = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'Composer'>) => {
|
||||
const {replyTo} = route.params
|
||||
const ref = useRef<{publish: () => Promise<boolean>}>()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: replyTo ? 'Reply' : 'New Post',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="x" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!ref.current) {
|
||||
return
|
||||
}
|
||||
ref.current.publish().then(
|
||||
posted => {
|
||||
if (posted) {
|
||||
navigation.goBack()
|
||||
}
|
||||
},
|
||||
err => console.error('Failed to create post', err),
|
||||
)
|
||||
}}>
|
||||
<Text>Post</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation, replyTo, ref])
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<ComposerComponent ref={ref} replyTo={replyTo} />
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../../lib/strings'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {PostLikedBy as PostLikedByComponent} from '../../com/post-thread/PostLikedBy'
|
||||
|
||||
export const PostLikedBy = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'PostLikedBy'>) => {
|
||||
const {name, recordKey} = route.params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Liked By',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="arrow-left" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<PostLikedByComponent uri={uri} onNavigateContent={onNavigateContent} />
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../../lib/strings'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {PostRepostedBy as PostRepostedByComponent} from '../../com/post-thread/PostRepostedBy'
|
||||
|
||||
export const PostRepostedBy = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'PostRepostedBy'>) => {
|
||||
const {name, recordKey} = route.params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Reposted By',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="arrow-left" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<PostRepostedByComponent
|
||||
uri={uri}
|
||||
onNavigateContent={onNavigateContent}
|
||||
/>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {makeRecordUri} from '../../lib/strings'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {PostThread as PostThreadComponent} from '../../com/post-thread/PostThread'
|
||||
|
||||
export const PostThread = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'PostThread'>) => {
|
||||
const {name, recordKey} = route.params
|
||||
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Thread',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="arrow-left" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<PostThreadComponent uri={uri} onNavigateContent={onNavigateContent} />
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import React, {useState, useEffect} from 'react'
|
||||
import {View, StyleSheet} from 'react-native'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {FeedViewModel} from '../../../state/models/feed-view'
|
||||
import {useStores} from '../../../state'
|
||||
import {ProfileHeader} from '../../com/profile/ProfileHeader'
|
||||
import {Feed} from '../../com/feed/Feed'
|
||||
|
||||
export const Profile = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'Profile'>) => {
|
||||
const store = useStores()
|
||||
const [hasSetup, setHasSetup] = useState<string>('')
|
||||
const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
const author = route.params.name
|
||||
if (feedView?.params.author === author) {
|
||||
return // no change needed? or trigger refresh?
|
||||
}
|
||||
console.log('Fetching profile feed', author)
|
||||
const newFeedView = new FeedViewModel(store, {author})
|
||||
setFeedView(newFeedView)
|
||||
newFeedView
|
||||
.setup()
|
||||
.catch(err => console.error('Failed to fetch feed', err))
|
||||
.then(() => setHasSetup(author))
|
||||
}, [route.params.name, feedView?.params.author, store])
|
||||
|
||||
useEffect(() => {
|
||||
return navigation.addListener('focus', () => {
|
||||
if (hasSetup === feedView?.params.author) {
|
||||
console.log('Updating profile feed', hasSetup)
|
||||
feedView?.update()
|
||||
}
|
||||
})
|
||||
}, [navigation, feedView, hasSetup])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<View style={styles.container}>
|
||||
<ProfileHeader
|
||||
user={route.params.name}
|
||||
onNavigateContent={onNavigateContent}
|
||||
/>
|
||||
<View style={styles.feed}>
|
||||
{feedView && (
|
||||
<Feed feed={feedView} onNavigateContent={onNavigateContent} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
feed: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
|
@ -1,39 +0,0 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {ProfileFollowers as ProfileFollowersComponent} from '../../com/profile/ProfileFollowers'
|
||||
|
||||
export const ProfileFollowers = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'ProfileFollowers'>) => {
|
||||
const {name} = route.params
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Followers',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="arrow-left" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<ProfileFollowersComponent
|
||||
name={name}
|
||||
onNavigateContent={onNavigateContent}
|
||||
/>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React, {useLayoutEffect} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Shell} from '../../shell'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {ProfileFollows as ProfileFollowsComponent} from '../../com/profile/ProfileFollows'
|
||||
|
||||
export const ProfileFollows = ({
|
||||
navigation,
|
||||
route,
|
||||
}: RootTabsScreenProps<'ProfileFollows'>) => {
|
||||
const {name} = route.params
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Following',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<FontAwesomeIcon icon="arrow-left" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.push(screen, props)
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<ProfileFollowsComponent
|
||||
name={name}
|
||||
onNavigateContent={onNavigateContent}
|
||||
/>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import React, {useState, useEffect, useLayoutEffect} from 'react'
|
||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Shell} from '../../shell'
|
||||
import {Feed} from '../../com/feed/Feed'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {useStores} from '../../../state'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
|
||||
export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) {
|
||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||
const store = useStores()
|
||||
useEffect(() => {
|
||||
console.log('Fetching home feed')
|
||||
store.homeFeed.setup().then(() => setHasSetup(true))
|
||||
}, [store.homeFeed])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.navigate(screen, props)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return navigation.addListener('focus', () => {
|
||||
if (hasSetup) {
|
||||
console.log('Updating home feed')
|
||||
store.homeFeed.update()
|
||||
}
|
||||
})
|
||||
}, [navigation, store.homeFeed, hasSetup])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'V I B E',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
|
||||
<Image source={AVIS['alice.com']} style={styles.avi} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.push('Composer', {})
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<View>
|
||||
<Feed feed={store.homeFeed} onNavigateContent={onNavigateContent} />
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
})
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Shell} from '../../shell'
|
||||
import {ScrollView, Text, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
|
||||
export const Menu = (_props: RootTabsScreenProps<'MenuTab'>) => {
|
||||
return (
|
||||
<Shell>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Menu</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Shell} from '../../shell'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
|
||||
export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => {
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
|
||||
<Button title="Home" onPress={() => navigation.navigate('HomeTab')} />
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import React, {useState, useEffect, useLayoutEffect} from 'react'
|
||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Shell} from '../../shell'
|
||||
import {Feed} from '../../com/notifications/Feed'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
import {useStores} from '../../../state'
|
||||
import {AVIS} from '../../lib/assets'
|
||||
|
||||
export const Notifications = ({
|
||||
navigation,
|
||||
}: RootTabsScreenProps<'NotificationsTab'>) => {
|
||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||
const store = useStores()
|
||||
useEffect(() => {
|
||||
console.log('Fetching home feed')
|
||||
store.notesFeed.setup().then(() => setHasSetup(true))
|
||||
}, [store.notesFeed])
|
||||
|
||||
const onNavigateContent = (screen: string, props: Record<string, string>) => {
|
||||
// @ts-ignore it's up to the callers to supply correct params -prf
|
||||
navigation.navigate(screen, props)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return navigation.addListener('focus', () => {
|
||||
if (hasSetup) {
|
||||
console.log('Updating home feed')
|
||||
store.notesFeed.update()
|
||||
}
|
||||
})
|
||||
}, [navigation, store.notesFeed, hasSetup])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
headerTitle: 'Notifications',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
|
||||
<Image source={AVIS['alice.com']} style={styles.avi} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.push('Composer', {})
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<View>
|
||||
<Feed view={store.notesFeed} onNavigateContent={onNavigateContent} />
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
})
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Shell} from '../../shell'
|
||||
import {Text, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../../routes/types'
|
||||
|
||||
export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => {
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import React from 'react'
|
||||
import {Pressable, View, StyleSheet} from 'react-native'
|
||||
import {Link} from '@react-navigation/native'
|
||||
import {useRoute} from '@react-navigation/native'
|
||||
|
||||
export const NavItem: React.FC<{label: string; screen: string}> = ({
|
||||
label,
|
||||
screen,
|
||||
}) => {
|
||||
const route = useRoute()
|
||||
const Link = <></> // TODO
|
||||
return (
|
||||
<View>
|
||||
<Pressable
|
||||
|
@ -18,7 +16,7 @@ export const NavItem: React.FC<{label: string; screen: string}> = ({
|
|||
<Link
|
||||
style={[
|
||||
styles.navItemLink,
|
||||
route.name === screen && styles.navItemLinkSelected,
|
||||
false /* TODO route.name === screen*/ && styles.navItemLinkSelected,
|
||||
]}
|
||||
to={{screen, params: {}}}>
|
||||
{label}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView} from 'react-native'
|
||||
import {isDesktopWeb} from '../../platform/detection'
|
||||
import {DesktopWebShell} from './desktop-web/shell'
|
||||
|
||||
export const Shell: React.FC = ({children}) => {
|
||||
return isDesktopWeb ? (
|
||||
<DesktopWebShell>{children}</DesktopWebShell>
|
||||
) : (
|
||||
<SafeAreaView>{children}</SafeAreaView>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import RootSiblings from 'react-native-root-siblings'
|
||||
import {NavigationTabModel} from '../../../state/models/navigation'
|
||||
|
||||
export function createBackMenu(tab: NavigationTabModel): RootSiblings {
|
||||
const onPressItem = (index: number) => {
|
||||
sibling.destroy()
|
||||
tab.goToIndex(index)
|
||||
}
|
||||
const onOuterPress = () => sibling.destroy()
|
||||
const sibling = new RootSiblings(
|
||||
(
|
||||
<>
|
||||
<TouchableWithoutFeedback onPress={onOuterPress}>
|
||||
<View style={styles.bg} />
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={[styles.menu, styles.back]}>
|
||||
{tab.backTen.map((item, i) => (
|
||||
<TouchableOpacity
|
||||
key={item.index}
|
||||
style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
|
||||
onPress={() => onPressItem(item.index)}>
|
||||
<Text>{item.title || item.url}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
),
|
||||
)
|
||||
return sibling
|
||||
}
|
||||
export function createForwardMenu(tab: NavigationTabModel): RootSiblings {
|
||||
const onPressItem = (index: number) => {
|
||||
sibling.destroy()
|
||||
tab.goToIndex(index)
|
||||
}
|
||||
const onOuterPress = () => sibling.destroy()
|
||||
const sibling = new RootSiblings(
|
||||
(
|
||||
<>
|
||||
<TouchableWithoutFeedback onPress={onOuterPress}>
|
||||
<View style={styles.bg} />
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={[styles.menu, styles.forward]}>
|
||||
{tab.forwardTen.reverse().map((item, i) => (
|
||||
<TouchableOpacity
|
||||
key={item.index}
|
||||
style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
|
||||
onPress={() => onPressItem(item.index)}>
|
||||
<Text>{item.title || item.url}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
),
|
||||
)
|
||||
return sibling
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bg: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: '#000',
|
||||
opacity: 0.1,
|
||||
},
|
||||
menu: {
|
||||
position: 'absolute',
|
||||
bottom: 80,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 8,
|
||||
opacity: 1,
|
||||
},
|
||||
back: {
|
||||
left: 10,
|
||||
},
|
||||
forward: {
|
||||
left: 60,
|
||||
},
|
||||
menuItem: {
|
||||
paddingVertical: 10,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 30,
|
||||
},
|
||||
menuItemBorder: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#ddd',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,235 @@
|
|||
import React, {useRef} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {ScreenContainer, Screen} from 'react-native-screens'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {useStores} from '../../../state'
|
||||
import {NavigationModel} from '../../../state/models/navigation'
|
||||
import {match, MatchResult} from '../../routes'
|
||||
import {TabsSelectorModal} from './tabs-selector'
|
||||
import {createBackMenu, createForwardMenu} from './history-menu'
|
||||
|
||||
const Location = ({icon, title}: {icon: IconProp; title?: string}) => {
|
||||
return (
|
||||
<TouchableOpacity style={styles.location}>
|
||||
{title ? (
|
||||
<FontAwesomeIcon size={16} style={styles.locationIcon} icon={icon} />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
size={16}
|
||||
style={styles.locationIconLight}
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
)}
|
||||
<Text style={title ? styles.locationText : styles.locationTextLight}>
|
||||
{title || 'Search'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const Btn = ({
|
||||
icon,
|
||||
inactive,
|
||||
onPress,
|
||||
onLongPress,
|
||||
}: {
|
||||
icon: IconProp
|
||||
inactive?: boolean
|
||||
onPress?: (event: GestureResponderEvent) => void
|
||||
onLongPress?: (event: GestureResponderEvent) => void
|
||||
}) => {
|
||||
if (inactive) {
|
||||
return (
|
||||
<View style={styles.ctrl}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
style={[styles.ctrlIcon, styles.inactive]}
|
||||
icon={icon}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.ctrl}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}>
|
||||
<FontAwesomeIcon size={18} style={styles.ctrlIcon} icon={icon} />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
export const MobileShell: React.FC = observer(() => {
|
||||
const stores = useStores()
|
||||
const tabSelectorRef = useRef<{open: () => void}>()
|
||||
const screenRenderDesc = constructScreenRenderDesc(stores.nav)
|
||||
|
||||
const onPressBack = () => stores.nav.tab.goBack()
|
||||
const onPressForward = () => stores.nav.tab.goForward()
|
||||
const onPressHome = () => stores.nav.navigate('/')
|
||||
const onPressNotifications = () => stores.nav.navigate('/notifications')
|
||||
const onPressTabs = () => tabSelectorRef.current?.open()
|
||||
|
||||
const onLongPressBack = () => createBackMenu(stores.nav.tab)
|
||||
const onLongPressForward = () => createForwardMenu(stores.nav.tab)
|
||||
|
||||
const onNewTab = () => stores.nav.newTab('/')
|
||||
const onChangeTab = (tabIndex: number) => stores.nav.setActiveTab(tabIndex)
|
||||
const onCloseTab = (tabIndex: number) => stores.nav.closeTab(tabIndex)
|
||||
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<View style={styles.topBar}>
|
||||
<Location
|
||||
icon={screenRenderDesc.icon}
|
||||
title={stores.nav.tab.current.title}
|
||||
/>
|
||||
</View>
|
||||
<SafeAreaView style={styles.innerContainer}>
|
||||
<ScreenContainer>
|
||||
{screenRenderDesc.screens.map(({Com, params, key, activityState}) => (
|
||||
<Screen
|
||||
key={key}
|
||||
style={{backgroundColor: '#fff'}}
|
||||
activityState={activityState}>
|
||||
<Com params={params} />
|
||||
</Screen>
|
||||
))}
|
||||
</ScreenContainer>
|
||||
</SafeAreaView>
|
||||
<View style={styles.bottomBar}>
|
||||
<Btn
|
||||
icon="angle-left"
|
||||
inactive={!stores.nav.tab.canGoBack}
|
||||
onPress={onPressBack}
|
||||
onLongPress={onLongPressBack}
|
||||
/>
|
||||
<Btn
|
||||
icon="angle-right"
|
||||
inactive={!stores.nav.tab.canGoForward}
|
||||
onPress={onPressForward}
|
||||
onLongPress={onLongPressForward}
|
||||
/>
|
||||
<Btn icon="house" onPress={onPressHome} />
|
||||
<Btn icon={['far', 'bell']} onPress={onPressNotifications} />
|
||||
<Btn icon={['far', 'clone']} onPress={onPressTabs} />
|
||||
</View>
|
||||
<TabsSelectorModal
|
||||
ref={tabSelectorRef}
|
||||
tabs={stores.nav.tabs}
|
||||
currentTabIndex={stores.nav.tabIndex}
|
||||
onNewTab={onNewTab}
|
||||
onChangeTab={onChangeTab}
|
||||
onCloseTab={onCloseTab}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* This method produces the information needed by the shell to
|
||||
* render the current screens with screen-caching behaviors.
|
||||
*/
|
||||
type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2}
|
||||
function constructScreenRenderDesc(nav: NavigationModel): {
|
||||
icon: IconProp
|
||||
screens: ScreenRenderDesc[]
|
||||
} {
|
||||
let icon: IconProp = 'magnifying-glass'
|
||||
let screens: ScreenRenderDesc[] = []
|
||||
for (const tab of nav.tabs) {
|
||||
const tabScreens = [
|
||||
...tab.getBackList(5),
|
||||
Object.assign({}, tab.current, {index: tab.index}),
|
||||
]
|
||||
const parsedTabScreens = tabScreens.map(screen => {
|
||||
const isCurrent = nav.isCurrentScreen(tab.id, screen.index)
|
||||
const matchRes = match(screen.url)
|
||||
if (isCurrent) {
|
||||
icon = matchRes.icon
|
||||
}
|
||||
return Object.assign(matchRes, {
|
||||
key: `t${tab.id}-s${screen.index}`,
|
||||
activityState: isCurrent ? 2 : 0,
|
||||
})
|
||||
})
|
||||
screens = screens.concat(parsedTabScreens)
|
||||
}
|
||||
return {
|
||||
icon,
|
||||
screens,
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
innerContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#ccc',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
paddingTop: 40,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
location: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 6,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
backgroundColor: '#F8F3F3',
|
||||
},
|
||||
locationIcon: {
|
||||
color: '#DB00FF',
|
||||
marginRight: 8,
|
||||
},
|
||||
locationIconLight: {
|
||||
color: '#909090',
|
||||
marginRight: 8,
|
||||
},
|
||||
locationText: {
|
||||
color: '#000',
|
||||
},
|
||||
locationTextLight: {
|
||||
color: '#868788',
|
||||
},
|
||||
bottomBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#ccc',
|
||||
paddingLeft: 5,
|
||||
paddingRight: 15,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
ctrl: {
|
||||
flex: 1,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
ctrlIcon: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
inactive: {
|
||||
color: '#888',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,158 @@
|
|||
import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react'
|
||||
import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import BottomSheet from '@gorhom/bottom-sheet'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {s} from '../../lib/styles'
|
||||
import {NavigationTabModel} from '../../../state/models/navigation'
|
||||
import {createCustomBackdrop} from '../../com/util/BottomSheetCustomBackdrop'
|
||||
import {match} from '../../routes'
|
||||
|
||||
const TAB_HEIGHT = 38
|
||||
const TAB_SPACING = 5
|
||||
const BOTTOM_MARGIN = 70
|
||||
|
||||
export const TabsSelectorModal = forwardRef(function TabsSelectorModal(
|
||||
{
|
||||
onNewTab,
|
||||
onChangeTab,
|
||||
onCloseTab,
|
||||
tabs,
|
||||
currentTabIndex,
|
||||
}: {
|
||||
onNewTab: () => void
|
||||
onChangeTab: (tabIndex: number) => void
|
||||
onCloseTab: (tabIndex: number) => void
|
||||
tabs: NavigationTabModel[]
|
||||
currentTabIndex: number
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const [snapPoints, setSnapPoints] = useState<number[]>([100])
|
||||
const bottomSheetRef = useRef<BottomSheet>(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open() {
|
||||
setIsOpen(true)
|
||||
setSnapPoints([
|
||||
(tabs.length + 1) * (TAB_HEIGHT + TAB_SPACING) + BOTTOM_MARGIN,
|
||||
])
|
||||
bottomSheetRef.current?.expand()
|
||||
},
|
||||
}))
|
||||
|
||||
const onShareBottomSheetChange = (snapPoint: number) => {
|
||||
if (snapPoint === -1) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
const onPressNewTab = () => {
|
||||
onNewTab()
|
||||
onClose()
|
||||
}
|
||||
const onPressChangeTab = (tabIndex: number) => {
|
||||
onChangeTab(tabIndex)
|
||||
onClose()
|
||||
}
|
||||
const onClose = () => {
|
||||
setIsOpen(false)
|
||||
bottomSheetRef.current?.close()
|
||||
}
|
||||
return (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
index={-1}
|
||||
snapPoints={snapPoints}
|
||||
enablePanDownToClose
|
||||
backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined}
|
||||
onChange={onShareBottomSheetChange}>
|
||||
<View style={s.p10}>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
const {icon} = match(tab.current.url)
|
||||
const isActive = tabIndex === currentTabIndex
|
||||
return (
|
||||
<View
|
||||
key={tabIndex}
|
||||
style={[styles.tab, styles.existing, isActive && styles.active]}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => onPressChangeTab(tabIndex)}>
|
||||
<View style={styles.tabIcon}>
|
||||
<FontAwesomeIcon size={16} icon={icon} />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => onPressChangeTab(tabIndex)}>
|
||||
<Text
|
||||
style={[styles.tabText, isActive && styles.tabTextActive]}>
|
||||
{tab.current.title || tab.current.url}
|
||||
</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
<TouchableWithoutFeedback onPress={() => onCloseTab(tabIndex)}>
|
||||
<View style={styles.tabClose}>
|
||||
<FontAwesomeIcon
|
||||
size={16}
|
||||
icon="x"
|
||||
style={styles.tabCloseIcon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
<TouchableWithoutFeedback onPress={onPressNewTab}>
|
||||
<View style={[styles.tab, styles.create]}>
|
||||
<View style={styles.tabIcon}>
|
||||
<FontAwesomeIcon size={16} icon="plus" />
|
||||
</View>
|
||||
<Text style={styles.tabText}>New tab</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tab: {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
borderRadius: 4,
|
||||
height: TAB_HEIGHT,
|
||||
marginBottom: TAB_SPACING,
|
||||
},
|
||||
existing: {
|
||||
borderColor: '#000',
|
||||
borderWidth: 1,
|
||||
},
|
||||
create: {
|
||||
backgroundColor: '#F8F3F3',
|
||||
},
|
||||
active: {
|
||||
backgroundColor: '#faf0f0',
|
||||
borderColor: '#f00',
|
||||
borderWidth: 1,
|
||||
},
|
||||
tabIcon: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 10,
|
||||
},
|
||||
tabText: {
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
tabTextActive: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
tabClose: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 15,
|
||||
},
|
||||
tabCloseIcon: {
|
||||
color: '#655',
|
||||
},
|
||||
})
|
|
@ -12,3 +12,6 @@ Paul's todo list
|
|||
- Reposted by
|
||||
- Followers list
|
||||
- Follows list
|
||||
- Navigation
|
||||
- Restore all functionality that was disabled during the refactor
|
||||
- Reduce extraneous triggers of useLoadEffect
|
120
yarn.lock
120
yarn.lock
|
@ -2375,65 +2375,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa"
|
||||
integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==
|
||||
|
||||
"@react-navigation/bottom-tabs@^6.3.1":
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.3.1.tgz#1552ccdb789b6c9fc05af0877f8f3a50ab28870c"
|
||||
integrity sha512-sL9F4WMhhR6I9bE7bpsPVHnK1cN9doaFHAuy5YmD+Sw6OyO0TAmNgQFx4xZWqboA5ZwSkN0tWcRCr6wGXaRRag==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^1.3.3"
|
||||
color "^3.1.3"
|
||||
warn-once "^0.1.0"
|
||||
|
||||
"@react-navigation/core@^6.2.1":
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.2.1.tgz#90459f9afd25b71a9471b0706ebea2cdd2534fc4"
|
||||
integrity sha512-3mjS6ujwGnPA/BC11DN9c2c42gFld6B6dQBgDedxP2djceXESpY2kVTTwISDHuqFnF7WjvRjsrDu3cKBX+JosA==
|
||||
dependencies:
|
||||
"@react-navigation/routers" "^6.1.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
nanoid "^3.1.23"
|
||||
query-string "^7.0.0"
|
||||
react-is "^16.13.0"
|
||||
|
||||
"@react-navigation/elements@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.3.tgz#9f56b650a9a1a8263a271628be7342c8121d1788"
|
||||
integrity sha512-Lv2lR7si5gNME8dRsqz57d54m4FJtrwHRjNQLOyQO546ZxO+g864cSvoLC6hQedQU0+IJnPTsZiEI2hHqfpEpw==
|
||||
|
||||
"@react-navigation/native-stack@^6.6.2":
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.6.2.tgz#09e696ad72299872f4c5c1e5b1ad309869853628"
|
||||
integrity sha512-pFMuzhxbPml5MBvJVAzHWoaUkQaefAOKpuUnAs/AxNQuHQwwnxRmDit1PQLuIPo7g7DlfwFXagDHE1R0tbnS8Q==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^1.3.3"
|
||||
warn-once "^0.1.0"
|
||||
|
||||
"@react-navigation/native@^6.0.10":
|
||||
version "6.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.0.10.tgz#c58aa176eb0e63f3641c83a65c509faf253e4385"
|
||||
integrity sha512-H6QhLeiieGxNcAJismIDXIPZgf1myr7Og8v116tezIGmincJTOcWavTd7lPHGnMMXaZg94LlVtbaBRIx9cexqw==
|
||||
dependencies:
|
||||
"@react-navigation/core" "^6.2.1"
|
||||
escape-string-regexp "^4.0.0"
|
||||
fast-deep-equal "^3.1.3"
|
||||
nanoid "^3.1.23"
|
||||
|
||||
"@react-navigation/routers@^6.1.0":
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-6.1.0.tgz#d5682be88f1eb7809527c48f9cd3dedf4f344e40"
|
||||
integrity sha512-8xJL+djIzpFdRW/sGlKojQ06fWgFk1c5jER9501HYJ12LF5DIJFr/tqBI2TJ6bk+y+QFu0nbNyeRC80OjRlmkA==
|
||||
dependencies:
|
||||
nanoid "^3.1.23"
|
||||
|
||||
"@react-navigation/stack@^6.2.1":
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-6.2.1.tgz#2b14473579eced6def5cca06860044d60e59d06e"
|
||||
integrity sha512-JI7boxtPAMCBXi4VJHVEq61jLVHFW5f3npvbndS+XfOsv7Gf0f91HOVJ28DS5c2Fn4+CO4AByjUozzlN296X+A==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^1.3.3"
|
||||
color "^3.1.3"
|
||||
warn-once "^0.1.0"
|
||||
|
||||
"@rollup/plugin-babel@^5.2.0":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||
|
@ -4691,7 +4632,7 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^1.9.0, color-convert@^1.9.3:
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
|
@ -4710,27 +4651,11 @@ color-name@1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
||||
color-name@^1.1.4, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-string@^1.6.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color@^3.1.3:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
|
||||
integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
|
||||
dependencies:
|
||||
color-convert "^1.9.3"
|
||||
color-string "^1.6.0"
|
||||
|
||||
colord@^2.9.1:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
|
||||
|
@ -6553,11 +6478,6 @@ fill-range@^7.0.1:
|
|||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
filter-obj@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
|
||||
integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
|
||||
|
||||
finalhandler@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||
|
@ -7474,11 +7394,6 @@ is-arrayish@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
is-arrayish@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-bigint@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
|
||||
|
@ -9781,7 +9696,7 @@ multiformats@^9.1.2, multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6
|
|||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.5.tgz#f2d894a26664b454a90abf5a8911b7e39195db80"
|
||||
integrity sha512-vMwf/FUO+qAPvl3vlSZEgEVFY/AxeZq5yg761ScF3CZsXgmTi/HGkicUiNN0CI4PW8FiY2P0OLklOcmQjdQJhw==
|
||||
|
||||
nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.3, nanoid@^3.3.4:
|
||||
nanoid@^3.3.1, nanoid@^3.3.3, nanoid@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||
|
@ -11234,16 +11149,6 @@ qs@6.10.3:
|
|||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
query-string@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1"
|
||||
integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
filter-obj "^1.1.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
queue-microtask@^1.2.2, queue-microtask@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
|
@ -11375,7 +11280,7 @@ react-freeze@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0:
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
@ -12333,13 +12238,6 @@ simple-plist@^1.1.0:
|
|||
bplist-parser "0.3.1"
|
||||
plist "^3.0.5"
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
|
||||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
sisteransi@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||
|
@ -12583,11 +12481,6 @@ spdy@^4.0.2:
|
|||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
|
@ -12647,11 +12540,6 @@ stream-buffers@2.2.x:
|
|||
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4"
|
||||
integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==
|
||||
|
||||
string-hash-64@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
|
||||
|
|
Loading…
Reference in New Issue