Bundle of UI modifications (#175)
* Adjust visual balance of SuggestedPosts and WhoToFollow * Fix bug in the discovery load trigger * Adjust search header aesthetic and have it scroll away * More visual balance tweaks on the search page * Even more visual balance tweaks on the search page * Hide the footer on scroll in search * Ditch the composer prompt buttons in the home feed * Center the view header title * Hide header on scroll on the home feed
This commit is contained in:
@@ -25,7 +25,7 @@ export const SuggestedPosts = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && (
|
{(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && (
|
||||||
<Text type="lg-heavy" style={[styles.heading, pal.text]}>
|
<Text type="title" style={[styles.heading, pal.text]}>
|
||||||
Recently, on Bluesky...
|
Recently, on Bluesky...
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -49,6 +49,7 @@ export const SuggestedPosts = observer(() => {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
heading: {
|
heading: {
|
||||||
|
fontWeight: 'bold',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingTop: 16,
|
paddingTop: 16,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import _omit from 'lodash.omit'
|
import _omit from 'lodash.omit'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +13,7 @@ import {
|
|||||||
SuggestedActor,
|
SuggestedActor,
|
||||||
} from '../../../state/models/suggested-actors-view'
|
} from '../../../state/models/suggested-actors-view'
|
||||||
import * as apilib from '../../../state/lib/api'
|
import * as apilib from '../../../state/lib/api'
|
||||||
import {s, gradients} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ProfileCard} from '../profile/ProfileCard'
|
import {ProfileCard} from '../profile/ProfileCard'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
@@ -59,7 +57,7 @@ export const WhoToFollow = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
|
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
|
||||||
<Text type="lg-heavy" style={[styles.heading, pal.text]}>
|
<Text type="title" style={[styles.heading, pal.text]}>
|
||||||
Who to follow
|
Who to follow
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -86,7 +84,7 @@ export const WhoToFollow = observer(() => {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressLoadMoreSuggestedActors}
|
onPress={onPressLoadMoreSuggestedActors}
|
||||||
style={styles.loadMore}>
|
style={styles.loadMore}>
|
||||||
<Text type="md-medium" style={pal.link}>
|
<Text type="lg" style={pal.link}>
|
||||||
Show more
|
Show more
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -110,33 +108,20 @@ function FollowBtn({
|
|||||||
onPress: () => void
|
onPress: () => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
if (isFollowing) {
|
|
||||||
return (
|
|
||||||
<TouchableOpacity onPress={onPress}>
|
|
||||||
<View style={[styles.btn, pal.btn]}>
|
|
||||||
<Text type="button" style={pal.text}>
|
|
||||||
Unfollow
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPress}>
|
<TouchableOpacity onPress={onPress}>
|
||||||
<LinearGradient
|
<View style={[styles.btn, pal.btn]}>
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
<Text type="button" style={[pal.text]}>
|
||||||
start={{x: 0, y: 0}}
|
{isFollowing ? 'Unfollow' : 'Follow'}
|
||||||
end={{x: 1, y: 1}}
|
</Text>
|
||||||
style={[styles.btn, styles.gradientBtn]}>
|
</View>
|
||||||
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} size={15} />
|
|
||||||
<Text style={[s.white, s.fw600, s.f15]}>Follow</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
heading: {
|
heading: {
|
||||||
|
fontWeight: 'bold',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingTop: 16,
|
paddingTop: 16,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
@@ -147,8 +132,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadMore: {
|
loadMore: {
|
||||||
paddingLeft: 12,
|
paddingLeft: 16,
|
||||||
paddingVertical: 10,
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
btn: {
|
btn: {
|
||||||
@@ -160,8 +145,4 @@ const styles = StyleSheet.create({
|
|||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
},
|
},
|
||||||
gradientBtn: {
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
paddingVertical: 6,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-11
@@ -13,30 +13,29 @@ import {EmptyState} from '../util/EmptyState'
|
|||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {FeedModel} from '../../../state/models/feed-view'
|
import {FeedModel} from '../../../state/models/feed-view'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {PromptButtons} from './PromptButtons'
|
|
||||||
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {useAnalytics} from '@segment/analytics-react-native'
|
import {useAnalytics} from '@segment/analytics-react-native'
|
||||||
|
|
||||||
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
|
const HEADER_SPACER_ITEM = {_reactKey: '__spacer__'}
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
feed,
|
feed,
|
||||||
style,
|
style,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
onPressCompose,
|
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onScroll,
|
onScroll,
|
||||||
testID,
|
testID,
|
||||||
|
headerSpacer,
|
||||||
}: {
|
}: {
|
||||||
feed: FeedModel
|
feed: FeedModel
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressCompose: (imagesOpen?: boolean) => void
|
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
onScroll?: OnScrollCb
|
onScroll?: OnScrollCb
|
||||||
testID?: string
|
testID?: string
|
||||||
|
headerSpacer?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
|
|
||||||
@@ -49,9 +48,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: any}) => {
|
const renderItem = ({item}: {item: any}) => {
|
||||||
if (item === COMPOSE_PROMPT_ITEM) {
|
if (item === EMPTY_FEED_ITEM) {
|
||||||
return <PromptButtons onPressCompose={onPressCompose} />
|
|
||||||
} else if (item === EMPTY_FEED_ITEM) {
|
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon="bars"
|
icon="bars"
|
||||||
@@ -59,6 +56,9 @@ export const Feed = observer(function Feed({
|
|||||||
style={styles.emptyState}
|
style={styles.emptyState}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
if (item === HEADER_SPACER_ITEM) {
|
||||||
|
return <View style={styles.headerSpacer} />
|
||||||
} else {
|
} else {
|
||||||
return <FeedItem item={item} />
|
return <FeedItem item={item} />
|
||||||
}
|
}
|
||||||
@@ -77,12 +77,15 @@ export const Feed = observer(function Feed({
|
|||||||
.loadMore()
|
.loadMore()
|
||||||
.catch(err => feed.rootStore.log.error('Failed to load more posts', err))
|
.catch(err => feed.rootStore.log.error('Failed to load more posts', err))
|
||||||
}
|
}
|
||||||
let data
|
let data = []
|
||||||
|
if (headerSpacer) {
|
||||||
|
data.push(HEADER_SPACER_ITEM)
|
||||||
|
}
|
||||||
if (feed.hasLoaded) {
|
if (feed.hasLoaded) {
|
||||||
if (feed.isEmpty) {
|
if (feed.isEmpty) {
|
||||||
data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM]
|
data.push(EMPTY_FEED_ITEM)
|
||||||
} else {
|
} else {
|
||||||
data = [COMPOSE_PROMPT_ITEM].concat(feed.feed)
|
data = data.concat(feed.feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const FeedFooter = () =>
|
const FeedFooter = () =>
|
||||||
@@ -95,7 +98,6 @@ export const Feed = observer(function Feed({
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{!data && <PromptButtons onPressCompose={onPressCompose} />}
|
|
||||||
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
|
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
|
||||||
{feed.hasError && (
|
{feed.hasError && (
|
||||||
<ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} />
|
<ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} />
|
||||||
@@ -120,6 +122,7 @@ export const Feed = observer(function Feed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
headerSpacer: {height: 42},
|
||||||
feedFooter: {paddingTop: 20},
|
feedFooter: {paddingTop: 20},
|
||||||
emptyState: {paddingVertical: 40},
|
emptyState: {paddingVertical: 40},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
|
||||||
import {useAnalytics} from '@segment/analytics-react-native'
|
|
||||||
|
|
||||||
export function PromptButtons({
|
|
||||||
onPressCompose,
|
|
||||||
}: {
|
|
||||||
onPressCompose: (imagesOpen?: boolean) => void
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
|
|
||||||
const onPressNewPost = () => {
|
|
||||||
track('PromptButtons:NewPost')
|
|
||||||
onPressCompose(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressSharePhoto = () => {
|
|
||||||
track('PromptButtons:SharePhoto')
|
|
||||||
onPressCompose(true)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={[pal.view, pal.border, styles.container]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="composePromptButton"
|
|
||||||
onPress={onPressNewPost}
|
|
||||||
style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}>
|
|
||||||
<Text type="button" style={pal.text}>
|
|
||||||
New post
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressSharePhoto}
|
|
||||||
style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}>
|
|
||||||
<Text type="button" style={pal.text}>
|
|
||||||
Share photo
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTopWidth: 1,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
borderRadius: 30,
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
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 {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {UserAvatar} from './UserAvatar'
|
import {UserAvatar} from './UserAvatar'
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
|
||||||
import {useAnalytics} from '@segment/analytics-react-native'
|
import {useAnalytics} from '@segment/analytics-react-native'
|
||||||
|
|
||||||
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
||||||
@@ -13,9 +14,11 @@ 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,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
|
hideOnScroll,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
canGoBack?: boolean
|
canGoBack?: boolean
|
||||||
|
hideOnScroll?: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
@@ -31,7 +34,7 @@ export const ViewHeader = observer(function ViewHeader({
|
|||||||
canGoBack = store.nav.tab.canGoBack
|
canGoBack = store.nav.tab.canGoBack
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={[styles.header, pal.view]}>
|
<Container hideOnScroll={hideOnScroll || false}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="viewHeaderBackOrMenuBtn"
|
testID="viewHeaderBackOrMenuBtn"
|
||||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||||
@@ -57,10 +60,56 @@ export const ViewHeader = observer(function ViewHeader({
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Container = observer(
|
||||||
|
({
|
||||||
|
children,
|
||||||
|
hideOnScroll,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
hideOnScroll: boolean
|
||||||
|
}) => {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const interp = useAnimatedValue(0)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (store.shell.minimalShellMode) {
|
||||||
|
Animated.timing(interp, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 100,
|
||||||
|
useNativeDriver: true,
|
||||||
|
isInteraction: false,
|
||||||
|
}).start()
|
||||||
|
} else {
|
||||||
|
Animated.timing(interp, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 100,
|
||||||
|
useNativeDriver: true,
|
||||||
|
isInteraction: false,
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
}, [interp, store.shell.minimalShellMode])
|
||||||
|
const transform = {
|
||||||
|
transform: [{translateY: Animated.multiply(interp, -100)}],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hideOnScroll) {
|
||||||
|
return <View style={[styles.header, pal.view]}>{children}</View>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.header, pal.view, styles.headerFloating, transform]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -69,11 +118,16 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 6,
|
paddingBottom: 6,
|
||||||
},
|
},
|
||||||
|
headerFloating: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
flexDirection: 'row',
|
marginLeft: 'auto',
|
||||||
alignItems: 'baseline',
|
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
|
paddingRight: 10,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
|||||||
@@ -88,17 +88,17 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={s.h100pct}>
|
<View style={s.h100pct}>
|
||||||
<ViewHeader title="Bluesky" canGoBack={false} />
|
|
||||||
<Feed
|
<Feed
|
||||||
testID="homeFeed"
|
testID="homeFeed"
|
||||||
key="default"
|
key="default"
|
||||||
feed={store.me.mainFeed}
|
feed={store.me.mainFeed}
|
||||||
scrollElRef={scrollElRef}
|
scrollElRef={scrollElRef}
|
||||||
style={s.h100pct}
|
style={s.h100pct}
|
||||||
onPressCompose={onPressCompose}
|
|
||||||
onPressTryAgain={onPressTryAgain}
|
onPressTryAgain={onPressTryAgain}
|
||||||
onScroll={onMainScroll}
|
onScroll={onMainScroll}
|
||||||
|
headerSpacer
|
||||||
/>
|
/>
|
||||||
|
<ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
|
||||||
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing ? (
|
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Followed" />
|
<ViewHeader title="Following" />
|
||||||
<ProfileFollowsComponent name={name} />
|
<ProfileFollowsComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
+45
-44
@@ -20,6 +20,7 @@ import {WhoToFollow} from '../com/discover/WhoToFollow'
|
|||||||
import {SuggestedPosts} from '../com/discover/SuggestedPosts'
|
import {SuggestedPosts} from '../com/discover/SuggestedPosts'
|
||||||
import {ProfileCard} from '../com/profile/ProfileCard'
|
import {ProfileCard} from '../com/profile/ProfileCard'
|
||||||
import {usePalette} from '../lib/hooks/usePalette'
|
import {usePalette} from '../lib/hooks/usePalette'
|
||||||
|
import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
|
||||||
import {useAnalytics} from '@segment/analytics-react-native'
|
import {useAnalytics} from '@segment/analytics-react-native'
|
||||||
|
|
||||||
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
||||||
@@ -30,8 +31,9 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const scrollElRef = React.useRef<ScrollView>(null)
|
const scrollElRef = React.useRef<ScrollView>(null)
|
||||||
|
const onMainScroll = useOnMainScroll(store)
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
const [lastRenderTime, setRenderTime] = React.useState<number>(0) // used to trigger reloads
|
const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
|
||||||
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
|
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
|
||||||
const [query, setQuery] = React.useState<string>('')
|
const [query, setQuery] = React.useState<string>('')
|
||||||
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
|
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
|
||||||
@@ -52,7 +54,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (lastRenderTime - now > FIVE_MIN) {
|
if (now - lastRenderTime > FIVE_MIN) {
|
||||||
setRenderTime(Date.now()) // trigger reload of suggestions
|
setRenderTime(Date.now()) // trigger reload of suggestions
|
||||||
}
|
}
|
||||||
store.shell.setMinimalShellMode(false)
|
store.shell.setMinimalShellMode(false)
|
||||||
@@ -83,7 +85,12 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
<View style={[pal.view, styles.container]}>
|
<ScrollView
|
||||||
|
ref={scrollElRef}
|
||||||
|
testID="searchScrollView"
|
||||||
|
style={[pal.view, styles.container]}
|
||||||
|
onScroll={onMainScroll}
|
||||||
|
scrollEventThrottle={100}>
|
||||||
<View style={[pal.view, pal.border, styles.header]}>
|
<View style={[pal.view, pal.border, styles.header]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="viewHeaderBackOrMenuBtn"
|
testID="viewHeaderBackOrMenuBtn"
|
||||||
@@ -128,40 +135,38 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.outputContainer}>
|
{query && autocompleteView.searchRes.length ? (
|
||||||
{query && autocompleteView.searchRes.length ? (
|
<>
|
||||||
<ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}>
|
{autocompleteView.searchRes.map(item => (
|
||||||
{autocompleteView.searchRes.map(item => (
|
<ProfileCard
|
||||||
<ProfileCard
|
key={item.did}
|
||||||
key={item.did}
|
handle={item.handle}
|
||||||
handle={item.handle}
|
displayName={item.displayName}
|
||||||
displayName={item.displayName}
|
avatar={item.avatar}
|
||||||
avatar={item.avatar}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</>
|
||||||
<View style={s.footerSpacer} />
|
) : query && !autocompleteView.searchRes.length ? (
|
||||||
</ScrollView>
|
<View>
|
||||||
) : query && !autocompleteView.searchRes.length ? (
|
<Text style={[pal.textLight, styles.searchPrompt]}>
|
||||||
<View>
|
No results found for {autocompleteView.prefix}
|
||||||
<Text style={[pal.textLight, styles.searchPrompt]}>
|
</Text>
|
||||||
No results found for {autocompleteView.prefix}
|
</View>
|
||||||
</Text>
|
) : isInputFocused ? (
|
||||||
</View>
|
<View>
|
||||||
) : isInputFocused ? (
|
<Text style={[pal.textLight, styles.searchPrompt]}>
|
||||||
<View>
|
Search for users on the network
|
||||||
<Text style={[pal.textLight, styles.searchPrompt]}>
|
</Text>
|
||||||
Search for users on the network
|
</View>
|
||||||
</Text>
|
) : (
|
||||||
</View>
|
<ScrollView onScroll={Keyboard.dismiss}>
|
||||||
) : (
|
<WhoToFollow key={`wtf-${lastRenderTime}`} />
|
||||||
<ScrollView onScroll={Keyboard.dismiss} ref={scrollElRef}>
|
<SuggestedPosts key={`sp-${lastRenderTime}`} />
|
||||||
<WhoToFollow key={`wtf-${lastRenderTime}`} />
|
<View style={s.footerSpacer} />
|
||||||
<SuggestedPosts key={`sp-${lastRenderTime}`} />
|
</ScrollView>
|
||||||
<View style={s.footerSpacer} />
|
)}
|
||||||
</ScrollView>
|
<View style={s.footerSpacer} />
|
||||||
)}
|
</ScrollView>
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -176,7 +181,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 5,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
headerMenuBtn: {
|
headerMenuBtn: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -189,7 +194,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 6,
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
headerSearchIcon: {
|
headerSearchIcon: {
|
||||||
marginRight: 6,
|
marginRight: 6,
|
||||||
@@ -197,7 +202,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
headerSearchInput: {
|
headerSearchInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 17,
|
||||||
},
|
},
|
||||||
headerCancelBtn: {
|
headerCancelBtn: {
|
||||||
width: 60,
|
width: 60,
|
||||||
@@ -208,8 +213,4 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
outputContainer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user