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 (
|
||||
<>
|
||||
{(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,
|
||||
|
||||
@@ -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
@@ -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},
|
||||
})
|
||||
|
||||
@@ -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 {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',
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user