Update web header and search

zio/stable
Paul Frazee 2023-02-23 01:07:20 -06:00
parent c5f28376c8
commit e828f380e7
8 changed files with 291 additions and 144 deletions

View File

@ -20,6 +20,7 @@ module.exports = {
alias: { alias: {
// This needs to be mirrored in tsconfig.json // This needs to be mirrored in tsconfig.json
lib: './src/lib', lib: './src/lib',
platform: './src/platform',
state: './src/state', state: './src/state',
view: './src/view', view: './src/view',
}, },

View File

@ -1,69 +1,51 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CenteredView} from './Views'
import {Text} from './text/Text' import {Text} from './text/Text'
import {useStores} from 'state/index' import {Link} from './Link'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {ComposeIcon, MagnifyingGlassIcon} from 'lib/icons'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
export const ViewHeader = observer(function ViewHeader({ export const ViewHeader = observer(function ViewHeader({
title, title,
subtitle,
canGoBack,
}: { }: {
title: string title: string
subtitle?: string
canGoBack?: boolean canGoBack?: boolean
}) { }) {
const pal = usePalette('default')
const store = useStores() const store = useStores()
const onPressBack = () => { const pal = usePalette('default')
store.nav.tab.goBack() const onPressCompose = () => store.shell.openComposer({})
}
if (typeof canGoBack === 'undefined') {
canGoBack = store.nav.tab.canGoBack
}
return ( return (
<CenteredView style={[styles.header, pal.view]}> <View style={[styles.header, pal.borderDark, pal.view]}>
{canGoBack ? ( <View style={styles.titleContainer} pointerEvents="none">
<> <Text type="title-2xl" style={[pal.text, styles.title]}>
<TouchableOpacity {title}
testID="viewHeaderBackOrMenuBtn" </Text>
onPress={onPressBack} </View>
hitSlop={BACK_HITSLOP} <TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
style={styles.backBtn}> <View style={styles.newPostBtnIconWrapper}>
<FontAwesomeIcon <ComposeIcon
size={18} size={16}
icon="angle-left" strokeWidth={1.5}
style={[styles.backIcon, pal.text]} style={styles.newPostBtnLabel}
/> />
</TouchableOpacity>
<View style={styles.titleContainer} pointerEvents="none">
<Text type="title" style={[pal.text, styles.title]}>
{title}
</Text>
{subtitle ? (
<Text
type="title-sm"
style={[styles.subtitle, pal.textLight]}
numberOfLines={1}>
{subtitle}
</Text>
) : undefined}
</View>
</>
) : (
<View style={styles.titleContainer} pointerEvents="none">
<Text type="title" style={[pal.text, styles.title]}>
Home
</Text>
</View> </View>
)} <Text type="md" style={styles.newPostBtnLabel}>
</CenteredView> New Post
</Text>
</TouchableOpacity>
<Link href="/search" style={[pal.view, pal.borderDark, styles.search]}>
<MagnifyingGlassIcon
size={18}
style={[pal.textLight, styles.searchIconWrapper]}
/>
<Text type="md-thin" style={pal.textLight}>
Search
</Text>
</Link>
</View>
) )
}) })
@ -71,44 +53,52 @@ const styles = StyleSheet.create({
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16, paddingTop: 24,
paddingVertical: 12, paddingBottom: 18,
paddingLeft: 30,
paddingRight: 40,
marginLeft: 300,
borderBottomWidth: 1,
}, },
titleContainer: { titleContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginRight: 'auto', marginRight: 'auto',
}, },
title: { title: {
fontWeight: 'bold', fontWeight: 'bold',
}, },
subtitle: {
marginLeft: 4, search: {
maxWidth: 200, flexDirection: 'row',
fontWeight: 'normal', alignItems: 'center',
width: 300,
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 10,
borderWidth: 1,
},
searchIconWrapper: {
flexDirection: 'row',
width: 30,
justifyContent: 'center',
marginRight: 2,
}, },
backBtn: { newPostBtn: {
width: 30,
},
backIcon: {
position: 'relative',
top: -1,
},
btn: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 36, borderRadius: 24,
height: 36, paddingTop: 8,
borderRadius: 20, paddingBottom: 9,
marginLeft: 4, paddingHorizontal: 18,
backgroundColor: colors.blue3,
marginRight: 10,
}, },
littleXIcon: { newPostBtnIconWrapper: {
color: colors.red3, marginRight: 8,
position: 'absolute', },
right: 7, newPostBtnLabel: {
bottom: 7, color: colors.white,
}, },
}) })

View File

@ -22,7 +22,6 @@ import {
View, View,
ViewProps, ViewProps,
} from 'react-native' } from 'react-native'
import {useTheme} from 'lib/ThemeContext'
import {addStyle, colors} from 'lib/styles' import {addStyle, colors} from 'lib/styles'
export function CenteredView({ export function CenteredView({
@ -40,15 +39,10 @@ export const FlatList = React.forwardRef(function <ItemT>(
}: React.PropsWithChildren<FlatListProps<ItemT>>, }: React.PropsWithChildren<FlatListProps<ItemT>>,
ref: React.Ref<RNFlatList>, ref: React.Ref<RNFlatList>,
) { ) {
const theme = useTheme()
contentContainerStyle = addStyle( contentContainerStyle = addStyle(
contentContainerStyle, contentContainerStyle,
styles.containerScroll, styles.containerScroll,
) )
contentContainerStyle = addStyle(
contentContainerStyle,
theme.colorScheme === 'dark' ? styles.containerDark : styles.containerLight,
)
return ( return (
<RNFlatList <RNFlatList
contentContainerStyle={contentContainerStyle} contentContainerStyle={contentContainerStyle}
@ -60,17 +54,12 @@ export const FlatList = React.forwardRef(function <ItemT>(
export const ScrollView = React.forwardRef(function ( export const ScrollView = React.forwardRef(function (
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
ref: React.Ref<RNFlatList>, ref: React.Ref<RNScrollView>,
) { ) {
const theme = useTheme()
contentContainerStyle = addStyle( contentContainerStyle = addStyle(
contentContainerStyle, contentContainerStyle,
styles.containerScroll, styles.containerScroll,
) )
contentContainerStyle = addStyle(
contentContainerStyle,
theme.colorScheme === 'dark' ? styles.containerDark : styles.containerLight,
)
return ( return (
<RNScrollView <RNScrollView
contentContainerStyle={contentContainerStyle} contentContainerStyle={contentContainerStyle}

View File

@ -11,6 +11,7 @@ import {ScreenParams} from '../routes'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {isWeb} from 'platform/detection'
const HEADER_HEIGHT = 42 const HEADER_HEIGHT = 42
@ -91,6 +92,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
return ( return (
<View style={s.h100pct}> <View style={s.h100pct}>
{isWeb && <ViewHeader title="Home Feed" canGoBack={false} />}
<Feed <Feed
testID="homeFeed" testID="homeFeed"
key="default" key="default"
@ -102,7 +104,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
onScroll={onMainScroll} onScroll={onMainScroll}
headerOffset={HEADER_HEIGHT} headerOffset={HEADER_HEIGHT}
/> />
<ViewHeader title="Bluesky" canGoBack={false} hideOnScroll /> {!isWeb && <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />}
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
<LoadLatestBtn onPress={onPressLoadLatest} /> <LoadLatestBtn onPress={onPressLoadLatest} />
)} )}

View File

@ -0,0 +1,217 @@
import React from 'react'
import {
Keyboard,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {ScrollView} from '../com/util/Views'
import {observer} from 'mobx-react-lite'
import {UserAvatar} from '../com/util/UserAvatar'
import {Text} from '../com/util/text/Text'
import {ScreenParams} from '../routes'
import {useStores} from 'state/index'
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
import {s} from 'lib/styles'
import {MagnifyingGlassIcon} from 'lib/icons'
import {ViewHeader} from '../com/util/ViewHeader'
import {WhoToFollow} from '../com/discover/WhoToFollow'
import {SuggestedPosts} from '../com/discover/SuggestedPosts'
import {ProfileCard} from '../com/profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics'
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
const FIVE_MIN = 5 * 60 * 1e3
export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics()
const scrollElRef = React.useRef<ScrollView>(null)
const onMainScroll = useOnMainScroll(store)
const textInput = React.useRef<TextInput>(null)
const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('')
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
() => new UserAutocompleteViewModel(store),
[store],
)
const {name} = params
const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0})
}
React.useEffect(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
if (visible) {
const now = Date.now()
if (now - lastRenderTime > FIVE_MIN) {
setRenderTime(Date.now()) // trigger reload of suggestions
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
store.nav.setTitle(navIdx, 'Search')
}
return cleanup
}, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
const onPressMenu = () => {
track('ViewHeader:MenuButtonClicked')
store.shell.setMainMenuOpen(true)
}
const onChangeQuery = (text: string) => {
setQuery(text)
if (text.length > 0) {
autocompleteView.setActive(true)
autocompleteView.setPrefix(text)
} else {
autocompleteView.setActive(false)
}
}
const onPressCancelSearch = () => {
setQuery('')
autocompleteView.setActive(false)
}
return (
<View style={s.h100pct}>
<ViewHeader title="Explore" />
<ScrollView
ref={scrollElRef}
testID="searchScrollView"
style={[pal.view, styles.container]}
onScroll={onMainScroll}
scrollEventThrottle={100}>
<View style={[pal.view, pal.border, styles.header]}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={onPressMenu}
hitSlop={MENU_HITSLOP}
style={styles.headerMenuBtn}>
<UserAvatar
size={30}
handle={store.me.handle}
displayName={store.me.displayName}
avatar={store.me.avatar}
/>
</TouchableOpacity>
<View
style={[
{backgroundColor: pal.colors.backgroundLight},
styles.headerSearchContainer,
]}>
<MagnifyingGlassIcon
style={[pal.icon, styles.headerSearchIcon]}
size={21}
/>
<TextInput
testID="searchTextInput"
ref={textInput}
placeholder="Search"
placeholderTextColor={pal.colors.textLight}
selectTextOnFocus
returnKeyType="search"
value={query}
style={[pal.text, styles.headerSearchInput]}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
/>
</View>
{query ? (
<View style={styles.headerCancelBtn}>
<TouchableOpacity onPress={onPressCancelSearch}>
<Text>Cancel</Text>
</TouchableOpacity>
</View>
) : undefined}
</View>
{query && autocompleteView.searchRes.length ? (
<>
{autocompleteView.searchRes.map(item => (
<ProfileCard
key={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
/>
))}
</>
) : query && !autocompleteView.searchRes.length ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
No results found for {autocompleteView.prefix}
</Text>
</View>
) : isInputFocused ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
Search for users on the network
</Text>
</View>
) : (
<ScrollView onScroll={Keyboard.dismiss}>
<WhoToFollow key={`wtf-${lastRenderTime}`} />
<SuggestedPosts key={`sp-${lastRenderTime}`} />
<View style={s.footerSpacer} />
</ScrollView>
)}
<View style={s.footerSpacer} />
</ScrollView>
</View>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 4,
marginBottom: 14,
},
headerMenuBtn: {
width: 40,
height: 30,
marginLeft: 6,
},
headerSearchContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 30,
paddingHorizontal: 12,
paddingVertical: 8,
},
headerSearchIcon: {
marginRight: 6,
alignSelf: 'center',
},
headerSearchInput: {
flex: 1,
fontSize: 17,
},
headerCancelBtn: {
width: 60,
paddingLeft: 10,
},
searchPrompt: {
textAlign: 'center',
paddingTop: 10,
},
})

View File

@ -70,12 +70,7 @@ export const DesktopLeftColumn = observer(() => {
styles.containerBgLight, styles.containerBgLight,
styles.containerBgDark, styles.containerBgDark,
) )
const hoverBg = useColorSchemeStyle(
styles.navItemHoverBgLight,
styles.navItemHoverBgDark,
)
const pal = usePalette('default') const pal = usePalette('default')
const onPressCompose = () => store.shell.openComposer({})
const avi = ( const avi = (
<UserAvatar <UserAvatar
handle={store.me.handle} handle={store.me.handle}
@ -90,15 +85,6 @@ export const DesktopLeftColumn = observer(() => {
<Link style={styles.logo} href="/"> <Link style={styles.logo} href="/">
<Text type="title-xl">Bluesky</Text> <Text type="title-xl">Bluesky</Text>
</Link> </Link>
<Link href="/search" style={[pal.view, pal.borderDark, styles.search]}>
<MagnifyingGlassIcon
size={18}
style={[pal.textLight, styles.searchIconWrapper]}
/>
<Text type="md-thin" style={pal.textLight}>
Search
</Text>
</Link>
<NavItem <NavItem
href="/" href="/"
label="Home" label="Home"
@ -124,19 +110,6 @@ export const DesktopLeftColumn = observer(() => {
icon={<CogIcon strokeWidth={2} size={21} />} icon={<CogIcon strokeWidth={2} size={21} />}
iconFilled={<CogIcon strokeWidth={2.5} size={21} />} iconFilled={<CogIcon strokeWidth={2.5} size={21} />}
/> />
<View style={[pal.border, styles.separator]} />
<Pressable
style={state => [
// @ts-ignore Pressable state differs for RNW -prf
state.hovered && hoverBg,
]}>
<TouchableOpacity style={styles.navItem} onPress={onPressCompose}>
<View style={styles.navItemIconWrapper}>
<ComposeIcon size={21} />
</View>
<Text type="xl-thin">New Post</Text>
</TouchableOpacity>
</Pressable>
</View> </View>
<View style={[styles.footer, pal.borderDark]}> <View style={[styles.footer, pal.borderDark]}>
<NavItem <NavItem
@ -183,24 +156,8 @@ const styles = StyleSheet.create({
}, },
logo: { logo: {
paddingTop: 6, paddingTop: 8,
paddingBottom: 12, paddingBottom: 14,
},
search: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 6,
marginBottom: 10,
borderWidth: 1,
},
searchIconWrapper: {
flexDirection: 'row',
width: 30,
justifyContent: 'center',
marginRight: 6,
}, },
navItem: { navItem: {
@ -240,13 +197,4 @@ const styles = StyleSheet.create({
paddingHorizontal: 4, paddingHorizontal: 4,
borderRadius: 6, borderRadius: 6,
}, },
composeBtn: {
marginTop: 20,
marginBottom: 10,
marginLeft: 10,
marginRight: 20,
borderRadius: 30,
paddingHorizontal: 20,
paddingVertical: 12,
},
}) })

View File

@ -3,10 +3,8 @@ import {StyleSheet, View} from 'react-native'
import {Text} from '../../com/util/text/Text' import {Text} from '../../com/util/text/Text'
import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows' import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index'
export const DesktopRightColumn: React.FC = () => { export const DesktopRightColumn: React.FC = () => {
const store = useStores()
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text type="lg-bold" style={s.mb10}> <Text type="lg-bold" style={s.mb10}>
@ -21,6 +19,7 @@ const styles = StyleSheet.create({
container: { container: {
position: 'absolute', position: 'absolute',
right: 0, right: 0,
top: 90,
width: '400px', width: '400px',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingRight: 32, paddingRight: 32,

View File

@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"lib/*": ["./src/lib/*"], "lib/*": ["./src/lib/*"],
"platform/*": ["./src/platform/*"],
"state/*": ["./src/state/*"], "state/*": ["./src/state/*"],
"view/*": ["./src/view/*"] "view/*": ["./src/view/*"]
} }