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:
Paul Frazee
2023-02-09 16:45:37 -06:00
committed by GitHub
parent 22820372df
commit 3ad0f3cc4a
8 changed files with 134 additions and 153 deletions
+2 -1
View File
@@ -25,7 +25,7 @@ export const SuggestedPosts = observer(() => {
return (
<>
{(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && (
<Text type="lg-heavy" style={[styles.heading, pal.text]}>
<Text type="title" style={[styles.heading, pal.text]}>
Recently, on Bluesky...
</Text>
)}
@@ -49,6 +49,7 @@ export const SuggestedPosts = observer(() => {
const styles = StyleSheet.create({
heading: {
fontWeight: 'bold',
paddingHorizontal: 12,
paddingTop: 16,
paddingBottom: 8,
+11 -30
View File
@@ -6,8 +6,6 @@ import {
View,
} from 'react-native'
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 {useStores} from '../../../state'
import {
@@ -15,7 +13,7 @@ import {
SuggestedActor,
} from '../../../state/models/suggested-actors-view'
import * as apilib from '../../../state/lib/api'
import {s, gradients} from '../../lib/styles'
import {s} from '../../lib/styles'
import {ProfileCard} from '../profile/ProfileCard'
import * as Toast from '../util/Toast'
import {Text} from '../util/text/Text'
@@ -59,7 +57,7 @@ export const WhoToFollow = observer(() => {
return (
<>
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
<Text type="lg-heavy" style={[styles.heading, pal.text]}>
<Text type="title" style={[styles.heading, pal.text]}>
Who to follow
</Text>
)}
@@ -86,7 +84,7 @@ export const WhoToFollow = observer(() => {
<TouchableOpacity
onPress={onPressLoadMoreSuggestedActors}
style={styles.loadMore}>
<Text type="md-medium" style={pal.link}>
<Text type="lg" style={pal.link}>
Show more
</Text>
</TouchableOpacity>
@@ -110,33 +108,20 @@ function FollowBtn({
onPress: () => void
}) {
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 (
<TouchableOpacity onPress={onPress}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn, styles.gradientBtn]}>
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} size={15} />
<Text style={[s.white, s.fw600, s.f15]}>Follow</Text>
</LinearGradient>
<View style={[styles.btn, pal.btn]}>
<Text type="button" style={[pal.text]}>
{isFollowing ? 'Unfollow' : 'Follow'}
</Text>
</View>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
heading: {
fontWeight: 'bold',
paddingHorizontal: 12,
paddingTop: 16,
paddingBottom: 8,
@@ -147,8 +132,8 @@ const styles = StyleSheet.create({
},
loadMore: {
paddingLeft: 12,
paddingVertical: 10,
paddingLeft: 16,
paddingVertical: 12,
},
btn: {
@@ -160,8 +145,4 @@ const styles = StyleSheet.create({
marginLeft: 6,
paddingHorizontal: 14,
},
gradientBtn: {
paddingHorizontal: 24,
paddingVertical: 6,
},
})
+14 -11
View File
@@ -13,30 +13,29 @@ import {EmptyState} from '../util/EmptyState'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {FeedModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem'
import {PromptButtons} from './PromptButtons'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
import {s} from '../../lib/styles'
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__'}
export const Feed = observer(function Feed({
feed,
style,
scrollElRef,
onPressCompose,
onPressTryAgain,
onScroll,
testID,
headerSpacer,
}: {
feed: FeedModel
style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressCompose: (imagesOpen?: boolean) => void
onPressTryAgain?: () => void
onScroll?: OnScrollCb
testID?: string
headerSpacer?: boolean
}) {
const {screen, track} = useAnalytics()
@@ -49,9 +48,7 @@ export const Feed = observer(function Feed({
// renderItem function renders components that follow React performance best practices
// like PureComponent, shouldComponentUpdate, etc
const renderItem = ({item}: {item: any}) => {
if (item === COMPOSE_PROMPT_ITEM) {
return <PromptButtons onPressCompose={onPressCompose} />
} else if (item === EMPTY_FEED_ITEM) {
if (item === EMPTY_FEED_ITEM) {
return (
<EmptyState
icon="bars"
@@ -59,6 +56,9 @@ export const Feed = observer(function Feed({
style={styles.emptyState}
/>
)
}
if (item === HEADER_SPACER_ITEM) {
return <View style={styles.headerSpacer} />
} else {
return <FeedItem item={item} />
}
@@ -77,12 +77,15 @@ export const Feed = observer(function Feed({
.loadMore()
.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.isEmpty) {
data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM]
data.push(EMPTY_FEED_ITEM)
} else {
data = [COMPOSE_PROMPT_ITEM].concat(feed.feed)
data = data.concat(feed.feed)
}
}
const FeedFooter = () =>
@@ -95,7 +98,6 @@ export const Feed = observer(function Feed({
)
return (
<View testID={testID} style={style}>
{!data && <PromptButtons onPressCompose={onPressCompose} />}
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
{feed.hasError && (
<ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} />
@@ -120,6 +122,7 @@ export const Feed = observer(function Feed({
})
const styles = StyleSheet.create({
headerSpacer: {height: 42},
feedFooter: {paddingTop: 20},
emptyState: {paddingVertical: 40},
})
-59
View File
@@ -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,
},
})
+59 -5
View File
@@ -1,11 +1,12 @@
import React from 'react'
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 {UserAvatar} from './UserAvatar'
import {Text} from './text/Text'
import {useStores} from '../../../state'
import {usePalette} from '../../lib/hooks/usePalette'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import {useAnalytics} from '@segment/analytics-react-native'
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({
title,
canGoBack,
hideOnScroll,
}: {
title: string
canGoBack?: boolean
hideOnScroll?: boolean
}) {
const pal = usePalette('default')
const store = useStores()
@@ -31,7 +34,7 @@ export const ViewHeader = observer(function ViewHeader({
canGoBack = store.nav.tab.canGoBack
}
return (
<View style={[styles.header, pal.view]}>
<Container hideOnScroll={hideOnScroll || false}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
@@ -57,10 +60,56 @@ export const ViewHeader = observer(function ViewHeader({
{title}
</Text>
</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({
header: {
flexDirection: 'row',
@@ -69,11 +118,16 @@ const styles = StyleSheet.create({
paddingTop: 6,
paddingBottom: 6,
},
headerFloating: {
position: 'absolute',
top: 0,
width: '100%',
},
titleContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginLeft: 'auto',
marginRight: 'auto',
paddingRight: 10,
},
title: {
fontWeight: 'bold',
+2 -2
View File
@@ -88,17 +88,17 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
return (
<View style={s.h100pct}>
<ViewHeader title="Bluesky" canGoBack={false} />
<Feed
testID="homeFeed"
key="default"
feed={store.me.mainFeed}
scrollElRef={scrollElRef}
style={s.h100pct}
onPressCompose={onPressCompose}
onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll}
headerSpacer
/>
<ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing ? (
<TouchableOpacity
style={[
+1 -1
View File
@@ -18,7 +18,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
return (
<View>
<ViewHeader title="Followed" />
<ViewHeader title="Following" />
<ProfileFollowsComponent name={name} />
</View>
)
+45 -44
View File
@@ -20,6 +20,7 @@ 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 '@segment/analytics-react-native'
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 {track} = useAnalytics()
const scrollElRef = React.useRef<ScrollView>(null)
const onMainScroll = useOnMainScroll(store)
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 [query, setQuery] = React.useState<string>('')
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
@@ -52,7 +54,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
if (visible) {
const now = Date.now()
if (lastRenderTime - now > FIVE_MIN) {
if (now - lastRenderTime > FIVE_MIN) {
setRenderTime(Date.now()) // trigger reload of suggestions
}
store.shell.setMinimalShellMode(false)
@@ -83,7 +85,12 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
return (
<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]}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
@@ -128,40 +135,38 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
</View>
) : undefined}
</View>
<View style={styles.outputContainer}>
{query && autocompleteView.searchRes.length ? (
<ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}>
{autocompleteView.searchRes.map(item => (
<ProfileCard
key={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
/>
))}
<View style={s.footerSpacer} />
</ScrollView>
) : 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} ref={scrollElRef}>
<WhoToFollow key={`wtf-${lastRenderTime}`} />
<SuggestedPosts key={`sp-${lastRenderTime}`} />
<View style={s.footerSpacer} />
</ScrollView>
)}
</View>
</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>
</TouchableWithoutFeedback>
)
})
@@ -176,7 +181,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 4,
paddingBottom: 5,
marginBottom: 14,
},
headerMenuBtn: {
width: 40,
@@ -189,7 +194,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
borderRadius: 30,
paddingHorizontal: 12,
paddingVertical: 6,
paddingVertical: 8,
},
headerSearchIcon: {
marginRight: 6,
@@ -197,7 +202,7 @@ const styles = StyleSheet.create({
},
headerSearchInput: {
flex: 1,
fontSize: 16,
fontSize: 17,
},
headerCancelBtn: {
width: 60,
@@ -208,8 +213,4 @@ const styles = StyleSheet.create({
textAlign: 'center',
paddingTop: 10,
},
outputContainer: {
flex: 1,
},
})