diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-09-18 11:44:29 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-18 11:44:29 -0700 |
commit | ea885339cf3a5cba4aa82fbe5e0176052c3b68e1 (patch) | |
tree | a02b0581c42a1a0aae4442a75391c99a1719ec3e /src/view | |
parent | 3118e3e93338c62d2466699b9f339544d3273823 (diff) | |
download | voidsky-ea885339cf3a5cba4aa82fbe5e0176052c3b68e1.tar.zst |
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>
Diffstat (limited to 'src/view')
36 files changed, 1127 insertions, 1073 deletions
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index d457d7136..4ca22282d 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -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> diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 0083e953b..02aa623cc 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -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() diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 5ce2906b3..30a712541 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -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> diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 7a5a45771..1cc177d17 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -357,6 +357,8 @@ export const PostThread = observer(function PostThread({ } onScrollToIndexFailed={onScrollToIndexFailed} style={s.hContentRegion} + // @ts-ignore our .web version only -prf + desktopFixedHeight /> ) }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5b5fee0ca..37c7ece47 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 0855f25bf..d7559e3c4 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -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, diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index bc7b7a7e6..59ab28d72 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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, diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index 47313ee27..1d26f6cbd 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -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={ diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx index 4491b2526..a73ffb68b 100644 --- a/src/view/com/posts/FollowingEmptyState.tsx +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -28,7 +28,7 @@ export function FollowingEmptyState() { }, [navigation]) const onPressDiscoverFeeds = React.useCallback(() => { - navigation.navigate('DiscoverFeeds') + navigation.navigate('Feeds') }, [navigation]) return ( diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx deleted file mode 100644 index 9c8f4f246..000000000 --- a/src/view/com/posts/MultiFeed.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 321b6ab63..d4df2bec4 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -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) diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index bf39fd50c..d7ab1be54 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -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, diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx new file mode 100644 index 000000000..4eff38a31 --- /dev/null +++ b/src/view/com/util/SimpleViewHeader.tsx @@ -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, + }, +}) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 0f34f75aa..7a42ab4d3 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -118,7 +118,7 @@ export function UserAvatar({ return { width: size, height: size, - borderRadius: 8, + borderRadius: size > 32 ? 8 : 3, } } return { diff --git a/src/view/com/util/forms/SearchInput.tsx b/src/view/com/util/forms/SearchInput.tsx new file mode 100644 index 000000000..c1eb82bd4 --- /dev/null +++ b/src/view/com/util/forms/SearchInput.tsx @@ -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, + }, +}) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index ae9cb9361..6b73edd4b 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -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, + }, +}) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx deleted file mode 100644 index 83c696f7e..000000000 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx deleted file mode 100644 index 3e8add5e9..000000000 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/index.ts b/src/view/index.ts index 2e4c08ec7..2fdc34e7b 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -13,6 +13,7 @@ import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrow import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft' +import {faArrowTrendUp} from '@fortawesome/free-solid-svg-icons/faArrowTrendUp' import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate' import {faAt} from '@fortawesome/free-solid-svg-icons/faAt' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' @@ -24,6 +25,7 @@ import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faB import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' +import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight' import {faCircle} from '@fortawesome/free-regular-svg-icons/faCircle' import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck' @@ -41,6 +43,7 @@ import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation' import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' +import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' @@ -54,15 +57,18 @@ import {faImage} from '@fortawesome/free-solid-svg-icons/faImage' import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo' import {faLanguage} from '@fortawesome/free-solid-svg-icons/faLanguage' import {faLink} from '@fortawesome/free-solid-svg-icons/faLink' +import {faList} from '@fortawesome/free-solid-svg-icons/faList' import {faListUl} from '@fortawesome/free-solid-svg-icons/faListUl' import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky' +import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste' import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' +import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' @@ -77,6 +83,7 @@ import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders' import {faSquare} from '@fortawesome/free-regular-svg-icons/faSquare' import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck' import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' +import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' @@ -88,11 +95,6 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' -import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' -import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' -import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' -import {faList} from '@fortawesome/free-solid-svg-icons/faList' -import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight' export function setup() { library.add( @@ -109,6 +111,7 @@ export function setup() { faArrowUpFromBracket, faArrowUpRightFromSquare, faArrowRotateLeft, + faArrowTrendUp, faArrowsRotate, faAt, faBan, @@ -120,6 +123,7 @@ export function setup() { farCalendar, faCamera, faCheck, + faChevronRight, faCircle, faCircleCheck, farCircleCheck, @@ -137,6 +141,7 @@ export function setup() { faExclamation, farEyeSlash, faFaceSmile, + faFire, faFloppyDisk, faGear, faGlobe, @@ -150,15 +155,18 @@ export function setup() { faInfo, faLanguage, faLink, + faList, faListUl, faLock, faMagnifyingGlass, faMessage, faNoteSticky, faPaste, + faPause, faPen, faPenNib, faPenToSquare, + faPlay, faPlus, faQuoteLeft, faReply, @@ -180,14 +188,10 @@ export function setup() { faUserPlus, faUserXmark, faUsersSlash, + faThumbtack, faTicket, faTrashCan, - faThumbtack, faX, faXmark, - faPlay, - faPause, - faList, - faChevronRight, ) } diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index af4d01843..eaa21f292 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -1,7 +1,7 @@ import React, {useMemo, useRef} from 'react' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useNavigation} from '@react-navigation/native' +import {useNavigation, useIsFocused} from '@react-navigation/native' import {usePalette} from 'lib/hooks/usePalette' import {HeartIcon, HeartIconSolid} from 'lib/icons' import {CommonNavigatorParams} from 'lib/routes/types' @@ -14,11 +14,8 @@ import {PostsFeedModel} from 'state/models/feeds/posts' import {useCustomFeed} from 'lib/hooks/useCustomFeed' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from 'view/com/posts/Feed' -import {pluralize} from 'lib/strings/helpers' -import {sanitizeHandle} from 'lib/strings/handles' import {TextLink} from 'view/com/util/Link' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {ViewHeader} from 'view/com/util/ViewHeader' +import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' @@ -34,7 +31,6 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {EmptyState} from 'view/com/util/EmptyState' import {useAnalytics} from 'lib/analytics/analytics' import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' -import {makeProfileLink} from 'lib/routes/links' import {resolveName} from 'lib/api' import {CenteredView} from 'view/com/util/Views' import {NavigationProp} from 'lib/routes/types' @@ -125,7 +121,10 @@ export const CustomFeedScreenInner = observer( }: Props & {feedOwnerDid: string}) { const store = useStores() const pal = usePalette('default') - const {isTabletOrDesktop} = useWebMediaQueries() + const palInverted = usePalette('inverted') + const navigation = useNavigation<NavigationProp>() + const isScreenFocused = useIsFocused() + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const {track} = useAnalytics() const {rkey, name: handleOrDid} = route.params const uri = useMemo( @@ -186,6 +185,10 @@ export const CustomFeedScreenInner = observer( }) }, [store, currentFeed]) + const onPressViewAuthor = React.useCallback(() => { + navigation.navigate('Profile', {name: handleOrDid}) + }, [handleOrDid, navigation]) + const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`) shareUrl(url) @@ -210,9 +213,40 @@ export const CustomFeedScreenInner = observer( store.shell.openComposer({}) }, [store]) + const onSoftReset = React.useCallback(() => { + if (isScreenFocused) { + onScrollToTop() + algoFeed.refresh() + } + }, [isScreenFocused, onScrollToTop, algoFeed]) + + // fires when page within screen is activated/deactivated + React.useEffect(() => { + if (!isScreenFocused) { + return + } + + const softResetSub = store.onScreenSoftReset(onSoftReset) + return () => { + softResetSub.remove() + } + }, [store, onSoftReset, isScreenFocused]) + const dropdownItems: DropdownItem[] = React.useMemo(() => { let items: DropdownItem[] = [ { + testID: 'feedHeaderDropdownViewAuthorBtn', + label: 'View author', + onPress: onPressViewAuthor, + icon: { + ios: { + name: 'person', + }, + android: '', + web: ['far', 'user'], + }, + }, + { testID: 'feedHeaderDropdownToggleSavedBtn', label: currentFeed?.isSaved ? 'Remove from my feeds' @@ -260,232 +294,12 @@ export const CustomFeedScreenInner = observer( }, ] return items - }, [currentFeed?.isSaved, onToggleSaved, onPressReport, onPressShare]) - - const renderHeaderBtns = React.useCallback(() => { - return ( - <View style={styles.headerBtns}> - <Button - type="default-light" - testID="toggleLikeBtn" - accessibilityLabel="Like this feed" - accessibilityHint="" - onPress={onToggleLiked}> - {currentFeed?.isLiked ? ( - <HeartIconSolid size={19} style={styles.liked} /> - ) : ( - <HeartIcon strokeWidth={3} size={19} style={pal.textLight} /> - )} - </Button> - {currentFeed?.isSaved ? ( - <Button - type="default-light" - accessibilityLabel={ - isPinned ? 'Unpin this feed' : 'Pin this feed' - } - accessibilityHint="" - onPress={onTogglePinned}> - <FontAwesomeIcon - icon="thumb-tack" - size={17} - color={isPinned ? colors.blue3 : pal.colors.textLight} - style={styles.top1} - /> - </Button> - ) : undefined} - {!currentFeed?.isSaved ? ( - <Button - type="default-light" - onPress={onToggleSaved} - accessibilityLabel="Add to my feeds" - accessibilityHint="" - style={styles.headerAddBtn}> - <FontAwesomeIcon icon="plus" color={pal.colors.link} size={19} /> - <Text type="xl-medium" style={pal.link}> - Add to My Feeds - </Text> - </Button> - ) : null} - <NativeDropdown testID="feedHeaderDropdownBtn" items={dropdownItems}> - <View - style={{ - paddingLeft: currentFeed?.isSaved ? 12 : 6, - paddingRight: 12, - paddingVertical: 8, - }}> - <FontAwesomeIcon - icon="ellipsis" - size={20} - color={pal.colors.textLight} - /> - </View> - </NativeDropdown> - </View> - ) }, [ - pal, currentFeed?.isSaved, - currentFeed?.isLiked, - isPinned, - onToggleSaved, - onTogglePinned, - onToggleLiked, - dropdownItems, - ]) - - const renderListHeaderComponent = React.useCallback(() => { - return ( - <> - <View style={[styles.header, pal.border]}> - <View style={s.flex1}> - <Text - testID="feedName" - type="title-xl" - style={[pal.text, s.bold]}> - {currentFeed?.displayName} - </Text> - {currentFeed && ( - <Text type="md" style={[pal.textLight]} numberOfLines={1}> - by{' '} - {currentFeed.data.creator.did === store.me.did ? ( - 'you' - ) : ( - <TextLink - text={sanitizeHandle( - currentFeed.data.creator.handle, - '@', - )} - href={makeProfileLink(currentFeed.data.creator)} - style={[pal.textLight]} - /> - )} - </Text> - )} - {isTabletOrDesktop && ( - <View style={[styles.headerBtns, styles.headerBtnsDesktop]}> - <Button - type={currentFeed?.isSaved ? 'default' : 'inverted'} - onPress={onToggleSaved} - accessibilityLabel={ - currentFeed?.isSaved - ? 'Unsave this feed' - : 'Save this feed' - } - accessibilityHint="" - label={ - currentFeed?.isSaved - ? 'Remove from My Feeds' - : 'Add to My Feeds' - } - /> - <Button - type="default" - accessibilityLabel={ - isPinned ? 'Unpin this feed' : 'Pin this feed' - } - accessibilityHint="" - onPress={onTogglePinned}> - <FontAwesomeIcon - icon="thumb-tack" - size={15} - color={isPinned ? colors.blue3 : pal.colors.icon} - style={styles.top2} - /> - </Button> - <Button - type="default" - accessibilityLabel="Like this feed" - accessibilityHint="" - onPress={onToggleLiked}> - {currentFeed?.isLiked ? ( - <HeartIconSolid size={18} style={styles.liked} /> - ) : ( - <HeartIcon strokeWidth={3} size={18} style={pal.icon} /> - )} - </Button> - <Button - type="default" - accessibilityLabel="Share this feed" - accessibilityHint="" - onPress={onPressShare}> - <FontAwesomeIcon - icon="share" - size={18} - color={pal.colors.icon} - /> - </Button> - <Button - type="default" - accessibilityLabel="Report this feed" - accessibilityHint="" - onPress={onPressReport}> - <FontAwesomeIcon - icon="circle-exclamation" - size={18} - color={pal.colors.icon} - /> - </Button> - </View> - )} - </View> - <View> - <UserAvatar - type="algo" - avatar={currentFeed?.data.avatar} - size={64} - /> - </View> - </View> - <View style={styles.headerDetails}> - {currentFeed?.data.description ? ( - <Text style={[pal.text, s.mb10]} numberOfLines={6}> - {currentFeed.data.description} - </Text> - ) : null} - <View style={styles.headerDetailsFooter}> - {currentFeed ? ( - <TextLink - type="md-medium" - style={pal.textLight} - href={`/profile/${handleOrDid}/feed/${rkey}/liked-by`} - text={`Liked by ${currentFeed.data.likeCount} ${pluralize( - currentFeed?.data.likeCount || 0, - 'user', - )}`} - /> - ) : null} - </View> - </View> - <View - style={[ - styles.fakeSelector, - { - paddingHorizontal: isTabletOrDesktop ? 16 : 6, - }, - pal.border, - ]}> - <View - style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}> - <Text type="md-medium" style={[pal.text]}> - Feed - </Text> - </View> - </View> - </> - ) - }, [ - pal, - currentFeed, - store.me.did, onToggleSaved, - onToggleLiked, - onPressShare, - handleOrDid, onPressReport, - rkey, - isPinned, - onTogglePinned, - isTabletOrDesktop, + onPressShare, + onPressViewAuthor, ]) const renderEmptyState = React.useCallback(() => { @@ -498,22 +312,100 @@ export const CustomFeedScreenInner = observer( return ( <View style={s.hContentRegion}> - {!isTabletOrDesktop && ( - <ViewHeader title="" renderButton={currentFeed && renderHeaderBtns} /> - )} + <SimpleViewHeader + showBackButton={isMobile} + style={ + !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] + }> + <Text type="title-lg" style={styles.headerText} numberOfLines={1}> + {currentFeed ? ( + <TextLink + type="title-lg" + href="/" + style={[pal.text, {fontWeight: 'bold'}]} + text={currentFeed?.displayName || ''} + onPress={() => store.emitScreenSoftReset()} + /> + ) : ( + 'Loading...' + )} + </Text> + {currentFeed ? ( + <> + <Button + type="default-light" + testID="toggleLikeBtn" + accessibilityLabel="Like this feed" + accessibilityHint="" + onPress={onToggleLiked} + style={styles.headerBtn}> + {currentFeed?.isLiked ? ( + <HeartIconSolid size={19} style={styles.liked} /> + ) : ( + <HeartIcon strokeWidth={3} size={19} style={pal.textLight} /> + )} + </Button> + {currentFeed?.isSaved ? ( + <Button + type="default-light" + accessibilityLabel={ + isPinned ? 'Unpin this feed' : 'Pin this feed' + } + accessibilityHint="" + onPress={onTogglePinned} + style={styles.headerBtn}> + <FontAwesomeIcon + icon="thumb-tack" + size={17} + color={isPinned ? colors.blue3 : pal.colors.textLight} + style={styles.top1} + /> + </Button> + ) : ( + <Button + type="inverted" + onPress={onToggleSaved} + accessibilityLabel="Add to my feeds" + accessibilityHint="" + style={styles.headerAddBtn}> + <FontAwesomeIcon + icon="plus" + color={palInverted.colors.text} + size={19} + /> + <Text type="button" style={palInverted.text}> + Add{!isMobile && ' to My Feeds'} + </Text> + </Button> + )} + </> + ) : null} + <NativeDropdown testID="feedHeaderDropdownBtn" items={dropdownItems}> + <View + style={{ + paddingLeft: 12, + paddingRight: isMobile ? 12 : 0, + }}> + <FontAwesomeIcon + icon="ellipsis" + size={20} + color={pal.colors.textLight} + /> + </View> + </NativeDropdown> + </SimpleViewHeader> <Feed scrollElRef={scrollElRef} feed={algoFeed} onScroll={onMainScroll} scrollEventThrottle={100} - ListHeaderComponent={renderListHeaderComponent} renderEmptyState={renderEmptyState} extraData={[uri, isPinned]} style={!isTabletOrDesktop ? {flex: 1} : undefined} /> {isScrolledDown ? ( <LoadLatestBtn - onPress={onScrollToTop} + onPress={onSoftReset} label="Scroll to top" showIndicator={false} /> @@ -540,36 +432,19 @@ const styles = StyleSheet.create({ paddingBottom: 16, borderTopWidth: 1, }, - headerBtns: { - flexDirection: 'row', - alignItems: 'center', + headerText: { + flex: 1, + fontWeight: 'bold', }, - headerBtnsDesktop: { - marginTop: 8, - gap: 4, + headerBtn: { + paddingVertical: 0, }, headerAddBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, - paddingLeft: 4, - }, - headerDetails: { - paddingHorizontal: 16, - paddingBottom: 16, - }, - headerDetailsFooter: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - fakeSelector: { - flexDirection: 'row', - }, - fakeSelectorItem: { - paddingHorizontal: 12, - paddingBottom: 8, - borderBottomWidth: 3, + paddingVertical: 4, + paddingLeft: 10, }, liked: { color: colors.red3, diff --git a/src/view/screens/DiscoverFeeds.tsx b/src/view/screens/DiscoverFeeds.tsx deleted file mode 100644 index 6aa7a9e31..000000000 --- a/src/view/screens/DiscoverFeeds.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react' -import {RefreshControl, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ViewHeader} from '../com/util/ViewHeader' -import {useStores} from 'state/index' -import {FeedsDiscoveryModel} from 'state/models/discovery/feeds' -import {CenteredView, FlatList} from 'view/com/util/Views' -import {CustomFeed} from 'view/com/feeds/CustomFeed' -import {Text} from 'view/com/util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s} from 'lib/styles' -import {CustomFeedModel} from 'state/models/feeds/custom-feed' -import {HeaderWithInput} from 'view/com/search/HeaderWithInput' -import debounce from 'lodash.debounce' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'DiscoverFeeds'> -export const DiscoverFeedsScreen = withAuthRequired( - observer(function DiscoverFeedsScreenImpl({}: Props) { - const store = useStores() - const pal = usePalette('default') - const feeds = React.useMemo(() => new FeedsDiscoveryModel(store), [store]) - const {isTabletOrDesktop} = useWebMediaQueries() - - // search stuff - const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) - const [query, setQuery] = React.useState<string>('') - const debouncedSearchFeeds = React.useMemo( - () => debounce(q => feeds.search(q), 500), // debounce for 500ms - [feeds], - ) - const onChangeQuery = React.useCallback( - (text: string) => { - setQuery(text) - if (text.length > 1) { - debouncedSearchFeeds(text) - } else { - feeds.refresh() - } - }, - [debouncedSearchFeeds, feeds], - ) - const onPressClearQuery = React.useCallback(() => { - setQuery('') - feeds.refresh() - }, [feeds]) - const onPressCancelSearch = React.useCallback(() => { - setIsInputFocused(false) - setQuery('') - feeds.refresh() - }, [feeds]) - const onSubmitQuery = React.useCallback(() => { - debouncedSearchFeeds(query) - debouncedSearchFeeds.flush() - }, [debouncedSearchFeeds, query]) - - useFocusEffect( - React.useCallback(() => { - store.shell.setMinimalShellMode(false) - if (!feeds.hasLoaded) { - feeds.refresh() - } - }, [store, feeds]), - ) - - const onRefresh = React.useCallback(() => { - feeds.refresh() - }, [feeds]) - - const renderListEmptyComponent = () => { - return ( - <View style={styles.empty}> - <Text type="lg" style={pal.textLight}> - {feeds.isLoading - ? isTabletOrDesktop - ? 'Loading...' - : '' - : query - ? `No results found for "${query}"` - : `We can't find any feeds for some reason. This is probably an error - try refreshing!`} - </Text> - </View> - ) - } - - const renderItem = React.useCallback( - ({item}: {item: CustomFeedModel}) => ( - <CustomFeed - key={item.data.uri} - item={item} - showSaveBtn - showDescription - showLikes - /> - ), - [], - ) - - return ( - <CenteredView style={[styles.container, pal.view]}> - <View - style={[isTabletOrDesktop && styles.containerDesktop, pal.border]}> - <ViewHeader title="Discover Feeds" showOnDesktop /> - </View> - <HeaderWithInput - isInputFocused={isInputFocused} - query={query} - setIsInputFocused={setIsInputFocused} - onChangeQuery={onChangeQuery} - onPressClearQuery={onPressClearQuery} - onPressCancelSearch={onPressCancelSearch} - onSubmitQuery={onSubmitQuery} - showMenu={false} - /> - <FlatList - style={[!isTabletOrDesktop && s.flex1]} - data={feeds.feeds} - keyExtractor={item => item.data.uri} - contentContainerStyle={styles.contentContainer} - refreshControl={ - <RefreshControl - refreshing={feeds.isRefreshing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - renderItem={renderItem} - initialNumToRender={10} - ListEmptyComponent={renderListEmptyComponent} - onEndReached={() => feeds.loadMore()} - extraData={feeds.isLoading} - /> - </CenteredView> - ) - }), -) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - contentContainer: { - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - empty: { - paddingHorizontal: 16, - paddingTop: 10, - }, -}) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 97c6e8672..d2c4a6d2d 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -1,90 +1,72 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' -import isEqual from 'lodash.isequal' +import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' +import {AtUri} from '@atproto/api' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {FlatList} from 'view/com/util/Views' import {ViewHeader} from 'view/com/util/ViewHeader' -import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' import {Link} from 'view/com/util/Link' import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' import {observer} from 'mobx-react-lite' -import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed' -import {MultiFeed} from 'view/com/posts/MultiFeed' import {usePalette} from 'lib/hooks/usePalette' -import {useTimer} from 'lib/hooks/useTimer' import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {ComposeIcon2, CogIcon} from 'lib/icons' import {s} from 'lib/styles' - -const LOAD_NEW_PROMPT_TIME = 60e3 // 60 seconds -const MOBILE_HEADER_OFFSET = 40 +import {SearchInput} from 'view/com/util/forms/SearchInput' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' +import {ErrorMessage} from 'view/com/util/error/ErrorMessage' +import debounce from 'lodash.debounce' +import {Text} from 'view/com/util/text/Text' +import {MyFeedsUIModel, MyFeedsItem} from 'state/models/ui/my-feeds' +import {FlatList} from 'view/com/util/Views' +import {useFocusEffect} from '@react-navigation/native' +import {CustomFeed} from 'view/com/feeds/CustomFeed' type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> export const FeedsScreen = withAuthRequired( observer<Props>(function FeedsScreenImpl({}: Props) { const pal = usePalette('default') const store = useStores() - const {isMobile} = useWebMediaQueries() - const flatListRef = React.useRef<FlatList>(null) - const multifeed = React.useMemo<PostsMultiFeedModel>( - () => new PostsMultiFeedModel(store), - [store], + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const myFeeds = React.useMemo(() => new MyFeedsUIModel(store), [store]) + const [query, setQuery] = React.useState<string>('') + const debouncedSearchFeeds = React.useMemo( + () => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms + [myFeeds], ) - const [onMainScroll, isScrolledDown, resetMainScroll] = - useOnMainScroll(store) - const [loadPromptVisible, setLoadPromptVisible] = React.useState(false) - const [resetPromptTimer] = useTimer(LOAD_NEW_PROMPT_TIME, () => { - setLoadPromptVisible(true) - }) - - const onSoftReset = React.useCallback(() => { - flatListRef.current?.scrollToOffset({offset: 0}) - multifeed.loadLatest() - resetPromptTimer() - setLoadPromptVisible(false) - resetMainScroll() - }, [ - flatListRef, - resetMainScroll, - multifeed, - resetPromptTimer, - setLoadPromptVisible, - ]) useFocusEffect( React.useCallback(() => { - const softResetSub = store.onScreenSoftReset(onSoftReset) - const multifeedCleanup = multifeed.registerListeners() - const cleanup = () => { - softResetSub.remove() - multifeedCleanup() - } - store.shell.setMinimalShellMode(false) - return cleanup - }, [store, multifeed, onSoftReset]), + myFeeds.setup() + }, [store.shell, myFeeds]), ) - React.useEffect(() => { - if ( - isEqual( - multifeed.feedInfos.map(f => f.uri), - store.me.savedFeeds.all.map(f => f.uri), - ) - ) { - // no changes - return - } - multifeed.refresh() - }, [multifeed, store.me.savedFeeds.all]) - const onPressCompose = React.useCallback(() => { store.shell.openComposer({}) }, [store]) + const onChangeQuery = React.useCallback( + (text: string) => { + setQuery(text) + if (text.length > 1) { + debouncedSearchFeeds(text) + } else { + myFeeds.discovery.refresh() + } + }, + [debouncedSearchFeeds, myFeeds.discovery], + ) + const onPressCancelSearch = React.useCallback(() => { + setQuery('') + myFeeds.discovery.refresh() + }, [myFeeds]) + const onSubmitQuery = React.useCallback(() => { + debouncedSearchFeeds(query) + debouncedSearchFeeds.flush() + }, [debouncedSearchFeeds, query]) const renderHeaderBtn = React.useCallback(() => { return ( @@ -99,30 +81,150 @@ export const FeedsScreen = withAuthRequired( ) }, [pal]) + const onRefresh = React.useCallback(() => { + myFeeds.refresh() + }, [myFeeds]) + + const renderItem = React.useCallback( + ({item}: {item: MyFeedsItem}) => { + if (item.type === 'discover-feeds-loading') { + return <FeedFeedLoadingPlaceholder /> + } else if (item.type === 'spinner') { + return ( + <View style={s.p10}> + <ActivityIndicator /> + </View> + ) + } else if (item.type === 'error') { + return <ErrorMessage message={item.error} /> + } else if (item.type === 'saved-feeds-header') { + if (!isMobile) { + return ( + <View + style={[ + pal.view, + styles.header, + pal.border, + { + borderBottomWidth: 1, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.bold]}> + My Feeds + </Text> + <Link href="/settings/saved-feeds"> + <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> + </Link> + </View> + ) + } + return <View /> + } else if (item.type === 'saved-feed') { + return ( + <SavedFeed + uri={item.feed.uri} + avatar={item.feed.data.avatar} + displayName={item.feed.displayName} + /> + ) + } else if (item.type === 'discover-feeds-header') { + return ( + <> + <View + style={[ + pal.view, + styles.header, + { + marginTop: 16, + paddingLeft: isMobile ? 12 : undefined, + paddingRight: 10, + paddingBottom: isMobile ? 6 : undefined, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.bold]}> + Discover new feeds + </Text> + {!isMobile && ( + <SearchInput + query={query} + onChangeQuery={onChangeQuery} + onPressCancelSearch={onPressCancelSearch} + onSubmitQuery={onSubmitQuery} + style={{flex: 1, maxWidth: 250}} + /> + )} + </View> + {isMobile && ( + <View style={{paddingHorizontal: 8, paddingBottom: 10}}> + <SearchInput + query={query} + onChangeQuery={onChangeQuery} + onPressCancelSearch={onPressCancelSearch} + onSubmitQuery={onSubmitQuery} + /> + </View> + )} + </> + ) + } else if (item.type === 'discover-feed') { + return ( + <CustomFeed + item={item.feed} + showSaveBtn + showDescription + showLikes + /> + ) + } else if (item.type === 'discover-feeds-no-results') { + return ( + <View + style={{ + paddingHorizontal: 16, + paddingTop: 10, + paddingBottom: '150%', + }}> + <Text type="lg" style={pal.textLight}> + No results found for "{query}" + </Text> + </View> + ) + } + return null + }, + [isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery], + ) + return ( <View style={[pal.view, styles.container]}> - <MultiFeed - scrollElRef={flatListRef} - multifeed={multifeed} - onScroll={onMainScroll} - scrollEventThrottle={100} - headerOffset={isMobile ? MOBILE_HEADER_OFFSET : undefined} - /> {isMobile && ( <ViewHeader - title="My Feeds" + title="Feeds" canGoBack={false} - hideOnScroll renderButton={renderHeaderBtn} + showBorder /> )} - {isScrolledDown || loadPromptVisible ? ( - <LoadLatestBtn - onPress={onSoftReset} - label="Load latest posts" - showIndicator={loadPromptVisible} - /> - ) : null} + + <FlatList + style={[!isTabletOrDesktop && s.flex1, styles.list]} + data={myFeeds.items} + keyExtractor={item => item._reactKey} + contentContainerStyle={styles.contentContainer} + refreshControl={ + <RefreshControl + refreshing={myFeeds.isRefreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + renderItem={renderItem} + initialNumToRender={10} + onEndReached={() => myFeeds.loadMore()} + extraData={myFeeds.isLoading} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> <FAB testID="composeFAB" onPress={onPressCompose} @@ -136,8 +238,76 @@ export const FeedsScreen = withAuthRequired( }), ) +function SavedFeed({ + uri, + avatar, + displayName, +}: { + uri: string + avatar: string | undefined + displayName: string +}) { + const pal = usePalette('default') + const urip = new AtUri(uri) + const href = `/profile/${urip.hostname}/feed/${urip.rkey}` + const {isMobile} = useWebMediaQueries() + return ( + <Link + testID={`saved-feed-${displayName}`} + href={href} + style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]} + hoverStyle={pal.viewLight} + accessibilityLabel={displayName} + accessibilityHint="" + asAnchor + anchorNoUnderline> + <UserAvatar type="algo" size={28} avatar={avatar} /> + <Text + type={isMobile ? 'lg' : 'lg-medium'} + style={[pal.text, s.flex1]} + numberOfLines={1}> + {displayName} + </Text> + {isMobile && ( + <FontAwesomeIcon + icon="chevron-right" + size={14} + style={pal.textLight as FontAwesomeIconStyle} + /> + )} + </Link> + ) +} + const styles = StyleSheet.create({ container: { flex: 1, }, + list: { + height: '100%', + }, + contentContainer: { + paddingBottom: 100, + }, + + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 16, + paddingHorizontal: 16, + paddingVertical: 12, + }, + + savedFeed: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 14, + gap: 12, + borderBottomWidth: 1, + }, + savedFeedMobile: { + paddingVertical: 10, + }, }) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 33cc2e110..60cda31db 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,6 +1,8 @@ import React from 'react' import {FlatList, View} from 'react-native' import {useFocusEffect, useIsFocused} from '@react-navigation/native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' @@ -8,6 +10,7 @@ import isEqual from 'lodash.isequal' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {PostsFeedModel} from 'state/models/feeds/posts' import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {TextLink} from 'view/com/util/Link' import {Feed} from '../com/posts/Feed' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' @@ -16,14 +19,16 @@ import {FeedsTabBar} from '../com/pager/FeedsTabBar' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {FAB} from '../com/util/fab/FAB' import {useStores} from 'state/index' -import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {s, colors} from 'lib/styles' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics/analytics' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {ComposeIcon2} from 'lib/icons' const HEADER_OFFSET_MOBILE = 78 -const HEADER_OFFSET_DESKTOP = 50 +const HEADER_OFFSET_TABLET = 50 +const HEADER_OFFSET_DESKTOP = 0 const POLL_FREQ = 30e3 // 30sec type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> @@ -154,17 +159,23 @@ const FeedPage = observer(function FeedPageImpl({ renderEmptyState?: () => JSX.Element }) { const store = useStores() - const {isMobile} = useWebMediaQueries() + const pal = usePalette('default') + const {isMobile, isTablet, isDesktop} = useWebMediaQueries() const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store) const {screen, track} = useAnalytics() const [headerOffset, setHeaderOffset] = React.useState( - isMobile ? HEADER_OFFSET_MOBILE : HEADER_OFFSET_DESKTOP, + isMobile + ? HEADER_OFFSET_MOBILE + : isTablet + ? HEADER_OFFSET_TABLET + : HEADER_OFFSET_DESKTOP, ) const scrollElRef = React.useRef<FlatList>(null) const {appState} = useAppState({ onForeground: () => doPoll(true), }) const isScreenFocused = useIsFocused() + const hasNew = feed.hasNewLatest && !feed.isRefreshing React.useEffect(() => { // called on first load @@ -205,8 +216,14 @@ const FeedPage = observer(function FeedPageImpl({ // listens for resize events React.useEffect(() => { - setHeaderOffset(isMobile ? HEADER_OFFSET_MOBILE : HEADER_OFFSET_DESKTOP) - }, [isMobile]) + setHeaderOffset( + isMobile + ? HEADER_OFFSET_MOBILE + : isTablet + ? HEADER_OFFSET_TABLET + : HEADER_OFFSET_DESKTOP, + ) + }, [isMobile, isTablet]) // fires when page within screen is activated/deactivated // - check for latest @@ -222,9 +239,6 @@ const FeedPage = observer(function FeedPageImpl({ screen('Feed') store.log.debug('HomeScreen: Updating feed') feed.checkForLatest() - if (feed.hasContent) { - feed.update() - } return () => { clearInterval(pollInterval) @@ -247,7 +261,59 @@ const FeedPage = observer(function FeedPageImpl({ feed.refresh() }, [feed, scrollToTop]) - const hasNew = feed.hasNewLatest && !feed.isRefreshing + const ListHeaderComponent = React.useCallback(() => { + if (isDesktop) { + return ( + <View + style={[ + pal.view, + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 18, + paddingVertical: 12, + }, + ]}> + <TextLink + type="title-lg" + href="/" + style={[pal.text, {fontWeight: 'bold'}]} + text={ + <> + {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} + {hasNew && ( + <View + style={{ + top: -8, + backgroundColor: colors.blue3, + width: 8, + height: 8, + borderRadius: 4, + }} + /> + )} + </> + } + onPress={() => store.emitScreenSoftReset()} + /> + <TextLink + type="title-lg" + href="/settings/home-feed" + style={{fontWeight: 'bold'}} + text={ + <FontAwesomeIcon + icon="sliders" + style={pal.textLight as FontAwesomeIconStyle} + /> + } + /> + </View> + ) + } + return <></> + }, [isDesktop, pal, store, hasNew]) + return ( <View testID={testID} style={s.h100pct}> <Feed @@ -259,6 +325,7 @@ const FeedPage = observer(function FeedPageImpl({ onScroll={onMainScroll} scrollEventThrottle={100} renderEmptyState={renderEmptyState} + ListHeaderComponent={ListHeaderComponent} headerOffset={headerOffset} /> {(isScrolledDown || hasNew) && ( diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 3c257fac8..243cc9596 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -9,12 +9,15 @@ import { import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' +import {TextLink} from 'view/com/util/Link' import {InvitedUsers} from '../com/notifications/InvitedUsers' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {useStores} from 'state/index' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect' -import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {s, colors} from 'lib/styles' import {useAnalytics} from 'lib/analytics/analytics' import {isWeb} from 'platform/detection' @@ -29,6 +32,12 @@ export const NotificationsScreen = withAuthRequired( useOnMainScroll(store) const scrollElRef = React.useRef<FlatList>(null) const {screen} = useAnalytics() + const pal = usePalette('default') + const {isDesktop} = useWebMediaQueries() + + const hasNew = + store.me.notifications.hasNewLatest && + !store.me.notifications.isRefreshing // event handlers // = @@ -88,9 +97,48 @@ export const NotificationsScreen = withAuthRequired( ), ) - const hasNew = - store.me.notifications.hasNewLatest && - !store.me.notifications.isRefreshing + const ListHeaderComponent = React.useCallback(() => { + if (isDesktop) { + return ( + <View + style={[ + pal.view, + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 18, + paddingVertical: 12, + }, + ]}> + <TextLink + type="title-lg" + href="/notifications" + style={[pal.text, {fontWeight: 'bold'}]} + text={ + <> + Notifications{' '} + {hasNew && ( + <View + style={{ + top: -8, + backgroundColor: colors.blue3, + width: 8, + height: 8, + borderRadius: 4, + }} + /> + )} + </> + } + onPress={() => store.emitScreenSoftReset()} + /> + </View> + ) + } + return <></> + }, [isDesktop, pal, store, hasNew]) + return ( <View testID="notificationsScreen" style={s.hContentRegion}> <ViewHeader title="Notifications" canGoBack={false} /> @@ -100,6 +148,7 @@ export const NotificationsScreen = withAuthRequired( onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} scrollElRef={scrollElRef} + ListHeaderComponent={ListHeaderComponent} /> {(isScrolledDown || hasNew) && ( <LoadLatestBtn diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 49c13bfa3..81bdfc95e 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -19,14 +19,7 @@ function RepliesThresholdInput({enabled}: {enabled: boolean}) { const [value, setValue] = useState(store.preferences.homeFeedRepliesThreshold) return ( - <View style={[s.mt10, !enabled && styles.dimmed]}> - <Text type="xs" style={pal.text}> - {value === 0 - ? `Show all replies` - : `Show replies with at least ${value} ${ - value > 1 ? `likes` : `like` - }`} - </Text> + <View style={[!enabled && styles.dimmed]}> <Slider value={value} onValueChange={(v: number | number[]) => { @@ -40,6 +33,13 @@ function RepliesThresholdInput({enabled}: {enabled: boolean}) { disabled={!enabled} thumbTintColor={colors.blue3} /> + <Text type="xs" style={pal.text}> + {value === 0 + ? `Show all replies` + : `Show replies with at least ${value} ${ + value > 1 ? `likes` : `like` + }`} + </Text> </View> ) } @@ -79,8 +79,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ Show Replies </Text> <Text style={[pal.text, s.pb10]}> - Adjust the number of likes a reply must have to be shown in your - feed. + Set this setting to "No" to hide all replies from your feed. </Text> <ToggleButton type="default-light" @@ -88,7 +87,36 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ isSelected={store.preferences.homeFeedRepliesEnabled} onPress={store.preferences.toggleHomeFeedRepliesEnabled} /> - + </View> + <View + style={[ + pal.viewLight, + styles.card, + !store.preferences.homeFeedRepliesEnabled && styles.dimmed, + ]}> + <Text type="title-sm" style={[pal.text, s.pb5]}> + Reply Filters + </Text> + <Text style={[pal.text, s.pb10]}> + Enable this setting to only see replies between people you follow. + </Text> + <ToggleButton + type="default-light" + label="Followed users only" + isSelected={ + store.preferences.homeFeedRepliesByFollowedOnlyEnabled + } + onPress={ + store.preferences.homeFeedRepliesEnabled + ? store.preferences.toggleHomeFeedRepliesByFollowedOnlyEnabled + : undefined + } + style={[s.mb10]} + /> + <Text style={[pal.text]}> + Adjust the number of likes a reply must have to be shown in your + feed. + </Text> <RepliesThresholdInput enabled={store.preferences.homeFeedRepliesEnabled} /> @@ -124,6 +152,22 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ onPress={store.preferences.toggleHomeFeedQuotePostsEnabled} /> </View> + + <View style={[pal.viewLight, styles.card]}> + <Text type="title-sm" style={[pal.text, s.pb5]}> + Show Posts from My Feeds (Experimental) + </Text> + <Text style={[pal.text, s.pb10]}> + Set this setting to "Yes" to show samples of your saved feeds in + your following feed. + </Text> + <ToggleButton + type="default-light" + label={store.preferences.homeFeedMergeFeedEnabled ? 'Yes' : 'No'} + isSelected={store.preferences.homeFeedMergeFeedEnabled} + onPress={store.preferences.toggleHomeFeedMergeFeedEnabled} + /> + </View> </View> </ScrollView> diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 69b5ceee6..241bae1ed 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -69,9 +69,7 @@ export const ProfileScreen = withAuthRequired( let aborted = false store.shell.setMinimalShellMode(false) const feedCleanup = uiState.feed.registerListeners() - if (hasSetup) { - uiState.update() - } else { + if (!hasSetup) { uiState.setup().then(() => { if (aborted) { return diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index d5c02ba63..5253c5bd6 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -70,7 +70,7 @@ export const SavedFeeds = withAuthRequired( return ( <> <View style={[styles.footerLinks, pal.border]}> - <Link style={styles.footerLink} href="/search/feeds"> + <Link style={styles.footerLink} href="/feeds"> <FontAwesomeIcon icon="search" size={18} diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 8a543fa4c..761f50d0a 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -40,7 +40,7 @@ import {AccountData} from 'state/models/session' import {useAnalytics} from 'lib/analytics/analytics' import {NavigationProp} from 'lib/routes/types' import {pluralize} from 'lib/strings/helpers' -import {HandIcon} from 'lib/icons' +import {HandIcon, HashtagIcon} from 'lib/icons' import {formatCount} from 'view/com/util/numeric/format' import Clipboard from '@react-native-clipboard/clipboard' import {reset as resetNavigation} from '../../Navigation' @@ -423,17 +423,14 @@ export const SettingsScreen = withAuthRequired( <TouchableOpacity testID="savedFeedsBtn" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - accessibilityHint="Saved Feeds" + accessibilityHint="My Saved Feeds" accessibilityLabel="Opens screen with all saved feeds" onPress={onPressSavedFeeds}> <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="satellite-dish" - style={pal.text as FontAwesomeIconStyle} - /> + <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> </View> <Text type="lg" style={pal.text}> - Saved Feeds + My Saved Feeds </Text> </TouchableOpacity> <TouchableOpacity diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 3a4b8947a..174e4a806 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -28,8 +28,7 @@ import { MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, UserIconSolid, - SatelliteDishIcon, - SatelliteDishIconSolid, + HashtagIcon, HandIcon, } from 'lib/icons' import {UserAvatar} from 'view/com/util/UserAvatar' @@ -258,21 +257,21 @@ export const DrawerContent = observer(function DrawerContentImpl() { <MenuItem icon={ isAtFeeds ? ( - <SatelliteDishIconSolid - strokeWidth={1.5} + <HashtagIcon + strokeWidth={3} style={pal.text as FontAwesomeIconStyle} size={24} /> ) : ( - <SatelliteDishIcon - strokeWidth={1.5} + <HashtagIcon + strokeWidth={2} style={pal.text as FontAwesomeIconStyle} size={24} /> ) } - label="My Feeds" - accessibilityLabel="My Feeds" + label="Feeds" + accessibilityLabel="Feeds" accessibilityHint="" onPress={onPressMyFeeds} /> diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 4a34371ea..8ba74da2e 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -18,8 +18,7 @@ import { HomeIconSolid, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, - SatelliteDishIcon, - SatelliteDishIconSolid, + HashtagIcon, BellIcon, BellIconSolid, } from 'lib/icons' @@ -134,16 +133,16 @@ export const BottomBar = observer(function BottomBarImpl({ testID="bottomBarFeedsBtn" icon={ isAtFeeds ? ( - <SatelliteDishIconSolid - size={25} - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} - strokeWidth={1.8} + <HashtagIcon + size={24} + style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} + strokeWidth={4} /> ) : ( - <SatelliteDishIcon - size={25} - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} - strokeWidth={1.8} + <HashtagIcon + size={24} + style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} + strokeWidth={2.25} /> ) } diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx index f31ab44cf..ae9381440 100644 --- a/src/view/shell/bottom-bar/BottomBarStyles.tsx +++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx @@ -49,6 +49,9 @@ export const styles = StyleSheet.create({ homeIcon: { top: 0, }, + feedsIcon: { + top: -2, + }, searchIcon: { top: -2, }, diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index af70d3364..6448eea63 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -15,8 +15,7 @@ import { HomeIconSolid, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, - SatelliteDishIcon, - SatelliteDishIconSolid, + HashtagIcon, UserIcon, UserIconSolid, } from 'lib/icons' @@ -68,12 +67,11 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() { </NavItem> <NavItem routeName="Feeds" href="/feeds"> {({isActive}) => { - const Icon = isActive ? SatelliteDishIconSolid : SatelliteDishIcon return ( - <Icon - size={25} - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} - strokeWidth={1.8} + <HashtagIcon + size={22} + style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} + strokeWidth={isActive ? 4 : 2.5} /> ) }} diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx new file mode 100644 index 000000000..4da1401c3 --- /dev/null +++ b/src/view/shell/desktop/Feeds.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import {View, StyleSheet} from 'react-native' +import {useNavigationState} from '@react-navigation/native' +import {AtUri} from '@atproto/api' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {TextLink} from 'view/com/util/Link' +import {getCurrentRoute} from 'lib/routes/helpers' + +export const DesktopFeeds = observer(function DesktopFeeds() { + const store = useStores() + const pal = usePalette('default') + + const route = useNavigationState(state => { + if (!state) { + return {name: 'Home'} + } + return getCurrentRoute(state) + }) + + return ( + <View style={[styles.container, pal.view, pal.border]}> + <FeedItem href="/" title="Following" current={route.name === 'Home'} /> + {store.me.savedFeeds.pinned.map(feed => { + try { + const {hostname, rkey} = new AtUri(feed.uri) + const href = `/profile/${hostname}/feed/${rkey}` + const params = route.params as Record<string, string> + return ( + <FeedItem + key={feed.uri} + href={href} + title={feed.displayName} + current={ + route.name === 'CustomFeed' && + params.name === hostname && + params.rkey === rkey + } + /> + ) + } catch { + return null + } + })} + <View style={{paddingTop: 8, paddingBottom: 6}}> + <TextLink + type="lg" + href="/feeds" + text="More feeds" + style={[pal.link]} + /> + </View> + </View> + ) +}) + +function FeedItem({ + title, + href, + current, +}: { + title: string + href: string + current: boolean +}) { + const pal = usePalette('default') + return ( + <View style={{paddingVertical: 6}}> + <TextLink + type="xl" + href={href} + text={title} + style={[ + current ? pal.text : pal.textLight, + {letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'}, + ]} + /> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + position: 'relative', + width: 300, + paddingHorizontal: 12, + borderTopWidth: 1, + borderBottomWidth: 1, + paddingVertical: 18, + }, +}) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 8c1a33245..907df8641 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -32,8 +32,7 @@ import { CogIconSolid, ComposeIcon2, HandIcon, - SatelliteDishIcon, - SatelliteDishIconSolid, + HashtagIcon, } from 'lib/icons' import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' @@ -272,20 +271,20 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { <NavItem href="/feeds" icon={ - <SatelliteDishIcon - strokeWidth={1.75} + <HashtagIcon + strokeWidth={2.25} style={pal.text as FontAwesomeIconStyle} size={isDesktop ? 24 : 28} /> } iconFilled={ - <SatelliteDishIconSolid - strokeWidth={1.75} + <HashtagIcon + strokeWidth={2.5} style={pal.text as FontAwesomeIconStyle} size={isDesktop ? 24 : 28} /> } - label="My Feeds" + label="Feeds" /> <NavItem href="/notifications" diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index e17fa6a84..12ca256d2 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -4,6 +4,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {DesktopSearch} from './Search' +import {DesktopFeeds} from './Feeds' import {Text} from 'view/com/util/text/Text' import {TextLink} from 'view/com/util/Link' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' @@ -26,6 +27,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { return ( <View style={[styles.rightNav, pal.view]}> {store.session.hasSession && <DesktopSearch />} + {store.session.hasSession && <DesktopFeeds />} <View style={styles.message}> {store.session.isSandbox ? ( <View style={[palError.view, styles.messageLine, s.p10]}> @@ -126,7 +128,7 @@ const styles = StyleSheet.create({ }, message: { - marginTop: 20, + paddingVertical: 18, paddingHorizontal: 10, }, messageLine: { @@ -134,7 +136,6 @@ const styles = StyleSheet.create({ }, inviteCodes: { - marginTop: 12, borderTopWidth: 1, paddingHorizontal: 16, paddingVertical: 12, diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index c7b322b58..dfd4f50bf 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -113,6 +113,7 @@ const styles = StyleSheet.create({ container: { position: 'relative', width: 300, + paddingBottom: 18, }, search: { paddingHorizontal: 16, |