Feed UI update working branch [WIP] (#1420)

* Feeds navigation on right side of desktop (#1403)

* Remove home feed header on desktop

* Add feeds to right sidebar

* Add simple non-moving header to desktop

* Improve loading state of custom feed header

* Remove log

Co-authored-by: Eric Bailey <git@esb.lol>

* Remove dead comment

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Redesign feeds tab (#1439)

* consolidate saved feeds and discover into one screen

* Add hoverStyle behavior to <Link>

* More UI work on SavedFeeds

* Replace satellite icon with a hashtag

* Tune My Feeds mobile ui

* Handle no results in my feeds

* Remove old DiscoverFeeds screen

* Remove multifeed

* Remove DiscoverFeeds from router

* Improve loading placeholders

* Small fixes

* Fix types

* Fix overflow issue on firefox

* Add icons prompting to open feeds

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Merge feed prototype [WIP] (#1398)

* POC WIP for the mergefeed

* Add feed API wrapper and move mergefeed into it

* Show feed source in mergefeed

* Add lodash.random dep

* Improve mergefeed sampling and reliability

* Tune source ui element

* Improve mergefeed edge condition handling

* Remove in-place update of feeds for performance

* Fix link on native

* Fix bad ref

* Improve variety in mergefeed sampling

* Fix types

* Fix rebase error

* Add missing source field (got dropped in merge)

* Update find more link

* Simplify the right hand feeds nav

* Bring back load latest button on desktop & unify impl

* Add 'From' to source

* Add simple headers to desktop home & notifications

* Fix thread view jumping around horizontally

* Add unread indicators to desktop headers

* Add home feed preference for enabling the mergefeed

* Add a preference for showing replies among followed users only (#1448)

* Add a preference for showing replies among followed users only

* Simplify the reply filter UI

* Fix typo

* Simplified custom feed header

* Add soft reset to custom feed screen

* Drop all the in-post translate links except when expanded (#1455)

* Update mobile feed settings links to match desktop

* Fixes to feeds screen loading states

* Bolder active state of feeds tab on mobile web

* Fix dark mode issue

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Ansh <anshnanda10@gmail.com>
This commit is contained in:
Paul Frazee 2023-09-18 11:44:29 -07:00 committed by GitHub
parent 3118e3e933
commit ea885339cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1884 additions and 1497 deletions

View file

@ -21,11 +21,13 @@ export const Feed = observer(function Feed({
scrollElRef,
onPressTryAgain,
onScroll,
ListHeaderComponent,
}: {
view: NotificationsFeedModel
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
onScroll?: OnScrollCb
ListHeaderComponent?: () => JSX.Element
}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false)
@ -142,6 +144,7 @@ export const Feed = observer(function Feed({
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl
@ -156,6 +159,8 @@ export const Feed = observer(function Feed({
onScroll={onScroll}
scrollEventThrottle={100}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
) : null}
</View>

View file

@ -12,15 +12,17 @@ import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
export const FeedsTabBar = observer(function FeedsTabBarImpl(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) {
const {isMobile} = useWebMediaQueries()
const {isMobile, isTablet} = useWebMediaQueries()
if (isMobile) {
return <FeedsTabBarMobile {...props} />
} else if (isTablet) {
return <FeedsTabBarTablet {...props} />
} else {
return <FeedsTabBarDesktop {...props} />
return null
}
})
const FeedsTabBarDesktop = observer(function FeedsTabBarDesktopImpl(
const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) {
const store = useStores()

View file

@ -9,8 +9,8 @@ import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {CogIcon} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {s} from 'lib/styles'
import {HITSLOP_10} from 'lib/constants'
@ -67,12 +67,15 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
</Text>
<View style={[pal.view]}>
<Link
href="/settings/saved-feeds"
href="/settings/home-feed"
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel="Edit Saved Feeds"
accessibilityHint="Opens screen to edit Saved Feeds">
<CogIcon size={21} strokeWidth={2} style={pal.textLight} />
accessibilityLabel="Home Feed Preferences"
accessibilityHint="">
<FontAwesomeIcon
icon="sliders"
style={pal.textLight as FontAwesomeIconStyle}
/>
</Link>
</View>
</View>

View file

@ -357,6 +357,8 @@ export const PostThread = observer(function PostThread({
}
onScrollToIndexFailed={onScrollToIndexFailed}
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)
})

View file

@ -483,15 +483,6 @@ export const PostThreadItem = observer(function PostThreadItem({
/>
</ContentHider>
)}
{needsTranslation && (
<View style={[pal.borderDark, styles.translateLink]}>
<Link href={translatorUrl} title="Translate">
<Text type="sm" style={pal.link}>
Translate this post
</Text>
</Link>
</View>
)}
<PostCtrls
itemUri={itemUri}
itemCid={itemCid}

View file

@ -1,4 +1,4 @@
import React, {useState, useMemo} from 'react'
import React, {useState} from 'react'
import {
ActivityIndicator,
Linking,
@ -28,7 +28,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {getTranslatorLink} from '../../../locale/helpers'
import {makeProfileLink} from 'lib/routes/links'
export const Post = observer(function PostImpl({
@ -116,12 +116,6 @@ const PostLoaded = observer(function PostLoadedImpl({
}
const translatorUrl = getTranslatorLink(record?.text || '')
const needsTranslation = useMemo(
() =>
store.preferences.contentLanguages.length > 0 &&
!isPostInLanguage(item.post, store.preferences.contentLanguages),
[item.post, store.preferences.contentLanguages],
)
const onPressReply = React.useCallback(() => {
store.shell.openComposer({
@ -256,15 +250,6 @@ const PostLoaded = observer(function PostLoadedImpl({
/>
</ContentHider>
) : null}
{needsTranslation && (
<View style={[pal.borderDark, styles.translateLink]}>
<Link href={translatorUrl} title="Translate">
<Text type="sm" style={pal.link}>
Translate this post
</Text>
</Link>
</View>
)}
</ContentHider>
<PostCtrls
itemUri={itemUri}
@ -322,9 +307,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
flexWrap: 'wrap',
},
translateLink: {
marginBottom: 12,
},
replyLine: {
position: 'absolute',
left: 36,

View file

@ -8,6 +8,7 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {PostsFeedItemModel} from 'state/models/feeds/post'
import {FeedSourceInfo} from 'lib/api/feed/types'
import {Link, DesktopWebTextLink} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserInfoText} from '../util/UserInfoText'
@ -26,17 +27,19 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {getTranslatorLink} from '../../../locale/helpers'
import {makeProfileLink} from 'lib/routes/links'
import {isEmbedByEmbedder} from 'lib/embeds'
export const FeedItem = observer(function FeedItemImpl({
item,
source,
isThreadChild,
isThreadLastChild,
isThreadParent,
}: {
item: PostsFeedItemModel
source?: FeedSourceInfo
isThreadChild?: boolean
isThreadLastChild?: boolean
isThreadParent?: boolean
@ -62,12 +65,6 @@ export const FeedItem = observer(function FeedItemImpl({
return urip.hostname
}, [record?.reply])
const translatorUrl = getTranslatorLink(record?.text || '')
const needsTranslation = useMemo(
() =>
store.preferences.contentLanguages.length > 0 &&
!isPostInLanguage(item.post, store.preferences.contentLanguages),
[item.post, store.preferences.contentLanguages],
)
const onPressReply = React.useCallback(() => {
track('FeedItem:PostReply')
@ -179,7 +176,27 @@ export const FeedItem = observer(function FeedItemImpl({
</View>
<View style={{paddingTop: 12}}>
{item.reasonRepost && (
{source ? (
<Link
title={sanitizeDisplayName(source.displayName)}
href={source.uri}>
<Text
type="sm-bold"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}>
From{' '}
<DesktopWebTextLink
type="sm-bold"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}
text={sanitizeDisplayName(source.displayName)}
href={source.uri}
/>
</Text>
</Link>
) : item.reasonRepost ? (
<Link
style={styles.includeReason}
href={makeProfileLink(item.reasonRepost.by)}
@ -188,10 +205,10 @@ export const FeedItem = observer(function FeedItemImpl({
)}>
<FontAwesomeIcon
icon="retweet"
style={[
styles.includeReasonIcon,
{color: pal.colors.textLight} as FontAwesomeIconStyle,
]}
style={{
marginRight: 4,
color: pal.colors.textLight,
}}
/>
<Text
type="sm-bold"
@ -212,7 +229,7 @@ export const FeedItem = observer(function FeedItemImpl({
/>
</Text>
</Link>
)}
) : null}
</View>
</View>
@ -304,15 +321,6 @@ export const FeedItem = observer(function FeedItemImpl({
/>
</ContentHider>
) : null}
{needsTranslation && (
<View style={[pal.borderDark, styles.translateLink]}>
<Link href={translatorUrl} title="Translate">
<Text type="sm" style={pal.link}>
Translate this post
</Text>
</Link>
</View>
)}
</ContentHider>
<PostCtrls
itemUri={itemUri}
@ -362,12 +370,9 @@ const styles = StyleSheet.create({
includeReason: {
flexDirection: 'row',
marginTop: 2,
marginBottom: 4,
marginBottom: 2,
marginLeft: -20,
},
includeReasonIcon: {
marginRight: 4,
},
layout: {
flexDirection: 'row',
marginTop: 1,

View file

@ -28,6 +28,7 @@ export const FeedSlice = observer(function FeedSliceImpl({
<FeedItem
key={slice.items[0]._reactKey}
item={slice.items[0]}
source={slice.source}
isThreadParent={slice.isThreadParentAt(0)}
isThreadChild={slice.isThreadChildAt(0)}
/>
@ -55,6 +56,7 @@ export const FeedSlice = observer(function FeedSliceImpl({
<FeedItem
key={item._reactKey}
item={item}
source={i === 0 ? slice.source : undefined}
isThreadParent={slice.isThreadParentAt(i)}
isThreadChild={slice.isThreadChildAt(i)}
isThreadLastChild={

View file

@ -28,7 +28,7 @@ export function FollowingEmptyState() {
}, [navigation])
const onPressDiscoverFeeds = React.useCallback(() => {
navigation.navigate('DiscoverFeeds')
navigation.navigate('Feeds')
}, [navigation])
return (

View file

@ -1,256 +0,0 @@
import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FlatList} from '../util/Views'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed'
import {FeedSlice} from './FeedSlice'
import {Text} from '../util/text/Text'
import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CogIcon} from 'lib/icons'
export const MultiFeed = observer(function Feed({
multifeed,
style,
scrollElRef,
onScroll,
scrollEventThrottle,
testID,
headerOffset = 0,
extraData,
}: {
multifeed: PostsMultiFeedModel
style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
onScroll?: OnScrollCb
scrollEventThrottle?: number
renderEmptyState?: () => JSX.Element
testID?: string
headerOffset?: number
extraData?: any
}) {
const pal = usePalette('default')
const theme = useTheme()
const {isMobile} = useWebMediaQueries()
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
// events
// =
const onRefresh = React.useCallback(async () => {
track('MultiFeed:onRefresh')
setIsRefreshing(true)
try {
await multifeed.refresh()
} catch (err) {
multifeed.rootStore.log.error('Failed to refresh posts feed', err)
}
setIsRefreshing(false)
}, [multifeed, track, setIsRefreshing])
const onEndReached = React.useCallback(async () => {
track('MultiFeed:onEndReached')
try {
await multifeed.loadMore()
} catch (err) {
multifeed.rootStore.log.error('Failed to load more posts', err)
}
}, [multifeed, track])
// rendering
// =
const renderItem = React.useCallback(
({item}: {item: MultiFeedItem}) => {
if (item.type === 'header') {
if (!isMobile) {
return (
<>
<View style={[pal.view, pal.border, styles.headerDesktop]}>
<Text type="2xl-bold" style={pal.text}>
My Feeds
</Text>
<Link href="/settings/saved-feeds">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
</View>
<DiscoverLink />
</>
)
}
return (
<>
<View style={[styles.header, pal.border]} />
<DiscoverLink />
</>
)
} else if (item.type === 'feed-header') {
return (
<View style={styles.feedHeader}>
<UserAvatar type="algo" avatar={item.avatar} size={28} />
<Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}>
{item.title}
</Text>
</View>
)
} else if (item.type === 'feed-slice') {
return <FeedSlice slice={item.slice} />
} else if (item.type === 'feed-loading') {
return <PostFeedLoadingPlaceholder />
} else if (item.type === 'feed-error') {
return <ErrorMessage message={item.error} />
} else if (item.type === 'feed-footer') {
return (
<Link
href={item.uri}
style={[styles.feedFooter, pal.border, pal.view]}>
<Text type="lg" style={pal.link}>
See more from {item.title}
</Text>
<FontAwesomeIcon
icon="angle-right"
size={18}
color={pal.colors.link}
/>
</Link>
)
} else if (item.type === 'footer') {
return <DiscoverLink />
}
return null
},
[pal, isMobile],
)
const ListFooter = React.useCallback(
() =>
multifeed.isLoading && !isRefreshing ? (
<View style={styles.loadMore}>
<ActivityIndicator color={pal.colors.text} />
</View>
) : (
<View />
),
[multifeed.isLoading, isRefreshing, pal],
)
return (
<View testID={testID} style={style}>
{multifeed.items.length > 0 && (
<FlatList
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={multifeed.items}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListFooterComponent={ListFooter}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={s.contentContainer}
style={[{paddingTop: headerOffset}, pal.view, styles.container]}
onScroll={onScroll}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
extraData={extraData}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
})
function DiscoverLink() {
const pal = usePalette('default')
return (
<Link style={[styles.discoverLink, pal.viewLight]} href="/search/feeds">
<FontAwesomeIcon icon="search" size={18} color={pal.colors.text} />
<Text type="xl-medium" style={pal.text}>
Discover new feeds
</Text>
</Link>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
},
header: {
borderTopWidth: 1,
marginBottom: 4,
},
headerDesktop: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
marginBottom: 4,
paddingHorizontal: 16,
paddingVertical: 8,
},
feedHeader: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
paddingHorizontal: 16,
paddingBottom: 8,
marginTop: 12,
},
feedHeaderTitle: {
fontWeight: 'bold',
},
feedFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 16,
marginBottom: 12,
borderTopWidth: 1,
borderBottomWidth: 1,
},
discoverLink: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
paddingHorizontal: 14,
paddingVertical: 12,
marginHorizontal: 8,
marginVertical: 8,
gap: 8,
},
loadMore: {
paddingTop: 10,
},
})

View file

@ -26,6 +26,7 @@ import {useStores, RootStoreModel} from 'state/index'
import {convertBskyAppUrlIfNeeded, isExternalUrl} from 'lib/strings/url-helpers'
import {isAndroid, isDesktopWeb} from 'platform/detection'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {PressableWithHover} from './PressableWithHover'
import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
type Event =
@ -38,6 +39,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
href?: string
title?: string
children?: React.ReactNode
hoverStyle?: StyleProp<ViewStyle>
noFeedback?: boolean
asAnchor?: boolean
anchorNoUnderline?: boolean
@ -112,8 +114,9 @@ export const Link = observer(function Link({
props.accessibilityLabel = title
}
const Com = props.hoverStyle ? PressableWithHover : Pressable
return (
<Pressable
<Com
testID={testID}
style={style}
onPress={onPress}
@ -123,7 +126,7 @@ export const Link = observer(function Link({
href={asAnchor ? sanitizeUrl(href) : undefined}
{...props}>
{children ? children : <Text>{title || 'link'}</Text>}
</Pressable>
</Com>
)
})
@ -137,6 +140,7 @@ export const TextLink = observer(function TextLink({
lineHeight,
dataSet,
title,
onPress,
}: {
testID?: string
type?: TypographyVariant
@ -154,9 +158,14 @@ export const TextLink = observer(function TextLink({
props.onPress = React.useCallback(
(e?: Event) => {
if (onPress) {
e?.preventDefault?.()
// @ts-ignore function signature differs by platform -prf
return onPress()
}
return onPressInner(store, navigation, sanitizeUrl(href), e)
},
[store, navigation, href],
[onPress, store, navigation, href],
)
const hrefAttrs = useMemo(() => {
const isExternal = isExternalUrl(href)

View file

@ -174,6 +174,60 @@ export function ProfileCardFeedLoadingPlaceholder() {
)
}
export function FeedLoadingPlaceholder({
style,
}: {
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
return (
<View
style={[
{paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
pal.border,
style,
]}>
<View style={[pal.view, {flexDirection: 'row', marginBottom: 10}]}>
<LoadingPlaceholder
width={36}
height={36}
style={[styles.avatar, {borderRadius: 6}]}
/>
<View style={[s.flex1]}>
<LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
<LoadingPlaceholder width={120} height={8} />
</View>
</View>
<View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder
width={260}
height={8}
style={{marginVertical: 12}}
/>
<LoadingPlaceholder width={120} height={8} />
</View>
</View>
)
}
export function FeedFeedLoadingPlaceholder() {
return (
<>
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
<FeedLoadingPlaceholder />
</>
)
}
const styles = StyleSheet.create({
loadingPlaceholder: {
borderRadius: 6,

View file

@ -0,0 +1,105 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {CenteredView} from './Views'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
showBackButton = true,
style,
children,
}: React.PropsWithChildren<{
showBackButton?: boolean
style?: StyleProp<ViewStyle>
}>) {
const pal = usePalette('default')
const store = useStores()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
const {isMobile} = useWebMediaQueries()
const canGoBack = navigation.canGoBack()
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
const onPressMenu = React.useCallback(() => {
track('ViewHeader:MenuButtonClicked')
store.shell.openDrawer()
}, [track, store])
const Container = isMobile ? View : CenteredView
return (
<Container style={[styles.header, isMobile && styles.headerMobile, style]}>
{showBackButton ? (
<TouchableOpacity
testID="viewHeaderDrawerBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}
accessibilityRole="button"
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
accessibilityHint="">
{canGoBack ? (
<FontAwesomeIcon
size={18}
icon="angle-left"
style={[styles.backIcon, pal.text]}
/>
) : (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
)}
</TouchableOpacity>
) : null}
{children}
</Container>
)
})
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 12,
width: '100%',
},
headerMobile: {
paddingHorizontal: 12,
paddingVertical: 10,
},
backBtn: {
width: 30,
height: 30,
},
backBtnWide: {
width: 30,
height: 30,
paddingHorizontal: 6,
},
backIcon: {
marginTop: 6,
},
})

View file

@ -118,7 +118,7 @@ export function UserAvatar({
return {
width: size,
height: size,
borderRadius: 8,
borderRadius: size > 32 ? 8 : 3,
}
}
return {

View file

@ -0,0 +1,104 @@
import React from 'react'
import {
StyleProp,
StyleSheet,
TextInput,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {MagnifyingGlassIcon} from 'lib/icons'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
interface Props {
query: string
setIsInputFocused?: (v: boolean) => void
onChangeQuery: (v: string) => void
onPressCancelSearch: () => void
onSubmitQuery: () => void
style?: StyleProp<ViewStyle>
}
export function SearchInput({
query,
setIsInputFocused,
onChangeQuery,
onPressCancelSearch,
onSubmitQuery,
style,
}: Props) {
const theme = useTheme()
const pal = usePalette('default')
const textInput = React.useRef<TextInput>(null)
const onPressCancelSearchInner = React.useCallback(() => {
onPressCancelSearch()
textInput.current?.blur()
}, [onPressCancelSearch, textInput])
return (
<View style={[pal.viewLight, styles.container, style]}>
<MagnifyingGlassIcon style={[pal.icon, styles.icon]} size={21} />
<TextInput
testID="searchTextInput"
ref={textInput}
placeholder="Search"
placeholderTextColor={pal.colors.textLight}
selectTextOnFocus
returnKeyType="search"
value={query}
style={[pal.text, styles.input]}
keyboardAppearance={theme.colorScheme}
onFocus={() => setIsInputFocused?.(true)}
onBlur={() => setIsInputFocused?.(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmitQuery}
accessibilityRole="search"
accessibilityLabel="Search"
accessibilityHint=""
autoCorrect={false}
autoCapitalize="none"
/>
{query ? (
<TouchableOpacity
onPress={onPressCancelSearchInner}
accessibilityRole="button"
accessibilityLabel="Clear search query"
accessibilityHint="">
<FontAwesomeIcon
icon="xmark"
size={16}
style={pal.textLight as FontAwesomeIconStyle}
/>
</TouchableOpacity>
) : undefined}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 30,
paddingHorizontal: 12,
paddingVertical: 8,
},
icon: {
marginRight: 6,
alignSelf: 'center',
},
input: {
flex: 1,
fontSize: 17,
minWidth: 0, // overflow mitigation for firefox
},
cancelBtn: {
paddingLeft: 10,
},
})

View file

@ -1 +1,86 @@
export * from './LoadLatestBtnMobile'
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {clamp} from 'lodash'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {colors} from 'lib/styles'
import {HITSLOP_20} from 'lib/constants'
export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
onPress,
label,
showIndicator,
}: {
onPress: () => void
label: string
showIndicator: boolean
minimalShellMode?: boolean // NOTE not used on mobile -prf
}) {
const store = useStores()
const pal = usePalette('default')
const {isDesktop, isTablet, isMobile} = useWebMediaQueries()
const safeAreaInsets = useSafeAreaInsets()
return (
<TouchableOpacity
style={[
styles.loadLatest,
isDesktop && styles.loadLatestDesktop,
isTablet && styles.loadLatestTablet,
pal.borderDark,
pal.view,
isMobile &&
!store.shell.minimalShellMode && {
bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
},
]}
onPress={onPress}
hitSlop={HITSLOP_20}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint="">
<FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
{showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
</TouchableOpacity>
)
})
const styles = StyleSheet.create({
loadLatest: {
position: 'absolute',
left: 18,
bottom: 35,
borderWidth: 1,
width: 52,
height: 52,
borderRadius: 26,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
loadLatestTablet: {
// @ts-ignore web only
left: '50vw',
// @ts-ignore web only -prf
transform: 'translateX(-282px)',
},
loadLatestDesktop: {
// @ts-ignore web only
left: '50vw',
// @ts-ignore web only -prf
transform: 'translateX(-382px)',
},
indicator: {
position: 'absolute',
top: 3,
right: 3,
backgroundColor: colors.blue3,
width: 12,
height: 12,
borderRadius: 6,
borderWidth: 1,
},
})

View file

@ -1,109 +0,0 @@
import React from 'react'
import {StyleSheet, TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile'
import {HITSLOP_20} from 'lib/constants'
export const LoadLatestBtn = ({
onPress,
label,
showIndicator,
minimalShellMode,
}: {
onPress: () => void
label: string
showIndicator: boolean
minimalShellMode?: boolean
}) => {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
if (isMobile) {
return (
<LoadLatestBtnMobile
onPress={onPress}
label={label}
showIndicator={showIndicator}
/>
)
}
return (
<>
{showIndicator && (
<TouchableOpacity
style={[
pal.view,
pal.borderDark,
styles.loadLatestCentered,
minimalShellMode && styles.loadLatestCenteredMinimal,
]}
onPress={onPress}
hitSlop={HITSLOP_20}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint="">
<Text type="md-bold" style={pal.text}>
{label}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={[pal.view, pal.borderDark, styles.loadLatest]}
onPress={onPress}
hitSlop={HITSLOP_20}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint="">
<Text type="md-bold" style={pal.text}>
<FontAwesomeIcon
icon="angle-up"
size={21}
style={[pal.text, styles.icon]}
/>
</Text>
</TouchableOpacity>
</>
)
}
const styles = StyleSheet.create({
loadLatest: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
// @ts-ignore web only
left: '50vw',
// @ts-ignore web only -prf
transform: 'translateX(-282px)',
bottom: 40,
width: 54,
height: 54,
borderRadius: 30,
borderWidth: 1,
},
icon: {
position: 'relative',
top: 2,
},
loadLatestCentered: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
// @ts-ignore web only
left: '50vw',
// @ts-ignore web only -prf
transform: 'translateX(-50%)',
top: 60,
paddingHorizontal: 24,
paddingVertical: 14,
borderRadius: 30,
borderWidth: 1,
},
loadLatestCenteredMinimal: {
top: 20,
},
})

View file

@ -1,69 +0,0 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {clamp} from 'lodash'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {colors} from 'lib/styles'
import {HITSLOP_20} from 'lib/constants'
export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
onPress,
label,
showIndicator,
}: {
onPress: () => void
label: string
showIndicator: boolean
minimalShellMode?: boolean // NOTE not used on mobile -prf
}) {
const store = useStores()
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
return (
<TouchableOpacity
style={[
styles.loadLatest,
pal.borderDark,
pal.view,
!store.shell.minimalShellMode && {
bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
},
]}
onPress={onPress}
hitSlop={HITSLOP_20}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint="">
<FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
{showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
</TouchableOpacity>
)
})
const styles = StyleSheet.create({
loadLatest: {
position: 'absolute',
left: 18,
bottom: 35,
borderWidth: 1,
width: 52,
height: 52,
borderRadius: 26,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
indicator: {
position: 'absolute',
top: 3,
right: 3,
backgroundColor: colors.blue3,
width: 12,
height: 12,
borderRadius: 6,
borderWidth: 1,
},
})