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",
|
"@gorhom/bottom-sheet": "^4",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
"@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",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"lodash.omit": "^4.5.0",
|
"lodash.omit": "^4.5.0",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||||
import {whenWebCrypto} from './platform/polyfills.native'
|
import {whenWebCrypto} from './platform/polyfills.native'
|
||||||
import * as view from './view/index'
|
import * as view from './view/index'
|
||||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import * as Routes from './view/routes'
|
import {MobileShell} from './view/shell/mobile'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
|
@ -31,7 +31,7 @@ function App() {
|
||||||
<GestureHandlerRootView style={{flex: 1}}>
|
<GestureHandlerRootView style={{flex: 1}}>
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<RootStoreProvider value={rootStore}>
|
<RootStoreProvider value={rootStore}>
|
||||||
<Routes.Root />
|
<MobileShell />
|
||||||
</RootStoreProvider>
|
</RootStoreProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import * as view from './view/index'
|
import * as view from './view/index'
|
||||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import * as Routes from './view/routes'
|
import {DesktopWebShell} from './view/shell/desktop-web'
|
||||||
import Toast from './view/com/util/Toast'
|
import Toast from './view/com/util/Toast'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -22,7 +22,7 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootStoreProvider value={rootStore}>
|
<RootStoreProvider value={rootStore}>
|
||||||
<Routes.Root />
|
<DesktopWebShell />
|
||||||
<Toast.ToastContainer />
|
<Toast.ToastContainer />
|
||||||
</RootStoreProvider>
|
</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 {createContext, useContext} from 'react'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
import {SessionModel} from './session'
|
import {SessionModel} from './session'
|
||||||
|
import {NavigationModel} from './navigation'
|
||||||
import {MeModel} from './me'
|
import {MeModel} from './me'
|
||||||
import {FeedViewModel} from './feed-view'
|
import {FeedViewModel} from './feed-view'
|
||||||
import {NotificationsViewModel} from './notifications-view'
|
import {NotificationsViewModel} from './notifications-view'
|
||||||
|
|
||||||
export class RootStoreModel {
|
export class RootStoreModel {
|
||||||
session = new SessionModel()
|
session = new SessionModel()
|
||||||
|
nav = new NavigationModel()
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
homeFeed = new FeedViewModel(this, {})
|
homeFeed = new FeedViewModel(this, {})
|
||||||
notesFeed = new NotificationsViewModel(this, {})
|
notesFeed = new NotificationsViewModel(this, {})
|
||||||
|
@ -35,6 +37,7 @@ export class RootStoreModel {
|
||||||
serialize(): unknown {
|
serialize(): unknown {
|
||||||
return {
|
return {
|
||||||
session: this.session.serialize(),
|
session: this.session.serialize(),
|
||||||
|
nav: this.nav.serialize(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +46,9 @@ export class RootStoreModel {
|
||||||
if (hasProp(v, 'session')) {
|
if (hasProp(v, 'session')) {
|
||||||
this.session.hydrate(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 React, {useRef} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Text, View, FlatList} from 'react-native'
|
import {Text, View, FlatList} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view'
|
import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {ShareModal} from '../modals/SharePost'
|
import {ShareModal} from '../modals/SharePost'
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
|
||||||
feed,
|
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
feed: FeedViewModel
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
|
||||||
const shareSheetRef = useRef<{open: (_uri: string) => void}>()
|
const shareSheetRef = useRef<{open: (_uri: string) => void}>()
|
||||||
|
|
||||||
const onPressShare = (uri: string) => {
|
const onPressShare = (uri: string) => {
|
||||||
|
@ -23,11 +16,7 @@ export const Feed = observer(function Feed({
|
||||||
// renderItem function renders components that follow React performance best practices
|
// renderItem function renders components that follow React performance best practices
|
||||||
// like PureComponent, shouldComponentUpdate, etc
|
// like PureComponent, shouldComponentUpdate, etc
|
||||||
const renderItem = ({item}: {item: FeedViewItemModel}) => (
|
const renderItem = ({item}: {item: FeedViewItemModel}) => (
|
||||||
<FeedItem
|
<FeedItem item={item} onPressShare={onPressShare} />
|
||||||
item={item}
|
|
||||||
onNavigateContent={onNavigateContent}
|
|
||||||
onPressShare={onPressShare}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
feed.refresh().catch(err => console.error('Failed to refresh', err))
|
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 {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||||
import {bsky, AdxUri} from '@adxp/mock-api'
|
import {bsky, AdxUri} from '@adxp/mock-api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {FeedViewItemModel} from '../../../state/models/feed-view'
|
import {FeedViewItemModel} from '../../../state/models/feed-view'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ago} from '../../lib/strings'
|
import {ago} from '../../lib/strings'
|
||||||
import {AVIS} from '../../lib/assets'
|
import {AVIS} from '../../lib/assets'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
|
||||||
export const FeedItem = observer(function FeedItem({
|
export const FeedItem = observer(function FeedItem({
|
||||||
item,
|
item,
|
||||||
onNavigateContent,
|
|
||||||
onPressShare,
|
onPressShare,
|
||||||
}: {
|
}: {
|
||||||
item: FeedViewItemModel
|
item: FeedViewItemModel
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
onPressShare: (_uri: string) => void
|
onPressShare: (_uri: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
const store = useStores()
|
||||||
const record = item.record as unknown as bsky.Post.Record
|
const record = item.record as unknown as bsky.Post.Record
|
||||||
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostThread', {
|
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||||
name: item.author.name,
|
|
||||||
recordKey: urip.recordKey,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressAuthor = () => {
|
const onPressAuthor = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.author.name}`)
|
||||||
name: item.author.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressReply = () => {
|
const onPressReply = () => {
|
||||||
onNavigateContent('Composer', {
|
store.nav.navigate('/composer')
|
||||||
replyTo: item.uri,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressToggleRepost = () => {
|
const onPressToggleRepost = () => {
|
||||||
item
|
item
|
||||||
|
@ -137,8 +129,11 @@ export const FeedItem = observer(function FeedItem({
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
borderTopWidth: 1,
|
// borderWidth: 1,
|
||||||
borderTopColor: '#e8e8e8',
|
// borderColor: '#e8e8e8',
|
||||||
|
borderRadius: 10,
|
||||||
|
margin: 2,
|
||||||
|
marginBottom: 0,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
padding: 10,
|
padding: 10,
|
||||||
},
|
},
|
||||||
|
@ -175,6 +170,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
postText: {
|
postText: {
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
|
fontFamily: 'Helvetica Neue',
|
||||||
},
|
},
|
||||||
ctrls: {
|
ctrls: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
@ -1,27 +1,10 @@
|
||||||
import React, {
|
import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react'
|
||||||
forwardRef,
|
import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||||
useState,
|
import BottomSheet from '@gorhom/bottom-sheet'
|
||||||
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 Toast from '../util/Toast'
|
import Toast from '../util/Toast'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
|
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||||
|
|
||||||
export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||||
|
@ -33,14 +16,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
||||||
console.log('sharing', uri)
|
console.log('sharing', uri)
|
||||||
setUri(uri)
|
setUri(uri)
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
|
bottomSheetRef.current?.expand()
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onPressCopy = () => {
|
const onPressCopy = () => {
|
||||||
Clipboard.setString(uri)
|
Clipboard.setString(uri)
|
||||||
console.log('showing')
|
console.log('showing')
|
||||||
console.log(Toast)
|
|
||||||
console.log(Toast.show)
|
|
||||||
Toast.show('Link copied', {
|
Toast.show('Link copied', {
|
||||||
position: Toast.positions.TOP,
|
position: Toast.positions.TOP,
|
||||||
})
|
})
|
||||||
|
@ -55,36 +37,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
|
||||||
bottomSheetRef.current?.close()
|
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 (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={onClose}>
|
|
||||||
<Animated.View style={containerStyle} />
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isOpen && (
|
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
|
index={-1}
|
||||||
snapPoints={['50%']}
|
snapPoints={['50%']}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
backdropComponent={CustomBackdrop}
|
backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined}
|
||||||
onChange={onShareBottomSheetChange}>
|
onChange={onShareBottomSheetChange}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
|
<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>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Text, View, FlatList} from 'react-native'
|
import {Text, View, FlatList} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
NotificationsViewModel,
|
NotificationsViewModel,
|
||||||
NotificationsViewItemModel,
|
NotificationsViewItemModel,
|
||||||
|
@ -10,17 +9,15 @@ import {FeedItem} from './FeedItem'
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
view,
|
view,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
view: NotificationsViewModel
|
view: NotificationsViewModel
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
|
// 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
|
// VirtualizedList: You have a large list that is slow to update - make sure your
|
||||||
// renderItem function renders components that follow React performance best practices
|
// renderItem function renders components that follow React performance best practices
|
||||||
// like PureComponent, shouldComponentUpdate, etc
|
// like PureComponent, shouldComponentUpdate, etc
|
||||||
const renderItem = ({item}: {item: NotificationsViewItemModel}) => (
|
const renderItem = ({item}: {item: NotificationsViewItemModel}) => (
|
||||||
<FeedItem item={item} onNavigateContent={onNavigateContent} />
|
<FeedItem item={item} />
|
||||||
)
|
)
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
view.refresh().catch(err => console.error('Failed to refresh', err))
|
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 {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||||
import {AdxUri} from '@adxp/mock-api'
|
import {AdxUri} from '@adxp/mock-api'
|
||||||
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
|
import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ago} from '../../lib/strings'
|
import {ago} from '../../lib/strings'
|
||||||
import {AVIS} from '../../lib/assets'
|
import {AVIS} from '../../lib/assets'
|
||||||
import {PostText} from '../post/PostText'
|
import {PostText} from '../post/PostText'
|
||||||
import {Post} from '../post/Post'
|
import {Post} from '../post/Post'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
|
||||||
export const FeedItem = observer(function FeedItem({
|
export const FeedItem = observer(function FeedItem({
|
||||||
item,
|
item,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
item: NotificationsViewItemModel
|
item: NotificationsViewItemModel
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
if (item.isLike || item.isRepost) {
|
if (item.isLike || item.isRepost) {
|
||||||
const urip = new AdxUri(item.subjectUri)
|
const urip = new AdxUri(item.subjectUri)
|
||||||
onNavigateContent('PostThread', {
|
store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
|
||||||
name: urip.host,
|
|
||||||
recordKey: urip.recordKey,
|
|
||||||
})
|
|
||||||
} else if (item.isFollow) {
|
} else if (item.isFollow) {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.author.name}`)
|
||||||
name: item.author.name,
|
|
||||||
})
|
|
||||||
} else if (item.isReply) {
|
} else if (item.isReply) {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostThread', {
|
store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
|
||||||
name: urip.host,
|
|
||||||
recordKey: urip.recordKey,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onPressAuthor = () => {
|
const onPressAuthor = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.author.name}`)
|
||||||
name: item.author.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = ''
|
let action = ''
|
||||||
|
@ -92,7 +82,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
</View>
|
</View>
|
||||||
{item.isReply ? (
|
{item.isReply ? (
|
||||||
<View style={s.pt5}>
|
<View style={s.pt5}>
|
||||||
<Post uri={item.uri} onNavigateContent={onNavigateContent} />
|
<Post uri={item.uri} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
LikedByViewModel,
|
LikedByViewModel,
|
||||||
LikedByViewItemModel,
|
LikedByViewItemModel,
|
||||||
|
@ -18,13 +17,7 @@ import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {AVIS} from '../../lib/assets'
|
import {AVIS} from '../../lib/assets'
|
||||||
|
|
||||||
export const PostLikedBy = observer(function PostLikedBy({
|
export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
|
||||||
uri,
|
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<LikedByViewModel | undefined>()
|
const [view, setView] = useState<LikedByViewModel | undefined>()
|
||||||
|
|
||||||
|
@ -66,7 +59,7 @@ export const PostLikedBy = observer(function PostLikedBy({
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: LikedByViewItemModel}) => (
|
const renderItem = ({item}: {item: LikedByViewItemModel}) => (
|
||||||
<LikedByItem item={item} onNavigateContent={onNavigateContent} />
|
<LikedByItem item={item} />
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -79,17 +72,10 @@ export const PostLikedBy = observer(function PostLikedBy({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const LikedByItem = ({
|
const LikedByItem = ({item}: {item: LikedByViewItemModel}) => {
|
||||||
item,
|
const store = useStores()
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
item: LikedByViewItemModel
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) => {
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.name}`)
|
||||||
name: item.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
RepostedByViewModel,
|
RepostedByViewModel,
|
||||||
RepostedByViewItemModel,
|
RepostedByViewItemModel,
|
||||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
||||||
|
|
||||||
export const PostRepostedBy = observer(function PostRepostedBy({
|
export const PostRepostedBy = observer(function PostRepostedBy({
|
||||||
uri,
|
uri,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
uri: string
|
uri: string
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<RepostedByViewModel | undefined>()
|
const [view, setView] = useState<RepostedByViewModel | undefined>()
|
||||||
|
@ -68,7 +65,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
|
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
|
||||||
<RepostedByItem item={item} onNavigateContent={onNavigateContent} />
|
<RepostedByItem item={item} />
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -81,17 +78,10 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const RepostedByItem = ({
|
const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
|
||||||
item,
|
const store = useStores()
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
item: RepostedByViewItemModel
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) => {
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.name}`)
|
||||||
name: item.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React, {useState, useEffect, useRef} from 'react'
|
import React, {useState, useEffect, useRef} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {ActivityIndicator, FlatList, Text, View} from 'react-native'
|
import {ActivityIndicator, FlatList, Text, View} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
PostThreadViewModel,
|
PostThreadViewModel,
|
||||||
PostThreadViewPostModel,
|
PostThreadViewPostModel,
|
||||||
|
@ -14,13 +12,7 @@ import {s} from '../../lib/styles'
|
||||||
|
|
||||||
const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates
|
const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates
|
||||||
|
|
||||||
export const PostThread = observer(function PostThread({
|
export const PostThread = observer(function PostThread({uri}: {uri: string}) {
|
||||||
uri,
|
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
||||||
const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
|
const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
|
||||||
|
@ -37,12 +29,13 @@ export const PostThread = observer(function PostThread({
|
||||||
newView.setup().catch(err => console.error('Failed to fetch thread', err))
|
newView.setup().catch(err => console.error('Failed to fetch thread', err))
|
||||||
}, [uri, view?.params.uri, store])
|
}, [uri, view?.params.uri, store])
|
||||||
|
|
||||||
useFocusEffect(() => {
|
// TODO
|
||||||
if (Date.now() - lastUpdate > UPDATE_DELAY) {
|
// useFocusEffect(() => {
|
||||||
view?.update()
|
// if (Date.now() - lastUpdate > UPDATE_DELAY) {
|
||||||
setLastUpdate(Date.now())
|
// view?.update()
|
||||||
}
|
// setLastUpdate(Date.now())
|
||||||
})
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
const onPressShare = (uri: string) => {
|
const onPressShare = (uri: string) => {
|
||||||
shareSheetRef.current?.open(uri)
|
shareSheetRef.current?.open(uri)
|
||||||
|
@ -79,11 +72,7 @@ export const PostThread = observer(function PostThread({
|
||||||
// =
|
// =
|
||||||
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
|
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
|
||||||
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
|
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
|
||||||
<PostThreadItem
|
<PostThreadItem item={item} onPressShare={onPressShare} />
|
||||||
item={item}
|
|
||||||
onNavigateContent={onNavigateContent}
|
|
||||||
onPressShare={onPressShare}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<View style={s.h100pct}>
|
<View style={s.h100pct}>
|
||||||
|
|
|
@ -3,11 +3,11 @@ import {observer} from 'mobx-react-lite'
|
||||||
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||||
import {bsky, AdxUri} from '@adxp/mock-api'
|
import {bsky, AdxUri} from '@adxp/mock-api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
|
import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ago, pluralize} from '../../lib/strings'
|
import {ago, pluralize} from '../../lib/strings'
|
||||||
import {AVIS} from '../../lib/assets'
|
import {AVIS} from '../../lib/assets'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
|
||||||
function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
|
function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
|
||||||
const arr: 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({
|
export const PostThreadItem = observer(function PostThreadItem({
|
||||||
item,
|
item,
|
||||||
onNavigateContent,
|
|
||||||
onPressShare,
|
onPressShare,
|
||||||
}: {
|
}: {
|
||||||
item: PostThreadViewPostModel
|
item: PostThreadViewPostModel
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
onPressShare: (_uri: string) => void
|
onPressShare: (_uri: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
const store = useStores()
|
||||||
const record = item.record as unknown as bsky.Post.Record
|
const record = item.record as unknown as bsky.Post.Record
|
||||||
const hasEngagement = item.likeCount || item.repostCount
|
const hasEngagement = item.likeCount || item.repostCount
|
||||||
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostThread', {
|
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||||
name: item.author.name,
|
|
||||||
recordKey: urip.recordKey,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressAuthor = () => {
|
const onPressAuthor = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.author.name}`)
|
||||||
name: item.author.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressLikes = () => {
|
const onPressLikes = () => {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostLikedBy', {
|
store.nav.navigate(
|
||||||
name: item.author.name,
|
`/profile/${item.author.name}/post/${urip.recordKey}/liked-by`,
|
||||||
recordKey: urip.recordKey,
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressReposts = () => {
|
const onPressReposts = () => {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostRepostedBy', {
|
store.nav.navigate(
|
||||||
name: item.author.name,
|
`/profile/${item.author.name}/post/${urip.recordKey}/reposted-by`,
|
||||||
recordKey: urip.recordKey,
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressReply = () => {
|
const onPressReply = () => {
|
||||||
onNavigateContent('Composer', {
|
store.nav.navigate(`/composer?replyTo=${item.uri}`)
|
||||||
replyTo: item.uri,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressToggleRepost = () => {
|
const onPressToggleRepost = () => {
|
||||||
item
|
item
|
||||||
|
@ -227,6 +217,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
postText: {
|
postText: {
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
|
fontFamily: 'Helvetica Neue',
|
||||||
},
|
},
|
||||||
expandedInfo: {
|
expandedInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
@ -10,20 +10,13 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {PostThreadViewModel} from '../../../state/models/post-thread-view'
|
import {PostThreadViewModel} from '../../../state/models/post-thread-view'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ago} from '../../lib/strings'
|
import {ago} from '../../lib/strings'
|
||||||
import {AVIS} from '../../lib/assets'
|
import {AVIS} from '../../lib/assets'
|
||||||
|
|
||||||
export const Post = observer(function Post({
|
export const Post = observer(function Post({uri}: {uri: string}) {
|
||||||
uri,
|
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
const [view, setView] = useState<PostThreadViewModel | undefined>()
|
||||||
|
|
||||||
|
@ -63,20 +56,13 @@ export const Post = observer(function Post({
|
||||||
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
const urip = new AdxUri(item.uri)
|
const urip = new AdxUri(item.uri)
|
||||||
onNavigateContent('PostThread', {
|
store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
|
||||||
name: item.author.name,
|
|
||||||
recordKey: urip.recordKey,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressAuthor = () => {
|
const onPressAuthor = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.author.name}`)
|
||||||
name: item.author.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressReply = () => {
|
const onPressReply = () => {
|
||||||
onNavigateContent('Composer', {
|
store.nav.navigate(`/composer?replyTo=${item.uri}`)
|
||||||
replyTo: item.uri,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const onPressToggleRepost = () => {
|
const onPressToggleRepost = () => {
|
||||||
item
|
item
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
UserFollowersViewModel,
|
UserFollowersViewModel,
|
||||||
FollowerItem,
|
FollowerItem,
|
||||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
||||||
|
|
||||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
name,
|
name,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<UserFollowersViewModel | undefined>()
|
const [view, setView] = useState<UserFollowersViewModel | undefined>()
|
||||||
|
@ -67,9 +64,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
|
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowerItem}) => (
|
const renderItem = ({item}: {item: FollowerItem}) => <User item={item} />
|
||||||
<User item={item} onNavigateContent={onNavigateContent} />
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
@ -81,17 +76,10 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = ({
|
const User = ({item}: {item: FollowerItem}) => {
|
||||||
item,
|
const store = useStores()
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
item: FollowerItem
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) => {
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.name}`)
|
||||||
name: item.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {
|
import {
|
||||||
UserFollowsViewModel,
|
UserFollowsViewModel,
|
||||||
FollowItem,
|
FollowItem,
|
||||||
|
@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
|
||||||
|
|
||||||
export const ProfileFollows = observer(function ProfileFollows({
|
export const ProfileFollows = observer(function ProfileFollows({
|
||||||
name,
|
name,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<UserFollowsViewModel | undefined>()
|
const [view, setView] = useState<UserFollowsViewModel | undefined>()
|
||||||
|
@ -67,9 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
|
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowItem}) => (
|
const renderItem = ({item}: {item: FollowItem}) => <User item={item} />
|
||||||
<User item={item} onNavigateContent={onNavigateContent} />
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
@ -81,17 +76,10 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = ({
|
const User = ({item}: {item: FollowItem}) => {
|
||||||
item,
|
const store = useStores()
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
|
||||||
item: FollowItem
|
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) => {
|
|
||||||
const onPressOuter = () => {
|
const onPressOuter = () => {
|
||||||
onNavigateContent('Profile', {
|
store.nav.navigate(`/profile/${item.name}`)
|
||||||
name: item.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {OnNavigateContent} from '../../routes/types'
|
|
||||||
import {ProfileViewModel} from '../../../state/models/profile-view'
|
import {ProfileViewModel} from '../../../state/models/profile-view'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {pluralize} from '../../lib/strings'
|
import {pluralize} from '../../lib/strings'
|
||||||
|
@ -19,10 +18,8 @@ import Toast from '../util/Toast'
|
||||||
|
|
||||||
export const ProfileHeader = observer(function ProfileHeader({
|
export const ProfileHeader = observer(function ProfileHeader({
|
||||||
user,
|
user,
|
||||||
onNavigateContent,
|
|
||||||
}: {
|
}: {
|
||||||
user: string
|
user: string
|
||||||
onNavigateContent: OnNavigateContent
|
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [view, setView] = useState<ProfileViewModel | undefined>()
|
const [view, setView] = useState<ProfileViewModel | undefined>()
|
||||||
|
@ -55,10 +52,10 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const onPressFollowers = () => {
|
const onPressFollowers = () => {
|
||||||
onNavigateContent('ProfileFollowers', {name: user})
|
store.nav.navigate(`/profile/${user}/followers`)
|
||||||
}
|
}
|
||||||
const onPressFollows = () => {
|
const onPressFollows = () => {
|
||||||
onNavigateContent('ProfileFollows', {name: user})
|
store.nav.navigate(`/profile/${user}/follows`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loading
|
// 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 {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 {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
|
||||||
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
||||||
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
|
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
|
||||||
|
import {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 {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 {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} from '@fortawesome/free-regular-svg-icons/faHeart'
|
||||||
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
|
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
|
||||||
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
|
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
|
||||||
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
|
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 {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
|
||||||
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
|
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
|
||||||
import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
|
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'
|
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
|
||||||
|
|
||||||
export function setup() {
|
export function setup() {
|
||||||
library.add(
|
library.add(
|
||||||
|
faAngleLeft,
|
||||||
|
faAngleRight,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faBars,
|
faBars,
|
||||||
faBell,
|
faBell,
|
||||||
|
farBell,
|
||||||
|
faBookmark,
|
||||||
|
farBookmark,
|
||||||
faCheck,
|
faCheck,
|
||||||
|
faClone,
|
||||||
faComment,
|
faComment,
|
||||||
|
faEllipsis,
|
||||||
faHeart,
|
faHeart,
|
||||||
fasHeart,
|
fasHeart,
|
||||||
faHouse,
|
faHouse,
|
||||||
faPlus,
|
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
|
faMessage,
|
||||||
|
faPenNib,
|
||||||
|
faPlus,
|
||||||
faRetweet,
|
faRetweet,
|
||||||
faShareFromSquare,
|
faShareFromSquare,
|
||||||
|
faUser,
|
||||||
|
faUsers,
|
||||||
faX,
|
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 React from 'react'
|
||||||
import {Text, View} from 'react-native'
|
import {Text, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Shell} from '../../shell'
|
|
||||||
// import type {RootTabsScreenProps} from '../routes/types'
|
|
||||||
// import {useStores} from '../../state'
|
// import {useStores} from '../../state'
|
||||||
|
|
||||||
export const Login = observer(
|
export const Login = observer(
|
||||||
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
||||||
// const store = useStores()
|
// const store = useStores()
|
||||||
return (
|
return (
|
||||||
<Shell>
|
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||||
{/*store.session.uiError && <Text>{store.session.uiError}</Text>}
|
{/*store.session.uiError && <Text>{store.session.uiError}</Text>}
|
||||||
|
@ -25,7 +22,6 @@ export const Login = observer(
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
)*/}
|
)*/}
|
||||||
</View>
|
</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 React from 'react'
|
||||||
import {Text, View} from 'react-native'
|
import {Text, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Shell} from '../../shell'
|
|
||||||
// import type {RootTabsScreenProps} from '../routes/types'
|
|
||||||
// import {useStores} from '../../state'
|
// import {useStores} from '../../state'
|
||||||
|
|
||||||
export const Signup = observer(
|
export const Signup = observer(
|
||||||
(/*{navigation}: RootTabsScreenProps<'Signup'>*/) => {
|
(/*{navigation}: RootTabsScreenProps<'Signup'>*/) => {
|
||||||
// const store = useStores()
|
// const store = useStores()
|
||||||
return (
|
return (
|
||||||
<Shell>
|
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||||
{/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
{/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
|
@ -28,7 +25,6 @@ export const Signup = observer(
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
)*/}
|
)*/}
|
||||||
</View>
|
</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 React from 'react'
|
||||||
import {Pressable, View, StyleSheet} from 'react-native'
|
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}> = ({
|
export const NavItem: React.FC<{label: string; screen: string}> = ({
|
||||||
label,
|
label,
|
||||||
screen,
|
screen,
|
||||||
}) => {
|
}) => {
|
||||||
const route = useRoute()
|
const Link = <></> // TODO
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
@ -18,7 +16,7 @@ export const NavItem: React.FC<{label: string; screen: string}> = ({
|
||||||
<Link
|
<Link
|
||||||
style={[
|
style={[
|
||||||
styles.navItemLink,
|
styles.navItemLink,
|
||||||
route.name === screen && styles.navItemLinkSelected,
|
false /* TODO route.name === screen*/ && styles.navItemLinkSelected,
|
||||||
]}
|
]}
|
||||||
to={{screen, params: {}}}>
|
to={{screen, params: {}}}>
|
||||||
{label}
|
{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
|
- Reposted by
|
||||||
- Followers list
|
- Followers list
|
||||||
- Follows 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"
|
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa"
|
||||||
integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==
|
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":
|
"@rollup/plugin-babel@^5.2.0":
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
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"
|
map-visit "^1.0.0"
|
||||||
object-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"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
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"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
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"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
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:
|
colord@^2.9.1:
|
||||||
version "2.9.2"
|
version "2.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
|
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
|
||||||
|
@ -6553,11 +6478,6 @@ fill-range@^7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
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:
|
finalhandler@1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
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"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
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:
|
is-bigint@^1.0.1:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
|
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"
|
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.5.tgz#f2d894a26664b454a90abf5a8911b7e39195db80"
|
||||||
integrity sha512-vMwf/FUO+qAPvl3vlSZEgEVFY/AxeZq5yg761ScF3CZsXgmTi/HGkicUiNN0CI4PW8FiY2P0OLklOcmQjdQJhw==
|
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"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||||
|
@ -11234,16 +11149,6 @@ qs@6.10.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel "^1.0.4"
|
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:
|
queue-microtask@^1.2.2, queue-microtask@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
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"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
@ -12333,13 +12238,6 @@ simple-plist@^1.1.0:
|
||||||
bplist-parser "0.3.1"
|
bplist-parser "0.3.1"
|
||||||
plist "^3.0.5"
|
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:
|
sisteransi@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
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"
|
select-hose "^2.0.0"
|
||||||
spdy-transport "^3.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:
|
split-string@^3.0.1, split-string@^3.0.2:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
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"
|
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4"
|
||||||
integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==
|
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:
|
string-hash-64@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
|
resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
|
||||||
|
|
Loading…
Reference in New Issue