Restructure the shell folder and fix the header positioning

This commit is contained in:
Paul Frazee
2023-03-10 13:24:50 -06:00
parent 3b0d72b776
commit ebb0b11774
16 changed files with 146 additions and 239 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ import {observer} from 'mobx-react-lite'
import {ThemeProvider} from 'lib/ThemeContext' import {ThemeProvider} from 'lib/ThemeContext'
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 {MobileShell} from './view/shell/mobile' import {Shell} from './view/shell'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import * as notifee from 'lib/notifee' import * as notifee from 'lib/notifee'
import * as analytics from 'lib/analytics' import * as analytics from 'lib/analytics'
@@ -54,7 +54,7 @@ const App = observer(() => {
<analytics.Provider> <analytics.Provider>
<RootStoreProvider value={rootStore}> <RootStoreProvider value={rootStore}>
<SafeAreaProvider> <SafeAreaProvider>
<MobileShell /> <Shell />
</SafeAreaProvider> </SafeAreaProvider>
</RootStoreProvider> </RootStoreProvider>
</analytics.Provider> </analytics.Provider>
+2 -2
View File
@@ -3,7 +3,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
import {getInitialURL} from 'platform/urls' import {getInitialURL} from 'platform/urls'
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 {WebShell} from './view/shell/web' import {Shell} from './view/shell/index'
import {ToastContainer} from './view/com/util/Toast.web' import {ToastContainer} from './view/com/util/Toast.web'
function App() { function App() {
@@ -33,7 +33,7 @@ function App() {
return ( return (
<RootStoreProvider value={rootStore}> <RootStoreProvider value={rootStore}>
<SafeAreaProvider> <SafeAreaProvider>
<WebShell /> <Shell />
</SafeAreaProvider> </SafeAreaProvider>
<ToastContainer /> <ToastContainer />
</RootStoreProvider> </RootStoreProvider>
-4
View File
@@ -1,4 +0,0 @@
{
"name": "xyz.blueskyweb.app",
"displayName": "Bluesky"
}
-12
View File
@@ -1,12 +0,0 @@
/**
* @format
*/
import {AppRegistry} from 'react-native'
import App from './App'
AppRegistry.registerComponent('App', () => App)
AppRegistry.runApplication('App', {
rootTag: document.getElementById('root'),
})
+77 -76
View File
@@ -1,19 +1,20 @@
import {LikelyType, LinkMeta} from './link-meta' import {LikelyType, LinkMeta} from './link-meta'
import {match as matchRoute} from 'view/routes' // import {match as matchRoute} from 'view/routes'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
import {RootStoreModel} from 'state/index' import {RootStoreModel} from 'state/index'
import {PostThreadViewModel} from 'state/models/post-thread-view' import {PostThreadViewModel} from 'state/models/post-thread-view'
import {ComposerOptsQuote} from 'state/models/shell-ui' import {ComposerOptsQuote} from 'state/models/shell-ui'
import {Home} from 'view/screens/Home' // TODO
import {Search} from 'view/screens/Search' // import {Home} from 'view/screens/Home'
import {Notifications} from 'view/screens/Notifications' // import {Search} from 'view/screens/Search'
import {PostThread} from 'view/screens/PostThread' // import {Notifications} from 'view/screens/Notifications'
import {PostUpvotedBy} from 'view/screens/PostUpvotedBy' // import {PostThread} from 'view/screens/PostThread'
import {PostRepostedBy} from 'view/screens/PostRepostedBy' // import {PostUpvotedBy} from 'view/screens/PostUpvotedBy'
import {Profile} from 'view/screens/Profile' // import {PostRepostedBy} from 'view/screens/PostRepostedBy'
import {ProfileFollowers} from 'view/screens/ProfileFollowers' // import {Profile} from 'view/screens/Profile'
import {ProfileFollows} from 'view/screens/ProfileFollows' // import {ProfileFollowers} from 'view/screens/ProfileFollowers'
// import {ProfileFollows} from 'view/screens/ProfileFollows'
// NOTE // NOTE
// this is a hack around the lack of hosted social metadata // this is a hack around the lack of hosted social metadata
@@ -24,77 +25,77 @@ export async function extractBskyMeta(
url: string, url: string,
): Promise<LinkMeta> { ): Promise<LinkMeta> {
url = convertBskyAppUrlIfNeeded(url) url = convertBskyAppUrlIfNeeded(url)
const route = matchRoute(url) // const route = matchRoute(url)
let meta: LinkMeta = { let meta: LinkMeta = {
likelyType: LikelyType.AtpData, likelyType: LikelyType.AtpData,
url, url,
title: route.defaultTitle, // title: route.defaultTitle,
} }
if (route.Com === Home) { // if (route.Com === Home) {
meta = { // meta = {
...meta, // ...meta,
title: 'Bluesky', // title: 'Bluesky',
description: 'A new kind of social network', // description: 'A new kind of social network',
} // }
} else if (route.Com === Search) { // } else if (route.Com === Search) {
meta = { // meta = {
...meta, // ...meta,
title: 'Search - Bluesky', // title: 'Search - Bluesky',
description: 'A new kind of social network', // description: 'A new kind of social network',
} // }
} else if (route.Com === Notifications) { // } else if (route.Com === Notifications) {
meta = { // meta = {
...meta, // ...meta,
title: 'Notifications - Bluesky', // title: 'Notifications - Bluesky',
description: 'A new kind of social network', // description: 'A new kind of social network',
} // }
} else if ( // } else if (
route.Com === PostThread || // route.Com === PostThread ||
route.Com === PostUpvotedBy || // route.Com === PostUpvotedBy ||
route.Com === PostRepostedBy // route.Com === PostRepostedBy
) { // ) {
// post and post-related screens // // post and post-related screens
const threadUri = makeRecordUri( // const threadUri = makeRecordUri(
route.params.name, // route.params.name,
'app.bsky.feed.post', // 'app.bsky.feed.post',
route.params.rkey, // route.params.rkey,
) // )
const threadView = new PostThreadViewModel(store, { // const threadView = new PostThreadViewModel(store, {
uri: threadUri, // uri: threadUri,
depth: 0, // depth: 0,
}) // })
await threadView.setup().catch(_err => undefined) // await threadView.setup().catch(_err => undefined)
const title = [ // const title = [
route.Com === PostUpvotedBy // route.Com === PostUpvotedBy
? 'Likes on a post by' // ? 'Likes on a post by'
: route.Com === PostRepostedBy // : route.Com === PostRepostedBy
? 'Reposts of a post by' // ? 'Reposts of a post by'
: 'Post by', // : 'Post by',
threadView.thread?.post.author.displayName || // threadView.thread?.post.author.displayName ||
threadView.thread?.post.author.handle || // threadView.thread?.post.author.handle ||
'a bluesky user', // 'a bluesky user',
].join(' ') // ].join(' ')
meta = { // meta = {
...meta, // ...meta,
title, // title,
description: threadView.thread?.postRecord?.text, // description: threadView.thread?.postRecord?.text,
} // }
} else if ( // } else if (
route.Com === Profile || // route.Com === Profile ||
route.Com === ProfileFollowers || // route.Com === ProfileFollowers ||
route.Com === ProfileFollows // route.Com === ProfileFollows
) { // ) {
// profile and profile-related screens // // profile and profile-related screens
const profile = await store.profiles.getProfile(route.params.name) // const profile = await store.profiles.getProfile(route.params.name)
if (profile?.data) { // if (profile?.data) {
meta = { // meta = {
...meta, // ...meta,
title: profile.data.displayName || profile.data.handle, // title: profile.data.displayName || profile.data.handle,
description: profile.data.description, // description: profile.data.description,
} // }
} // }
} // }
return meta return meta
} }
+19 -23
View File
@@ -11,21 +11,21 @@ import {
State, State,
} from 'lib/routes/types' } from 'lib/routes/types'
import {Drawer} from './shell/Drawer' import {Drawer} from './view/shell/Drawer'
import {BottomBar} from './shell/BottomBar' import {BottomBar} from './view/shell/BottomBar'
import {HomeScreen} from './screens/Home' import {HomeScreen} from './view/screens/Home'
import {SearchScreen} from './screens/Search' import {SearchScreen} from './view/screens/Search'
import {NotificationsScreen} from './screens/Notifications' import {NotificationsScreen} from './view/screens/Notifications'
import {SettingsScreen} from './screens/Settings' import {SettingsScreen} from './view/screens/Settings'
import {ProfileScreen} from './screens/Profile' import {ProfileScreen} from './view/screens/Profile'
import {ProfileFollowersScreen} from './screens/ProfileFollowers' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './screens/ProfileFollows' import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
import {PostThreadScreen} from './screens/PostThread' import {PostThreadScreen} from './view/screens/PostThread'
import {PostUpvotedByScreen} from './screens/PostUpvotedBy' import {PostUpvotedByScreen} from './view/screens/PostUpvotedBy'
import {PostRepostedByScreen} from './screens/PostRepostedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {DebugScreen} from './screens/Debug' import {DebugScreen} from './view/screens/Debug'
import {LogScreen} from './screens/Log' import {LogScreen} from './view/screens/Log'
const HomeDrawer = createDrawerNavigator() const HomeDrawer = createDrawerNavigator()
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>() const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
@@ -52,7 +52,7 @@ function r(pattern: string): Route {
match(path: string) { match(path: string) {
const res = matcherRe.exec(path) const res = matcherRe.exec(path)
if (res) { if (res) {
return {params: res.groups} return {params: res.groups || {}}
} }
return undefined return undefined
}, },
@@ -101,7 +101,7 @@ export const LINKING = {
getPathFromState(state: State) { getPathFromState(state: State) {
// find the current node in the navigation tree // find the current node in the navigation tree
let node = state.routes[state.index] let node = state.routes[state.index || 0]
while (node.state?.routes && typeof node.state?.index === 'number') { while (node.state?.routes && typeof node.state?.index === 'number') {
node = node.state?.routes[node.state?.index] node = node.state?.routes[node.state?.index]
} }
@@ -239,7 +239,7 @@ function SearchTabNavigator() {
) )
} }
function TabsNavigator() { export function TabsNavigator() {
const tabBar = React.useCallback(props => <BottomBar {...props} />, []) const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
return ( return (
<Tab.Navigator <Tab.Navigator
@@ -257,10 +257,6 @@ function TabsNavigator() {
) )
} }
export function Screens() { export function RoutesContainer({children}: React.PropsWithChildren<{}>) {
return ( return <NavigationContainer linking={LINKING}>{children}</NavigationContainer>
<NavigationContainer linking={LINKING}>
<TabsNavigator />
</NavigationContainer>
)
} }
+2 -2
View File
@@ -15,7 +15,7 @@ import {useLinkProps, useNavigation} from '@react-navigation/native'
import {Text} from './text/Text' import {Text} from './text/Text'
import {TypographyVariant} from 'lib/ThemeContext' import {TypographyVariant} from 'lib/ThemeContext'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {matchPath} from 'view/screens' import {matchPath} from '../../../Routes'
import {useStores, RootStoreModel} from 'state/index' import {useStores, RootStoreModel} from 'state/index'
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers' import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
@@ -36,7 +36,7 @@ export const Link = observer(function Link({
children?: React.ReactNode children?: React.ReactNode
noFeedback?: boolean noFeedback?: boolean
}) { }) {
let {...props} = useLinkProps({to: href}) let {...props} = useLinkProps({to: href || ''})
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
-82
View File
@@ -1,82 +0,0 @@
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 {NotFound} from './screens/NotFound'
import {PostThread} from './screens/PostThread'
import {PostUpvotedBy} from './screens/PostUpvotedBy'
import {PostRepostedBy} from './screens/PostRepostedBy'
import {Profile} from './screens/Profile'
import {ProfileFollowers} from './screens/ProfileFollowers'
import {ProfileFollows} from './screens/ProfileFollows'
import {Settings} from './screens/Settings'
import {Debug} from './screens/Debug'
import {Log} from './screens/Log'
export type ScreenParams = {
navIdx: string
params: Record<string, any>
visible: boolean
}
export type Route = [React.FC<ScreenParams>, string, IconProp, RegExp]
export type MatchResult = {
Com: React.FC<ScreenParams>
defaultTitle: string
icon: IconProp
params: Record<string, any>
isNotFound?: boolean
}
const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i')
export const routes: Route[] = [
[Home, 'Home', 'house', r('/')],
[Search, 'Search', 'magnifying-glass', r('/search')],
[Notifications, 'Notifications', 'bell', r('/notifications')],
[Settings, 'Settings', 'bell', r('/settings')],
[Profile, 'User', ['far', 'user'], r('/profile/(?<name>[^/]+)')],
[
ProfileFollowers,
'Followers',
'users',
r('/profile/(?<name>[^/]+)/followers'),
],
[ProfileFollows, 'Follows', 'users', r('/profile/(?<name>[^/]+)/follows')],
[
PostThread,
'Post',
['far', 'message'],
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)'),
],
[
PostUpvotedBy,
'Liked by',
'heart',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/upvoted-by'),
],
[
PostRepostedBy,
'Reposted by',
'retweet',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/reposted-by'),
],
[Debug, 'Debug', 'house', r('/sys/debug')],
[Log, 'Log', 'house', r('/sys/log')],
]
export function match(url: string): MatchResult {
for (const [Com, defaultTitle, icon, pattern] of routes) {
const res = pattern.exec(url)
if (res) {
// TODO: query params
return {Com, defaultTitle, icon, params: res.groups || {}}
}
}
return {
Com: NotFound,
defaultTitle: 'Not found',
icon: 'magnifying-glass',
params: {},
isNotFound: true,
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ import {ComposeIcon2} from 'lib/icons'
const HEADER_HEIGHT = 42 const HEADER_HEIGHT = 42
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = observer(function Home({}: Props) { export const HomeScreen = observer(function Home(_opts: Props) {
const store = useStores() const store = useStores()
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
@@ -1,7 +1,7 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Animated, Easing, Platform, StyleSheet, View} from 'react-native' import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
import {ComposePost} from '../../com/composer/ComposePost' import {ComposePost} from '../com/composer/ComposePost'
import {ComposerOpts} from 'state/models/shell-ui' import {ComposerOpts} from 'state/models/shell-ui'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {ComposePost} from '../../com/composer/ComposePost' import {ComposePost} from '../com/composer/ComposePost'
import {ComposerOpts} from 'state/models/shell-ui' import {ComposerOpts} from 'state/models/shell-ui'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
+1 -1
View File
@@ -318,7 +318,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
paddingTop: 20, paddingTop: 20,
paddingBottom: 50, paddingBottom: 50,
paddingLeft: 30, paddingLeft: 20,
}, },
viewDarkMode: { viewDarkMode: {
backgroundColor: '#1B1919', backgroundColor: '#1B1919',
@@ -16,7 +16,7 @@ import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
CogIcon, CogIcon,
} from 'lib/icons' } from 'lib/icons'
import {DesktopSearch} from './DesktopSearch' import {DesktopSearch} from './Search'
interface NavItemProps { interface NavItemProps {
count?: number count?: number
@@ -5,7 +5,7 @@ import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {MagnifyingGlassIcon} from 'lib/icons' import {MagnifyingGlassIcon} from 'lib/icons'
import {ProfileCard} from '../../com/profile/ProfileCard' import {ProfileCard} from '../com/profile/ProfileCard'
export const DesktopSearch = observer(function DesktopSearch() { export const DesktopSearch = observer(function DesktopSearch() {
const store = useStores() const store = useStores()
@@ -1,23 +1,28 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {StatusBar, StyleSheet, useWindowDimensions, View} from 'react-native' import {StatusBar, StyleSheet, useWindowDimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {Login} from '../../screens/Login' import {Login} from '../screens/Login'
import {ModalsContainer} from '../../com/modals/Modal' import {ModalsContainer} from '../com/modals/Modal'
import {Lightbox} from '../../com/lightbox/Lightbox' import {Lightbox} from '../com/lightbox/Lightbox'
import {Text} from '../../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {Composer} from './Composer' import {Composer} from './Composer'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {RoutesContainer, TabsNavigator} from '../../Routes'
import {Screens} from '../../screens' export const Shell: React.FC = observer(() => {
export const MobileShell: React.FC = observer(() => {
const theme = useTheme() const theme = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const safeAreaInsets = useSafeAreaInsets()
const containerPadding = React.useMemo(
() => ({height: '100%', paddingTop: safeAreaInsets.top}),
[safeAreaInsets],
)
if (store.hackUpgradeNeeded) { if (store.hackUpgradeNeeded) {
return ( return (
@@ -73,18 +78,22 @@ export const MobileShell: React.FC = observer(() => {
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content' theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
} }
/> />
<Screens /> <RoutesContainer>
<ModalsContainer /> <View style={containerPadding}>
<Lightbox /> <TabsNavigator />
<Composer </View>
active={store.shell.isComposerActive} <ModalsContainer />
onClose={() => store.shell.closeComposer()} <Lightbox />
winHeight={winDim.height} <Composer
replyTo={store.shell.composerOpts?.replyTo} active={store.shell.isComposerActive}
imagesOpen={store.shell.composerOpts?.imagesOpen} onClose={() => store.shell.closeComposer()}
onPost={store.shell.composerOpts?.onPost} winHeight={winDim.height}
quote={store.shell.composerOpts?.quote} replyTo={store.shell.composerOpts?.replyTo}
/> imagesOpen={store.shell.composerOpts?.imagesOpen}
onPost={store.shell.composerOpts?.onPost}
quote={store.shell.composerOpts?.quote}
/>
</RoutesContainer>
</View> </View>
) )
}) })
@@ -3,23 +3,22 @@ import {observer} from 'mobx-react-lite'
import {View, StyleSheet} from 'react-native' import {View, StyleSheet} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core' import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {NavigationModel} from 'state/models/navigation' import {match, MatchResult} from '../routes'
import {match, MatchResult} from '../../routes' import {DesktopHeader} from './desktop/Header'
import {DesktopHeader} from './DesktopHeader' import {Login} from '../screens/Login'
import {Login} from '../../screens/Login' import {ErrorBoundary} from '../com/util/ErrorBoundary'
import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {Lightbox} from '../com/lightbox/Lightbox'
import {Lightbox} from '../../com/lightbox/Lightbox' import {ModalsContainer} from '../com/modals/Modal'
import {ModalsContainer} from '../../com/modals/Modal'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {Composer} from './Composer' import {Composer} from './Composer.web'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {isMobileWeb} from 'platform/detection' import {isMobileWeb} from 'platform/detection'
import {Screens} from '../../screens' import {RoutesContainer} from '../../Routes'
export const WebShell: React.FC = observer(() => { export const Shell: React.FC = observer(() => {
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores() const store = useStores()
const screenRenderDesc = constructScreenRenderDesc(store.nav) const screenRenderDesc = constructScreenRenderDesc(store.nav)
@@ -28,7 +27,7 @@ export const WebShell: React.FC = observer(() => {
return <NoMobileWeb /> return <NoMobileWeb />
} }
return <Screens /> return <RoutesContainer />
if (!store.session.hasSession) { if (!store.session.hasSession) {
return ( return (