From f01d43f9e8107160088296fe6b0a9bb753d61032 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 16 Mar 2023 19:09:19 -0500 Subject: Get home screen's swipable pager working with the drawer --- src/App.native.tsx | 10 +++++--- src/lib/api/feed-manip.ts | 3 ++- src/state/models/ui/shell.ts | 5 ++++ src/view/screens/Home.tsx | 61 +++++++++++++++++++++++++++++++++++++++++++- src/view/shell/index.tsx | 6 ++++- 5 files changed, 79 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/App.native.tsx b/src/App.native.tsx index fcd6e787b..ebe6a7cd6 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -4,8 +4,10 @@ import {Linking} from 'react-native' import {RootSiblingParent} from 'react-native-root-siblings' import SplashScreen from 'react-native-splash-screen' import {SafeAreaProvider} from 'react-native-safe-area-context' +import {GestureHandlerRootView} from 'react-native-gesture-handler' import {observer} from 'mobx-react-lite' import {ThemeProvider} from 'lib/ThemeContext' +import {s} from 'lib/styles' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import {Shell} from './view/shell' @@ -51,9 +53,11 @@ const App = observer(() => { - - - + + + + + diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 00938be93..7abcaffc6 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -140,7 +140,8 @@ export class FeedTuner { for (const item of slice.items) { this.seenUris.add(item.post.uri) } - slice.logSelf() + // DEBUG uncomment to get a quick view of the data + // slice.logSelf() } return slices diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index d6fefb850..4e3aa5332 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -122,6 +122,7 @@ export class ShellUiModel { darkMode = false minimalShellMode = false isDrawerOpen = false + isDrawerSwipeDisabled = false isModalActive = false activeModals: Modal[] = [] isLightboxActive = false @@ -168,6 +169,10 @@ export class ShellUiModel { this.isDrawerOpen = false } + setIsDrawerSwipeDisabled(v: boolean) { + this.isDrawerSwipeDisabled = v + } + openModal(modal: Modal) { this.rootStore.emitNavigation() this.isModalActive = true diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index adc73315c..49915cd04 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {FlatList, View} from 'react-native' +import {FlatList, StyleSheet, View, useWindowDimensions} from 'react-native' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' @@ -9,15 +9,73 @@ import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {WelcomeBanner} from '../com/util/WelcomeBanner' +import {UserAvatar} from 'view/com/util/UserAvatar' import {FAB} from '../com/util/FAB' import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' +import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {Text} from 'view/com/util/text/Text' + const HEADER_HEIGHT = 42 +type Props = NativeStackScreenProps +export const HomeScreen = withAuthRequired((_opts: Props) => { + const store = useStores() + const onPageSelected = React.useCallback( + (e: PagerViewOnPageSelectedEvent) => { + store.shell.setIsDrawerSwipeDisabled(e.nativeEvent.position > 0) + }, + [store], + ) + + useFocusEffect( + React.useCallback(() => { + return () => { + store.shell.setIsDrawerSwipeDisabled(false) + } + }, [store]), + ) + + return ( + + + First page + + + Second page + + + ) +}) +function MyPage({children}) { + return ( + + {children} + + ) +} + +const styles = StyleSheet.create({ + tabBar: { + flexDirection: 'row', + }, +}) +/* type Props = NativeStackScreenProps export const HomeScreen = withAuthRequired( observer(function Home(_opts: Props) { @@ -113,3 +171,4 @@ export const HomeScreen = withAuthRequired( ) }), ) +*/ diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index d7877804b..eec0f8ed4 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -46,7 +46,11 @@ const ShellInner = observer(() => { onOpen={onOpenDrawer} onClose={onCloseDrawer} swipeEdgeWidth={winDim.width} - swipeEnabled={!canGoBack && store.session.hasSession}> + swipeEnabled={ + !canGoBack && + store.session.hasSession && + !store.shell.isDrawerSwipeDisabled + }> -- cgit 1.4.1 From ad9da82612a33a796bcb2c679dbff357f4829dc8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 16 Mar 2023 19:54:32 -0500 Subject: Add tab bar to pager --- src/view/com/util/Pager.tsx | 72 ++++++++++++++++++++++++++ src/view/com/util/TabBar.tsx | 119 +++++++++++++++++++++++++++++++++++++++++++ src/view/screens/Home.tsx | 14 ++--- 3 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/view/com/util/Pager.tsx create mode 100644 src/view/com/util/TabBar.tsx (limited to 'src') diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx new file mode 100644 index 000000000..1a3ff642c --- /dev/null +++ b/src/view/com/util/Pager.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import {Animated, StyleSheet, View} from 'react-native' +import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {TabBar} from './TabBar' + +export type PageSelectedEvent = PagerViewOnPageSelectedEvent +const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) + +interface Props { + onPageSelected?: (e: PageSelectedEvent) => void +} +export const Pager = ({ + children, + onPageSelected, +}: React.PropsWithChildren) => { + const [selectedPage, setSelectedPage] = React.useState(0) + const position = useAnimatedValue(0) + const offset = useAnimatedValue(0) + const pagerView = React.useRef() + + const onPageSelectedInner = React.useCallback( + (e: PageSelectedEvent) => { + setSelectedPage(e.nativeEvent.position) + onPageSelected?.(e) + }, + [setSelectedPage, onPageSelected], + ) + + const onTabBarSelect = React.useCallback( + (index: number) => { + pagerView.current?.setPage(index) + }, + [pagerView], + ) + + return ( + + + + {children} + + + ) +} + +const styles = StyleSheet.create({ + tabBar: { + flexDirection: 'row', + }, +}) diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx new file mode 100644 index 000000000..3a823e42c --- /dev/null +++ b/src/view/com/util/TabBar.tsx @@ -0,0 +1,119 @@ +import React, {createRef, useState, useMemo} from 'react' +import { + Animated, + StyleSheet, + TouchableWithoutFeedback, + View, +} from 'react-native' +import {Text} from './text/Text' +import {usePalette} from 'lib/hooks/usePalette' + +interface Layout { + x: number + width: number +} + +export function TabBar({ + selectedPage, + items, + position, + offset, + onSelect, +}: { + selectedPage: number + items: string[] + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +}) { + const pal = usePalette('default') + const [itemLayouts, setItemLayouts] = useState( + items.map(() => ({x: 0, width: 0})), + ) + const itemRefs = useMemo( + () => Array.from({length: items.length}).map(() => createRef()), + [items.length], + ) + const panX = Animated.add(position, offset) + + const underlineStyle = { + backgroundColor: pal.colors.text, + left: panX.interpolate({ + inputRange: items.map((_item, i) => i), + outputRange: itemLayouts.map(l => l.x), + }), + width: panX.interpolate({ + inputRange: items.map((_item, i) => i), + outputRange: itemLayouts.map(l => l.width), + }), + } + + const onLayout = () => { + const promises = [] + for (let i = 0; i < items.length; i++) { + promises.push( + new Promise(resolve => { + itemRefs[i].current?.measure( + (x: number, _y: number, width: number) => { + resolve({x, width}) + }, + ) + }), + ) + } + Promise.all(promises).then((layouts: Layout[]) => { + setItemLayouts(layouts) + }) + } + + const onPressItem = (index: number) => { + onSelect?.(index) + } + + return ( + + + {items.map((item, i) => { + const selected = i === selectedPage + return ( + onPressItem(i)}> + + + {item} + + + + ) + })} + + ) +} + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 14, + }, + item: { + paddingTop: 8, + paddingBottom: 12, + marginRight: 14, + paddingHorizontal: 10, + }, + label: { + fontWeight: '600', + }, + labelSelected: { + fontWeight: '600', + }, + underline: { + position: 'absolute', + height: 4, + bottom: 0, + }, +}) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 49915cd04..6c708e2fd 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -18,7 +18,7 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' -import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {Pager, PageSelectedEvent} from 'view/com/util/Pager' import {Text} from 'view/com/util/text/Text' const HEADER_HEIGHT = 42 @@ -27,7 +27,7 @@ type Props = NativeStackScreenProps export const HomeScreen = withAuthRequired((_opts: Props) => { const store = useStores() const onPageSelected = React.useCallback( - (e: PagerViewOnPageSelectedEvent) => { + (e: PageSelectedEvent) => { store.shell.setIsDrawerSwipeDisabled(e.nativeEvent.position > 0) }, [store], @@ -42,17 +42,17 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { ) return ( - + First page Second page - + + Third page + + ) }) function MyPage({children}) { -- cgit 1.4.1 From 71209bb3aca1104773ee9fab20ec9f9ab4fc3ad2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 16 Mar 2023 21:47:11 -0500 Subject: Implement popular & following views on home screen --- src/state/models/feed-view.ts | 12 +- src/view/com/profile/FollowButton.tsx | 2 +- src/view/com/util/Pager.tsx | 27 ++- src/view/com/util/PostMeta.tsx | 8 +- src/view/com/util/TabBar.tsx | 25 +-- src/view/screens/Home.tsx | 360 ++++++++++++++++++++++------------ 6 files changed, 272 insertions(+), 162 deletions(-) (limited to 'src') diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 42b753b24..81760132e 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -257,7 +257,7 @@ export class FeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested', + public feedType: 'home' | 'author' | 'suggested' | 'goodstuff', params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, ) { makeAutoObservable( @@ -634,6 +634,16 @@ export class FeedModel { return this.rootStore.api.app.bsky.feed.getTimeline( params as GetTimeline.QueryParams, ) + } else if (this.feedType === 'goodstuff') { + const res = await this.rootStore.api.app.bsky.feed.getAuthorFeed({ + ...params, + author: 'jay.bsky.social', + } as GetAuthorFeed.QueryParams) + res.data.feed = mergePosts([res], {repostsOnly: true}) + res.data.feed.forEach(item => { + delete item.reason + }) + return res } else { return this.rootStore.api.app.bsky.feed.getAuthorFeed( params as GetAuthorFeed.QueryParams, diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx index f24c3d0c9..7a194cee9 100644 --- a/src/view/com/profile/FollowButton.tsx +++ b/src/view/com/profile/FollowButton.tsx @@ -42,7 +42,7 @@ const FollowButton = observer( return ( - - ) : ( - <> - - Follow at least {remaining} {remaining === 1 ? 'person' : 'people'}{' '} - to build your feed. - - - - - - )} - - ) -}) - -const styles = StyleSheet.create({ - container: { - paddingTop: 16, - paddingBottom: 16, - paddingHorizontal: 20, - borderTopWidth: 1, - borderBottomWidth: 1, - }, - controls: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 10, - }, - progress: { - marginTop: 12, - }, -}) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index f1d03233a..5250f1155 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -14,7 +14,6 @@ import {FeedModel} from 'state/models/feed-view' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from '../com/posts/Feed' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {WelcomeBanner} from '../com/util/WelcomeBanner' import {TabBar} from 'view/com/util/TabBar' import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager' import {FAB} from '../com/util/FAB' @@ -202,7 +201,6 @@ const FeedPage = observer( return ( - {store.shell.isOnboarding && } Date: Fri, 17 Mar 2023 15:20:34 -0500 Subject: Choose good stuff based on service --- src/lib/constants.ts | 10 ++++++++++ src/state/models/feed-view.ts | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0cde9b014..ffc1aaed5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -161,6 +161,16 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) { } } +export function GOOD_STUFF(serviceUrl: string) { + if (serviceUrl.includes('localhost')) { + return 'alice.test' + } else if (serviceUrl.includes('staging')) { + return 'paul.staging.bsky.dev' + } else { + return 'jay.bsky.social' + } +} + export const POST_IMG_MAX_WIDTH = 2000 export const POST_IMG_MAX_HEIGHT = 2000 export const POST_IMG_MAX_SIZE = 1000000 diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index a62dbc983..3d7680fbc 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -16,7 +16,7 @@ import {RootStoreModel} from './root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {RichText} from 'lib/strings/rich-text' -import {SUGGESTED_FOLLOWS} from 'lib/constants' +import {SUGGESTED_FOLLOWS, GOOD_STUFF} from 'lib/constants' import { getCombinedCursors, getMultipleAuthorsPosts, @@ -643,7 +643,7 @@ export class FeedModel { } else if (this.feedType === 'goodstuff') { const res = await this.rootStore.api.app.bsky.feed.getAuthorFeed({ ...params, - author: 'jay.bsky.social', + author: GOOD_STUFF(String(this.rootStore.agent.service)), } as GetAuthorFeed.QueryParams) res.data.feed = mergePosts([res], {repostsOnly: true}) res.data.feed.forEach(item => { -- cgit 1.4.1 From ff39b072f3110f018edd6797ee642728f0dae259 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 17 Mar 2023 18:01:53 -0500 Subject: Add foaf-based follow discovery --- src/lib/hooks/usePalette.ts | 4 + src/state/models/discovery/foafs.ts | 98 ++++++++++++++++++ src/view/com/discover/SuggestedFollows.tsx | 159 ++++++++++------------------- src/view/com/profile/ProfileCard.tsx | 62 +++++++++-- src/view/com/util/LoadingPlaceholder.tsx | 49 +++++++++ src/view/screens/Search.tsx | 93 +++++++++++++---- 6 files changed, 335 insertions(+), 130 deletions(-) create mode 100644 src/state/models/discovery/foafs.ts (limited to 'src') diff --git a/src/lib/hooks/usePalette.ts b/src/lib/hooks/usePalette.ts index 5b9929c7d..7eeb74228 100644 --- a/src/lib/hooks/usePalette.ts +++ b/src/lib/hooks/usePalette.ts @@ -4,6 +4,7 @@ import {useTheme, PaletteColorName, PaletteColor} from '../ThemeContext' export interface UsePaletteValue { colors: PaletteColor view: ViewStyle + viewLight: ViewStyle btn: ViewStyle border: ViewStyle borderDark: ViewStyle @@ -20,6 +21,9 @@ export function usePalette(color: PaletteColorName): UsePaletteValue { view: { backgroundColor: palette.background, }, + viewLight: { + backgroundColor: palette.backgroundLight, + }, btn: { backgroundColor: palette.backgroundLight, }, diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts new file mode 100644 index 000000000..4a46ae9de --- /dev/null +++ b/src/state/models/discovery/foafs.ts @@ -0,0 +1,98 @@ +import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {makeAutoObservable, runInAction} from 'mobx' +import sampleSize from 'lodash.samplesize' +import {bundleAsync} from 'lib/async/bundle' +import {RootStoreModel} from '../root-store' + +export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & { + followers: AppBskyActorProfile.View[] +} + +export type ProfileViewFollows = AppBskyActorProfile.View & { + follows: AppBskyActorRef.WithInfo[] +} + +export class FoafsModel { + isLoading = false + hasData = false + sources: string[] = [] + foafs: Map = new Map() + popular: RefWithInfoAndFollowers[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable(this) + } + + fetch = bundleAsync(async () => { + try { + this.isLoading = true + await this.rootStore.me.follows.fetchIfNeeded() + // grab 10 of the users followed by the user + this.sources = sampleSize( + Object.keys(this.rootStore.me.follows.followDidToRecordMap), + 10, + ) + if (this.sources.length === 0) { + return + } + this.foafs.clear() + this.popular.length = 0 + + // fetch their profiles + const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({ + actors: this.sources, + }) + + // fetch their follows + const results = await Promise.allSettled( + this.sources.map(source => + this.rootStore.api.app.bsky.graph.getFollows({user: source}), + ), + ) + + // store the follows and construct a "most followed" set + const popular: RefWithInfoAndFollowers[] = [] + for (let i = 0; i < results.length; i++) { + const res = results[i] + const profile = profiles.data.profiles[i] + const source = this.sources[i] + if (res.status === 'fulfilled' && profile) { + // filter out users already followed by the user or that *is* the user + res.value.data.follows = res.value.data.follows.filter(follow => { + return ( + follow.did !== this.rootStore.me.did && + !this.rootStore.me.follows.isFollowing(follow.did) + ) + }) + + runInAction(() => { + this.foafs.set(source, { + ...profile, + follows: res.value.data.follows, + }) + }) + for (const follow of res.value.data.follows) { + let item = popular.find(p => p.did === follow.did) + if (!item) { + item = {...follow, followers: []} + popular.push(item) + } + item.followers.push(profile) + } + } + } + + popular.sort((a, b) => b.followers.length - a.followers.length) + runInAction(() => { + this.popular = popular.filter(p => p.followers.length > 1).slice(0, 20) + }) + this.hasData = true + } catch (e) { + console.error('Failed to fetch FOAFs', e) + } finally { + runInAction(() => { + this.isLoading = false + }) + } + }) +} diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index 1e40956ce..dd1136a48 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -1,116 +1,67 @@ import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import {CenteredView, FlatList} from '../util/Views' -import {observer} from 'mobx-react-lite' -import {ErrorScreen} from '../util/error/ErrorScreen' +import {StyleSheet, View} from 'react-native' +import {AppBskyActorRef, AppBskyActorProfile} from '@atproto/api' +import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' -import {useStores} from 'state/index' -import { - SuggestedActorsViewModel, - SuggestedActor, -} from 'state/models/suggested-actors-view' -import {s} from 'lib/styles' +import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' -export const SuggestedFollows = observer( - ({onNoSuggestions}: {onNoSuggestions?: () => void}) => { - const pal = usePalette('default') - const store = useStores() - - const view = React.useMemo( - () => new SuggestedActorsViewModel(store), - [store], - ) - - React.useEffect(() => { - view - .loadMore() - .catch((err: any) => - store.log.error('Failed to fetch suggestions', err), - ) - }, [view, store.log]) - - React.useEffect(() => { - if (!view.isLoading && !view.hasError && !view.hasContent) { - onNoSuggestions?.() - } - }, [view, view.isLoading, view.hasError, view.hasContent, onNoSuggestions]) - - const onRefresh = () => { - view - .refresh() - .catch((err: any) => - store.log.error('Failed to fetch suggestions', err), - ) - } - const onEndReached = () => { - view - .loadMore() - .catch(err => - view?.rootStore.log.error('Failed to load more suggestions', err), - ) - } - - const renderItem = ({item}: {item: SuggestedActor}) => { - return ( - - ) - } - return ( - - {view.hasError ? ( - - - - ) : view.isEmpty ? ( - - ) : ( - - item.did} - refreshing={view.isRefreshing} - onRefresh={onRefresh} - onEndReached={onEndReached} - renderItem={renderItem} - initialNumToRender={15} - ListFooterComponent={() => ( - - {view.isLoading && } - - )} - contentContainerStyle={s.contentContainer} - /> - - )} - - ) - }, -) +export const SuggestedFollows = ({ + title, + suggestions, +}: { + title: string + suggestions: (AppBskyActorRef.WithInfo | RefWithInfoAndFollowers)[] +}) => { + const pal = usePalette('default') + return ( + + + {title} + + {suggestions.map(item => ( + + + + ))} + + ) +} const styles = StyleSheet.create({ container: { - height: '100%', + paddingVertical: 10, + paddingHorizontal: 4, + }, + + heading: { + fontWeight: 'bold', + paddingHorizontal: 4, + paddingBottom: 8, }, - suggestionsContainer: { - height: '100%', + card: { + borderRadius: 12, + marginBottom: 2, + borderWidth: 1, }, - footer: { - height: 200, - paddingTop: 20, + + loadMore: { + paddingLeft: 16, + paddingVertical: 12, }, }) diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 087536c36..ebb427664 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,6 +1,7 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {AppBskyActorProfile} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' @@ -16,6 +17,7 @@ export function ProfileCard({ description, isFollowedBy, noBorder, + followers, renderButton, }: { handle: string @@ -24,17 +26,13 @@ export function ProfileCard({ description?: string isFollowedBy?: boolean noBorder?: boolean + followers?: AppBskyActorProfile.View[] | undefined renderButton?: () => JSX.Element }) { const pal = usePalette('default') return ( ) : undefined} + {followers?.length ? ( + + + Followed by{' '} + {followers.map(f => f.displayName || f.handle).join(', ')} + + {followers.slice(0, 3).map(f => ( + + + + + + ))} + + ) : undefined} ) } @@ -86,6 +103,8 @@ export const ProfileCardWithFollowBtn = observer( avatar, description, isFollowedBy, + noBorder, + followers, }: { did: string declarationCid: string @@ -94,6 +113,8 @@ export const ProfileCardWithFollowBtn = observer( avatar?: string description?: string isFollowedBy?: boolean + noBorder?: boolean + followers?: AppBskyActorProfile.View[] | undefined }) => { const store = useStores() const isMe = store.me.handle === handle @@ -105,6 +126,8 @@ export const ProfileCardWithFollowBtn = observer( avatar={avatar} description={description} isFollowedBy={isFollowedBy} + noBorder={noBorder} + followers={followers} renderButton={ isMe ? undefined @@ -128,8 +151,8 @@ const styles = StyleSheet.create({ alignItems: 'center', }, layoutAvi: { - width: 60, - paddingLeft: 10, + width: 54, + paddingLeft: 4, paddingTop: 8, paddingBottom: 10, }, @@ -164,4 +187,27 @@ const styles = StyleSheet.create({ marginLeft: 6, paddingHorizontal: 14, }, + + followedBy: { + flexDirection: 'row', + alignItems: 'flex-start', + paddingLeft: 54, + paddingRight: 20, + marginBottom: 10, + marginTop: -6, + }, + followedByAviContainer: { + width: 24, + height: 36, + }, + followedByAvi: { + width: 36, + height: 36, + borderRadius: 18, + padding: 2, + }, + followsByDesc: { + flex: 1, + paddingRight: 10, + }, }) diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 9e72640d2..2f653ee09 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -128,6 +128,46 @@ export function NotificationFeedLoadingPlaceholder() { ) } +export function ProfileCardLoadingPlaceholder({ + style, +}: { + style?: StyleProp +}) { + const pal = usePalette('default') + return ( + + + + + + + + + ) +} + +export function ProfileCardFeedLoadingPlaceholder() { + return ( + <> + + + + + + + + + + + + + ) +} + const styles = StyleSheet.create({ loadingPlaceholder: { borderRadius: 6, @@ -147,6 +187,15 @@ const styles = StyleSheet.create({ paddingLeft: 46, margin: 1, }, + profileCard: { + flexDirection: 'row', + padding: 10, + margin: 1, + }, + profileCardAvi: { + borderRadius: 20, + marginRight: 10, + }, smallAvatar: { borderRadius: 15, marginRight: 10, diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 19535a164..246aa13f5 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -1,6 +1,7 @@ import React from 'react' import { Keyboard, + RefreshControl, StyleSheet, TextInput, TouchableOpacity, @@ -13,21 +14,23 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ScrollView} from '../com/util/Views' +import {ScrollView} from 'view/com/util/Views' import { NativeStackScreenProps, SearchTabNavigatorParams, } from 'lib/routes/types' import {observer} from 'mobx-react-lite' -import {UserAvatar} from '../com/util/UserAvatar' -import {Text} from '../com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {Text} from 'view/com/util/text/Text' import {useStores} from 'state/index' import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' +import {FoafsModel} from 'state/models/discovery/foafs' import {s} from 'lib/styles' import {MagnifyingGlassIcon} from 'lib/icons' -import {WhoToFollow} from '../com/discover/WhoToFollow' -import {SuggestedPosts} from '../com/discover/SuggestedPosts' -import {ProfileCard} from '../com/profile/ProfileCard' +import {WhoToFollow} from 'view/com/discover/WhoToFollow' +import {SuggestedFollows} from 'view/com/discover/SuggestedFollows' +import {ProfileCard} from 'view/com/profile/ProfileCard' +import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' @@ -53,6 +56,11 @@ export const SearchScreen = withAuthRequired( () => new UserAutocompleteViewModel(store), [store], ) + const foafsView = React.useMemo( + () => new FoafsModel(store), + [store], + ) + const [refreshing, setRefreshing] = React.useState(false) const onSoftReset = () => { scrollElRef.current?.scrollTo({x: 0, y: 0}) @@ -71,9 +79,12 @@ export const SearchScreen = withAuthRequired( } store.shell.setMinimalShellMode(false) autocompleteView.setup() + if (!foafsView.hasData) { + foafsView.fetch() + } return cleanup - }, [store, autocompleteView, lastRenderTime, setRenderTime]), + }, [store, autocompleteView, foafsView, lastRenderTime, setRenderTime]), ) const onPressMenu = () => { @@ -98,15 +109,18 @@ export const SearchScreen = withAuthRequired( autocompleteView.setActive(false) textInput.current?.blur() } + const onRefresh = React.useCallback(async () => { + setRefreshing(true) + try { + await foafsView.fetch() + } finally { + setRefreshing(false) + } + }, [foafsView, setRefreshing]) return ( - + ) : ( - - - + + }> + {foafsView.isLoading ? ( + + ) : foafsView.sources.length ? ( + <> + {foafsView.popular.length > 0 && ( + + + + )} + {foafsView.sources.map((source, i) => { + const item = foafsView.foafs.get(source) + if (!item || item.follows.length === 0) { + return + } + return ( + + + + ) + })} + + ) : ( + + + + )} )} - - + ) }), @@ -235,4 +288,8 @@ const styles = StyleSheet.create({ textAlign: 'center', paddingTop: 10, }, + + suggestions: { + marginBottom: 8, + }, }) -- cgit 1.4.1 From 025681c2f2c161325bac7fa7c435c580b947d6f4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 17 Mar 2023 18:09:27 -0500 Subject: Fall back to who to follow --- src/state/models/discovery/foafs.ts | 12 ++++++++++++ src/view/screens/Search.tsx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts index 4a46ae9de..241338a16 100644 --- a/src/state/models/discovery/foafs.ts +++ b/src/state/models/discovery/foafs.ts @@ -23,6 +23,18 @@ export class FoafsModel { makeAutoObservable(this) } + get hasContent() { + if (this.popular.length > 0) { + return true + } + for (const foaf of this.foafs.values()) { + if (foaf.follows.length) { + return true + } + } + return false + } + fetch = bundleAsync(async () => { try { this.isLoading = true diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 246aa13f5..6ae5fba0d 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -205,7 +205,7 @@ export const SearchScreen = withAuthRequired( }> {foafsView.isLoading ? ( - ) : foafsView.sources.length ? ( + ) : foafsView.hasContent ? ( <> {foafsView.popular.length > 0 && ( -- cgit 1.4.1 From 8364e1a081affa2f51efb216688650410a3122b3 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 17 Mar 2023 18:11:57 -0500 Subject: Fix backgrounds --- src/view/com/discover/SuggestedFollows.tsx | 1 + src/view/com/profile/ProfileCard.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index dd1136a48..7a64a15f6 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -28,6 +28,7 @@ export const SuggestedFollows = ({ handle={item.handle} displayName={item.displayName} avatar={item.avatar} + noBg noBorder description="" followers={ diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index ebb427664..53f45fb11 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -16,6 +16,7 @@ export function ProfileCard({ avatar, description, isFollowedBy, + noBg, noBorder, followers, renderButton, @@ -25,6 +26,7 @@ export function ProfileCard({ avatar?: string description?: string isFollowedBy?: boolean + noBg?: boolean noBorder?: boolean followers?: AppBskyActorProfile.View[] | undefined renderButton?: () => JSX.Element @@ -32,7 +34,12 @@ export function ProfileCard({ const pal = usePalette('default') return ( { @@ -126,6 +135,7 @@ export const ProfileCardWithFollowBtn = observer( avatar={avatar} description={description} isFollowedBy={isFollowedBy} + noBg={noBg} noBorder={noBorder} followers={followers} renderButton={ -- cgit 1.4.1 From 808affc78b9f1e0ed07c8cfdc8787f6f1c84d497 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 17 Mar 2023 18:38:16 -0500 Subject: Switch to the off-spec goodstuff route --- src/lib/constants.ts | 10 -------- src/state/models/feed-view.ts | 60 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ffc1aaed5..0cde9b014 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -161,16 +161,6 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) { } } -export function GOOD_STUFF(serviceUrl: string) { - if (serviceUrl.includes('localhost')) { - return 'alice.test' - } else if (serviceUrl.includes('staging')) { - return 'paul.staging.bsky.dev' - } else { - return 'jay.bsky.social' - } -} - export const POST_IMG_MAX_WIDTH = 2000 export const POST_IMG_MAX_HEIGHT = 2000 export const POST_IMG_MAX_SIZE = 1000000 diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 3d7680fbc..cff9680ff 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -16,7 +16,7 @@ import {RootStoreModel} from './root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {RichText} from 'lib/strings/rich-text' -import {SUGGESTED_FOLLOWS, GOOD_STUFF} from 'lib/constants' +import {SUGGESTED_FOLLOWS} from 'lib/constants' import { getCombinedCursors, getMultipleAuthorsPosts, @@ -399,6 +399,7 @@ export class FeedModel { params: this.params, e, }) + this.hasMore = false } } finally { this.lock.release() @@ -641,14 +642,13 @@ export class FeedModel { params as GetTimeline.QueryParams, ) } else if (this.feedType === 'goodstuff') { - const res = await this.rootStore.api.app.bsky.feed.getAuthorFeed({ - ...params, - author: GOOD_STUFF(String(this.rootStore.agent.service)), - } as GetAuthorFeed.QueryParams) - res.data.feed = mergePosts([res], {repostsOnly: true}) - res.data.feed.forEach(item => { - delete item.reason - }) + const res = await getGoodStuff( + this.rootStore.session.currentSession?.accessJwt || '', + params as GetTimeline.QueryParams, + ) + res.data.feed = res.data.feed.filter( + item => !item.post.author.viewer?.muted, + ) return res } else { return this.rootStore.api.app.bsky.feed.getAuthorFeed( @@ -657,3 +657,45 @@ export class FeedModel { } } } + +// HACK +// temporary off-spec route to get the good stuff +// -prf +async function getGoodStuff( + accessJwt: string, + params: GetTimeline.QueryParams, +): Promise { + const controller = new AbortController() + const to = setTimeout(() => controller.abort(), 15e3) + + const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular') + let k: keyof GetTimeline.QueryParams + for (k in params) { + if (typeof params[k] !== 'undefined') { + uri.searchParams.set(k, String(params[k])) + } + } + + const res = await fetch(String(uri), { + method: 'get', + headers: { + accept: 'application/json', + authorization: `Bearer ${accessJwt}`, + }, + signal: controller.signal, + }) + + const resHeaders: Record = {} + res.headers.forEach((value: string, key: string) => { + resHeaders[key] = value + }) + let resBody = await res.json() + + clearTimeout(to) + + return { + success: res.status === 200, + headers: resHeaders, + data: resBody, + } +} -- cgit 1.4.1 From 5def9eb238ed51a45a781f6c91520526bb3ae7df Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 17 Mar 2023 18:40:26 -0500 Subject: Fix for dev & staging --- src/state/models/feed-view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index cff9680ff..664d6bd01 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -646,7 +646,7 @@ export class FeedModel { this.rootStore.session.currentSession?.accessJwt || '', params as GetTimeline.QueryParams, ) - res.data.feed = res.data.feed.filter( + res.data.feed = (res.data.feed || []).filter( item => !item.post.author.viewer?.muted, ) return res -- cgit 1.4.1 From 6578d2bfadaa0aa937a5467973f4c44505634afe Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 19 Mar 2023 16:25:42 -0500 Subject: Swap the tab bar items and rename suggested to what's hot --- src/view/com/util/Pager.tsx | 10 ++-------- src/view/com/util/TabBar.tsx | 22 ++++++++++++---------- src/view/screens/Home.tsx | 23 +++++++++++++---------- 3 files changed, 27 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx index 47b786fb1..c4f17ce65 100644 --- a/src/view/com/util/Pager.tsx +++ b/src/view/com/util/Pager.tsx @@ -1,19 +1,13 @@ import React from 'react' -import {Animated, StyleSheet, View} from 'react-native' +import {Animated, View} from 'react-native' import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {TabBarProps} from './TabBar' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) -export interface TabBarProps { - selectedPage: number - position: Animated.Value - offset: Animated.Value - onSelect?: (index: number) => void -} - interface Props { tabBarPosition?: 'top' | 'bottom' renderTabBar: (props: TabBarProps) => JSX.Element diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx index 67c9276c6..dd8fdcb56 100644 --- a/src/view/com/util/TabBar.tsx +++ b/src/view/com/util/TabBar.tsx @@ -13,6 +13,17 @@ interface Layout { width: number } +export interface TabBarProps { + selectedPage: number + items: string[] + position: Animated.Value + offset: Animated.Value + indicatorPosition?: 'top' | 'bottom' + indicatorColor?: string + onSelect?: (index: number) => void + onPressSelected?: () => void +} + export function TabBar({ selectedPage, items, @@ -22,16 +33,7 @@ export function TabBar({ indicatorColor, onSelect, onPressSelected, -}: { - selectedPage: number - items: string[] - position: Animated.Value - offset: Animated.Value - indicatorPosition?: 'top' | 'bottom' - indicatorColor?: string - onSelect?: (index: number) => void - onPressSelected?: () => void -}) { +}: TabBarProps) { const pal = usePalette('default') const [itemLayouts, setItemLayouts] = useState( items.map(() => ({x: 0, width: 0})), diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 5250f1155..1c3ee091a 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -14,8 +14,8 @@ import {FeedModel} from 'state/models/feed-view' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from '../com/posts/Feed' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {TabBar} from 'view/com/util/TabBar' -import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager' +import {TabBar, TabBarProps} from 'view/com/util/TabBar' +import {Pager, PageSelectedEvent} from 'view/com/util/Pager' import {FAB} from '../com/util/FAB' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' @@ -62,21 +62,24 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { store.emitScreenSoftReset() }, [store]) - const renderTabBar = React.useCallback((props: TabBarProps) => { - return - }, []) + const renderTabBar = React.useCallback( + (props: TabBarProps) => { + return + }, + [onPressSelected], + ) return ( - + ) }) @@ -112,8 +115,8 @@ const FloatingTabBar = observer((props: TabBarProps) => { @@ -232,7 +235,7 @@ const styles = StyleSheet.create({ bottom: 0, flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 8, + paddingHorizontal: 10, borderTopWidth: 1, paddingTop: 0, paddingBottom: 30, -- cgit 1.4.1 From 7a754850bc71a46f4b198e942b7427536b253587 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 19 Mar 2023 17:30:58 -0500 Subject: Go to whats-hot by default if you have no follows --- src/state/models/me.ts | 1 + src/state/models/my-follows.ts | 6 ++ src/state/models/session.ts | 10 +-- src/view/com/posts/Feed.tsx | 68 +++--------------- src/view/com/posts/FollowingEmptyState.tsx | 81 ++++++++++++++++++++++ src/view/com/util/Pager.tsx | 15 +++- src/view/screens/Home.tsx | 107 ++++++++++++++++++----------- 7 files changed, 178 insertions(+), 110 deletions(-) create mode 100644 src/view/com/posts/FollowingEmptyState.tsx (limited to 'src') diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 077c65595..192e8f19f 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -33,6 +33,7 @@ export class MeModel { clear() { this.mainFeed.clear() this.notifications.clear() + this.follows.clear() this.did = '' this.handle = '' this.displayName = '' diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts index 732c2fe73..bf1bf9600 100644 --- a/src/state/models/my-follows.ts +++ b/src/state/models/my-follows.ts @@ -35,6 +35,12 @@ export class MyFollowsModel { // public api // = + clear() { + this.followDidToRecordMap = {} + this.lastSync = 0 + this.myDid = undefined + } + fetchIfNeeded = bundleAsync(async () => { if ( this.myDid !== this.rootStore.me.did || diff --git a/src/state/models/session.ts b/src/state/models/session.ts index efd7a5fa8..e131b2b2c 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -154,13 +154,13 @@ export class SessionModel { /** * Sets the active session */ - setActiveSession(agent: AtpAgent, did: string) { + async setActiveSession(agent: AtpAgent, did: string) { this._log('SessionModel:setActiveSession') this.data = { service: agent.service.toString(), did, } - this.rootStore.handleSessionChange(agent) + await this.rootStore.handleSessionChange(agent) } /** @@ -304,7 +304,7 @@ export class SessionModel { return false } - this.setActiveSession(agent, account.did) + await this.setActiveSession(agent, account.did) return true } @@ -337,7 +337,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) + await this.setActiveSession(agent, did) this._log('SessionModel:login succeeded') } @@ -376,7 +376,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) + await this.setActiveSession(agent, did) this._log('SessionModel:createAccount succeeded') } diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index d23455b51..c910b70e7 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -7,23 +7,15 @@ import { StyleSheet, ViewStyle, } from 'react-native' -import {useNavigation} from '@react-navigation/native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {CenteredView, FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ViewHeader} from '../util/ViewHeader' -import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' import {FeedModel} from 'state/models/feed-view' import {FeedSlice} from './FeedSlice' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {MagnifyingGlassIcon} from 'lib/icons' -import {NavigationProp} from 'lib/routes/types' const HEADER_ITEM = {_reactKey: '__header__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -36,6 +28,7 @@ export const Feed = observer(function Feed({ scrollElRef, onPressTryAgain, onScroll, + renderEmptyState, testID, headerOffset = 0, }: { @@ -45,14 +38,12 @@ export const Feed = observer(function Feed({ scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb + renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number }) { - const pal = usePalette('default') - const palInverted = usePalette('inverted') const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) - const navigation = useNavigation() const data = React.useMemo(() => { let feedItems: any[] = [HEADER_ITEM] @@ -82,6 +73,7 @@ export const Feed = observer(function Feed({ } setIsRefreshing(false) }, [feed, track, setIsRefreshing]) + const onEndReached = React.useCallback(async () => { track('Feed:onEndReached') try { @@ -97,37 +89,10 @@ export const Feed = observer(function Feed({ const renderItem = React.useCallback( ({item}: {item: any}) => { if (item === EMPTY_FEED_ITEM) { - return ( - - - - - - Your feed is empty! You should follow some accounts to fix this. - - - - ) + if (renderEmptyState) { + return renderEmptyState() + } + return } else if (item === ERROR_FEED_ITEM) { return ( }, - [feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, navigation], + [feed, onPressTryAgain, showPostFollowBtn, renderEmptyState], ) const FeedFooter = React.useCallback( @@ -187,21 +152,4 @@ export const Feed = observer(function Feed({ const styles = StyleSheet.create({ feedFooter: {paddingTop: 20}, - emptyContainer: { - paddingVertical: 40, - paddingHorizontal: 30, - }, - emptyIconContainer: { - marginBottom: 16, - }, - emptyIcon: { - marginLeft: 'auto', - marginRight: 'auto', - }, - emptyBtn: { - marginTop: 20, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, }) diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx new file mode 100644 index 000000000..acd035f21 --- /dev/null +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useNavigation} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {MagnifyingGlassIcon} from 'lib/icons' +import {NavigationProp} from 'lib/routes/types' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' + +export function FollowingEmptyState() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const navigation = useNavigation() + + const onPressFindAccounts = React.useCallback(() => { + navigation.navigate('SearchTab') + navigation.popToTop() + }, [navigation]) + + return ( + + + + + + Your following feed is empty! Find some accounts to follow to fix this. + + + + ) +} +const styles = StyleSheet.create({ + emptyContainer: { + // flex: 1, + height: '100%', + paddingVertical: 40, + paddingHorizontal: 30, + }, + emptyIconContainer: { + marginBottom: 16, + }, + emptyIcon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + emptyBtn: { + marginVertical: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 18, + paddingHorizontal: 24, + borderRadius: 30, + }, + + feedsTip: { + position: 'absolute', + left: 22, + }, + feedsTipArrow: { + marginLeft: 32, + marginTop: 8, + }, +}) diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx index c4f17ce65..d71cb7f7f 100644 --- a/src/view/com/util/Pager.tsx +++ b/src/view/com/util/Pager.tsx @@ -1,21 +1,30 @@ import React from 'react' import {Animated, View} from 'react-native' import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' -import {TabBarProps} from './TabBar' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + interface Props { tabBarPosition?: 'top' | 'bottom' - renderTabBar: (props: TabBarProps) => JSX.Element + initialPage?: number + renderTabBar: RenderTabBarFn onPageSelected?: (e: PageSelectedEvent) => void } export const Pager = ({ children, tabBarPosition = 'top', + initialPage = 0, renderTabBar, onPageSelected, }: React.PropsWithChildren) => { @@ -51,7 +60,7 @@ export const Pager = ({ { }, [store]) const renderTabBar = React.useCallback( - (props: TabBarProps) => { + (props: RenderTabBarFnProps) => { return }, [onPressSelected], ) + const renderFollowingEmptyState = React.useCallback(() => { + return + }, []) + + const initialPage = store.me.follows.isEmpty ? 1 : 0 return ( + tabBarPosition="bottom" + initialPage={initialPage}> ) }) -const FloatingTabBar = observer((props: TabBarProps) => { - const store = useStores() - const safeAreaInsets = useSafeAreaInsets() - const pal = usePalette('default') - const interp = useAnimatedValue(0) - - const pad = React.useMemo( - () => ({ - paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), - }), - [safeAreaInsets], - ) +const FloatingTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const store = useStores() + const safeAreaInsets = useSafeAreaInsets() + const pal = usePalette('default') + const interp = useAnimatedValue(0) - React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) - const transform = { - transform: [ - {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, - ], - } + const pad = React.useMemo( + () => ({ + paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), + }), + [safeAreaInsets], + ) - return ( - - - - ) -}) + React.useEffect(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 0 : 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [ + {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, + ], + } + + return ( + + + + ) + }, +) const FeedPage = observer( - ({isPageFocused, feed}: {feed: FeedModel; isPageFocused: boolean}) => { + ({ + isPageFocused, + feed, + renderEmptyState, + }: { + feed: FeedModel + isPageFocused: boolean + renderEmptyState?: () => JSX.Element + }) => { const store = useStores() const onMainScroll = useOnMainScroll(store) const {screen, track} = useAnalytics() @@ -213,6 +235,7 @@ const FeedPage = observer( showPostFollowBtn onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} + renderEmptyState={renderEmptyState} /> {feed.hasNewLatest && !feed.isRefreshing && ( -- cgit 1.4.1 From 23e8484986acc2926d795d0406dd3c820ec83a6c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 19 Mar 2023 18:11:33 -0500 Subject: Implement pager and tabbar for desktop web --- ios/Podfile.lock | 48 ++++++++-------- src/lib/styles.ts | 1 + src/view/com/util/Pager.tsx | 87 ---------------------------- src/view/com/util/TabBar.tsx | 75 ++++++++++++++++-------- src/view/com/util/pager/Pager.tsx | 87 ++++++++++++++++++++++++++++ src/view/com/util/pager/Pager.web.tsx | 65 +++++++++++++++++++++ src/view/screens/Home.tsx | 94 ++++--------------------------- src/view/screens/home/FeedsTabBar.tsx | 72 +++++++++++++++++++++++ src/view/screens/home/FeedsTabBar.web.tsx | 22 ++++++++ 9 files changed, 331 insertions(+), 220 deletions(-) delete mode 100644 src/view/com/util/Pager.tsx create mode 100644 src/view/com/util/pager/Pager.tsx create mode 100644 src/view/com/util/pager/Pager.web.tsx create mode 100644 src/view/screens/home/FeedsTabBar.tsx create mode 100644 src/view/screens/home/FeedsTabBar.web.tsx (limited to 'src') diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a27db8a6c..12f262ddd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -19,10 +19,10 @@ PODS: - EXJSONUtils (0.5.1) - EXManifests (0.5.2): - EXJSONUtils - - EXMediaLibrary (15.2.2): + - EXMediaLibrary (15.2.3): - ExpoModulesCore - React-Core - - Expo (48.0.6): + - Expo (48.0.7): - ExpoModulesCore - expo-dev-client (2.1.5): - EXManifests @@ -100,7 +100,7 @@ PODS: - ExpoModulesCore - ExpoKeepAwake (12.0.1): - ExpoModulesCore - - ExpoModulesCore (1.2.4): + - ExpoModulesCore (1.2.5): - React-Core - React-RCTAppDelegate - ReactCommon/turbomodule/core @@ -384,11 +384,11 @@ PODS: - glog - react-native-blur (4.3.0): - React-Core - - react-native-cameraroll (5.2.4): + - react-native-cameraroll (5.3.1): - React-Core - react-native-image-resizer (3.0.5): - React-Core - - react-native-pager-view (6.1.4): + - react-native-pager-view (6.1.2): - React-Core - react-native-paste-input (0.6.2): - React-Core @@ -403,7 +403,7 @@ PODS: - React-Core - react-native-version-number (0.3.6): - React - - react-native-webview (11.26.1): + - react-native-webview (11.26.0): - React-Core - React-perflogger (0.71.3) - React-RCTActionSheet (0.71.3): @@ -491,9 +491,9 @@ PODS: - React-perflogger (= 0.71.3) - rn-fetch-blob (0.12.0): - React-Core - - RNBackgroundFetch (4.1.8): + - RNBackgroundFetch (4.1.9): - React-Core - - RNCAsyncStorage (1.17.11): + - RNCAsyncStorage (1.17.12): - React-Core - RNCClipboard (1.11.2): - React-Core @@ -516,10 +516,10 @@ PODS: - TOCropViewController - RNInAppBrowser (3.7.0): - React-Core - - RNNotifee (7.5.0): + - RNNotifee (7.6.1): - React-Core - - RNNotifee/NotifeeCore (= 7.5.0) - - RNNotifee/NotifeeCore (7.5.0): + - RNNotifee/NotifeeCore (= 7.6.1) + - RNNotifee/NotifeeCore (7.6.1): - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core @@ -553,7 +553,7 @@ PODS: - RNScreens (3.20.0): - React-Core - React-RCTImage - - RNSVG (13.8.0): + - RNSVG (13.4.0): - React-Core - SDWebImage (5.11.1): - SDWebImage/Core (= 5.11.1) @@ -561,7 +561,7 @@ PODS: - SDWebImageWebPCoder (0.8.5): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - - segment-analytics-react-native (2.13.1): + - segment-analytics-react-native (2.13.4): - React-Core - sovran-react-native - sovran-react-native (0.4.5): @@ -835,15 +835,15 @@ SPEC CHECKSUMS: EXImageLoader: fd053169a8ee932dd83bf1fe5487a50c26d27c2b EXJSONUtils: 48b1e764ac35160e6f54d21ab60d7d9501f3e473 EXManifests: 500666d48e8dd7ca5a482c9e729e4a7a6c34081b - EXMediaLibrary: 792fe9b828b5bfa2c5a8b629730f175af2938285 - Expo: 04ba1ddde0be07aff4306ae636a1804810679145 + EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480 + Expo: 707f9b0039eacc6a1dce90c08c9e37b9c417bba2 expo-dev-client: 7c1ef51516853465f4d448c14ddf365167d20361 expo-dev-launcher: 90de99d9e5d1a883d81355ca10e87c2f3c81d46e expo-dev-menu: d4369e74d8d21a0ccdee35f7c732e7118b0fee16 expo-dev-menu-interface: 6c82ae323c4b8724dead4763ce3ff24a2108bdb1 ExpoImagePicker: 270dea232b3a072d981dd564e2cafc63a864edb1 ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a - ExpoModulesCore: 1667335d4f4c9b7801990930e6f0eea42c916a21 + ExpoModulesCore: 397fc99e9d6c9dcc010f36d5802097c17b90424c EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca EXUpdatesInterface: dd699d1930e28639dcbd70a402caea98e86364ca FBLazyVector: 60195509584153283780abdac5569feffb8f08cc @@ -868,14 +868,14 @@ SPEC CHECKSUMS: React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 - react-native-cameraroll: cb752fda6d5268f1646b4390bd5be1f27706b9a0 + react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2 react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa - react-native-pager-view: b58cb9e9f42f64e50cab3040815772c1d119a2e2 + react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-paste-input: 3392800944a47c00dddbff23c31c281482209679 react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f - react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1 + react-native-webview: 994b9f8fbb504d6314dc40d83f94f27c6831b3bf React-perflogger: af8a3d31546077f42d729b949925cc4549f14def React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673 React-RCTAnimation: 11c61e94da700c4dc915cf134513764d87fc5e2b @@ -890,22 +890,22 @@ SPEC CHECKSUMS: React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba - RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623 - RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60 + RNBackgroundFetch: 642777e4e76435773c149d565a043d66f1781237 + RNCAsyncStorage: 09fc8595e6d6f6d5abf16b23a56b257d9c6b7c5b RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNNotifee: 053c0ace9c73634709a0214fd9c436a5777a562f + RNNotifee: bdc064c29f4d558046f51f0c3ae02bab4fd3cd85 RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f - RNSVG: c1e76b81c76cdcd34b4e1188852892dc280eb902 + RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d - segment-analytics-react-native: f962dff3a084655a29f9403b8c139c75a3362524 + segment-analytics-react-native: cc12d9422f7ce863ee57c1b650ab48eec4b6d5bd sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 5d7f7f82d..aa255b21f 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -70,6 +70,7 @@ export const s = StyleSheet.create({ borderRight1: {borderRightWidth: 1}, borderBottom1: {borderBottomWidth: 1}, borderLeft1: {borderLeftWidth: 1}, + hidden: {display: 'none'}, // font weights fw600: {fontWeight: '600'}, diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx deleted file mode 100644 index d71cb7f7f..000000000 --- a/src/view/com/util/Pager.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import {Animated, View} from 'react-native' -import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {s} from 'lib/styles' - -export type PageSelectedEvent = PagerViewOnPageSelectedEvent -const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) - -export interface RenderTabBarFnProps { - selectedPage: number - position: Animated.Value - offset: Animated.Value - onSelect?: (index: number) => void -} -export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element - -interface Props { - tabBarPosition?: 'top' | 'bottom' - initialPage?: number - renderTabBar: RenderTabBarFn - onPageSelected?: (e: PageSelectedEvent) => void -} -export const Pager = ({ - children, - tabBarPosition = 'top', - initialPage = 0, - renderTabBar, - onPageSelected, -}: React.PropsWithChildren) => { - const [selectedPage, setSelectedPage] = React.useState(0) - const position = useAnimatedValue(0) - const offset = useAnimatedValue(0) - const pagerView = React.useRef() - - const onPageSelectedInner = React.useCallback( - (e: PageSelectedEvent) => { - setSelectedPage(e.nativeEvent.position) - onPageSelected?.(e) - }, - [setSelectedPage, onPageSelected], - ) - - const onTabBarSelect = React.useCallback( - (index: number) => { - pagerView.current?.setPage(index) - }, - [pagerView], - ) - - return ( - - {tabBarPosition === 'top' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - - {children} - - {tabBarPosition === 'bottom' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - - ) -} diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx index dd8fdcb56..4b67b8a80 100644 --- a/src/view/com/util/TabBar.tsx +++ b/src/view/com/util/TabBar.tsx @@ -7,6 +7,7 @@ import { } from 'react-native' import {Text} from './text/Text' import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' interface Layout { x: number @@ -46,8 +47,9 @@ export function TabBar({ const indicatorStyle = { backgroundColor: indicatorColor || pal.colors.link, - bottom: indicatorPosition === 'bottom' ? -1 : undefined, - top: indicatorPosition === 'top' ? -1 : undefined, + bottom: + indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined, + top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined, transform: [ { translateX: panX.interpolate({ @@ -112,26 +114,49 @@ export function TabBar({ ) } -const styles = StyleSheet.create({ - outer: { - flexDirection: 'row', - paddingHorizontal: 14, - }, - itemTop: { - paddingTop: 10, - paddingBottom: 10, - marginRight: 24, - }, - itemBottom: { - paddingTop: 8, - paddingBottom: 12, - marginRight: 24, - }, - indicator: { - position: 'absolute', - left: 0, - width: 1, - height: 3, - borderRadius: 4, - }, -}) +const styles = isDesktopWeb + ? StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 18, + }, + itemTop: { + paddingTop: 16, + paddingBottom: 14, + marginRight: 24, + }, + itemBottom: { + paddingTop: 14, + paddingBottom: 16, + marginRight: 24, + }, + indicator: { + position: 'absolute', + left: 0, + width: 1, + height: 3, + }, + }) + : StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 14, + }, + itemTop: { + paddingTop: 10, + paddingBottom: 10, + marginRight: 24, + }, + itemBottom: { + paddingTop: 8, + paddingBottom: 12, + marginRight: 24, + }, + indicator: { + position: 'absolute', + left: 0, + width: 1, + height: 3, + borderRadius: 4, + }, + }) diff --git a/src/view/com/util/pager/Pager.tsx b/src/view/com/util/pager/Pager.tsx new file mode 100644 index 000000000..416828a27 --- /dev/null +++ b/src/view/com/util/pager/Pager.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import {Animated, View} from 'react-native' +import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {s} from 'lib/styles' + +export type PageSelectedEvent = PagerViewOnPageSelectedEvent +const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) + +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + +interface Props { + tabBarPosition?: 'top' | 'bottom' + initialPage?: number + renderTabBar: RenderTabBarFn + onPageSelected?: (index: number) => void +} +export const Pager = ({ + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, +}: React.PropsWithChildren) => { + const [selectedPage, setSelectedPage] = React.useState(0) + const position = useAnimatedValue(0) + const offset = useAnimatedValue(0) + const pagerView = React.useRef() + + const onPageSelectedInner = React.useCallback( + (e: PageSelectedEvent) => { + setSelectedPage(e.nativeEvent.position) + onPageSelected?.(e.nativeEvent.position) + }, + [setSelectedPage, onPageSelected], + ) + + const onTabBarSelect = React.useCallback( + (index: number) => { + pagerView.current?.setPage(index) + }, + [pagerView], + ) + + return ( + + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + + {children} + + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + + ) +} diff --git a/src/view/com/util/pager/Pager.web.tsx b/src/view/com/util/pager/Pager.web.tsx new file mode 100644 index 000000000..d50100de9 --- /dev/null +++ b/src/view/com/util/pager/Pager.web.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import {Animated, View} from 'react-native' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {s} from 'lib/styles' + +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + +interface Props { + tabBarPosition?: 'top' | 'bottom' + initialPage?: number + renderTabBar: RenderTabBarFn + onPageSelected?: (index: number) => void +} +export const Pager = ({ + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, +}: React.PropsWithChildren) => { + const [selectedPage, setSelectedPage] = React.useState(initialPage) + const position = useAnimatedValue(0) + const offset = useAnimatedValue(0) + + const onTabBarSelect = React.useCallback( + (index: number) => { + setSelectedPage(index) + onPageSelected?.(index) + Animated.timing(position, { + toValue: index, + duration: 200, + useNativeDriver: true, + }).start() + }, + [setSelectedPage, onPageSelected, position], + ) + + return ( + + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + {children.map((child, i) => ( + {child} + ))} + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + + ) +} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 822174446..4950bc0fd 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,11 +1,5 @@ import React from 'react' -import { - Animated, - FlatList, - StyleSheet, - View, - useWindowDimensions, -} from 'react-native' +import {FlatList, View, useWindowDimensions} from 'react-native' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' @@ -15,25 +9,17 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from '../com/posts/Feed' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {TabBar} from 'view/com/util/TabBar' -import { - Pager, - PageSelectedEvent, - RenderTabBarFnProps, -} from 'view/com/util/Pager' +import {FeedsTabBar} from './home/FeedsTabBar' +import {Pager, RenderTabBarFnProps} from 'view/com/util/pager/Pager' import {FAB} from '../com/util/FAB' import {useStores} from 'state/index' -import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' -import {clamp} from 'lodash' +import {isDesktopWeb} from 'platform/detection' const TAB_BAR_HEIGHT = 82 -const BOTTOM_BAR_HEIGHT = 48 type Props = NativeStackScreenProps export const HomeScreen = withAuthRequired((_opts: Props) => { @@ -56,9 +42,9 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { ) const onPageSelected = React.useCallback( - (e: PageSelectedEvent) => { - setSelectedPage(e.nativeEvent.position) - store.shell.setIsDrawerSwipeDisabled(e.nativeEvent.position > 0) + (index: number) => { + setSelectedPage(index) + store.shell.setIsDrawerSwipeDisabled(index > 0) }, [store], ) @@ -69,7 +55,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { - return + return }, [onPressSelected], ) @@ -83,7 +69,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { { ) }) -const FloatingTabBar = observer( - (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { - const store = useStores() - const safeAreaInsets = useSafeAreaInsets() - const pal = usePalette('default') - const interp = useAnimatedValue(0) - - const pad = React.useMemo( - () => ({ - paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), - }), - [safeAreaInsets], - ) - - React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) - const transform = { - transform: [ - {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, - ], - } - - return ( - - - - ) - }, -) - const FeedPage = observer( ({ isPageFocused, @@ -158,7 +102,7 @@ const FeedPage = observer( const isScreenFocused = useIsFocused() const winDim = useWindowDimensions() const containerStyle = React.useMemo( - () => ({height: winDim.height - TAB_BAR_HEIGHT}), + () => ({height: winDim.height - (isDesktopWeb ? 0 : TAB_BAR_HEIGHT)}), [winDim], ) @@ -249,21 +193,3 @@ const FeedPage = observer( ) }, ) - -const styles = StyleSheet.create({ - tabBar: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 10, - borderTopWidth: 1, - paddingTop: 0, - paddingBottom: 30, - }, - tabBarAvi: { - marginRight: 4, - }, -}) diff --git a/src/view/screens/home/FeedsTabBar.tsx b/src/view/screens/home/FeedsTabBar.tsx new file mode 100644 index 000000000..d34034103 --- /dev/null +++ b/src/view/screens/home/FeedsTabBar.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import {Animated, StyleSheet} from 'react-native' +import {observer} from 'mobx-react-lite' +import {TabBar} from 'view/com/util/TabBar' +import {RenderTabBarFnProps} from 'view/com/util/pager/Pager' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {clamp} from 'lodash' + +const BOTTOM_BAR_HEIGHT = 48 + +export const FeedsTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const store = useStores() + const safeAreaInsets = useSafeAreaInsets() + const pal = usePalette('default') + const interp = useAnimatedValue(0) + + const pad = React.useMemo( + () => ({ + paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), + }), + [safeAreaInsets], + ) + + React.useEffect(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 0 : 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [ + {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, + ], + } + + return ( + + + + ) + }, +) + +const styles = StyleSheet.create({ + tabBar: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 10, + borderTopWidth: 1, + paddingTop: 0, + paddingBottom: 30, + }, + tabBarAvi: { + marginRight: 4, + }, +}) diff --git a/src/view/screens/home/FeedsTabBar.web.tsx b/src/view/screens/home/FeedsTabBar.web.tsx new file mode 100644 index 000000000..59ea42988 --- /dev/null +++ b/src/view/screens/home/FeedsTabBar.web.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import {observer} from 'mobx-react-lite' +import {TabBar} from 'view/com/util/TabBar' +import {CenteredView} from 'view/com/util/Views' +import {RenderTabBarFnProps} from 'view/com/util/pager/Pager' +import {usePalette} from 'lib/hooks/usePalette' + +export const FeedsTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const pal = usePalette('default') + return ( + + + + ) + }, +) -- cgit 1.4.1 From 4787c8383d09550e0ec845a7926298e6cc084d7d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 19 Mar 2023 18:47:49 -0500 Subject: Add language filtering to goodstuff --- package.json | 1 + src/lib/api/feed-manip.ts | 29 +++++++++++++++++++++++++++++ src/state/models/feed-view.ts | 35 +++++++++++++++++------------------ src/view/com/util/pager/Pager.web.tsx | 6 +++++- yarn.lock | 12 ++++++++++++ 5 files changed, 64 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index 796c7aee5..33c72458e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "he": "^1.2.0", "history": "^5.3.0", "js-sha256": "^0.9.0", + "lande": "^1.0.10", "lodash.chunk": "^4.2.0", "lodash.clonedeep": "^4.5.0", "lodash.debounce": "^4.0.8", diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 7abcaffc6..67ca8f952 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -1,5 +1,7 @@ import {AppBskyFeedFeedViewPost} from '@atproto/api' +import lande from 'lande' type FeedViewPost = AppBskyFeedFeedViewPost.Main +import {hasProp} from '@atproto/lexicon' export type FeedTunerFn = ( tuner: FeedTuner, @@ -178,6 +180,33 @@ export class FeedTuner { } } } + + static englishOnly(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { + // TEMP + // remove slices with no english in them + // we very soon need to get the local user's language and filter + // according to their preferences, but for the moment + // we're just rolling with english + // -prf + for (let i = slices.length - 1; i >= 0; i--) { + let hasEnglish = false + for (const item of slices[i].items) { + if ( + hasProp(item.post.record, 'text') && + typeof item.post.record.text === 'string' + ) { + const res = lande(item.post.record.text) + if (res[0][0] === 'eng') { + hasEnglish = true + break + } + } + } + if (!hasEnglish) { + slices.splice(i, 1) + } + } + } } function getSelfReplyUri(item: FeedViewPost): string | undefined { diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 664d6bd01..c412065dd 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -336,6 +336,20 @@ export class FeedModel { return this.setup() } + private get feedTuners() { + if (this.feedType === 'goodstuff') { + return [ + FeedTuner.dedupReposts, + FeedTuner.likedRepliesOnly, + FeedTuner.englishOnly, + ] + } + if (this.feedType === 'home') { + return [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] + } + return [] + } + /** * Load for first render */ @@ -477,12 +491,7 @@ export class FeedModel { } const res = await this._getFeed({limit: 1}) const currentLatestUri = this.pollCursor - const slices = this.tuner.tune( - res.data.feed, - this.feedType === 'home' - ? [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] - : [], - ) + const slices = this.tuner.tune(res.data.feed, this.feedTuners) const item = slices[0]?.rootItem if (!item) { return @@ -548,12 +557,7 @@ export class FeedModel { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor - const slices = this.tuner.tune( - res.data.feed, - this.feedType === 'home' - ? [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] - : [], - ) + const slices = this.tuner.tune(res.data.feed, this.feedTuners) const toAppend: FeedSliceModel[] = [] for (const slice of slices) { @@ -578,12 +582,7 @@ export class FeedModel { ) { this.pollCursor = res.data.feed[0]?.post.uri - const slices = this.tuner.tune( - res.data.feed, - this.feedType === 'home' - ? [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] - : [], - ) + const slices = this.tuner.tune(res.data.feed, this.feedTuners) const toPrepend: FeedSliceModel[] = [] for (const slice of slices) { diff --git a/src/view/com/util/pager/Pager.web.tsx b/src/view/com/util/pager/Pager.web.tsx index d50100de9..3c2805833 100644 --- a/src/view/com/util/pager/Pager.web.tsx +++ b/src/view/com/util/pager/Pager.web.tsx @@ -51,7 +51,11 @@ export const Pager = ({ onSelect: onTabBarSelect, })} {children.map((child, i) => ( - {child} + + {child} + ))} {tabBarPosition === 'bottom' && renderTabBar({ diff --git a/yarn.lock b/yarn.lock index 6d7564bea..f3b747e8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10492,6 +10492,13 @@ kysely@^0.22.0: resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.22.0.tgz#8aac53942da3cadc604d7d154a746d983fe8f7b9" integrity sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ== +lande@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/lande/-/lande-1.0.10.tgz#1f6c6542e628338eb18def22edd1038f5fce9e7a" + integrity sha512-yT52DQh+UV2pEp08jOYrA4drDv0DbjpiRyZYgl25ak9G2cVR2AimzrqkYQWrD9a7Ud+qkAcaiDDoNH9DXfHPmw== + dependencies: + toygrad "^2.6.0" + language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -15679,6 +15686,11 @@ tough-cookie@^4.0.0, tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" +toygrad@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/toygrad/-/toygrad-2.6.0.tgz#e814bb7da026db8e08dc7da14c7155f49cdb4d54" + integrity sha512-g4zBmlSbvzOE5FOILxYkAybTSxijKLkj1WoNqVGnbMcWDyj4wWQ+eYSr3ik7XOpIgMq/7eBcPRTJX3DM2E0YMg== + tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" -- cgit 1.4.1 From 994332af917418c833ffd092e8ded904a0030212 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 19 Mar 2023 19:02:55 -0500 Subject: Add missing deps and an atob polyfill --- ios/Podfile.lock | 6 ++++++ package.json | 1 + src/platform/polyfills.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 12 ++++++++++++ 4 files changed, 68 insertions(+) (limited to 'src') diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 12f262ddd..f0de3762e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -386,6 +386,8 @@ PODS: - React-Core - react-native-cameraroll (5.3.1): - React-Core + - react-native-get-random-values (1.8.0): + - React-Core - react-native-image-resizer (3.0.5): - React-Core - react-native-pager-view (6.1.2): @@ -615,6 +617,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" @@ -748,6 +751,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/blur" react-native-cameraroll: :path: "../node_modules/@react-native-camera-roll/camera-roll" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" react-native-image-resizer: :path: "../node_modules/@bam.tech/react-native-image-resizer" react-native-pager-view: @@ -869,6 +874,7 @@ SPEC CHECKSUMS: React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2 + react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-paste-input: 3392800944a47c00dddbff23c31c281482209679 diff --git a/package.json b/package.json index 33c72458e..194c649e3 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "react-native-fast-image": "^8.6.3", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.9.0", + "react-native-get-random-values": "^1.8.0", "react-native-haptic-feedback": "^1.14.0", "react-native-image-crop-picker": "^0.38.1", "react-native-inappbrowser-reborn": "^3.6.3", diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts index 336ce12bb..3dbd13981 100644 --- a/src/platform/polyfills.ts +++ b/src/platform/polyfills.ts @@ -1 +1,50 @@ export {} + +/** +https://github.com/MaxArt2501/base64-js +The MIT License (MIT) +Copyright (c) 2014 MaxArt2501 + */ + +const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' +// Regular expression to check formal correctness of base64 encoded strings +const b64re = + /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/ + +globalThis.atob = (str: string): string => { + // atob can work with strings with whitespaces, even inside the encoded part, + // but only \t, \n, \f, \r and ' ', which can be stripped. + str = String(str).replace(/[\t\n\f\r ]+/g, '') + if (!b64re.test(str)) { + throw new TypeError( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + ) + } + + // Adding the padding if missing, for semplicity + str += '=='.slice(2 - (str.length & 3)) + var bitmap, + result = '', + r1, + r2, + i = 0 + for (; i < str.length; ) { + bitmap = + (b64.indexOf(str.charAt(i++)) << 18) | + (b64.indexOf(str.charAt(i++)) << 12) | + ((r1 = b64.indexOf(str.charAt(i++))) << 6) | + (r2 = b64.indexOf(str.charAt(i++))) + + result += + r1 === 64 + ? String.fromCharCode((bitmap >> 16) & 255) + : r2 === 64 + ? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255) + : String.fromCharCode( + (bitmap >> 16) & 255, + (bitmap >> 8) & 255, + bitmap & 255, + ) + } + return result +} diff --git a/yarn.lock b/yarn.lock index f3b747e8f..19ed4cdaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7503,6 +7503,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -13680,6 +13685,13 @@ react-native-gesture-handler@~2.9.0: lodash "^4.17.21" prop-types "^15.7.2" +react-native-get-random-values@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16" + integrity sha512-H/zghhun0T+UIJLmig3+ZuBCvF66rdbiWUfRSNS6kv5oDSpa1ZiVyvRWtuPesQpT8dXj+Bv7WJRQOUP+5TB1sA== + dependencies: + fast-base64-decode "^1.0.0" + react-native-gradle-plugin@^0.71.15: version "0.71.16" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.16.tgz#822bb0c680e03b5df5aa65f2e5ffc2bc2930854a" -- cgit 1.4.1