From 678f75b4951d891bbc2651220fe99a2f040af88f Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 13:51:07 -0700 Subject: add window dimensions to global styles --- src/lib/styles.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/lib') diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 00a8638f9..07315c9f2 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -1,4 +1,4 @@ -import {StyleProp, StyleSheet, TextStyle} from 'react-native' +import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' import {Theme, TypographyVariant} from './ThemeContext' import {isMobileWeb} from 'platform/detection' @@ -169,6 +169,10 @@ export const s = StyleSheet.create({ w100pct: {width: '100%'}, h100pct: {height: '100%'}, hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'}, + window: { + width: Dimensions.get('window').width, + height: Dimensions.get('window').height, + }, // text align textLeft: {textAlign: 'left'}, -- cgit 1.4.1 From 370d52bd1f4ba3b5effb7a48cd1c8b14aea88781 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 14:40:58 -0700 Subject: add custom algorithm screen to settings under moderation --- src/Navigation.tsx | 2 ++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/view/screens/CustomAlgorithms.tsx | 27 +++++++++++++++++++++++++++ src/view/screens/Settings.tsx | 16 ++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 src/view/screens/CustomAlgorithms.tsx (limited to 'src/lib') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index afc7b39b8..8b6e1b453 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -52,6 +52,7 @@ import {AppPasswords} from 'view/screens/AppPasswords' import {MutedAccounts} from 'view/screens/MutedAccounts' import {BlockedAccounts} from 'view/screens/BlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' +import CustomAlgorithms from 'view/screens/CustomAlgorithms' const navigationRef = createNavigationContainerRef() @@ -91,6 +92,7 @@ function commonScreens(Stack: typeof HomeTab) { /> + diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 34e6e6a46..b91495640 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -20,6 +20,7 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined + CustomAlgorithms: undefined MutedAccounts: undefined BlockedAccounts: undefined } diff --git a/src/routes.ts b/src/routes.ts index 43d31ee09..c1b441984 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -14,6 +14,7 @@ export const router = new Router({ Debug: '/sys/debug', Log: '/sys/log', AppPasswords: '/settings/app-passwords', + CustomAlgorithms: '/settings/custom-algorithms', MutedAccounts: '/settings/muted-accounts', BlockedAccounts: '/settings/blocked-accounts', Support: '/support', diff --git a/src/view/screens/CustomAlgorithms.tsx b/src/view/screens/CustomAlgorithms.tsx new file mode 100644 index 000000000..3e2fa7e73 --- /dev/null +++ b/src/view/screens/CustomAlgorithms.tsx @@ -0,0 +1,27 @@ +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {usePalette} from 'lib/hooks/usePalette' +import {CommonNavigatorParams} from 'lib/routes/types' +import {observer} from 'mobx-react-lite' +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {Text} from 'view/com/util/text/Text' + +type Props = NativeStackScreenProps + +const CustomAlgorithms = withAuthRequired( + observer((props: Props) => { + const pal = usePalette('default') + return ( + + + CustomAlgorithms + + ) + }), +) + +export default CustomAlgorithms + +const styles = StyleSheet.create({}) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index f98cdc0c8..fd8fb4f4a 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -290,6 +290,22 @@ export const SettingsScreen = withAuthRequired( Moderation + + + + + + Custom Algorithms + + Date: Sun, 14 May 2023 18:37:18 -0700 Subject: renamed page to savedfeeds --- src/Navigation.tsx | 4 +- src/lib/routes/types.ts | 2 +- src/routes.ts | 2 +- src/view/screens/CustomAlgorithms.tsx | 105 ---------------------------------- src/view/screens/SavedFeeds.tsx | 103 +++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 109 deletions(-) delete mode 100644 src/view/screens/CustomAlgorithms.tsx create mode 100644 src/view/screens/SavedFeeds.tsx (limited to 'src/lib') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 8b6e1b453..26dc9f7ad 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -52,7 +52,7 @@ import {AppPasswords} from 'view/screens/AppPasswords' import {MutedAccounts} from 'view/screens/MutedAccounts' import {BlockedAccounts} from 'view/screens/BlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' -import CustomAlgorithms from 'view/screens/CustomAlgorithms' +import {SavedFeeds} from './view/screens/SavedFeeds' const navigationRef = createNavigationContainerRef() @@ -92,7 +92,7 @@ function commonScreens(Stack: typeof HomeTab) { /> - + diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index b91495640..5a4126f6a 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -20,7 +20,7 @@ export type CommonNavigatorParams = { CommunityGuidelines: undefined CopyrightPolicy: undefined AppPasswords: undefined - CustomAlgorithms: undefined + SavedFeeds: undefined MutedAccounts: undefined BlockedAccounts: undefined } diff --git a/src/routes.ts b/src/routes.ts index c1b441984..b510f66f6 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -14,7 +14,7 @@ export const router = new Router({ Debug: '/sys/debug', Log: '/sys/log', AppPasswords: '/settings/app-passwords', - CustomAlgorithms: '/settings/custom-algorithms', + SavedFeeds: '/settings/saved-feeds', MutedAccounts: '/settings/muted-accounts', BlockedAccounts: '/settings/blocked-accounts', Support: '/support', diff --git a/src/view/screens/CustomAlgorithms.tsx b/src/view/screens/CustomAlgorithms.tsx deleted file mode 100644 index b838660df..000000000 --- a/src/view/screens/CustomAlgorithms.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, {useCallback, useMemo} from 'react' -import { - RefreshControl, - StyleSheet, - View, - FlatList, - ActivityIndicator, -} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {useAnalytics} from 'lib/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {CommonNavigatorParams} from 'lib/routes/types' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' -import {SavedFeedsModel} from 'state/models/feeds/algo/saved' -import AlgoItem from 'view/com/algos/AlgoItem' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {CenteredView} from 'view/com/util/Views' -import {Text} from 'view/com/util/text/Text' -import {isDesktopWeb} from 'platform/detection' -import {s} from 'lib/styles' - -type Props = NativeStackScreenProps - -const CustomAlgorithms = withAuthRequired( - observer(({}: Props) => { - const pal = usePalette('default') - const rootStore = useStores() - const {screen} = useAnalytics() - - const savedFeeds = useMemo( - () => new SavedFeedsModel(rootStore), - [rootStore], - ) - - useFocusEffect( - useCallback(() => { - screen('SavedFeeds') - rootStore.shell.setMinimalShellMode(false) - savedFeeds.refresh() - }, [screen, rootStore, savedFeeds]), - ) - - return ( - - - item.data.uri} - refreshControl={ - savedFeeds.refresh()} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - onEndReached={() => savedFeeds.loadMore()} - renderItem={({item}) => } - initialNumToRender={15} - ListFooterComponent={() => ( - - {savedFeeds.isLoading && } - - )} - ListEmptyComponent={() => ( - - - You don't have any saved feeds. To save a feed, click the save - button when a custom feed or algorithm shows up. - - - )} - extraData={savedFeeds.isLoading} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - - ) - }), -) - -export default CustomAlgorithms - -const styles = StyleSheet.create({ - footer: { - paddingVertical: 20, - }, - empty: { - paddingHorizontal: 20, - paddingVertical: 20, - borderRadius: 16, - marginHorizontal: 24, - marginTop: 10, - }, -}) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx new file mode 100644 index 000000000..7b04a6474 --- /dev/null +++ b/src/view/screens/SavedFeeds.tsx @@ -0,0 +1,103 @@ +import React, {useCallback, useMemo} from 'react' +import { + RefreshControl, + StyleSheet, + View, + FlatList, + ActivityIndicator, +} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useAnalytics} from 'lib/analytics' +import {usePalette} from 'lib/hooks/usePalette' +import {CommonNavigatorParams} from 'lib/routes/types' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {SavedFeedsModel} from 'state/models/feeds/algo/saved' +import AlgoItem from 'view/com/algos/AlgoItem' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {Text} from 'view/com/util/text/Text' +import {isDesktopWeb} from 'platform/detection' +import {s} from 'lib/styles' + +type Props = NativeStackScreenProps + +export const SavedFeeds = withAuthRequired( + observer(({}: Props) => { + const pal = usePalette('default') + const rootStore = useStores() + const {screen} = useAnalytics() + + const savedFeeds = useMemo( + () => new SavedFeedsModel(rootStore), + [rootStore], + ) + + useFocusEffect( + useCallback(() => { + screen('SavedFeeds') + rootStore.shell.setMinimalShellMode(false) + savedFeeds.refresh() + }, [screen, rootStore, savedFeeds]), + ) + + return ( + + + item.data.uri} + refreshControl={ + savedFeeds.refresh()} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + onEndReached={() => savedFeeds.loadMore()} + renderItem={({item}) => } + initialNumToRender={15} + ListFooterComponent={() => ( + + {savedFeeds.isLoading && } + + )} + ListEmptyComponent={() => ( + + + You don't have any saved feeds. To save a feed, click the save + button when a custom feed or algorithm shows up. + + + )} + extraData={savedFeeds.isLoading} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + + ) + }), +) + +const styles = StyleSheet.create({ + footer: { + paddingVertical: 20, + }, + empty: { + paddingHorizontal: 20, + paddingVertical: 20, + borderRadius: 16, + marginHorizontal: 24, + marginTop: 10, + }, +}) -- cgit 1.4.1 From 50108611600ae2addb3bff4f00562b02f9c35fdf Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 15 May 2023 10:42:23 -0700 Subject: custom feed screen --- src/Navigation.tsx | 2 ++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/feeds/algo/algo-item.ts | 20 +++++++++++++ src/state/models/feeds/posts.ts | 31 +++++++++++++++----- src/view/com/algos/AlgoItem.tsx | 24 +++++++++++++-- src/view/screens/CustomFeed.tsx | 50 ++++++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/view/screens/CustomFeed.tsx (limited to 'src/lib') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 26dc9f7ad..d4c992eb6 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -53,6 +53,7 @@ import {MutedAccounts} from 'view/screens/MutedAccounts' import {BlockedAccounts} from 'view/screens/BlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' import {SavedFeeds} from './view/screens/SavedFeeds' +import {CustomFeed} from './view/screens/CustomFeed' const navigationRef = createNavigationContainerRef() @@ -93,6 +94,7 @@ function commonScreens(Stack: typeof HomeTab) { + diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 5a4126f6a..29fadd709 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -21,6 +21,7 @@ export type CommonNavigatorParams = { CopyrightPolicy: undefined AppPasswords: undefined SavedFeeds: undefined + CustomFeed: {name: string; rkey: string} MutedAccounts: undefined BlockedAccounts: undefined } diff --git a/src/routes.ts b/src/routes.ts index b510f66f6..2cdaa28bb 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -15,6 +15,7 @@ export const router = new Router({ Log: '/sys/log', AppPasswords: '/settings/app-passwords', SavedFeeds: '/settings/saved-feeds', + CustomFeed: '/profile/:name/feed/:rkey', MutedAccounts: '/settings/muted-accounts', BlockedAccounts: '/settings/blocked-accounts', Support: '/support', diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts index 88e9c0662..3dee5e2bf 100644 --- a/src/state/models/feeds/algo/algo-item.ts +++ b/src/state/models/feeds/algo/algo-item.ts @@ -29,6 +29,10 @@ export class AlgoItemModel { } } + get getUri() { + return this.data.uri + } + // public apis // = async save() { @@ -52,4 +56,20 @@ export class AlgoItemModel { this.rootStore.log.error('Failed to unsanve feed', e) } } + + // async getFeedSkeleton() { + // const res = await this.rootStore.agent.app.bsky.feed.getFeedSkeleton({ + // feed: this.data.uri, + // }) + // const skeleton = res.data.feed + // console.log('skeleton', skeleton) + // return skeleton + // } + // async getFeed() { + // const feed = await this.rootStore.agent.app.bsky.feed.getFeed({ + // feed: this.data.uri, + // }) + // console.log('feed', feed) + // return feed + // } } diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 44cec3af7..7adc1cb1c 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -4,6 +4,7 @@ import { AppBskyFeedDefs, AppBskyFeedPost, AppBskyFeedGetAuthorFeed as GetAuthorFeed, + AppBskyFeedGetFeed as GetCustomFeed, RichText, jsonToLex, } from '@atproto/api' @@ -305,8 +306,11 @@ export class PostsFeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested' | 'goodstuff', - params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, + public feedType: 'home' | 'author' | 'suggested' | 'goodstuff' | 'custom', + params: + | GetTimeline.QueryParams + | GetAuthorFeed.QueryParams + | GetCustomFeed.QueryParams, ) { makeAutoObservable( this, @@ -595,13 +599,15 @@ export class PostsFeedModel { // helper functions // = - async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { + async _replaceAll( + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, + ) { this.pollCursor = res.data.feed[0]?.post.uri return this._appendAll(res, true) } async _appendAll( - res: GetTimeline.Response | GetAuthorFeed.Response, + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, replace = false, ) { this.loadMoreCursor = res.data.cursor @@ -640,7 +646,9 @@ export class PostsFeedModel { }) } - _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { + _updateAll( + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, + ) { for (const item of res.data.feed) { const existingSlice = this.slices.find(slice => slice.containsUri(item.post.uri), @@ -657,8 +665,13 @@ export class PostsFeedModel { } protected async _getFeed( - params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {}, - ): Promise { + params: + | GetTimeline.QueryParams + | GetAuthorFeed.QueryParams + | GetCustomFeed.QueryParams, + ): Promise< + GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response + > { params = Object.assign({}, this.params, params) if (this.feedType === 'suggested') { const responses = await getMultipleAuthorsPosts( @@ -680,6 +693,10 @@ export class PostsFeedModel { } } else if (this.feedType === 'home') { return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams) + } else if (this.feedType === 'custom') { + return this.rootStore.agent.app.bsky.feed.getFeed( + params as GetCustomFeed.QueryParams, + ) } else if (this.feedType === 'goodstuff') { const res = await getGoodStuff( this.rootStore.session.currentSession?.accessJwt || '', diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx index e475624c5..51de89bd6 100644 --- a/src/view/com/algos/AlgoItem.tsx +++ b/src/view/com/algos/AlgoItem.tsx @@ -1,5 +1,11 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + StyleProp, + StyleSheet, + View, + ViewStyle, + TouchableOpacity, +} from 'react-native' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' @@ -7,13 +13,25 @@ import {UserAvatar} from '../util/UserAvatar' import {Button} from '../util/forms/Button' import {observer} from 'mobx-react-lite' import {AlgoItemModel} from 'state/models/feeds/algo/algo-item' +import {useNavigation} from '@react-navigation/native' +import {NavigationProp} from 'lib/routes/types' const AlgoItem = observer( ({item, style}: {item: AlgoItemModel; style?: StyleProp}) => { const pal = usePalette('default') + const navigation = useNavigation() return ( - + { + navigation.navigate('CustomFeed', { + name: item.data.creator.did, + rkey: item.data.uri, + }) + }} + key={item.data.uri}> @@ -54,7 +72,7 @@ const AlgoItem = observer( /> - + ) }, ) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx new file mode 100644 index 000000000..1d4343b29 --- /dev/null +++ b/src/view/screens/CustomFeed.tsx @@ -0,0 +1,50 @@ +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {CommonNavigatorParams} from 'lib/routes/types' +import {observer} from 'mobx-react-lite' +import React, {useEffect, useMemo, useRef} from 'react' +import {FlatList, StyleSheet, View} from 'react-native' +import {useStores} from 'state/index' +import {AlgoItemModel} from 'state/models/feeds/algo/algo-item' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {Feed} from 'view/com/posts/Feed' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {Text} from 'view/com/util/text/Text' + +type Props = NativeStackScreenProps +export const CustomFeed = withAuthRequired( + observer(({route}: Props) => { + const rootStore = useStores() + const scrollElRef = useRef(null) + + const {rkey, name} = route.params + + const algoFeed: PostsFeedModel = useMemo(() => { + const feed = new PostsFeedModel(rootStore, 'custom', { + feed: rkey, + }) + feed.setup() + return feed + }, [rkey, rootStore]) + + return ( + + + + + + ) + }), +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + height: '100%', + }, +}) -- cgit 1.4.1 From c4a666c2210c2e9c14812ddc5a0f797dd538014c Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 15 May 2023 17:59:36 -0700 Subject: new design for custom feed preview --- src/lib/icons.tsx | 4 +- src/lib/routes/types.ts | 2 +- src/state/models/feeds/algo/algo-item.ts | 77 ++++++++++++++++++----- src/view/com/algos/AlgoItem.tsx | 18 +++--- src/view/com/algos/useCustomFeed.ts | 27 ++++++++ src/view/com/util/PostCtrls.tsx | 2 +- src/view/screens/CustomFeed.tsx | 103 ++++++++++++++++++++++++++++--- 7 files changed, 199 insertions(+), 34 deletions(-) create mode 100644 src/view/com/algos/useCustomFeed.ts (limited to 'src/lib') diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 4cb491e46..960090ad7 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -443,7 +443,7 @@ export function HeartIcon({ size = 24, strokeWidth = 1.5, }: { - style?: StyleProp + style?: StyleProp size?: string | number strokeWidth: number }) { @@ -464,7 +464,7 @@ export function HeartIconSolid({ style, size = 24, }: { - style?: StyleProp + style?: StyleProp size?: string | number }) { return ( diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 29fadd709..77ed58cc3 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -21,7 +21,7 @@ export type CommonNavigatorParams = { CopyrightPolicy: undefined AppPasswords: undefined SavedFeeds: undefined - CustomFeed: {name: string; rkey: string} + CustomFeed: {name?: string; rkey: string} MutedAccounts: undefined BlockedAccounts: undefined } diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts index 3dee5e2bf..41fe6a976 100644 --- a/src/state/models/feeds/algo/algo-item.ts +++ b/src/state/models/feeds/algo/algo-item.ts @@ -33,6 +33,20 @@ export class AlgoItemModel { return this.data.uri } + get isSaved() { + return this.data.viewer?.saved + } + + get isLiked() { + return this.data.viewer?.liked + } + + set toggleLiked(value: boolean) { + if (this.data.viewer) { + this.data.viewer.liked = value + } + } + // public apis // = async save() { @@ -57,19 +71,52 @@ export class AlgoItemModel { } } - // async getFeedSkeleton() { - // const res = await this.rootStore.agent.app.bsky.feed.getFeedSkeleton({ - // feed: this.data.uri, - // }) - // const skeleton = res.data.feed - // console.log('skeleton', skeleton) - // return skeleton - // } - // async getFeed() { - // const feed = await this.rootStore.agent.app.bsky.feed.getFeed({ - // feed: this.data.uri, - // }) - // console.log('feed', feed) - // return feed - // } + async like() { + try { + this.toggleLiked = true + await this.rootStore.agent.app.bsky.feed.like.create( + { + repo: this.rootStore.me.did, + }, + { + subject: { + uri: this.data.uri, + cid: this.data.cid, + }, + createdAt: new Date().toString(), + }, + ) + } catch (e: any) { + this.rootStore.log.error('Failed to like feed', e) + } + } + + static async getView(store: RootStoreModel, uri: string) { + const res = await store.agent.app.bsky.feed.getFeedGenerator({ + feed: uri, + }) + const view = res.data.view + return view + } + + async checkIsValid() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + return res.data.isValid + } + + async checkIsOnline() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + return res.data.isOnline + } + + async reload() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + this.data = res.data.view + } } diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx index 04117e589..f2fb075da 100644 --- a/src/view/com/algos/AlgoItem.tsx +++ b/src/view/com/algos/AlgoItem.tsx @@ -29,7 +29,7 @@ const AlgoItem = observer( style={[styles.container, style]} onPress={() => { navigation.navigate('CustomFeed', { - name: item.data.creator.did, + name: item.data.displayName, rkey: item.data.uri, }) }} @@ -40,25 +40,27 @@ const AlgoItem = observer( - {item.data.displayName ? item.data.displayName : 'Feed name'} + {item.data.displayName ?? 'Feed name'} - {item.data.description ?? - 'THIS IS A FEED DESCRIPTION, IT WILL TELL YOU WHAT THE FEED IS ABOUT. THIS IS A COOL FEED ABOUT COOL PEOPLE.'} + {item.data.description ?? 'Feed description'} - {/* TODO: this feed is like by *3* people UserAvatars and others */} - + {/* - + */} - Liked by 3 others + + {item.data.likeCount && item.data.likeCount > 1 + ? `Liked by ${item.data.likeCount} others` + : 'Be the first to like this'} + + + + )} + + + + + Feed + + + + + ) + }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + + return ( + + + + + ) + }), +) + +/* + + + + + + + + @{currentFeed?.data.creator.handle} + + + + {currentFeed?.data.description} + + + + + + */ + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + header: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 16, + borderTopWidth: 1, + }, + headerDetails: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + fakeSelector: { + flexDirection: 'row', + paddingHorizontal: isDesktopWeb ? 16 : 6, + }, + fakeSelectorItem: { + paddingHorizontal: 12, + paddingBottom: 8, + borderBottomWidth: 3, + }, + liked: { + color: colors.red3, + }, + + /* headerContainer: { + alignItems: 'center', + justifyContent: 'center', + gap: 8, + marginBottom: 12, + }, + header: { + alignItems: 'center', + gap: 4, + }, + avatarContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + buttonsContainer: { + flexDirection: 'row', + gap: 8, + }, + saveButton: { + minWidth: 100, + alignItems: 'center', + }, + liked: { + color: colors.red3, + }, + notLiked: { + color: colors.gray3, + }, + likeButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 4, + paddingHorizontal: 8, + borderRadius: 24, + gap: 4, + },*/ +}) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 01f27bae1..7c3ed831c 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -87,7 +87,7 @@ export const ProfileListScreen = withAuthRequired( return }, []) - const renderHeaderBtn = React.useCallback(() => { + const renderHeaderBtns = React.useCallback(() => { return ( {list?.isOwner && ( @@ -148,7 +148,7 @@ export const ProfileListScreen = withAuthRequired( pal.border, ]} testID="moderationMutelistsScreen"> - + { - navigation.navigate('CustomFeed', { + navigation.navigate('ProfileCustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, - displayName: - item.data.displayName ?? - `${item.data.creator.displayName}'s feed`, }) }} style={styles.pinnedItem}> -- cgit 1.4.1 From 998879d6d60b59e65250af395fd6ce389c89189b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 22:04:01 -0500 Subject: Remove redundant feed-settings page --- src/Navigation.tsx | 4 +- src/lib/routes/types.ts | 1 - src/routes.ts | 1 - src/view/com/feeds/SavedFeeds.tsx | 2 +- src/view/index.ts | 12 +-- src/view/screens/PinnedFeeds.tsx | 179 -------------------------------------- src/view/screens/SavedFeeds.tsx | 158 +++++++++++++++------------------ src/view/screens/Settings.tsx | 27 +++--- 8 files changed, 94 insertions(+), 290 deletions(-) delete mode 100644 src/view/screens/PinnedFeeds.tsx (limited to 'src/lib') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 025020afa..ea36b0f20 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -55,9 +55,8 @@ import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {AppPasswords} from 'view/screens/AppPasswords' import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' +import {SavedFeeds} from 'view/screens/SavedFeeds' import {getRoutingInstrumentation} from 'lib/sentry' -import {SavedFeeds} from './view/screens/SavedFeeds' -import {PinnedFeeds} from 'view/screens/PinnedFeeds' import {bskyTitle} from 'lib/strings/headings' const navigationRef = createNavigationContainerRef() @@ -189,7 +188,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title('App Passwords')}} /> - ) } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 52d0e9af2..5c5185602 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -27,7 +27,6 @@ export type CommonNavigatorParams = { CopyrightPolicy: undefined AppPasswords: undefined SavedFeeds: undefined - PinnedFeeds: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 7501e7abf..c5dc4fb5b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -21,7 +21,6 @@ export const router = new Router({ Log: '/sys/log', AppPasswords: '/settings/app-passwords', SavedFeeds: '/settings/saved-feeds', - PinnedFeeds: '/settings/pinned-feeds', Support: '/support', PrivacyPolicy: '/support/privacy', TermsOfService: '/support/tos', diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 06c47d114..2d0057cfb 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -50,7 +50,7 @@ export const SavedFeeds = observer( return ( + href="/settings/saved-feeds"> Settings diff --git a/src/view/index.ts b/src/view/index.ts index 84fc3f315..f06bdaccc 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -60,14 +60,17 @@ import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' +import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' +import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' +import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' +import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' -import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' -import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' -import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' +import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' +import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' @@ -75,8 +78,6 @@ import {faUserSlash} from '@fortawesome/free-solid-svg-icons/faUserSlash' import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' -import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' -import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' @@ -148,6 +149,7 @@ export function setup() { faReply, faRetweet, faRss, + faSatelliteDish, faShare, faShareFromSquare, faShield, diff --git a/src/view/screens/PinnedFeeds.tsx b/src/view/screens/PinnedFeeds.tsx deleted file mode 100644 index a90012093..000000000 --- a/src/view/screens/PinnedFeeds.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, {useCallback, useMemo} from 'react' -import { - RefreshControl, - StyleSheet, - View, - ActivityIndicator, - Pressable, - TouchableOpacity, -} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {useAnalytics} from 'lib/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {CommonNavigatorParams} from 'lib/routes/types' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {CenteredView} from 'view/com/util/Views' -import {Text} from 'view/com/util/text/Text' -import {isDesktopWeb, isWeb} from 'platform/detection' -import {s} from 'lib/styles' -import DraggableFlatList, { - ShadowDecorator, - ScaleDecorator, -} from 'react-native-draggable-flatlist' -import {SavedFeedItem} from 'view/com/feeds/SavedFeedItem' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {CustomFeedModel} from 'state/models/feeds/custom-feed' - -type Props = NativeStackScreenProps - -export const PinnedFeeds = withAuthRequired( - observer(({}: Props) => { - // hooks for global items - const pal = usePalette('default') - const rootStore = useStores() - const {screen} = useAnalytics() - - // hooks for local - const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) - useFocusEffect( - useCallback(() => { - screen('SavedFeeds') - rootStore.shell.setMinimalShellMode(false) - savedFeeds.refresh() - }, [screen, rootStore, savedFeeds]), - ) - const _ListEmptyComponent = () => { - return ( - - - You don't have any pinned feeds. To pin a feed, go back to the Saved - Feeds screen and click the pin icon! - - - ) - } - const _ListFooterComponent = () => { - return ( - - {savedFeeds.isLoading && } - - ) - } - - return ( - - - item.data.uri} - refreshing={savedFeeds.isRefreshing} - refreshControl={ - savedFeeds.refresh()} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - renderItem={({item, drag}) => } - initialNumToRender={10} - ListFooterComponent={_ListFooterComponent} - ListEmptyComponent={_ListEmptyComponent} - extraData={savedFeeds.isLoading} - onDragEnd={({data}) => savedFeeds.reorderPinnedFeeds(data)} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - - ) - }), -) - -const ListItem = observer( - ({item, drag}: {item: CustomFeedModel; drag: () => void}) => { - const pal = usePalette('default') - const rootStore = useStores() - const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) - const isPinned = savedFeeds.isPinned(item) - return ( - - - - {isPinned && isWeb ? ( - - { - savedFeeds.movePinnedItem(item, 'up') - }}> - - - { - savedFeeds.movePinnedItem(item, 'down') - }}> - - - - ) : isPinned ? ( - - ) : null} - - - - - ) - }, -) - -const styles = StyleSheet.create({ - footer: { - paddingVertical: 20, - }, - empty: { - paddingHorizontal: 20, - paddingVertical: 20, - borderRadius: 16, - marginHorizontal: 24, - marginTop: 10, - }, - itemContainer: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - borderTopWidth: 1, - }, - webArrowButtonsContainer: { - flexDirection: 'column', - justifyContent: 'space-around', - }, - webArrowUpButton: {marginBottom: 10}, -}) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index c32639889..b1ea27af8 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -4,9 +4,8 @@ import { StyleSheet, View, ActivityIndicator, - FlatList, + Pressable, TouchableOpacity, - ScrollView, } from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' @@ -21,16 +20,18 @@ import {CenteredView} from 'view/com/util/Views' import {Text} from 'view/com/util/text/Text' import {isDesktopWeb, isWeb} from 'platform/detection' import {s} from 'lib/styles' -import {SavedFeedsModel} from 'state/models/ui/saved-feeds' -import {Link} from 'view/com/util/Link' -import {UserAvatar} from 'view/com/util/UserAvatar' +import DraggableFlatList, { + ShadowDecorator, + ScaleDecorator, +} from 'react-native-draggable-flatlist' import {SavedFeedItem} from 'view/com/feeds/SavedFeedItem' -import {AtUri} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {CustomFeedModel} from 'state/models/feeds/custom-feed' type Props = NativeStackScreenProps export const SavedFeeds = withAuthRequired( - observer(({navigation}: Props) => { + observer(({}: Props) => { // hooks for global items const pal = usePalette('default') const rootStore = useStores() @@ -55,8 +56,8 @@ export const SavedFeeds = withAuthRequired( styles.empty, ]}> - You don't have any saved feeds. To save a feed, click the save - button when a custom feed or algorithm shows up. + You don't have any pinned feeds. To pin a feed, go back to the Saved + Feeds screen and click the pin icon! ) @@ -71,10 +72,10 @@ export const SavedFeeds = withAuthRequired( return ( - - + item.data.uri} refreshing={savedFeeds.isRefreshing} refreshControl={ @@ -85,19 +86,12 @@ export const SavedFeeds = withAuthRequired( titleColor={pal.colors.text} /> } - renderItem={({item}) => ( - - )} + renderItem={({item, drag}) => } initialNumToRender={10} - ListHeaderComponent={() => ( - - )} ListFooterComponent={_ListFooterComponent} ListEmptyComponent={_ListEmptyComponent} extraData={savedFeeds.isLoading} + onDragEnd={({data}) => savedFeeds.reorderPinnedFeeds(data)} // @ts-ignore our .web version only -prf desktopFixedHeight /> @@ -106,64 +100,56 @@ export const SavedFeeds = withAuthRequired( }), ) -const ListHeaderComponent = observer( - ({ - savedFeeds, - navigation, - }: { - savedFeeds: SavedFeedsModel - navigation: Props['navigation'] - }) => { +const ListItem = observer( + ({item, drag}: {item: CustomFeedModel; drag: () => void}) => { const pal = usePalette('default') + const rootStore = useStores() + const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) + const isPinned = savedFeeds.isPinned(item) return ( - - {savedFeeds.pinned.length > 0 ? ( - - - - Pinned Feeds - - - Edit - - - - - {savedFeeds.pinned.map(item => { - return ( - { - navigation.navigate('ProfileCustomFeed', { - name: item.data.creator.did, - rkey: new AtUri(item.data.uri).rkey, - }) - }} - style={styles.pinnedItem}> - - - {item.data.displayName ?? - `${item.data.creator.displayName}'s feed`} - - - ) - })} - - - ) : null} - - All Saved Feeds - + + + + {isPinned && isWeb ? ( + + { + savedFeeds.movePinnedItem(item, 'up') + }}> + + + { + savedFeeds.movePinnedItem(item, 'down') + }}> + + + + ) : isPinned ? ( + + ) : null} + + + + ) }, ) @@ -179,15 +165,15 @@ const styles = StyleSheet.create({ marginHorizontal: 24, marginTop: 10, }, - headerContainer: {paddingHorizontal: 18, paddingTop: 18}, - pinnedContainer: {marginBottom: 18, gap: 18}, - pinnedHeader: {flexDirection: 'row', justifyContent: 'space-between'}, - pinnedItem: { + itemContainer: { flex: 1, + flexDirection: 'row', alignItems: 'center', - marginRight: 18, - maxWidth: 100, + borderTopWidth: 1, + }, + webArrowButtonsContainer: { + flexDirection: 'column', + justifyContent: 'space-around', }, - pinnedItemName: {marginTop: 8, textAlign: 'center'}, - editPinned: {textDecorationLine: 'underline'}, + webArrowUpButton: {marginBottom: 10}, }) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index a919f11b0..3ce41f8c0 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -284,38 +284,37 @@ export const SettingsScreen = withAuthRequired( + + Advanced + + href="/settings/app-passwords"> - Custom Algorithms + App passwords - - - Advanced - + accessibilityHint="Saved Feeds" + accessibilityLabel="Opens screen with all saved feeds" + href="/settings/saved-feeds"> - App passwords + Saved Feeds Date: Wed, 17 May 2023 22:12:14 -0500 Subject: Add custom feed liked by screen --- src/Navigation.tsx | 9 +- src/lib/routes/types.ts | 3 +- src/routes.ts | 1 + src/view/com/feeds/CustomFeed.tsx | 2 +- src/view/screens/CustomFeed.tsx | 235 ++++++++++++++++++++++++++ src/view/screens/CustomFeedLikedBy.tsx | 29 ++++ src/view/screens/ProfileCustomFeed.tsx | 293 --------------------------------- 7 files changed, 275 insertions(+), 297 deletions(-) create mode 100644 src/view/screens/CustomFeed.tsx create mode 100644 src/view/screens/CustomFeedLikedBy.tsx delete mode 100644 src/view/screens/ProfileCustomFeed.tsx (limited to 'src/lib') diff --git a/src/Navigation.tsx b/src/Navigation.tsx index ea36b0f20..d84167d63 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -40,7 +40,8 @@ import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' -import {ProfileCustomFeed} from './view/screens/ProfileCustomFeed' +import {CustomFeedScreen} from './view/screens/CustomFeed' +import {CustomFeedLikedByScreen} from './view/screens/CustomFeedLikedBy' import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' @@ -126,7 +127,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { title: title(`People followed by @${route.params.name}`), })} /> - ({title: title(`Post by @${route.params.name}`)})} /> + + { - navigation.navigate('ProfileCustomFeed', { + navigation.navigate('CustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx new file mode 100644 index 000000000..9f7f81691 --- /dev/null +++ b/src/view/screens/CustomFeed.tsx @@ -0,0 +1,235 @@ +import React, {useMemo, useRef} from 'react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {usePalette} from 'lib/hooks/usePalette' +import {HeartIcon, HeartIconSolid} from 'lib/icons' +import {CommonNavigatorParams} from 'lib/routes/types' +import {makeRecordUri} from 'lib/strings/url-helpers' +import {colors, s} from 'lib/styles' +import {observer} from 'mobx-react-lite' +import {FlatList, StyleSheet, View} from 'react-native' +import {useStores} from 'state/index' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {useCustomFeed} from 'lib/hooks/useCustomFeed' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {Feed} from 'view/com/posts/Feed' +import {pluralize} from 'lib/strings/helpers' +import {TextLink} from 'view/com/util/Link' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {Button} from 'view/com/util/forms/Button' +import {Text} from 'view/com/util/text/Text' +import * as Toast from 'view/com/util/Toast' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps +export const CustomFeedScreen = withAuthRequired( + observer(({route}: Props) => { + const store = useStores() + const pal = usePalette('default') + const {rkey, name} = route.params + const uri = useMemo( + () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), + [rkey, name], + ) + const scrollElRef = useRef(null) + const currentFeed = useCustomFeed(uri) + const algoFeed: PostsFeedModel = useMemo(() => { + const feed = new PostsFeedModel(store, 'custom', { + feed: uri, + }) + feed.setup() + return feed + }, [store, uri]) + + const onToggleSaved = React.useCallback(async () => { + try { + if (currentFeed?.isSaved) { + await currentFeed?.unsave() + } else { + await currentFeed?.save() + } + } catch (err) { + Toast.show( + 'There was an an issue updating your feeds, please check your internet connection and try again.', + ) + store.log.error('Failed up update feeds', {err}) + } + }, [store, currentFeed]) + + const onToggleLiked = React.useCallback(async () => { + try { + if (currentFeed?.isLiked) { + await currentFeed?.unlike() + } else { + await currentFeed?.like() + } + } catch (err) { + Toast.show( + 'There was an an issue contacting the server, please check your internet connection and try again.', + ) + store.log.error('Failed up toggle like', {err}) + } + }, [store, currentFeed]) + + const renderHeaderBtns = React.useCallback(() => { + return ( + + + + + )} + + + + + Feed + + + + + ) + }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + + return ( + + + + + ) + }), +) + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + header: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 16, + borderTopWidth: 1, + }, + headerDetails: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + fakeSelector: { + flexDirection: 'row', + paddingHorizontal: isDesktopWeb ? 16 : 6, + }, + fakeSelectorItem: { + paddingHorizontal: 12, + paddingBottom: 8, + borderBottomWidth: 3, + }, + liked: { + color: colors.red3, + }, +}) diff --git a/src/view/screens/CustomFeedLikedBy.tsx b/src/view/screens/CustomFeedLikedBy.tsx new file mode 100644 index 000000000..49d0d0482 --- /dev/null +++ b/src/view/screens/CustomFeedLikedBy.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import {View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from '../com/util/ViewHeader' +import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' + +type Props = NativeStackScreenProps +export const CustomFeedLikedByScreen = withAuthRequired(({route}: Props) => { + const store = useStores() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + }, [store]), + ) + + return ( + + + + + ) +}) diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx deleted file mode 100644 index 681798308..000000000 --- a/src/view/screens/ProfileCustomFeed.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React, {useMemo, useRef} from 'react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {usePalette} from 'lib/hooks/usePalette' -import {HeartIcon, HeartIconSolid} from 'lib/icons' -import {CommonNavigatorParams} from 'lib/routes/types' -import {makeRecordUri} from 'lib/strings/url-helpers' -import {colors, s} from 'lib/styles' -import {observer} from 'mobx-react-lite' -import {FlatList, StyleSheet, View} from 'react-native' -import {useStores} from 'state/index' -import {PostsFeedModel} from 'state/models/feeds/posts' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {Feed} from 'view/com/posts/Feed' -import {pluralize} from 'lib/strings/helpers' -import {TextLink} from 'view/com/util/Link' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {Button} from 'view/com/util/forms/Button' -import {Text} from 'view/com/util/text/Text' -import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb} from 'platform/detection' - -type Props = NativeStackScreenProps -export const ProfileCustomFeed = withAuthRequired( - observer(({route}: Props) => { - const store = useStores() - const pal = usePalette('default') - const {rkey, name} = route.params - const uri = useMemo( - () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), - [rkey, name], - ) - const scrollElRef = useRef(null) - const currentFeed = useCustomFeed(uri) - const algoFeed: PostsFeedModel = useMemo(() => { - const feed = new PostsFeedModel(store, 'custom', { - feed: uri, - }) - feed.setup() - return feed - }, [store, uri]) - - const onToggleSaved = React.useCallback(async () => { - try { - if (currentFeed?.isSaved) { - await currentFeed?.unsave() - } else { - await currentFeed?.save() - } - } catch (err) { - Toast.show( - 'There was an an issue updating your feeds, please check your internet connection and try again.', - ) - store.log.error('Failed up update feeds', {err}) - } - }, [store, currentFeed]) - - const onToggleLiked = React.useCallback(async () => { - try { - if (currentFeed?.isLiked) { - await currentFeed?.unlike() - } else { - await currentFeed?.like() - } - } catch (err) { - Toast.show( - 'There was an an issue contacting the server, please check your internet connection and try again.', - ) - store.log.error('Failed up toggle like', {err}) - } - }, [store, currentFeed]) - - const renderHeaderBtns = React.useCallback(() => { - return ( - - - - - )} - - - - - Feed - - - - - ) - }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) - - return ( - - - - - ) - }), -) - -/* - - - - - - - - @{currentFeed?.data.creator.handle} - - - - {currentFeed?.data.description} - - - - - - */ - -const styles = StyleSheet.create({ - headerBtns: { - flexDirection: 'row', - gap: 8, - }, - header: { - flexDirection: 'row', - gap: 12, - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 16, - borderTopWidth: 1, - }, - headerDetails: { - paddingHorizontal: 16, - paddingBottom: 16, - }, - fakeSelector: { - flexDirection: 'row', - paddingHorizontal: isDesktopWeb ? 16 : 6, - }, - fakeSelectorItem: { - paddingHorizontal: 12, - paddingBottom: 8, - borderBottomWidth: 3, - }, - liked: { - color: colors.red3, - }, - - /* headerContainer: { - alignItems: 'center', - justifyContent: 'center', - gap: 8, - marginBottom: 12, - }, - header: { - alignItems: 'center', - gap: 4, - }, - avatarContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - buttonsContainer: { - flexDirection: 'row', - gap: 8, - }, - saveButton: { - minWidth: 100, - alignItems: 'center', - }, - liked: { - color: colors.red3, - }, - notLiked: { - color: colors.gray3, - }, - likeButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 4, - paddingHorizontal: 8, - borderRadius: 24, - gap: 4, - },*/ -}) -- cgit 1.4.1 From 2f4408582bf27a83ba8d22605077d067f8433d7c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 15:06:32 -0500 Subject: Set default feeds --- src/lib/constants.ts | 43 ++++++++++++++++++++++++++++++++++++++ src/state/models/ui/preferences.ts | 34 ++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) (limited to 'src/lib') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6d0d4797b..88e429d83 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -94,6 +94,49 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) { } } +export const STAGING_DEFAULT_FEED = (rkey: string) => + `at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}` +export const PROD_DEFAULT_FEED = (rkey: string) => + `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` +export async function DEFAULT_FEEDS( + serviceUrl: string, + resolveHandle: (name: string) => Promise, +) { + if (serviceUrl.includes('localhost')) { + const aliceDid = await resolveHandle('alice.test') + return { + pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], + saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], + } + } else if (serviceUrl.includes('staging')) { + return { + pinned: [ + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + ], + saved: [ + STAGING_DEFAULT_FEED('bsky-team'), + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + STAGING_DEFAULT_FEED('hot-classic'), + ], + } + } else { + return { + pinned: [ + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + ], + saved: [ + STAGING_DEFAULT_FEED('bsky-team'), + STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('whats-hot'), + STAGING_DEFAULT_FEED('hot-classic'), + ], + } + } +} + export const POST_IMG_MAX = { width: 2000, height: 2000, diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 120b4adcc..c85faf658 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -11,6 +11,7 @@ import { ALWAYS_FILTER_LABEL_GROUP, ALWAYS_WARN_LABEL_GROUP, } from 'lib/labeling/const' +import {DEFAULT_FEEDS} from 'lib/constants' import {isIOS} from 'platform/detection' const deviceLocales = getLocales() @@ -95,6 +96,8 @@ export class PreferencesModel { } async sync() { + // fetch preferences + let hasSavedFeedsPref = false const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) runInAction(() => { for (const pref of res.data.preferences) { @@ -120,14 +123,41 @@ export class PreferencesModel { ) { this.savedFeeds = pref.saved this.pinnedFeeds = pref.pinned + hasSavedFeedsPref = true } } }) + + // set defaults on missing items + if (!hasSavedFeedsPref) { + const {saved, pinned} = await DEFAULT_FEEDS( + this.rootStore.agent.service.toString(), + (handle: string) => + this.rootStore.agent + .resolveHandle({handle}) + .then(({data}) => data.did), + ) + runInAction(() => { + this.savedFeeds = saved + this.pinnedFeeds = pinned + }) + res.data.preferences.push({ + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved, + pinned, + }) + await this.rootStore.agent.app.bsky.actor.putPreferences({ + preferences: res.data.preferences, + }) + /* dont await */ this.rootStore.me.savedFeeds.refresh() + } } - async update(cb: (prefs: AppBskyActorDefs.Preferences) => void) { + async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) { const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) - cb(res.data.preferences) + if (cb(res.data.preferences) === false) { + return + } await this.rootStore.agent.app.bsky.actor.putPreferences({ preferences: res.data.preferences, }) -- cgit 1.4.1 From 1ecf0da81b6e5eaf7959e1416df1e8f004e2566f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 16:22:11 -0500 Subject: Add feed sharing --- src/lib/api/index.ts | 93 ++++++++++++++------------- src/lib/link-meta/bsky.ts | 27 ++++++++ src/lib/strings/url-helpers.ts | 12 ++++ src/view/com/composer/useExternalLinkFetch.ts | 22 ++++++- src/view/screens/CustomFeed.tsx | 35 +++++++++- 5 files changed, 141 insertions(+), 48 deletions(-) (limited to 'src/lib') diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 3877b3ef7..81b61a444 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -18,6 +18,7 @@ export interface ExternalEmbedDraft { uri: string isLoading: boolean meta?: LinkMeta + embed?: AppBskyEmbedRecord.Main localThumb?: ImageModel } @@ -135,40 +136,54 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } if (opts.extLink && !opts.images?.length) { - let thumb - if (opts.extLink.localThumb) { - opts.onStateChange?.('Uploading link thumbnail...') - let encoding - if (opts.extLink.localThumb.mime) { - encoding = opts.extLink.localThumb.mime - } else if (opts.extLink.localThumb.path.endsWith('.png')) { - encoding = 'image/png' - } else if ( - opts.extLink.localThumb.path.endsWith('.jpeg') || - opts.extLink.localThumb.path.endsWith('.jpg') - ) { - encoding = 'image/jpeg' - } else { - store.log.warn( - 'Unexpected image format for thumbnail, skipping', - opts.extLink.localThumb.path, - ) - } - if (encoding) { - const thumbUploadRes = await uploadBlob( - store, - opts.extLink.localThumb.path, - encoding, - ) - thumb = thumbUploadRes.data.blob + if (opts.extLink.embed) { + embed = opts.extLink.embed + } else { + let thumb + if (opts.extLink.localThumb) { + opts.onStateChange?.('Uploading link thumbnail...') + let encoding + if (opts.extLink.localThumb.mime) { + encoding = opts.extLink.localThumb.mime + } else if (opts.extLink.localThumb.path.endsWith('.png')) { + encoding = 'image/png' + } else if ( + opts.extLink.localThumb.path.endsWith('.jpeg') || + opts.extLink.localThumb.path.endsWith('.jpg') + ) { + encoding = 'image/jpeg' + } else { + store.log.warn( + 'Unexpected image format for thumbnail, skipping', + opts.extLink.localThumb.path, + ) + } + if (encoding) { + const thumbUploadRes = await uploadBlob( + store, + opts.extLink.localThumb.path, + encoding, + ) + thumb = thumbUploadRes.data.blob + } } - } - if (opts.quote) { - embed = { - $type: 'app.bsky.embed.recordWithMedia', - record: embed, - media: { + if (opts.quote) { + embed = { + $type: 'app.bsky.embed.recordWithMedia', + record: embed, + media: { + $type: 'app.bsky.embed.external', + external: { + uri: opts.extLink.uri, + title: opts.extLink.meta?.title || '', + description: opts.extLink.meta?.description || '', + thumb, + }, + } as AppBskyEmbedExternal.Main, + } as AppBskyEmbedRecordWithMedia.Main + } else { + embed = { $type: 'app.bsky.embed.external', external: { uri: opts.extLink.uri, @@ -176,18 +191,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) { description: opts.extLink.meta?.description || '', thumb, }, - } as AppBskyEmbedExternal.Main, - } as AppBskyEmbedRecordWithMedia.Main - } else { - embed = { - $type: 'app.bsky.embed.external', - external: { - uri: opts.extLink.uri, - title: opts.extLink.meta?.title || '', - description: opts.extLink.meta?.description || '', - thumb, - }, - } as AppBskyEmbedExternal.Main + } as AppBskyEmbedExternal.Main + } } } diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index f4a96a22f..cf43feca8 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -1,3 +1,4 @@ +import * as apilib from 'lib/api/index' import {LikelyType, LinkMeta} from './link-meta' // import {match as matchRoute} from 'view/routes' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' @@ -128,3 +129,29 @@ export async function getPostAsQuote( }, } } + +export async function getFeedAsEmbed( + store: RootStoreModel, + url: string, +): Promise { + url = convertBskyAppUrlIfNeeded(url) + const [_0, user, _1, rkey] = url.split('/').filter(Boolean) + const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey) + const res = await store.agent.app.bsky.feed.getFeedGenerator({feed}) + return { + isLoading: false, + uri: feed, + meta: { + url: feed, + likelyType: LikelyType.AtpData, + title: res.data.view.displayName, + }, + embed: { + $type: 'app.bsky.embed.record', + record: { + uri: res.data.view.uri, + cid: res.data.view.cid, + }, + }, + } +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index a5412920e..d6d43b89d 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -82,6 +82,18 @@ export function isBskyPostUrl(url: string): boolean { return false } +export function isBskyCustomFeedUrl(url: string): boolean { + if (isBskyAppUrl(url)) { + try { + const urlp = new URL(url) + return /profile\/(?[^/]+)\/feed\/(?[^/]+)/i.test( + urlp.pathname, + ) + } catch {} + } + return false +} + export function convertBskyAppUrlIfNeeded(url: string): string { if (isBskyAppUrl(url)) { try { diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 45c2dfd0d..8d3b8cac2 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -2,9 +2,9 @@ import {useState, useEffect} from 'react' import {useStores} from 'state/index' import * as apilib from 'lib/api/index' import {getLinkMeta} from 'lib/link-meta/link-meta' -import {getPostAsQuote} from 'lib/link-meta/bsky' +import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky' import {downloadAndResize} from 'lib/media/manip' -import {isBskyPostUrl} from 'lib/strings/url-helpers' +import {isBskyPostUrl, isBskyCustomFeedUrl} from 'lib/strings/url-helpers' import {ComposerOpts} from 'state/models/ui/shell' import {POST_IMG_MAX} from 'lib/constants' @@ -41,6 +41,24 @@ export function useExternalLinkFetch({ setExtLink(undefined) }, ) + } else if (isBskyCustomFeedUrl(extLink.uri)) { + getFeedAsEmbed(store, extLink.uri).then( + ({embed, meta}) => { + if (aborted) { + return + } + setExtLink({ + uri: extLink.uri, + isLoading: false, + meta, + embed, + }) + }, + err => { + store.log.error('Failed to fetch feed for embedding', {err}) + setExtLink(undefined) + }, + ) } else { getLinkMeta(store, extLink.uri).then(meta => { if (aborted) { diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index bbcc08513..d2b9041f9 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -1,5 +1,6 @@ import React, {useMemo, useRef} from 'react' import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {HeartIcon, HeartIconSolid} from 'lib/icons' import {CommonNavigatorParams} from 'lib/routes/types' @@ -21,6 +22,8 @@ import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' +import {shareUrl} from 'lib/sharing' +import {toShareUrl} from 'lib/strings/url-helpers' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -73,10 +76,22 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up toggle like', {err}) } }, [store, currentFeed]) + const onPressShare = React.useCallback(() => { + const url = toShareUrl(`/profile/${name}/feed/${rkey}`) + shareUrl(url) + }, [name, rkey]) const renderHeaderBtns = React.useCallback(() => { return ( + + )} @@ -202,6 +232,7 @@ export const CustomFeedScreen = withAuthRequired( currentFeed, onToggleLiked, onToggleSaved, + onPressShare, name, rkey, ]) -- cgit 1.4.1 From 37acc9e9304b594ff21443e1be896e1e576bb488 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:22:46 -0500 Subject: A few more UX tweaks --- src/lib/constants.ts | 14 ++++-------- src/view/com/feeds/SavedFeeds.tsx | 39 ++++++++++++++++++++++++-------- src/view/com/pager/FeedsTabBar.web.tsx | 2 +- src/view/com/pager/FeedsTabBarMobile.tsx | 2 +- 4 files changed, 36 insertions(+), 21 deletions(-) (limited to 'src/lib') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 88e429d83..f4c6f5021 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -110,26 +110,20 @@ export async function DEFAULT_FEEDS( } } else if (serviceUrl.includes('staging')) { return { - pinned: [ - STAGING_DEFAULT_FEED('skyline'), - STAGING_DEFAULT_FEED('whats-hot'), - ], + pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('with-friends'), STAGING_DEFAULT_FEED('whats-hot'), STAGING_DEFAULT_FEED('hot-classic'), ], } } else { return { - pinned: [ - STAGING_DEFAULT_FEED('skyline'), - STAGING_DEFAULT_FEED('whats-hot'), - ], + pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('skyline'), + STAGING_DEFAULT_FEED('with-friends'), STAGING_DEFAULT_FEED('whats-hot'), STAGING_DEFAULT_FEED('hot-classic'), ], diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 110a6e894..e92e741da 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -8,7 +8,7 @@ import {FlatList} from 'view/com/util/Views' import {Text} from 'view/com/util/text/Text' import {isDesktopWeb} from 'platform/detection' import {s} from 'lib/styles' -import {Link} from 'view/com/util/Link' +import {Link, TextLink} from 'view/com/util/Link' import {CustomFeed} from './CustomFeed' export const SavedFeeds = observer( @@ -52,14 +52,35 @@ export const SavedFeeds = observer( const renderListFooterComponent = useCallback(() => { return ( - - - - Settings - - + <> + + + + Change Order + + + + + Feeds are custom algorithms that users build with a little coding + expertise.{' '} + {' '} + for more information. + + + ) }, [pal]) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 78937611b..fc04c3b2c 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -28,7 +28,7 @@ const FeedsTabBarDesktop = observer( ) => { const store = useStores() const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My Feeds'], [store.me.savedFeeds.pinnedFeedNames], ) const pal = usePalette('default') diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index a41f0ef32..5954e7f2e 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -33,7 +33,7 @@ export const FeedsTabBar = observer( }, [store]) const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My Feeds'], [store.me.savedFeeds.pinnedFeedNames], ) -- cgit 1.4.1 From 762bd15ed625c9a7d50b0ee832a1422bac3321eb Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 16:12:05 -0700 Subject: fix prod default feeds not working --- src/lib/constants.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/lib') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f4c6f5021..e492dd61a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -102,13 +102,13 @@ export async function DEFAULT_FEEDS( serviceUrl: string, resolveHandle: (name: string) => Promise, ) { - if (serviceUrl.includes('localhost')) { + if (serviceUrl.includes('localhost')) { // local dev const aliceDid = await resolveHandle('alice.test') return { pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], } - } else if (serviceUrl.includes('staging')) { + } else if (serviceUrl.includes('staging')) { // staging return { pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ @@ -118,14 +118,14 @@ export async function DEFAULT_FEEDS( STAGING_DEFAULT_FEED('hot-classic'), ], } - } else { + } else { // production return { - pinned: [STAGING_DEFAULT_FEED('whats-hot')], + pinned: [PROD_DEFAULT_FEED('whats-hot')], saved: [ - STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('with-friends'), - STAGING_DEFAULT_FEED('whats-hot'), - STAGING_DEFAULT_FEED('hot-classic'), + PROD_DEFAULT_FEED('bsky-team'), + PROD_DEFAULT_FEED('with-friends'), + PROD_DEFAULT_FEED('whats-hot'), + PROD_DEFAULT_FEED('hot-classic'), ], } } -- cgit 1.4.1 From 64e303d911d351a2f492a23ae97207e5c6035b6e Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 16:35:37 -0700 Subject: optimistic updates for liking custom feeds --- src/lib/async/revertible.ts | 16 ++++++++++++++++ src/state/models/feeds/custom-feed.ts | 36 +++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 12 deletions(-) (limited to 'src/lib') diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts index 3c8e3e8f9..43383b61e 100644 --- a/src/lib/async/revertible.ts +++ b/src/lib/async/revertible.ts @@ -4,6 +4,22 @@ import set from 'lodash.set' const ongoingActions = new Set() +/** + * This is a TypeScript function that optimistically updates data on the client-side before sending a + * request to the server and rolling back changes if the request fails. + * @param {T} model - The object or record that needs to be updated optimistically. + * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It + * can be used to perform any necessary actions or updates on the model or UI before the server update + * is initiated. + * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server + * update operation. This function is called after the previous state of the model has been recorded + * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` + * function is called with the result + * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the + * server update is successful. It takes in the response from the server update as its parameter. If + * this parameter is not provided, nothing will happen after the server update. + * @returns A Promise that resolves to `void`. + */ export const updateDataOptimistically = async < T extends Record, U, diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index e457d2d1e..9ac69ac28 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -2,6 +2,7 @@ import {AppBskyFeedDefs} from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {updateDataOptimistically} from 'lib/async/revertible' export class CustomFeedModel { // data @@ -58,12 +59,19 @@ export class CustomFeedModel { async like() { try { - const res = await this.rootStore.agent.like(this.data.uri, this.data.cid) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = res.uri - this.data.likeCount = (this.data.likeCount || 0) + 1 - }) + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = 'pending' + this.data.likeCount = (this.data.likeCount || 0) + 1 + }, + () => this.rootStore.agent.like(this.data.uri, this.data.cid), + res => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = res.uri + }, + ) } catch (e: any) { this.rootStore.log.error('Failed to like feed', e) } @@ -74,12 +82,16 @@ export class CustomFeedModel { return } try { - await this.rootStore.agent.deleteLike(this.data.viewer.like!) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = undefined - this.data.likeCount = (this.data.likeCount || 1) - 1 - }) + const likeUri = this.data.viewer.like + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = undefined + this.data.likeCount = (this.data.likeCount || 1) - 1 + }, + () => this.rootStore.agent.deleteLike(likeUri), + ) } catch (e: any) { this.rootStore.log.error('Failed to unlike feed', e) } -- cgit 1.4.1 From dfcdd37087c0be4055c92a0f88431b32646ced6f Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 18:46:36 -0700 Subject: add haptics to save, like, and pin actions on feed --- src/lib/haptics.ts | 24 ++++++++++++++++++++++++ src/view/com/util/post-ctrls/PostCtrls.tsx | 15 +++++++-------- src/view/screens/CustomFeed.tsx | 5 +++++ src/view/screens/SavedFeeds.tsx | 16 ++++++++-------- 4 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 src/lib/haptics.ts (limited to 'src/lib') diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts new file mode 100644 index 000000000..23a321796 --- /dev/null +++ b/src/lib/haptics.ts @@ -0,0 +1,24 @@ +import { isIOS } from 'platform/detection' +import ReactNativeHapticFeedback, { + HapticFeedbackTypes, +} from 'react-native-haptic-feedback' + + +const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s + + +export class Haptics { + static default = () => ReactNativeHapticFeedback.trigger(hapticImpact) + static impact = (type: HapticFeedbackTypes = hapticImpact) => ReactNativeHapticFeedback.trigger(type) + static selection = () => ReactNativeHapticFeedback.trigger('selection') + static notification = (type: 'success' | 'warning' | 'error') => { + switch (type) { + case 'success': + return ReactNativeHapticFeedback.trigger('notificationSuccess') + case 'warning': + return ReactNativeHapticFeedback.trigger('notificationWarning') + case 'error': + return ReactNativeHapticFeedback.trigger('notificationError') + } + } +} \ No newline at end of file diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 9980e9de0..0d2f83ce7 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -10,9 +10,6 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import ReactNativeHapticFeedback, { - HapticFeedbackTypes, -} from 'react-native-haptic-feedback' // DISABLED see #135 // import { // TriggerableAnimated, @@ -24,8 +21,9 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' -import {isIOS, isNative} from 'platform/detection' +import {isNative} from 'platform/detection' import {RepostButton} from './RepostButton' +import {Haptics} from 'lib/haptics' interface PostCtrlsOpts { itemUri: string @@ -58,7 +56,6 @@ interface PostCtrlsOpts { } const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} -const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537 // DISABLED see #135 /* @@ -112,7 +109,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { store.shell.closeModal() if (!opts.isReposted) { if (isNative) { - ReactNativeHapticFeedback.trigger(hapticImpact) + Haptics.default() } opts.onPressToggleRepost().catch(_e => undefined) // DISABLED see #135 @@ -141,7 +138,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { }) if (isNative) { - ReactNativeHapticFeedback.trigger(hapticImpact) + Haptics.default() } }, [ opts.author, @@ -154,7 +151,9 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onPressToggleLikeWrapper = async () => { if (!opts.isLiked) { - ReactNativeHapticFeedback.trigger(hapticImpact) + if (isNative) { + Haptics.default() + } await opts.onPressToggleLike().catch(_e => undefined) // DISABLED see #135 // likeRef.current?.trigger( diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 7ff22f7f3..353995540 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -24,6 +24,9 @@ import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' +import { Haptics } from 'lib/haptics' + +const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -49,6 +52,7 @@ export const CustomFeedScreen = withAuthRequired( const onToggleSaved = React.useCallback(async () => { try { + Haptics.default() if (currentFeed?.isSaved) { await currentFeed?.unsave() } else { @@ -63,6 +67,7 @@ export const CustomFeedScreen = withAuthRequired( }, [store, currentFeed]) const onToggleLiked = React.useCallback(async () => { + Haptics.default() try { if (currentFeed?.isLiked) { await currentFeed?.unlike() diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 613e42fbf..0213b36a9 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -28,6 +28,7 @@ import {CustomFeed} from 'view/com/feeds/CustomFeed' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CustomFeedModel} from 'state/models/feeds/custom-feed' import * as Toast from 'view/com/util/Toast' +import {Haptics} from 'lib/haptics' type Props = NativeStackScreenProps @@ -128,14 +129,13 @@ const ListItem = observer( const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) const isPinned = savedFeeds.isPinned(item) - const onTogglePinned = useCallback( - () => - savedFeeds.togglePinnedFeed(item).catch(e => { - Toast.show('There was an issue contacting the server') - store.log.error('Failed to toggle pinned feed', {e}) - }), - [savedFeeds, item, store], - ) + const onTogglePinned = useCallback(() => { + Haptics.default() + savedFeeds.togglePinnedFeed(item).catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }) + }, [savedFeeds, item, store]) const onPressUp = useCallback( () => savedFeeds.movePinnedFeed(item, 'up').catch(e => { -- cgit 1.4.1 From 858ec6438da9cc9bee765857ea925f77e074fde2 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:48:14 -0700 Subject: show scroll to top button when scrolling stops --- src/lib/hooks/useOnMainScroll.ts | 3 +++ src/view/com/posts/Feed.tsx | 8 +++++++- src/view/screens/CustomFeed.tsx | 20 ++++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) (limited to 'src/lib') diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 41b35dd4f..994a35714 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -2,6 +2,9 @@ import {useState} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {RootStoreModel} from 'state/index' +export type onMomentumScrollEndCb = ( + event: NativeSyntheticEvent, +) => void export type OnScrollCb = ( event: NativeSyntheticEvent, ) => void diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 5b0110df8..50398e706 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -14,7 +14,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' @@ -31,6 +31,8 @@ export const Feed = observer(function Feed({ scrollElRef, onPressTryAgain, onScroll, + scrollEventThrottle, + onMomentumScrollEnd, renderEmptyState, testID, headerOffset = 0, @@ -43,6 +45,8 @@ export const Feed = observer(function Feed({ scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb + scrollEventThrottle?: number + onMomentumScrollEnd?: onMomentumScrollEndCb renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number @@ -180,6 +184,8 @@ export const Feed = observer(function Feed({ contentContainerStyle={s.contentContainer} style={{paddingTop: headerOffset}} onScroll={onScroll} + scrollEventThrottle={scrollEventThrottle} + onMomentumScrollEnd={onMomentumScrollEnd} onEndReached={onEndReached} onEndReachedThreshold={0.6} removeClippedSubviews={true} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 952461c9c..2316d7f06 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef} from 'react' +import React, {useMemo, useRef, useState} from 'react' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -25,6 +25,8 @@ import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' +import { LoadLatestBtn } from 'view/com/util/load-latest/LoadLatestBtn' +import { onMomentumScrollEndCb } from 'lib/hooks/useOnMainScroll' const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} @@ -48,7 +50,7 @@ export const CustomFeedScreen = withAuthRequired( return feed }, [store, uri]) const isPinned = store.me.savedFeeds.isPinned(uri) - + const [allowScrollToTop, setAllowScrollToTop] = useState(false) useSetTitle(currentFeed?.displayName) const onToggleSaved = React.useCallback(async () => { @@ -266,15 +268,29 @@ export const CustomFeedScreen = withAuthRequired( isPinned, ]) + const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback((event) => { + if (event.nativeEvent.contentOffset.y > 200) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } + }, []) + return ( + {allowScrollToTop ? { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + }} + label='Scroll to top' + /> : null} ) }), -- cgit 1.4.1 From 58a0489ce3028c6e6d205557e3edcf03c85f6626 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 13:59:53 -0700 Subject: add isWeb check to disable haptics on web --- src/lib/haptics.ts | 30 +++++++++++++++++++++++------- src/view/com/util/post-ctrls/PostCtrls.tsx | 16 ++++------------ 2 files changed, 27 insertions(+), 19 deletions(-) (limited to 'src/lib') diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index 23a321796..516940c1c 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -1,17 +1,33 @@ -import { isIOS } from 'platform/detection' +import {isIOS, isWeb} from 'platform/detection' import ReactNativeHapticFeedback, { HapticFeedbackTypes, } from 'react-native-haptic-feedback' - const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s - export class Haptics { - static default = () => ReactNativeHapticFeedback.trigger(hapticImpact) - static impact = (type: HapticFeedbackTypes = hapticImpact) => ReactNativeHapticFeedback.trigger(type) - static selection = () => ReactNativeHapticFeedback.trigger('selection') + static default() { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger(hapticImpact) + } + static impact(type: HapticFeedbackTypes = hapticImpact) { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger(type) + } + static selection() { + if (isWeb) { + return + } + ReactNativeHapticFeedback.trigger('selection') + } static notification = (type: 'success' | 'warning' | 'error') => { + if (isWeb) { + return + } switch (type) { case 'success': return ReactNativeHapticFeedback.trigger('notificationSuccess') @@ -21,4 +37,4 @@ export class Haptics { return ReactNativeHapticFeedback.trigger('notificationError') } } -} \ No newline at end of file +} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 0d2f83ce7..41d66641f 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -21,7 +21,6 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' -import {isNative} from 'platform/detection' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' @@ -108,9 +107,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onRepost = useCallback(() => { store.shell.closeModal() if (!opts.isReposted) { - if (isNative) { - Haptics.default() - } + Haptics.default() opts.onPressToggleRepost().catch(_e => undefined) // DISABLED see #135 // repostRef.current?.trigger( @@ -136,10 +133,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { indexedAt: opts.indexedAt, }, }) - - if (isNative) { - Haptics.default() - } + Haptics.default() }, [ opts.author, opts.indexedAt, @@ -151,9 +145,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { const onPressToggleLikeWrapper = async () => { if (!opts.isLiked) { - if (isNative) { - Haptics.default() - } + Haptics.default() await opts.onPressToggleLike().catch(_e => undefined) // DISABLED see #135 // likeRef.current?.trigger( @@ -200,7 +192,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { accessibilityRole="button" accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'} accessibilityHint={ - opts.isReposted ? `Removes like from the post` : `Like the post` + opts.isReposted ? 'Removes like from the post' : 'Like the post' }> {opts.isLiked ? ( Date: Wed, 24 May 2023 15:00:36 -0700 Subject: fix lint errors --- src/lib/constants.ts | 9 ++++++--- src/view/screens/Notifications.tsx | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'src/lib') diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e492dd61a..c42e6f3a9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -102,13 +102,15 @@ export async function DEFAULT_FEEDS( serviceUrl: string, resolveHandle: (name: string) => Promise, ) { - if (serviceUrl.includes('localhost')) { // local dev + if (serviceUrl.includes('localhost')) { + // local dev const aliceDid = await resolveHandle('alice.test') return { pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], } - } else if (serviceUrl.includes('staging')) { // staging + } else if (serviceUrl.includes('staging')) { + // staging return { pinned: [STAGING_DEFAULT_FEED('whats-hot')], saved: [ @@ -118,7 +120,8 @@ export async function DEFAULT_FEEDS( STAGING_DEFAULT_FEED('hot-classic'), ], } - } else { // production + } else { + // production return { pinned: [PROD_DEFAULT_FEED('whats-hot')], saved: [ diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index df84b541b..67507d009 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -98,7 +98,10 @@ export const NotificationsScreen = withAuthRequired( /> {store.me.notifications.hasNewLatest && !store.me.notifications.isRefreshing && ( - + )} ) -- cgit 1.4.1 From 32c9dabb7467149baf39d8f5c2eb3d0b81236d92 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 15:04:30 -0700 Subject: make tab bar scroll view draggable on web --- src/lib/hooks/useDraggableScrollView.ts | 84 ++++++++++++++++++++++++++++++ src/lib/merge-refs.ts | 27 ++++++++++ src/view/com/pager/DraggableScrollView.tsx | 15 ++++++ src/view/com/pager/FeedsTabBar.web.tsx | 2 +- src/view/com/pager/TabBar.tsx | 5 +- 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/lib/hooks/useDraggableScrollView.ts create mode 100644 src/lib/merge-refs.ts create mode 100644 src/view/com/pager/DraggableScrollView.tsx (limited to 'src/lib') diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts new file mode 100644 index 000000000..b0f7465d7 --- /dev/null +++ b/src/lib/hooks/useDraggableScrollView.ts @@ -0,0 +1,84 @@ +import {useEffect, useRef, useMemo, ForwardedRef} from 'react' +import {Platform, findNodeHandle} from 'react-native' +import type {ScrollView} from 'react-native' +import {mergeRefs} from 'lib/merge-refs' + +type Props = { + cursor?: string + outerRef?: ForwardedRef +} + +export function useDraggableScroll({ + outerRef, + cursor = 'grab', +}: Props = {}) { + const ref = useRef(null) + + useEffect(() => { + if (Platform.OS !== 'web' || !ref.current) { + return + } + const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement + if (!slider) { + return + } + let isDragging = false + let isMouseDown = false + let startX = 0 + let scrollLeft = 0 + + const mouseDown = (e: MouseEvent) => { + isMouseDown = true + startX = e.pageX - slider.offsetLeft + scrollLeft = slider.scrollLeft + + slider.style.cursor = cursor + } + + const mouseUp = () => { + if (isDragging) { + slider.addEventListener('click', e => e.stopPropagation(), {once: true}) + } + + isMouseDown = false + isDragging = false + slider.style.cursor = 'default' + } + + const mouseMove = (e: MouseEvent) => { + if (!isMouseDown) { + return + } + + // Require n pixels momement before start of drag (3 in this case ) + const x = e.pageX - slider.offsetLeft + if (Math.abs(x - startX) < 3) { + return + } + + isDragging = true + e.preventDefault() + const walk = x - startX + slider.scrollLeft = scrollLeft - walk + } + + slider.addEventListener('mousedown', mouseDown) + window.addEventListener('mouseup', mouseUp) + window.addEventListener('mousemove', mouseMove) + + return () => { + slider.removeEventListener('mousedown', mouseDown) + window.removeEventListener('mouseup', mouseUp) + window.removeEventListener('mousemove', mouseMove) + } + }, [cursor]) + + const refs = useMemo( + () => mergeRefs(outerRef ? [ref, outerRef] : [ref]), + [ref, outerRef], + ) + + return { + refs, + } +} diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts new file mode 100644 index 000000000..4617b5260 --- /dev/null +++ b/src/lib/merge-refs.ts @@ -0,0 +1,27 @@ +/** + * This TypeScript function merges multiple React refs into a single ref callback. + * When developing low level UI components, it is common to have to use a local ref + * but also support an external one using React.forwardRef. + * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility. + * Today a ref can be a function or an object, tomorrow it could be another thing, who knows. + * This utility handles compatibility for you. + * This function is inspired by https://github.com/gregberge/react-merge-refs + * @param refs - An array of React refs, which can be either `React.MutableRefObject` or + * `React.LegacyRef`. These refs are used to store references to DOM elements or React components. + * The `mergeRefs` function takes in an array of these refs and returns a callback function that + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and + * returns a ref callback function that can be used to merge multiple refs into a single ref. + */ +export function mergeRefs( + refs: Array | React.LegacyRef>, +): React.RefCallback { + return value => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value) + } else if (ref != null) { + ;(ref as React.MutableRefObject).current = value + } + }) + } +} diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx new file mode 100644 index 000000000..4b7396eaa --- /dev/null +++ b/src/view/com/pager/DraggableScrollView.tsx @@ -0,0 +1,15 @@ +import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView' +import React, {ComponentProps} from 'react' +import {ScrollView} from 'react-native' + +export const DraggableScrollView = React.forwardRef< + ScrollView, + ComponentProps +>(function DraggableScrollView(props, ref) { + const {refs} = useDraggableScroll({ + outerRef: ref, + cursor: 'grab', // optional, default + }) + + return +}) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index fc04c3b2c..b51db1741 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -53,8 +53,8 @@ const FeedsTabBarDesktop = observer( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 485219730..cebf58b48 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -11,6 +11,7 @@ import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' import {isDesktopWeb} from 'platform/detection' +import {DraggableScrollView} from './DraggableScrollView' export interface TabBarProps { testID?: string @@ -75,7 +76,7 @@ export function TabBar({ return ( - ) })} - + ) } -- cgit 1.4.1 From 4e1876fe85ab3a70eba50466a62bff8a9d01c16c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 18:46:27 -0500 Subject: Refactor the scroll-to-top UX --- src/lib/hooks/useOnMainScroll.ts | 58 ++++++++++++------ src/view/com/notifications/Feed.tsx | 1 + src/view/com/posts/Feed.tsx | 3 +- src/view/com/util/fab/FABInner.tsx | 2 +- .../com/util/load-latest/LoadLatestBtnMobile.tsx | 39 +++++------- src/view/screens/CustomFeed.tsx | 70 +++++++++------------- src/view/screens/Home.tsx | 11 ++-- src/view/screens/Notifications.tsx | 16 +++-- src/view/screens/SearchMobile.tsx | 2 +- 9 files changed, 102 insertions(+), 100 deletions(-) (limited to 'src/lib') diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 994a35714..782c4704b 100644 --- a/src/lib/hooks/useOnMainScroll.ts +++ b/src/lib/hooks/useOnMainScroll.ts @@ -1,28 +1,50 @@ -import {useState} from 'react' +import {useState, useCallback, useRef} from 'react' import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' import {RootStoreModel} from 'state/index' +import {s} from 'lib/styles' -export type onMomentumScrollEndCb = ( - event: NativeSyntheticEvent, -) => void export type OnScrollCb = ( event: NativeSyntheticEvent, ) => void +export type ResetCb = () => void + +export function useOnMainScroll( + store: RootStoreModel, +): [OnScrollCb, boolean, ResetCb] { + let lastY = useRef(0) + let [isScrolledDown, setIsScrolledDown] = useState(false) + return [ + useCallback( + (event: NativeSyntheticEvent) => { + const y = event.nativeEvent.contentOffset.y + const dy = y - (lastY.current || 0) + lastY.current = y -export function useOnMainScroll(store: RootStoreModel) { - let [lastY, setLastY] = useState(0) - let isMinimal = store.shell.minimalShellMode - return function onMainScroll(event: NativeSyntheticEvent) { - const y = event.nativeEvent.contentOffset.y - const dy = y - (lastY || 0) - setLastY(y) + if (!store.shell.minimalShellMode && y > 10 && dy > 10) { + store.shell.setMinimalShellMode(true) + } else if (store.shell.minimalShellMode && (y <= 10 || dy < -10)) { + store.shell.setMinimalShellMode(false) + } - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { + if ( + !isScrolledDown && + event.nativeEvent.contentOffset.y > s.window.height + ) { + setIsScrolledDown(true) + } else if ( + isScrolledDown && + event.nativeEvent.contentOffset.y < s.window.height + ) { + setIsScrolledDown(false) + } + }, + [store, isScrolledDown], + ), + isScrolledDown, + useCallback(() => { + setIsScrolledDown(false) store.shell.setMinimalShellMode(false) - isMinimal = false - } - } + lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf + }, [store, setIsScrolledDown]), + ] } diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 50bdc5dc9..d457d7136 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -154,6 +154,7 @@ export const Feed = observer(function Feed({ onEndReached={onEndReached} onEndReachedThreshold={0.6} onScroll={onScroll} + scrollEventThrottle={100} contentContainerStyle={s.contentContainer} /> ) : null} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 2726ff7d3..b90213472 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -14,7 +14,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' @@ -47,7 +47,6 @@ export const Feed = observer(function Feed({ onPressTryAgain?: () => void onScroll?: OnScrollCb scrollEventThrottle?: number - onMomentumScrollEnd?: onMomentumScrollEndCb renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 5eb4a6588..76824e575 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ outer: { position: 'absolute', zIndex: 1, - right: 28, + right: 24, bottom: 94, width: 60, height: 60, diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 548d30d5a..5e03e2285 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -1,23 +1,25 @@ import React from 'react' import {StyleSheet, TouchableOpacity} from 'react-native' import {observer} from 'mobx-react-lite' -import LinearGradient from 'react-native-linear-gradient' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {Text} from '../text/Text' -import {colors, gradients} from 'lib/styles' import {clamp} from 'lodash' import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} export const LoadLatestBtn = observer( ({onPress, label}: {onPress: () => void; label: string}) => { const store = useStores() + const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() return ( - - - {label} - - + accessibilityHint=""> + ) }, @@ -44,19 +38,14 @@ export const LoadLatestBtn = observer( const styles = StyleSheet.create({ loadLatest: { position: 'absolute', - left: 20, + left: 18, bottom: 35, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowOffset: {width: 0, height: 1}, - }, - loadLatestInner: { + borderWidth: 1, + width: 52, + height: 52, + borderRadius: 26, flexDirection: 'row', - paddingHorizontal: 14, - paddingVertical: 10, - borderRadius: 30, - }, - loadLatestText: { - color: colors.white, + alignItems: 'center', + justifyContent: 'center', }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index dcb726873..1409762d1 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -20,13 +20,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb, isWeb} from 'platform/detection' +import {isDesktopWeb} from 'platform/detection' import {useSetTitle} from 'lib/hooks/useSetTitle' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -48,7 +48,8 @@ export const CustomFeedScreen = withAuthRequired( return feed }, [store, uri]) const isPinned = store.me.savedFeeds.isPinned(uri) - const [allowScrollToTop, setAllowScrollToTop] = useState(false) + const [onMainScroll, isScrolledDown, resetMainScroll] = + useOnMainScroll(store) useSetTitle(currentFeed?.displayName) const onToggleSaved = React.useCallback(async () => { @@ -66,6 +67,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up update feeds', {err}) } }, [store, currentFeed]) + const onToggleLiked = React.useCallback(async () => { Haptics.default() try { @@ -81,6 +83,7 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up toggle like', {err}) } }, [store, currentFeed]) + const onTogglePinned = React.useCallback(async () => { Haptics.default() store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { @@ -88,11 +91,17 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed to toggle pinned feed', {e}) }) }, [store, currentFeed]) + const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/feed/${rkey}`) shareUrl(url) }, [name, rkey]) + const onScrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + resetMainScroll() + }, [scrollElRef, resetMainScroll]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -220,15 +229,17 @@ export const CustomFeedScreen = withAuthRequired( ) : null} - + {currentFeed ? ( + + ) : null}