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

@ -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,
},
})