From 678f75b4951d891bbc2651220fe99a2f040af88f Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 13:51:07 -0700 Subject: [PATCH 001/170] add window dimensions to global styles --- src/lib/styles.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 00a8638f9d..07315c9f22 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'}, From 370d52bd1f4ba3b5effb7a48cd1c8b14aea88781 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 14:40:58 -0700 Subject: [PATCH 002/170] 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 diff --git a/src/Navigation.tsx b/src/Navigation.tsx index afc7b39b87..8b6e1b4539 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 34e6e6a468..b914956405 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 43d31ee099..c1b441984d 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 0000000000..3e2fa7e73f --- /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 f98cdc0c8a..fd8fb4f4a1 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: Fri, 12 May 2023 17:21:11 -0700 Subject: [PATCH 003/170] bookmarked feeds model --- src/state/models/feeds/bookmarked.ts | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/state/models/feeds/bookmarked.ts diff --git a/src/state/models/feeds/bookmarked.ts b/src/state/models/feeds/bookmarked.ts new file mode 100644 index 0000000000..d472f0480f --- /dev/null +++ b/src/state/models/feeds/bookmarked.ts @@ -0,0 +1,134 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyFeedGetBookmarkedFeeds as GetBookmarkedFeeds, + // AppBskyFeedBookmarkFeed as bookmarkedFeed, + // AppBskyFeedUnbookmarkFeed as unbookmarkFeed, + AppBskyFeedDefs as FeedDefs, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {bundleAsync} from 'lib/async/bundle' +import {cleanError} from 'lib/strings/errors' + +const PAGE_SIZE = 30 + +export class BookmarkedFeedsModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + hasMore = true + loadMoreCursor?: string + + // data + feeds: FeedDefs.GeneratorView[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.feeds.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + clear() { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.hasMore = true + this.loadMoreCursor = undefined + this.feeds = [] + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.feed.getBookmarkedFeeds({ + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + async bookmark(feed: FeedDefs.GeneratorView) { + try { + await this.rootStore.agent.app.bsky.feed.bookmarkFeed({feed: feed.uri}) + } catch (e: any) { + this.rootStore.log.error('Failed to bookmark feed', e) + } + } + + async unbookmark(feed: FeedDefs.GeneratorView) { + try { + await this.rootStore.agent.app.bsky.feed.unbookmarkFeed({feed: feed.uri}) + } catch (e: any) { + this.rootStore.log.error('Failed to unbookmark feed', e) + } + } + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch user followers', err) + } + } + + // helper functions + // = + + _replaceAll(res: GetBookmarkedFeeds.Response) { + this.feeds = [] + this._appendAll(res) + } + + _appendAll(res: GetBookmarkedFeeds.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.feeds = this.feeds.concat(res.data.feeds) + } +} From 06ce42158ea31a857393ba1b9b00877f501398c4 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 17:21:17 -0700 Subject: [PATCH 004/170] actor feeds model --- src/state/models/feeds/actor.ts | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/state/models/feeds/actor.ts diff --git a/src/state/models/feeds/actor.ts b/src/state/models/feeds/actor.ts new file mode 100644 index 0000000000..85b55f383e --- /dev/null +++ b/src/state/models/feeds/actor.ts @@ -0,0 +1,135 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyFeedGetBookmarkedFeeds as GetBookmarkedFeeds, + // AppBskyFeedBookmarkFeed as bookmarkedFeed, + // AppBskyFeedUnbookmarkFeed as unbookmarkFeed, + AppBskyFeedDefs as FeedDefs, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {bundleAsync} from 'lib/async/bundle' +import {cleanError} from 'lib/strings/errors' + +const PAGE_SIZE = 30 + +export class BookmarkedFeedsModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + hasMore = true + loadMoreCursor?: string + + // data + feeds: FeedDefs.GeneratorView[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.feeds.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async refresh() { + return this.loadMore(true) + } + + clear() { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.hasMore = true + this.loadMoreCursor = undefined + this.feeds = [] + } + + loadMore = bundleAsync(async (replace: boolean = false) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({ + actor: 'did:plc:dpny6d4qwwxu5b6dp3qob5ok', // TODO: take this as input param + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + async bookmark(feed: FeedDefs.GeneratorView) { + try { + await this.rootStore.agent.app.bsky.feed.bookmarkFeed({feed: feed.uri}) + } catch (e: any) { + this.rootStore.log.error('Failed to bookmark feed', e) + } + } + + async unbookmark(feed: FeedDefs.GeneratorView) { + try { + await this.rootStore.agent.app.bsky.feed.unbookmarkFeed({feed: feed.uri}) + } catch (e: any) { + this.rootStore.log.error('Failed to unbookmark feed', e) + } + } + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch user followers', err) + } + } + + // helper functions + // = + + _replaceAll(res: GetBookmarkedFeeds.Response) { + this.feeds = [] + this._appendAll(res) + } + + _appendAll(res: GetBookmarkedFeeds.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.feeds = this.feeds.concat(res.data.feeds) + } +} From fa4af20764b0596420c1e2a8dd981dc34020e205 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 17:22:53 -0700 Subject: [PATCH 005/170] remove unncessary code from actorFeedModel --- src/state/models/feeds/actor.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/state/models/feeds/actor.ts b/src/state/models/feeds/actor.ts index 85b55f383e..e660d4eb8a 100644 --- a/src/state/models/feeds/actor.ts +++ b/src/state/models/feeds/actor.ts @@ -11,7 +11,7 @@ import {cleanError} from 'lib/strings/errors' const PAGE_SIZE = 30 -export class BookmarkedFeedsModel { +export class ActorFeedsModel { // state isLoading = false isRefreshing = false @@ -84,22 +84,6 @@ export class BookmarkedFeedsModel { } }) - async bookmark(feed: FeedDefs.GeneratorView) { - try { - await this.rootStore.agent.app.bsky.feed.bookmarkFeed({feed: feed.uri}) - } catch (e: any) { - this.rootStore.log.error('Failed to bookmark feed', e) - } - } - - async unbookmark(feed: FeedDefs.GeneratorView) { - try { - await this.rootStore.agent.app.bsky.feed.unbookmarkFeed({feed: feed.uri}) - } catch (e: any) { - this.rootStore.log.error('Failed to unbookmark feed', e) - } - } - // state transitions // = From 760b5309e09251023682d8a93ed331ff921817ea Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 19:32:39 -0700 Subject: [PATCH 006/170] show algos by user on profile --- src/state/models/feeds/actor.ts | 16 +++++---- src/state/models/ui/profile.ts | 30 ++++++++++++++--- src/view/com/algos/AlgoItem.tsx | 57 +++++++++++++++++++++++++++++++++ src/view/screens/Profile.tsx | 13 ++++---- 4 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 src/view/com/algos/AlgoItem.tsx diff --git a/src/state/models/feeds/actor.ts b/src/state/models/feeds/actor.ts index e660d4eb8a..08b7c2a744 100644 --- a/src/state/models/feeds/actor.ts +++ b/src/state/models/feeds/actor.ts @@ -1,9 +1,7 @@ import {makeAutoObservable} from 'mobx' import { - AppBskyFeedGetBookmarkedFeeds as GetBookmarkedFeeds, - // AppBskyFeedBookmarkFeed as bookmarkedFeed, - // AppBskyFeedUnbookmarkFeed as unbookmarkFeed, AppBskyFeedDefs as FeedDefs, + AppBskyFeedGetActorFeeds as GetActorFeeds, } from '@atproto/api' import {RootStoreModel} from '../root-store' import {bundleAsync} from 'lib/async/bundle' @@ -23,7 +21,10 @@ export class ActorFeedsModel { // data feeds: FeedDefs.GeneratorView[] = [] - constructor(public rootStore: RootStoreModel) { + constructor( + public rootStore: RootStoreModel, + public params: GetActorFeeds.QueryParams, + ) { makeAutoObservable( this, { @@ -69,10 +70,11 @@ export class ActorFeedsModel { this._xLoading(replace) try { const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({ - actor: 'did:plc:dpny6d4qwwxu5b6dp3qob5ok', // TODO: take this as input param + actor: this.params.actor, limit: PAGE_SIZE, cursor: replace ? undefined : this.loadMoreCursor, }) + console.log('res', res.data.feeds) if (replace) { this._replaceAll(res) } else { @@ -106,12 +108,12 @@ export class ActorFeedsModel { // helper functions // = - _replaceAll(res: GetBookmarkedFeeds.Response) { + _replaceAll(res: GetActorFeeds.Response) { this.feeds = [] this._appendAll(res) } - _appendAll(res: GetBookmarkedFeeds.Response) { + _appendAll(res: GetActorFeeds.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.feeds = this.feeds.concat(res.data.feeds) diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index d06a196f3d..86199108e2 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -2,13 +2,20 @@ import {makeAutoObservable} from 'mobx' import {RootStoreModel} from '../root-store' import {ProfileModel} from '../content/profile' import {PostsFeedModel} from '../feeds/posts' +import {ActorFeedsModel} from '../feeds/actor' +import {AppBskyFeedDefs} from '@atproto/api' export enum Sections { Posts = 'Posts', PostsWithReplies = 'Posts & replies', + CustomAlgorithms = 'Algos', } -const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies] +const USER_SELECTOR_ITEMS = [ + Sections.Posts, + Sections.PostsWithReplies, + Sections.CustomAlgorithms, +] export interface ProfileUiParams { user: string @@ -22,6 +29,7 @@ export class ProfileUiModel { // data profile: ProfileModel feed: PostsFeedModel + algos: ActorFeedsModel // ui state selectedViewIndex = 0 @@ -43,15 +51,19 @@ export class ProfileUiModel { actor: params.user, limit: 10, }) + this.algos = new ActorFeedsModel(rootStore, {actor: params.user}) } - get currentView(): PostsFeedModel { + get currentView(): PostsFeedModel | ActorFeedsModel { if ( this.selectedView === Sections.Posts || this.selectedView === Sections.PostsWithReplies ) { return this.feed } + if (this.selectedView === Sections.CustomAlgorithms) { + return this.algos + } throw new Error(`Invalid selector value: ${this.selectedViewIndex}`) } @@ -71,12 +83,17 @@ export class ProfileUiModel { get selectedView() { return this.selectorItems[this.selectedViewIndex] } + isGeneratorView(v: any) { + return AppBskyFeedDefs.isGeneratorView(v) + } get uiItems() { let arr: any[] = [] + // if loading, return loading item to show loading spinner if (this.isInitialLoading) { arr = arr.concat([ProfileUiModel.LOADING_ITEM]) } else if (this.currentView.hasError) { + // if error, return error item to show error message arr = arr.concat([ { _reactKey: '__error__', @@ -84,12 +101,16 @@ export class ProfileUiModel { }, ]) } else { + // not loading, no error, show content if ( this.selectedView === Sections.Posts || - this.selectedView === Sections.PostsWithReplies + this.selectedView === Sections.PostsWithReplies || + this.selectedView === Sections.CustomAlgorithms ) { if (this.feed.hasContent) { - if (this.selectedView === Sections.Posts) { + if (this.selectedView === Sections.CustomAlgorithms) { + arr = this.algos.feeds + } else if (this.selectedView === Sections.Posts) { arr = this.feed.nonReplyFeed } else { arr = this.feed.slices.slice() @@ -101,6 +122,7 @@ export class ProfileUiModel { arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) } } else { + // fallback, add empty item, to show empty message arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) } } diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx new file mode 100644 index 0000000000..57a1428e68 --- /dev/null +++ b/src/view/com/algos/AlgoItem.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {Text} from '../util/text/Text' +import {AppBskyFeedDefs} from '@atproto/api' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' +import {UserAvatar} from '../util/UserAvatar' + +const AlgoItem = ({item}: {item: AppBskyFeedDefs.GeneratorView}) => { + const pal = usePalette('default') + return ( + + + + + + + + {item.displayName ?? 'Feed name'} + + + {item.description ?? + 'THIS IS A FEED DESCRIPTION, IT WILL TELL YOU WHAT THE FEED IS ABOUT. THIS IS A COOL FEED ABOUT COOL PEOPLE.'} + + + + + {/* TODO: this feed is like by *3* people UserAvatars and others */} + + ) +} + +export default AlgoItem + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 18, + paddingVertical: 20, + flexDirection: 'column', + columnGap: 36, + flex: 1, + borderTopWidth: 1, + borderTopColor: '#E5E5E5', + }, + headerContainer: { + flexDirection: 'row', + }, + headerTextContainer: { + flexDirection: 'column', + columnGap: 4, + flex: 1, + }, + description: { + flex: 1, + flexWrap: 'wrap', + }, +}) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5fb212554b..b88caf1f83 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -21,6 +21,8 @@ import {FAB} from '../com/util/fab/FAB' import {s, colors} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' +import {AppBskyFeedDefs} from '@atproto/api' +import AlgoItem from 'view/com/algos/AlgoItem' type Props = NativeStackScreenProps export const ProfileScreen = withAuthRequired( @@ -152,15 +154,14 @@ export const ProfileScreen = withAuthRequired( ) } else if (item instanceof PostsFeedSliceModel) { return + } else if (item.creator) { + // TODO: this is a hack to see if it is a custom feed. fix it to something more robust + const typedItem = item as AppBskyFeedDefs.GeneratorView + return } return }, - [ - onPressTryAgain, - uiState.profile.did, - uiState.feed.isBlocking, - uiState.feed.isBlockedBy, - ], + [onPressTryAgain, uiState], ) return ( From c24389df87cc8a079c447aad6a0e0b025def84f5 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 12 May 2023 19:46:50 -0700 Subject: [PATCH 007/170] custom feed embed --- src/view/com/algos/AlgoItem.tsx | 12 +++++++++--- src/view/com/util/post-embeds/index.tsx | 26 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx index 57a1428e68..979518f1d3 100644 --- a/src/view/com/algos/AlgoItem.tsx +++ b/src/view/com/algos/AlgoItem.tsx @@ -1,15 +1,21 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Text} from '../util/text/Text' import {AppBskyFeedDefs} from '@atproto/api' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {UserAvatar} from '../util/UserAvatar' -const AlgoItem = ({item}: {item: AppBskyFeedDefs.GeneratorView}) => { +const AlgoItem = ({ + item, + style, +}: { + item: AppBskyFeedDefs.GeneratorView + style?: StyleProp +}) => { const pal = usePalette('default') return ( - + diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 2dda9069ee..72158af42e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -13,6 +13,7 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedPost, + AppBskyFeedDefs, } from '@atproto/api' import {Link} from '../Link' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' @@ -24,6 +25,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' import QuoteEmbed from './QuoteEmbed' import {AutoSizedImage} from '../images/AutoSizedImage' +import AlgoItem from 'view/com/algos/AlgoItem' type Embed = | AppBskyEmbedRecord.View @@ -42,6 +44,8 @@ export function PostEmbeds({ const pal = usePalette('default') const store = useStores() + // quote post with media + // = if ( AppBskyEmbedRecordWithMedia.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record.record) && @@ -65,6 +69,8 @@ export function PostEmbeds({ ) } + // quote post + // = if (AppBskyEmbedRecord.isView(embed)) { if ( AppBskyEmbedRecord.isViewRecord(embed.record) && @@ -87,6 +93,8 @@ export function PostEmbeds({ } } + // image embed + // = if (AppBskyEmbedImages.isView(embed)) { const {images} = embed @@ -132,10 +140,11 @@ export function PostEmbeds({ /> ) - // } } } + // external link embed + // = if (AppBskyEmbedExternal.isView(embed)) { const link = embed.external const youtubeVideoId = getYoutubeVideoId(link.uri) @@ -153,6 +162,21 @@ export function PostEmbeds({ ) } + + // custom feed embed (i.e. generator view) + // = + if ( + AppBskyEmbedRecord.isView(embed) && + AppBskyFeedDefs.isGeneratorView(embed.record) + ) { + return ( + + ) + } + return } From 047024a5ac96a5292b6a2122835673b5a034f4c3 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Sat, 13 May 2023 11:59:08 -0700 Subject: [PATCH 008/170] create algo-item model and redefine data models --- src/state/models/feeds/{ => algo}/actor.ts | 14 +-- src/state/models/feeds/algo/algo-item.ts | 56 +++++++++++ .../feeds/{bookmarked.ts => algo/saved.ts} | 40 +++----- src/state/models/ui/profile.ts | 2 +- src/view/com/algos/AlgoItem.tsx | 93 +++++++++++++------ src/view/com/util/post-embeds/index.tsx | 3 +- src/view/screens/Profile.tsx | 8 +- 7 files changed, 144 insertions(+), 72 deletions(-) rename src/state/models/feeds/{ => algo}/actor.ts (88%) create mode 100644 src/state/models/feeds/algo/algo-item.ts rename src/state/models/feeds/{bookmarked.ts => algo/saved.ts} (65%) diff --git a/src/state/models/feeds/actor.ts b/src/state/models/feeds/algo/actor.ts similarity index 88% rename from src/state/models/feeds/actor.ts rename to src/state/models/feeds/algo/actor.ts index 08b7c2a744..e42df8495c 100644 --- a/src/state/models/feeds/actor.ts +++ b/src/state/models/feeds/algo/actor.ts @@ -1,11 +1,9 @@ import {makeAutoObservable} from 'mobx' -import { - AppBskyFeedDefs as FeedDefs, - AppBskyFeedGetActorFeeds as GetActorFeeds, -} from '@atproto/api' -import {RootStoreModel} from '../root-store' +import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api' +import {RootStoreModel} from '../../root-store' import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' +import {AlgoItemModel} from './algo-item' const PAGE_SIZE = 30 @@ -19,7 +17,7 @@ export class ActorFeedsModel { loadMoreCursor?: string // data - feeds: FeedDefs.GeneratorView[] = [] + feeds: AlgoItemModel[] = [] constructor( public rootStore: RootStoreModel, @@ -116,6 +114,8 @@ export class ActorFeedsModel { _appendAll(res: GetActorFeeds.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor - this.feeds = this.feeds.concat(res.data.feeds) + for (const f of res.data.feeds) { + this.feeds.push(new AlgoItemModel(this.rootStore, f)) + } } } diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts new file mode 100644 index 0000000000..555d1d56d3 --- /dev/null +++ b/src/state/models/feeds/algo/algo-item.ts @@ -0,0 +1,56 @@ +import {AppBskyFeedDefs} from '@atproto/api' +import {makeAutoObservable, makeObservable} from 'mobx' +import {RootStoreModel} from 'state/models/root-store' + +// algoitemmodel implemented in mobx +export class AlgoItemModel { + // data + data: AppBskyFeedDefs.GeneratorView + + constructor( + public rootStore: RootStoreModel, + view: AppBskyFeedDefs.GeneratorView, + ) { + this.data = view + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + set toggleSaved(value: boolean) { + console.log('toggleSaved', this.data.viewer) + if (this.data.viewer) { + this.data.viewer.saved = value + } + } + + async save() { + try { + // runInAction(() => { + this.toggleSaved = true + // }) + const res = await this.rootStore.agent.app.bsky.feed.saveFeed({ + feed: this.data.uri, + }) + } catch (e: any) { + this.rootStore.log.error('Failed to save feed', e) + } + } + + async unsave() { + try { + // runInAction(() => { + this.toggleSaved = false + // }) + const res = await this.rootStore.agent.app.bsky.feed.unsaveFeed({ + feed: this.data.uri, + }) + } catch (e: any) { + this.rootStore.log.error('Failed to unsanve feed', e) + } + } +} diff --git a/src/state/models/feeds/bookmarked.ts b/src/state/models/feeds/algo/saved.ts similarity index 65% rename from src/state/models/feeds/bookmarked.ts rename to src/state/models/feeds/algo/saved.ts index d472f0480f..fabb75ae0f 100644 --- a/src/state/models/feeds/bookmarked.ts +++ b/src/state/models/feeds/algo/saved.ts @@ -1,17 +1,13 @@ import {makeAutoObservable} from 'mobx' -import { - AppBskyFeedGetBookmarkedFeeds as GetBookmarkedFeeds, - // AppBskyFeedBookmarkFeed as bookmarkedFeed, - // AppBskyFeedUnbookmarkFeed as unbookmarkFeed, - AppBskyFeedDefs as FeedDefs, -} from '@atproto/api' -import {RootStoreModel} from '../root-store' +import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api' +import {RootStoreModel} from '../../root-store' import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' +import {AlgoItemModel} from './algo-item' const PAGE_SIZE = 30 -export class BookmarkedFeedsModel { +export class SavedFeedsModel { // state isLoading = false isRefreshing = false @@ -21,7 +17,7 @@ export class BookmarkedFeedsModel { loadMoreCursor?: string // data - feeds: FeedDefs.GeneratorView[] = [] + feeds: AlgoItemModel[] = [] constructor(public rootStore: RootStoreModel) { makeAutoObservable( @@ -68,7 +64,7 @@ export class BookmarkedFeedsModel { } this._xLoading(replace) try { - const res = await this.rootStore.agent.app.bsky.feed.getBookmarkedFeeds({ + const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ limit: PAGE_SIZE, cursor: replace ? undefined : this.loadMoreCursor, }) @@ -83,22 +79,6 @@ export class BookmarkedFeedsModel { } }) - async bookmark(feed: FeedDefs.GeneratorView) { - try { - await this.rootStore.agent.app.bsky.feed.bookmarkFeed({feed: feed.uri}) - } catch (e: any) { - this.rootStore.log.error('Failed to bookmark feed', e) - } - } - - async unbookmark(feed: FeedDefs.GeneratorView) { - try { - await this.rootStore.agent.app.bsky.feed.unbookmarkFeed({feed: feed.uri}) - } catch (e: any) { - this.rootStore.log.error('Failed to unbookmark feed', e) - } - } - // state transitions // = @@ -121,14 +101,16 @@ export class BookmarkedFeedsModel { // helper functions // = - _replaceAll(res: GetBookmarkedFeeds.Response) { + _replaceAll(res: GetSavedFeeds.Response) { this.feeds = [] this._appendAll(res) } - _appendAll(res: GetBookmarkedFeeds.Response) { + _appendAll(res: GetSavedFeeds.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor - this.feeds = this.feeds.concat(res.data.feeds) + for (const f of res.data.feeds) { + this.feeds.push(new AlgoItemModel(f)) + } } } diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index 86199108e2..855955d126 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx' import {RootStoreModel} from '../root-store' import {ProfileModel} from '../content/profile' import {PostsFeedModel} from '../feeds/posts' -import {ActorFeedsModel} from '../feeds/actor' +import {ActorFeedsModel} from '../feeds/algo/actor' import {AppBskyFeedDefs} from '@atproto/api' export enum Sections { diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx index 979518f1d3..987bfd68d1 100644 --- a/src/view/com/algos/AlgoItem.tsx +++ b/src/view/com/algos/AlgoItem.tsx @@ -1,41 +1,62 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Text} from '../util/text/Text' -import {AppBskyFeedDefs} from '@atproto/api' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' 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' -const AlgoItem = ({ - item, - style, -}: { - item: AppBskyFeedDefs.GeneratorView - style?: StyleProp -}) => { - const pal = usePalette('default') - return ( - - - - +const AlgoItem = observer( + ({item, style}: {item: AlgoItemModel; style?: StyleProp}) => { + const pal = usePalette('default') + return ( + + + + + + + + {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.displayName ?? 'Feed name'} - - - {item.description ?? - 'THIS IS A FEED DESCRIPTION, IT WILL TELL YOU WHAT THE FEED IS ABOUT. THIS IS A COOL FEED ABOUT COOL PEOPLE.'} - + + {/* TODO: this feed is like by *3* people UserAvatars and others */} + + + + + + + + + Liked by 3 others + + + + + )} - {isDesktopWeb && ( - - - - )} ) - }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + }, [ + store.me.did, + pal, + currentFeed, + onToggleLiked, + onToggleSaved, + name, + rkey, + ]) return ( @@ -207,10 +221,6 @@ export const CustomFeedScreen = withAuthRequired( ) const styles = StyleSheet.create({ - headerBtns: { - flexDirection: 'row', - gap: 8, - }, header: { flexDirection: 'row', gap: 12, @@ -219,6 +229,11 @@ const styles = StyleSheet.create({ paddingBottom: 16, borderTopWidth: 1, }, + headerBtns: { + flexDirection: 'row', + gap: 8, + marginTop: 10, + }, headerDetails: { paddingHorizontal: 16, paddingBottom: 16, From acea0e074d75ac549abb01dc4ac16573a43ad7fa Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 12:05:32 -0500 Subject: [PATCH 058/170] Tab bar fixes --- src/view/com/pager/FeedsTabBarMobile.tsx | 2 +- src/view/com/pager/TabBar.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index de3f125838..cb910ccb96 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -73,7 +73,7 @@ const styles = StyleSheet.create({ top: 0, flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 18, + paddingLeft: 18, borderBottomWidth: 1, }, tabBarAvi: { diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index fe76a08b65..f6c41ce7ca 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -10,7 +10,7 @@ import {StyleSheet, View, ScrollView} from 'react-native' import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' -import {isDesktopWeb} from 'platform/detection' +import {isDesktopWeb, isWeb} from 'platform/detection' export interface TabBarProps { testID?: string @@ -120,9 +120,9 @@ const styles = isDesktopWeb }) : StyleSheet.create({ outer: { + flex: 1, flexDirection: 'row', paddingLeft: 14, - paddingRight: 24, }, item: { paddingTop: 8, From 571fc37a9920d3b7b13a9eed2c46513036f3a4f4 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 18 May 2023 10:34:34 -0700 Subject: [PATCH 059/170] fix error & empty state when rendering custom feeds on profile --- src/view/screens/Profile.tsx | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index bf312cd06d..5f31c89c9d 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -120,6 +120,7 @@ export const ProfileScreen = withAuthRequired( }, [uiState.showLoadingMoreFooter]) const renderItem = React.useCallback( (item: any) => { + // if section is lists if (uiState.selectedView === Sections.Lists) { if (item === ProfileUiModel.LOADING_ITEM) { return @@ -144,6 +145,32 @@ export const ProfileScreen = withAuthRequired( } else { return } + // if section is custom algorithms + } else if (uiState.selectedView === Sections.CustomAlgorithms) { + if (item === ProfileUiModel.LOADING_ITEM) { + return + } else if (item._reactKey === '__error__') { + return ( + + + + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { + return ( + + ) + } else if (item instanceof CustomFeedModel) { + return + } + // if section is posts or posts & replies } else { if (item === ProfileUiModel.END_ITEM) { return - end of feed - @@ -188,8 +215,6 @@ export const ProfileScreen = withAuthRequired( return ( ) - } else if (item instanceof CustomFeedModel) { - return } } return From f1d2166c2911456fc60c83eb3204e5d823dff475 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 18 May 2023 10:36:16 -0700 Subject: [PATCH 060/170] fix spacing when user has no feeds --- src/view/com/feeds/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 1cb109a432..110a6e8940 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -104,6 +104,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 26, paddingVertical: 18, gap: 18, + marginTop: 8, }, empty: { paddingHorizontal: 18, From 211fce47ce566fb5703be0019ba587150ff043b3 Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Thu, 18 May 2023 11:56:09 -0700 Subject: [PATCH 061/170] Fix /api/waitlist API (#726) * move /waitlist to /api/waitlist where its expected * parse waitlist API request as JSON, duh --- bskyweb/cmd/bskyweb/server.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7c230041e7..0b9f34504c 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "fmt" "io/fs" + "io/ioutil" "net/http" "os" "strings" @@ -132,11 +134,36 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) // Mailmodo - e.POST("/waitlist", func(c echo.Context) error { - email := strings.TrimSpace(c.FormValue("email")) - if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { + e.POST("/api/waitlist", func(c echo.Context) error { + type jsonError struct { + Error string `json:"error"` + } + + // Read the API request. + type apiRequest struct { + Email string `json:"email"` + } + + bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) + payload, err := ioutil.ReadAll(bodyReader) + if err != nil { return err } + var req apiRequest + if err := json.Unmarshal(payload, &req); err != nil { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) + } + + if req.Email == "" { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) + } + + if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, req.Email); err != nil { + log.Errorf("adding email to waitlist failed: %s", err) + return c.JSON(http.StatusBadRequest, jsonError{ + Error: "Storing email in waitlist failed. Please enter a valid email address.", + }) + } return c.JSON(http.StatusOK, map[string]bool{"success": true}) }) From 5537d19e555c39f5f9a0ec16735ea4c3860357c4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 14:39:04 -0500 Subject: [PATCH 062/170] Update saved feeds to use preferences --- src/state/models/feeds/custom-feed.ts | 20 ++------ src/state/models/media/image.ts | 2 +- src/state/models/ui/preferences.ts | 57 ++++++++++++++++----- src/state/models/ui/saved-feeds.ts | 25 ++++----- src/state/models/ui/shell.ts | 2 +- src/view/com/feeds/CustomFeed.tsx | 2 +- src/view/com/util/ViewHeader.tsx | 2 +- src/view/com/util/moderation/ImageHider.tsx | 8 +-- 8 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 5e550ec694..e457d2d1ee 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -38,7 +38,7 @@ export class CustomFeedModel { } get isSaved() { - return this.data.viewer?.saved + return this.rootStore.preferences.savedFeeds.includes(this.uri) } get isLiked() { @@ -49,23 +49,11 @@ export class CustomFeedModel { // = async save() { - await this.rootStore.agent.app.bsky.feed.saveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = true - }) + await this.rootStore.preferences.addSavedFeed(this.uri) } async unsave() { - await this.rootStore.agent.app.bsky.feed.unsaveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = false - }) + await this.rootStore.preferences.removeSavedFeed(this.uri) } async like() { @@ -82,7 +70,7 @@ export class CustomFeedModel { } async unlike() { - if (!this.data.viewer.like) { + if (!this.data.viewer?.like) { return } try { diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index ec93bf5b65..6edf88d9df 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -135,7 +135,7 @@ export class ImageModel implements RNImage { // Only for mobile async crop() { try { - const cropped = await openCropper({ + const cropped = await openCropper(this.rootStore, { mediaType: 'photo', path: this.path, freeStyleCropEnabled: true, diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 05a1eb128f..120b4adcc8 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -46,6 +46,7 @@ export class PreferencesModel { contentLanguages: string[] = deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() + savedFeeds: string[] = [] pinnedFeeds: string[] = [] constructor(public rootStore: RootStoreModel) { @@ -56,6 +57,7 @@ export class PreferencesModel { return { contentLanguages: this.contentLanguages, contentLabels: this.contentLabels, + savedFeeds: this.savedFeeds, pinnedFeeds: this.pinnedFeeds, } } @@ -75,6 +77,13 @@ export class PreferencesModel { // default to the device languages this.contentLanguages = deviceLocales.map(locale => locale.languageCode) } + if ( + hasProp(v, 'savedFeeds') && + Array.isArray(v.savedFeeds) && + typeof v.savedFeeds.every(item => typeof item === 'string') + ) { + this.savedFeeds = v.savedFeeds + } if ( hasProp(v, 'pinnedFeeds') && Array.isArray(v.pinnedFeeds) && @@ -106,10 +115,11 @@ export class PreferencesModel { pref.visibility as LabelPreference } } else if ( - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success ) { - this.pinnedFeeds = pref.feeds + this.savedFeeds = pref.saved + this.pinnedFeeds = pref.pinned } } }) @@ -220,38 +230,57 @@ export class PreferencesModel { return res } - async setPinnedFeeds(v: string[]) { - const old = this.pinnedFeeds - this.pinnedFeeds = v + async setSavedFeeds(saved: string[], pinned: string[]) { + const oldSaved = this.savedFeeds + const oldPinned = this.pinnedFeeds + this.savedFeeds = saved + this.pinnedFeeds = pinned try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( pref => - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success, + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success, ) if (existing) { - existing.feeds = v + existing.saved = saved + existing.pinned = pinned } else { prefs.push({ - $type: 'app.bsky.actor.defs#pinnedFeedsPref', - feeds: v, + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved, + pinned, }) } }) } catch (e) { runInAction(() => { - this.pinnedFeeds = old + this.savedFeeds = oldSaved + this.pinnedFeeds = oldPinned }) throw e } } + async addSavedFeed(v: string) { + return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds) + } + + async removeSavedFeed(v: string) { + return this.setSavedFeeds( + this.savedFeeds.filter(uri => uri !== v), + this.pinnedFeeds.filter(uri => uri !== v), + ) + } + async addPinnedFeed(v: string) { - return this.setPinnedFeeds([...this.pinnedFeeds, v]) + return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v]) } async removePinnedFeed(v: string) { - return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v)) + return this.setSavedFeeds( + this.savedFeeds, + this.pinnedFeeds.filter(uri => uri !== v), + ) } } diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index f500aef2e4..9de28e0284 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -5,8 +5,6 @@ import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' import {CustomFeedModel} from '../feeds/custom-feed' -const PAGE_SIZE = 100 - export class SavedFeedsModel { // state isLoading = false @@ -69,16 +67,15 @@ export class SavedFeedsModel { try { let feeds: AppBskyFeedDefs.GeneratorView[] = [] let cursor - for (let i = 0; i < 100; i++) { - const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ - limit: PAGE_SIZE, - cursor, + for ( + let i = 0; + i < this.rootStore.preferences.savedFeeds.length; + i += 25 + ) { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({ + feeds: this.rootStore.preferences.savedFeeds.slice(i, 25), }) feeds = feeds.concat(res.data.feeds) - cursor = res.data.cursor - if (!cursor) { - break - } } runInAction(() => { this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f)) @@ -127,7 +124,8 @@ export class SavedFeedsModel { } async reorderPinnedFeeds(feeds: CustomFeedModel[]) { - return this.rootStore.preferences.setPinnedFeeds( + return this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri), ) } @@ -151,7 +149,10 @@ export class SavedFeedsModel { pinned[index] = pinned[index + 1] pinned[index + 1] = temp } - await this.rootStore.preferences.setPinnedFeeds(pinned) + await this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, + pinned, + ) } // state transitions diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 9b9a176bed..95b6662433 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -119,7 +119,7 @@ export type Modal = // Moderation | ReportAccountModal | ReportPostModal - | CreateMuteListModal + | CreateOrEditMuteListModal | ListAddRemoveUserModal // Posts diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index d4e843b677..9a71eb8468 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -40,7 +40,7 @@ export const CustomFeed = observer( const navigation = useNavigation() const onToggleSaved = React.useCallback(async () => { - if (item.data.viewer?.saved) { + if (item.isSaved) { store.shell.openModal({ name: 'confirm', title: 'Remove from my feeds', diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 7f13f18383..c17a65b140 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -121,7 +121,7 @@ const Container = observer( }: { children: React.ReactNode hideOnScroll: boolean - showBorder: boolean + showBorder?: boolean }) => { const store = useStores() const pal = usePalette('default') diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx index b42c6397da..40add5b67b 100644 --- a/src/view/com/util/moderation/ImageHider.tsx +++ b/src/view/com/util/moderation/ImageHider.tsx @@ -27,6 +27,10 @@ export function ImageHider({ setOverride(false) }, [setOverride]) + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null + } + if (moderation.behavior !== ModerationBehaviorCode.WarnImages) { return ( @@ -35,10 +39,6 @@ export function ImageHider({ ) } - if (moderation.behavior === ModerationBehaviorCode.Hide) { - return null - } - return ( From 2f4408582bf27a83ba8d22605077d067f8433d7c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 15:06:32 -0500 Subject: [PATCH 063/170] Set default feeds --- src/lib/constants.ts | 43 ++++++++++++++++++++++++++++++ src/state/models/ui/preferences.ts | 34 +++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6d0d4797b2..88e429d83f 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 120b4adcc8..c85faf6588 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, }) From 84990c509e9feb0cd44921a318aedcbad92b1da7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 15:12:18 -0500 Subject: [PATCH 064/170] Drop the hard-coded what's hot algo --- src/state/models/feeds/posts.ts | 56 +------------------ .../com/modals/ContentLanguagesSettings.tsx | 4 +- src/view/com/pager/FeedsTabBar.web.tsx | 7 +-- src/view/com/pager/FeedsTabBarMobile.tsx | 7 +-- ...mptyState.tsx => CustomFeedEmptyState.tsx} | 27 ++++++++- src/view/screens/Home.tsx | 35 ++---------- 6 files changed, 36 insertions(+), 100 deletions(-) rename src/view/com/posts/{WhatsHotEmptyState.tsx => CustomFeedEmptyState.tsx} (69%) diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index dfd92b35c3..5a5b287856 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -310,7 +310,7 @@ export class PostsFeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested' | 'goodstuff' | 'custom', + public feedType: 'home' | 'author' | 'suggested' | 'custom', params: | GetTimeline.QueryParams | GetAuthorFeed.QueryParams @@ -391,10 +391,9 @@ export class PostsFeedModel { } get feedTuners() { - if (this.feedType === 'goodstuff') { + if (this.feedType === 'custom') { return [ FeedTuner.dedupReposts, - FeedTuner.likedRepliesOnly, FeedTuner.preferredLangOnly( this.rootStore.preferences.contentLanguages, ), @@ -701,15 +700,6 @@ export class PostsFeedModel { 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 || '', - params as GetTimeline.QueryParams, - ) - res.data.feed = (res.data.feed || []).filter( - item => !item.post.author.viewer?.muted, - ) - return res } else { return this.rootStore.agent.getAuthorFeed( params as GetAuthorFeed.QueryParams, @@ -717,45 +707,3 @@ export class PostsFeedModel { } } } - -// HACK -// temporary off-spec route to get the good stuff -// -prf -async function getGoodStuff( - accessJwt: string, - params: GetTimeline.QueryParams, -): Promise { - const controller = new AbortController() - const to = setTimeout(() => controller.abort(), 15e3) - - const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular') - let k: keyof GetTimeline.QueryParams - for (k in params) { - if (typeof params[k] !== 'undefined') { - uri.searchParams.set(k, String(params[k])) - } - } - - const res = await fetch(String(uri), { - method: 'get', - headers: { - accept: 'application/json', - authorization: `Bearer ${accessJwt}`, - }, - signal: controller.signal, - }) - - const resHeaders: Record = {} - res.headers.forEach((value: string, key: string) => { - resHeaders[key] = value - }) - let resBody = await res.json() - - clearTimeout(to) - - return { - success: res.status === 200, - headers: resHeaders, - data: jsonToLex(resBody), - } -} diff --git a/src/view/com/modals/ContentLanguagesSettings.tsx b/src/view/com/modals/ContentLanguagesSettings.tsx index 0c750fe0ea..700f1cbcb3 100644 --- a/src/view/com/modals/ContentLanguagesSettings.tsx +++ b/src/view/com/modals/ContentLanguagesSettings.tsx @@ -41,8 +41,8 @@ export function Component({}: {}) { Content Languages - Which languages would you like to see in the What's Hot feed? (Leave - them all unchecked to see any language.) + Which languages would you like to see in the your feed? (Leave them all + unchecked to see any language.) {languages.map(lang => ( diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 56ca6f2a1d..78937611b5 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -28,12 +28,7 @@ const FeedsTabBarDesktop = observer( ) => { const store = useStores() const items = useMemo( - () => [ - 'Following', - "What's hot", - ...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 cb910ccb96..a41f0ef327 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -33,12 +33,7 @@ export const FeedsTabBar = observer( }, [store]) const items = useMemo( - () => [ - 'Following', - "What's hot", - ...store.me.savedFeeds.pinnedFeedNames, - 'My feeds', - ], + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames, 'My feeds'], [store.me.savedFeeds.pinnedFeedNames], ) diff --git a/src/view/com/posts/WhatsHotEmptyState.tsx b/src/view/com/posts/CustomFeedEmptyState.tsx similarity index 69% rename from src/view/com/posts/WhatsHotEmptyState.tsx rename to src/view/com/posts/CustomFeedEmptyState.tsx index ade94ca3f3..69dd799022 100644 --- a/src/view/com/posts/WhatsHotEmptyState.tsx +++ b/src/view/com/posts/CustomFeedEmptyState.tsx @@ -1,5 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' +import {useNavigation} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -7,14 +8,21 @@ import { import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {MagnifyingGlassIcon} from 'lib/icons' +import {NavigationProp} from 'lib/routes/types' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' -export function WhatsHotEmptyState() { +export function CustomFeedEmptyState() { const pal = usePalette('default') const palInverted = usePalette('inverted') const store = useStores() + const navigation = useNavigation() + + const onPressFindAccounts = React.useCallback(() => { + navigation.navigate('SearchTab') + navigation.popToTop() + }, [navigation]) const onPressSettings = React.useCallback(() => { store.shell.openModal({name: 'content-languages-settings'}) @@ -26,9 +34,22 @@ export function WhatsHotEmptyState() { - Your What's Hot feed is empty! This is because there aren't enough users - posting in your selected language. + This feed is empty! You may need to follow more users or tune your + language settings. + + )} @@ -202,6 +232,7 @@ export const CustomFeedScreen = withAuthRequired( currentFeed, onToggleLiked, onToggleSaved, + onPressShare, name, rkey, ]) From 3c89dd40f90deda00dd4d717bec0bb2f4217c1d1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 16:54:17 -0500 Subject: [PATCH 066/170] Fix lint --- src/state/models/feeds/posts.ts | 1 - src/state/models/ui/saved-feeds.ts | 1 - src/view/com/pager/TabBar.tsx | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 5a5b287856..ac32044b46 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -6,7 +6,6 @@ import { AppBskyFeedGetAuthorFeed as GetAuthorFeed, AppBskyFeedGetFeed as GetCustomFeed, RichText, - jsonToLex, } from '@atproto/api' import AwaitLock from 'await-lock' import {bundleAsync} from 'lib/async/bundle' diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 9de28e0284..0d04f9c8d0 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -66,7 +66,6 @@ export class SavedFeedsModel { this._xLoading(!quietRefresh) try { let feeds: AppBskyFeedDefs.GeneratorView[] = [] - let cursor for ( let i = 0; i < this.rootStore.preferences.savedFeeds.length; diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index f6c41ce7ca..4852197306 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -10,7 +10,7 @@ import {StyleSheet, View, ScrollView} from 'react-native' import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' -import {isDesktopWeb, isWeb} from 'platform/detection' +import {isDesktopWeb} from 'platform/detection' export interface TabBarProps { testID?: string From ad8778ab10d853b0de59aa25355fdf61ea6f2b57 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 16:55:50 -0500 Subject: [PATCH 067/170] Add server-side routes --- bskyweb/cmd/bskyweb/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7c230041e7..07df85146f 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -112,6 +112,7 @@ func serve(cctx *cli.Context) error { e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) @@ -125,6 +126,8 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/follows", server.WebGeneric) e.GET("/profile/:handle/followers", server.WebGeneric) e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) // post endpoints; only first populates info e.GET("/profile/:handle/post/:rkey", server.WebPost) From 324c9209dc5777dcf3019926fb5847f6073fd2e4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:01:48 -0500 Subject: [PATCH 068/170] Only show algos and lists on profiles if there are items --- src/state/models/ui/profile.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index 54ee461b08..35831d1f77 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -12,13 +12,6 @@ export enum Sections { Lists = 'Lists', } -const USER_SELECTOR_ITEMS = [ - Sections.Posts, - Sections.PostsWithReplies, - Sections.CustomAlgorithms, - Sections.Lists, -] - export interface ProfileUiParams { user: string } @@ -83,7 +76,14 @@ export class ProfileUiModel { } get selectorItems() { - return USER_SELECTOR_ITEMS + const items = [Sections.Posts, Sections.PostsWithReplies] + if (this.algos.hasLoaded && !this.algos.isEmpty) { + items.push(Sections.CustomAlgorithms) + } + if (this.lists.hasLoaded && !this.lists.isEmpty) { + items.push(Sections.Lists) + } + return items } get selectedView() { @@ -166,6 +166,7 @@ export class ProfileUiModel { .setup() .catch(err => this.rootStore.log.error('Failed to fetch feed', err)), ]) + this.algos.refresh() // HACK: need to use the DID as a param, not the username -prf this.lists.source = this.profile.did this.lists From 46ed910cdaeaa675b858ccdad3d64425f8e63031 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:10:42 -0500 Subject: [PATCH 069/170] Add list-type avatar --- src/view/com/lists/ListCard.tsx | 2 +- src/view/com/lists/ListItems.tsx | 2 +- src/view/com/modals/CreateOrEditMuteList.tsx | 1 + src/view/com/util/UserAvatar.tsx | 29 ++++++++++++++++++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index 7cbdaaf648..0e13ca3335 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -60,7 +60,7 @@ export const ListCard = ({ anchorNoUnderline> - + - + diff --git a/src/view/com/modals/CreateOrEditMuteList.tsx b/src/view/com/modals/CreateOrEditMuteList.tsx index 0c13f243aa..736deae74b 100644 --- a/src/view/com/modals/CreateOrEditMuteList.tsx +++ b/src/view/com/modals/CreateOrEditMuteList.tsx @@ -143,6 +143,7 @@ export function Component({ List Avatar ) } + if (type === 'list') { + // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. + return ( + + + + + + ) + } return ( { - if (type === 'algo') { + if (type === 'algo' || type === 'list') { return { width: size, height: size, From 4fa4c67cc5ef856a9548bb4154f82aebf447e91d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 17:36:16 -0500 Subject: [PATCH 070/170] Some fixes --- src/view/com/posts/CustomFeedEmptyState.tsx | 16 ---------------- src/view/screens/Home.tsx | 1 - 2 files changed, 17 deletions(-) diff --git a/src/view/com/posts/CustomFeedEmptyState.tsx b/src/view/com/posts/CustomFeedEmptyState.tsx index 69dd799022..e51794e7ce 100644 --- a/src/view/com/posts/CustomFeedEmptyState.tsx +++ b/src/view/com/posts/CustomFeedEmptyState.tsx @@ -9,14 +9,12 @@ import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {MagnifyingGlassIcon} from 'lib/icons' import {NavigationProp} from 'lib/routes/types' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' export function CustomFeedEmptyState() { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() const navigation = useNavigation() const onPressFindAccounts = React.useCallback(() => { @@ -24,10 +22,6 @@ export function CustomFeedEmptyState() { navigation.popToTop() }, [navigation]) - const onPressSettings = React.useCallback(() => { - store.shell.openModal({name: 'content-languages-settings'}) - }, [store]) - return ( @@ -50,16 +44,6 @@ export function CustomFeedEmptyState() { size={14} /> - ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index d761994f33..f8a497028e 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -53,7 +53,6 @@ export const HomeScreen = withAuthRequired( model.setup() feeds.push(model) } - pagerRef.current?.setPage(0) setCustomFeeds(feeds) }, [store, store.me.savedFeeds.pinned, customFeeds, setCustomFeeds]) From da6fa2dede7e1fd14023623d33ebe0be5187193f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:04:32 -0500 Subject: [PATCH 071/170] Bump deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c8eb64997f..81cb54937d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.3", + "@atproto/api": "0.3.6", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -141,7 +141,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.8", + "@atproto/pds": "^0.1.9", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", From 0b8dd95f2a653ba13ef84ad0e0940f112cb3ae91 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:22:34 -0500 Subject: [PATCH 072/170] Bump deps again --- package.json | 4 ++-- yarn.lock | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 81cb54937d..e589f22d20 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.6", + "@atproto/api": "0.3.7", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", @@ -141,7 +141,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.1.9", + "@atproto/pds": "^0.1.10", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", diff --git a/yarn.lock b/yarn.lock index 81772e67ca..d42660edae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,7 +29,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@*", "@atproto/api@0.3.3": +"@atproto/api@*": version "0.3.3" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.3.tgz#8c8d41567beb7b37217f76d2aacf2c280e9fd07e" integrity sha512-BlgpYbdPO0KSBypg2KgqHM0kS2Pk82P3X0w2rJs/vrdcMl72d2WeI9kQ5PPFiz80p6C6XcLcpnzzKKtQeFvh4A== @@ -40,6 +40,17 @@ tlds "^1.234.0" typed-emitter "^2.1.0" +"@atproto/api@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.7.tgz#5cc4b0ccc5c6690eb0e5a3ae138a84ce20697e2f" + integrity sha512-JHN3rHNGro4AaJWU64hsmpTUzd2+FbfMBiDkqyBmoKtj972ueBJeH8tz6WdnPcsIRfCj1kRthKFj2yJwgt6aSQ== + dependencies: + "@atproto/common-web" "*" + "@atproto/uri" "*" + "@atproto/xrpc" "*" + tlds "^1.234.0" + typed-emitter "^2.1.0" + "@atproto/common-web@*": version "0.1.0" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d" @@ -137,10 +148,10 @@ resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4" integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw== -"@atproto/pds@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.8.tgz#cf1a9bab2301c3fe1120c63576153ac5a20bf70d" - integrity sha512-I493U+/NNU9D8L8tVbM/OpD6gQ6/Mv7uE+/i4a1vfBGO6NqYJ6jKw3qeCy4jq3NVbTxcs+lSSpK27hgApx4PtA== +"@atproto/pds@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.10.tgz#cde4a06982ec0ba7166e978afed78f58abc5c6b1" + integrity sha512-Yxnpv3mQNrIcR5GFPUUoffSSDpZHzXXHuk36wtPXm7dDSg+ACgtILPcDSpkjr27JwE5OcfgD2UbQwXt7az7OLA== dependencies: "@atproto/api" "*" "@atproto/common" "*" From 37acc9e9304b594ff21443e1be896e1e576bb488 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 18:22:46 -0500 Subject: [PATCH 073/170] 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(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 88e429d83f..f4c6f5021a 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 110a6e8940..e92e741dac 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 78937611b5..fc04c3b2cc 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 a41f0ef327..5954e7f2e4 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], ) From 2a5ac1a6de6315a2e7d9af8c86d132667e3fec4c Mon Sep 17 00:00:00 2001 From: renahlee Date: Thu, 18 May 2023 17:29:46 -0700 Subject: [PATCH 074/170] Update labels for avatar --- src/view/com/pager/FeedsTabBarMobile.tsx | 4 ++-- src/view/com/search/HeaderWithInput.tsx | 4 ++-- src/view/com/util/ViewHeader.tsx | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 725c44603e..b42ffe7269 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -39,8 +39,8 @@ export const FeedsTabBar = observer( style={styles.tabBarAvi} onPress={onPressAvi} accessibilityRole="button" - accessibilityLabel="Open navigation" - accessibilityHint="Access profile and other navigation links"> + accessibilityLabel="Menu" + accessibilityHint="Access navigation links and settings"> + accessibilityLabel="Menu" + accessibilityHint="Access navigation links and settings"> + accessibilityHint={ + canGoBack ? '' : 'Access navigation links and settings' + }> {canGoBack ? ( Date: Thu, 18 May 2023 21:06:00 -0500 Subject: [PATCH 075/170] 1.29 testflight --- app.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index 8a2fe039a9..25290e20d3 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "slug": "bluesky", "scheme": "bluesky", "owner": "blueskysocial", - "version": "1.28.0", + "version": "1.29.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -14,7 +14,7 @@ "backgroundColor": "#ffffff" }, "ios": { - "buildNumber": "3", + "buildNumber": "1", "supportsTablet": false, "bundleIdentifier": "xyz.blueskyweb.app", "config": { diff --git a/package.json b/package.json index e589f22d20..5c30a0cae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.28.0", + "version": "1.29.0", "private": true, "scripts": { "postinstall": "patch-package", From 48a9e1b1dd98aa6f730c7d3ad88d393508f836e4 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:27:13 -0700 Subject: [PATCH 076/170] fix refresh control color in ViewSelector.tsx --- src/view/com/util/ViewSelector.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index f9ef0945d4..c44d372f54 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react' -import {Pressable, StyleSheet, View} from 'react-native' +import {Pressable, RefreshControl, StyleSheet, View} from 'react-native' import {FlatList} from './Views' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' @@ -41,6 +41,7 @@ export function ViewSelector({ onRefresh?: () => void onEndReached?: (info: {distanceFromEnd: number}) => void }) { + const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) // events @@ -97,6 +98,13 @@ export function ViewSelector({ onScroll={onScroll} onRefresh={onRefresh} onEndReached={onEndReached} + refreshControl={ + + } onEndReachedThreshold={0.6} contentContainerStyle={s.contentContainer} removeClippedSubviews={true} From 8bcbbb869af22e482cceaaf6c754c5c126a7a1a6 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:30:24 -0700 Subject: [PATCH 077/170] fix dark mode color for creator handle on CustomFeed screen --- src/view/screens/CustomFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index d2b9041f9f..7ff22f7f39 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -146,6 +146,7 @@ export const CustomFeedScreen = withAuthRequired( )} From 7cad7d12f1b6d97ae3395a2b3ce6ad0c102aea56 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Fri, 19 May 2023 18:32:21 -0700 Subject: [PATCH 078/170] add refreshControl to tab ViewSelector --- src/view/com/util/ViewSelector.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index c44d372f54..5b671d06cb 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -94,9 +94,7 @@ export function ViewSelector({ ListFooterComponent={ListFooterComponent} // NOTE sticky header disabled on android due to major performance issues -prf stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} - refreshing={refreshing} onScroll={onScroll} - onRefresh={onRefresh} onEndReached={onEndReached} refreshControl={ Date: Mon, 22 May 2023 16:12:05 -0700 Subject: [PATCH 079/170] fix prod default feeds not working --- src/lib/constants.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f4c6f5021a..e492dd61a0 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'), ], } } From 64e303d911d351a2f492a23ae97207e5c6035b6e Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 16:35:37 -0700 Subject: [PATCH 080/170] 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(-) diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts index 3c8e3e8f9e..43383b61e8 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 e457d2d1ee..9ac69ac28b 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) } From dfcdd37087c0be4055c92a0f88431b32646ced6f Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 18:46:36 -0700 Subject: [PATCH 081/170] 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 diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts new file mode 100644 index 0000000000..23a3217965 --- /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 9980e9de07..0d2f83ce7d 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 7ff22f7f39..353995540d 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 613e42fbf4..0213b36a9c 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 => { From 512c918c033abf974260ea6a87a85ef14cdabe38 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 19:10:03 -0700 Subject: [PATCH 082/170] decrease long press time required to reoreder pinned feed --- src/view/screens/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 0213b36a9c..4a060c2fd6 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -159,6 +159,7 @@ const ListItem = observer( {isPinned && isWeb ? ( From 8a2349c55ffcff4f833c015f9b10296aa9d77738 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 19:14:10 -0700 Subject: [PATCH 083/170] increase pin button hitslop --- src/view/screens/SavedFeeds.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 4a060c2fd6..2f9165b374 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -198,6 +198,7 @@ const ListItem = observer( /> Date: Mon, 22 May 2023 20:07:40 -0700 Subject: [PATCH 084/170] update pinned feed from custom feed view --- src/state/models/ui/saved-feeds.ts | 10 ++++-- src/view/screens/CustomFeed.tsx | 52 +++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 0d04f9c8d0..244e758984 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -129,8 +129,14 @@ export class SavedFeedsModel { ) } - isPinned(feed: CustomFeedModel) { - return this.rootStore.preferences.pinnedFeeds.includes(feed.uri) + isPinned(feedOrUri: CustomFeedModel | string) { + let uri: string + if (typeof feedOrUri === 'string') { + uri = feedOrUri + } else { + uri = feedOrUri.uri + } + return this.rootStore.preferences.pinnedFeeds.includes(uri) } async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') { diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 353995540d..952461c9c8 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -24,7 +24,7 @@ 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 {Haptics} from 'lib/haptics' const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} @@ -47,6 +47,7 @@ export const CustomFeedScreen = withAuthRequired( feed.setup() return feed }, [store, uri]) + const isPinned = store.me.savedFeeds.isPinned(uri) useSetTitle(currentFeed?.displayName) @@ -65,7 +66,6 @@ export const CustomFeedScreen = withAuthRequired( store.log.error('Failed up update feeds', {err}) } }, [store, currentFeed]) - const onToggleLiked = React.useCallback(async () => { Haptics.default() try { @@ -81,6 +81,13 @@ 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 => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }) + }, [store, currentFeed]) const onPressShare = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/feed/${rkey}`) shareUrl(url) @@ -212,15 +219,30 @@ export const CustomFeedScreen = withAuthRequired( {currentFeed.data.description} ) : null} - + + + + ) @@ -275,6 +298,11 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingBottom: 16, }, + headerDetailsFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, fakeSelector: { flexDirection: 'row', paddingHorizontal: isDesktopWeb ? 16 : 6, From b561a51ed9f798194c3c6a72eefab562a773f2c9 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 14:18:35 -0700 Subject: [PATCH 085/170] add button to reset preferences in dev mode --- src/state/models/ui/preferences.ts | 32 ++++++++++++++++++++++++++++++ src/view/screens/Settings.tsx | 15 ++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index c85faf6588..c4b6da0f69 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -63,6 +63,11 @@ export class PreferencesModel { } } + /** + * The function hydrates an object with properties related to content languages, labels, saved feeds, + * and pinned feeds that it gets from the parameter `v` (probably local storage) + * @param {unknown} v - the data object to hydrate from + */ hydrate(v: unknown) { if (isObj(v)) { if ( @@ -95,6 +100,9 @@ export class PreferencesModel { } } + /** + * This function fetches preferences and sets defaults for missing items. + */ async sync() { // fetch preferences let hasSavedFeedsPref = false @@ -153,6 +161,15 @@ export class PreferencesModel { } } + /** + * This function updates the preferences of a user and allows for a callback function to be executed + * before the update. + * @param cb - cb is a callback function that takes in a single parameter of type + * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to + * update the preferences of the user. The function is called with the current preferences as an + * argument and if the callback returns false, the preferences are not updated. + * @returns void + */ async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) { const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) if (cb(res.data.preferences) === false) { @@ -163,6 +180,21 @@ export class PreferencesModel { }) } + /** + * This function resets the preferences to an empty array of no preferences. + */ + async reset() { + runInAction(() => { + this.contentLabels = new LabelPreferencesModel() + this.contentLanguages = deviceLocales.map(locale => locale.languageCode) + this.savedFeeds = [] + this.pinnedFeeds = [] + }) + await this.rootStore.agent.app.bsky.actor.putPreferences({ + preferences: [], + }) + } + hasContentLanguage(code2: string) { return this.contentLanguages.includes(code2) } diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 3ce41f8c07..ac4e5a9e0b 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -141,6 +141,11 @@ export const SettingsScreen = withAuthRequired( store.shell.openModal({name: 'delete-account'}) }, [store]) + const onPressResetPreferences = React.useCallback(async () => { + await store.preferences.reset() + Toast.show('Preferences reset') + }, [store]) + return ( @@ -393,6 +398,16 @@ export const SettingsScreen = withAuthRequired( Storybook + {__DEV__ ? ( + + + Reset preferences state + + + ) : null} Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) From fc9e28ca72ce498df8d0902c8e51d226affefd83 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:28:46 -0700 Subject: [PATCH 086/170] slight performance improvements --- src/state/models/log.ts | 16 ++++++++++++++++ src/state/models/ui/preferences.ts | 8 ++++++-- src/state/models/ui/saved-feeds.ts | 4 ++++ src/view/screens/SavedFeeds.tsx | 9 +++++++-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/state/models/log.ts b/src/state/models/log.ts index d80617139f..7c9c37c0d9 100644 --- a/src/state/models/log.ts +++ b/src/state/models/log.ts @@ -27,6 +27,7 @@ function genId(): string { export class LogModel { entries: LogEntry[] = [] + timers = new Map() constructor() { makeAutoObservable(this) @@ -74,6 +75,21 @@ export class LogModel { ts: Date.now(), }) } + + time = (label = 'default') => { + this.timers.set(label, performance.now()) + } + + timeEnd = (label = 'default', warn = false) => { + const endTime = performance.now() + if (this.timers.has(label)) { + const elapsedTime = endTime - this.timers.get(label)! + console.log(`${label}: ${elapsedTime.toFixed(3)}ms`) + this.timers.delete(label) + } else { + warn && console.warn(`Timer with label '${label}' does not exist.`) + } + } } function detailsToStr(details?: any) { diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index c4b6da0f69..dcf6b9a7ad 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -292,11 +292,15 @@ export class PreferencesModel { return res } + setFeeds(saved: string[], pinned: string[]) { + this.savedFeeds = saved + this.pinnedFeeds = pinned + } + async setSavedFeeds(saved: string[], pinned: string[]) { const oldSaved = this.savedFeeds const oldPinned = this.pinnedFeeds - this.savedFeeds = saved - this.pinnedFeeds = pinned + this.setFeeds(saved, pinned) try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 244e758984..979fddf497 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -47,6 +47,10 @@ export class SavedFeedsModel { return this.feeds.filter(f => !this.isPinned(f)) } + get all() { + return this.pinned.concat(this.unpinned) + } + get pinnedFeedNames() { return this.pinned.map(f => f.displayName) } diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 2f9165b374..e305e63056 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -99,7 +99,7 @@ export const SavedFeeds = withAuthRequired( /> item.data.uri} refreshing={savedFeeds.isRefreshing} refreshControl={ @@ -111,6 +111,11 @@ export const SavedFeeds = withAuthRequired( /> } renderItem={({item, drag}) => } + getItemLayout={(data, index) => ({ + length: 77, + offset: 77 * index, + index, + })} initialNumToRender={10} ListFooterComponent={renderListFooterComponent} ListEmptyComponent={renderListEmptyComponent} @@ -198,7 +203,7 @@ const ListItem = observer( /> Date: Tue, 23 May 2023 15:33:27 -0700 Subject: [PATCH 087/170] refactor load latest btn --- src/view/com/util/load-latest/LoadLatestBtn.web.tsx | 4 ++-- src/view/com/util/load-latest/LoadLatestBtnMobile.tsx | 6 +++--- src/view/screens/Home.tsx | 2 +- src/view/screens/Notifications.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index 839685029d..85fb5a0144 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -25,11 +25,11 @@ export const LoadLatestBtn = ({ onPress={onPress} hitSlop={HITSLOP} accessibilityRole="button" - accessibilityLabel={`Load new ${label}`} + accessibilityLabel={label} accessibilityHint=""> - Load new {label} + {label} ) diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 5279696a27..548d30d5a9 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -25,15 +25,15 @@ export const LoadLatestBtn = observer( onPress={onPress} hitSlop={HITSLOP} accessibilityRole="button" - accessibilityLabel={`Load new ${label}`} - accessibilityHint={`Loads new ${label}`}> + accessibilityLabel={label} + accessibilityHint={label}> - Load new {label} + {label} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index f8a497028e..4fe175fc1e 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -258,7 +258,7 @@ const FeedPage = observer( headerOffset={HEADER_OFFSET} /> {feed.hasNewLatest && !feed.isRefreshing && ( - + )} {store.me.notifications.hasNewLatest && !store.me.notifications.isRefreshing && ( - + )} ) From 858ec6438da9cc9bee765857ea925f77e074fde2 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:48:14 -0700 Subject: [PATCH 088/170] 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(-) diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 41b35dd4f6..994a357141 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 5b0110df83..50398e7065 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 952461c9c8..2316d7f060 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} ) }), From 58a0489ce3028c6e6d205557e3edcf03c85f6626 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 13:59:53 -0700 Subject: [PATCH 089/170] 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(-) diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index 23a3217965..516940c1ce 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 0d2f83ce7d..41d66641f4 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 14:18:49 -0700 Subject: [PATCH 090/170] fix scrollToTop for web --- src/view/com/posts/Feed.tsx | 3 ++ src/view/screens/CustomFeed.tsx | 51 ++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 50398e7065..2726ff7d35 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -18,6 +18,7 @@ import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -54,6 +55,7 @@ export const Feed = observer(function Feed({ extraData?: any }) { const pal = usePalette('default') + const theme = useTheme() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) @@ -186,6 +188,7 @@ export const Feed = observer(function Feed({ onScroll={onScroll} scrollEventThrottle={scrollEventThrottle} onMomentumScrollEnd={onMomentumScrollEnd} + indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={0.6} removeClippedSubviews={true} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 2316d7f060..dcb726873c 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -20,15 +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} from 'platform/detection' +import {isDesktopWeb, isWeb} 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 { onMomentumScrollEndCb } from 'lib/hooks/useOnMainScroll' - -const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {OnScrollCb, onMomentumScrollEndCb} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -257,22 +255,37 @@ export const CustomFeedScreen = withAuthRequired( ) }, [ - store.me.did, pal, currentFeed, - onToggleLiked, + store.me.did, onToggleSaved, + onToggleLiked, onPressShare, name, rkey, isPinned, + onTogglePinned, ]) - const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback((event) => { - if (event.nativeEvent.contentOffset.y > 200) { - setAllowScrollToTop(true) - } else { - setAllowScrollToTop(false) + const onMomentumScrollEnd: onMomentumScrollEndCb = React.useCallback( + event => { + console.log('onMomentumScrollEnd') + if (event.nativeEvent.contentOffset.y > s.window.height * 3) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } + }, + [], + ) + const onScroll: OnScrollCb = React.useCallback(event => { + // since onMomentumScrollEnd is not supported in react-native-web, we have to use onScroll which fires more often so is not desirable on mobile + if (isWeb) { + if (event.nativeEvent.contentOffset.y > s.window.height * 2) { + setAllowScrollToTop(true) + } else { + setAllowScrollToTop(false) + } } }, []) @@ -283,14 +296,18 @@ export const CustomFeedScreen = withAuthRequired( scrollElRef={scrollElRef} feed={algoFeed} onMomentumScrollEnd={onMomentumScrollEnd} + onScroll={onScroll} // same logic as onMomentumScrollEnd but for web ListHeaderComponent={renderListHeaderComponent} extraData={[uri, isPinned]} /> - {allowScrollToTop ? { - scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) - }} - label='Scroll to top' - /> : null} + {allowScrollToTop ? ( + { + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) + }} + label="Scroll to top" + /> + ) : null} ) }), From 04468eb1b7cccc6dcee127c7a89d001fa2ea352b Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 14:59:42 -0700 Subject: [PATCH 091/170] make prettier and eslint work together --- .eslintrc.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2d59d36dd5..19fcf23083 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { root: true, - extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], + extends: [ + '@react-native-community', + 'plugin:react-native-a11y/ios', + 'prettier', + ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ From 7e555ecc1b04fed96192d3c68b87cf679993abfa Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 15:00:36 -0700 Subject: [PATCH 092/170] fix lint errors --- src/lib/constants.ts | 9 ++++++--- src/view/screens/Notifications.tsx | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e492dd61a0..c42e6f3a93 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 df84b541ba..67507d009e 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 && ( - + )} ) From 32c9dabb7467149baf39d8f5c2eb3d0b81236d92 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Wed, 24 May 2023 15:04:30 -0700 Subject: [PATCH 093/170] 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 diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts new file mode 100644 index 0000000000..b0f7465d79 --- /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 0000000000..4617b5260d --- /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 0000000000..4b7396eaa9 --- /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 fc04c3b2cc..b51db1741f 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 4852197306..cebf58b480 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 ( - ) })} - + ) } From 4e1876fe85ab3a70eba50466a62bff8a9d01c16c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 18:46:27 -0500 Subject: [PATCH 094/170] 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 +- .../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(-) diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts index 994a357141..782c4704b6 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) { - 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) +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 - if (!isMinimal && y > 10 && dy > 10) { - store.shell.setMinimalShellMode(true) - isMinimal = true - } else if (isMinimal && (y <= 10 || dy < -10)) { + 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 ( + !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 50bdc5dc93..d457d71362 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 2726ff7d35..b90213472c 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 5eb4a65889..76824e575c 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 548d30d5a9..5e03e2285b 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 dcb726873c..1409762d1c 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} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index f406c43d5a..0ade47c51a 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -28,6 +28,7 @@ import {Haptics} from 'lib/haptics' import {ComposeIcon2} from 'lib/icons' import {FAB} from '../com/util/fab/FAB' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps @@ -108,6 +109,22 @@ export const CustomFeedScreen = withAuthRequired( store.shell.openComposer({}) }, [store]) + const dropdownItems: DropdownItem[] = React.useMemo(() => { + let items: DropdownItem[] = [ + { + testID: 'feedHeaderDropdownRemoveBtn', + label: 'Remove from my feeds', + onPress: onToggleSaved, + }, + { + testID: 'feedHeaderDropdownShareBtn', + label: 'Share link', + onPress: onPressShare, + }, + ] + return items + }, [onToggleSaved, onPressShare]) + const renderHeaderBtns = React.useCallback(() => { return ( @@ -132,25 +149,46 @@ export const CustomFeedScreen = withAuthRequired( )} + {currentFeed?.isSaved ? ( + + + + ) : ( + @@ -286,7 +325,7 @@ export const CustomFeedScreen = withAuthRequired( return ( - + Date: Wed, 24 May 2023 19:27:04 -0500 Subject: [PATCH 097/170] Tune the custom feeds header a bit more --- src/view/screens/CustomFeed.tsx | 70 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 0ade47c51a..0690a17d81 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -129,53 +129,57 @@ export const CustomFeedScreen = withAuthRequired( return ( - - + {currentFeed?.isSaved ? ( + + ) : undefined} {currentFeed?.isSaved ? ( - + ) : ( )} ) @@ -187,7 +191,6 @@ export const CustomFeedScreen = withAuthRequired( onToggleSaved, onTogglePinned, onToggleLiked, - onPressShare, dropdownItems, ]) @@ -361,8 +364,13 @@ const styles = StyleSheet.create({ }, headerBtns: { flexDirection: 'row', - gap: 12, - marginTop: 10, + alignItems: 'center', + }, + headerAddBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingLeft: 4, }, headerDetails: { paddingHorizontal: 16, From dfb39e7c4fcaff3effcc82b412191177fdfdaf22 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 24 May 2023 22:09:39 -0500 Subject: [PATCH 098/170] Add feed discovery page --- bskyweb/cmd/bskyweb/server.go | 1 + package.json | 2 +- src/Navigation.tsx | 6 ++ src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/discovery/feeds.ts | 97 +++++++++++++++++++++++++ src/view/com/feeds/SavedFeeds.tsx | 41 +++++++---- src/view/screens/CustomFeed.tsx | 6 +- src/view/screens/DiscoverFeeds.tsx | 109 ++++++++++++++++++++++++++++ yarn.lock | 8 +- 10 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 src/state/models/discovery/feeds.ts create mode 100644 src/view/screens/DiscoverFeeds.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 07df85146f..462740f546 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -105,6 +105,7 @@ func serve(cctx *cli.Context) error { // generic routes e.GET("/search", server.WebGeneric) + e.GET("/search/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/mute-lists", server.WebGeneric) diff --git a/package.json b/package.json index 5c30a0cae4..253c3b7820 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.7", + "@atproto/api": "0.3.8", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index ff7a5f5c26..0664ac5265 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -35,6 +35,7 @@ import {SearchScreen} from './view/screens/Search' import {NotificationsScreen} from './view/screens/Notifications' import {ModerationScreen} from './view/screens/Moderation' import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' +import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' @@ -103,6 +104,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { component={ModerationBlockedAccounts} options={{title: title('Blocked Accounts')}} /> + 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + refresh = bundleAsync(async () => { + this._xLoading() + try { + const res = + await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators( + {}, + ) + this._replaceAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + clear() { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.feeds = [] + } + + // state transitions + // = + + _xLoading() { + this.isLoading = true + this.isRefreshing = true + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch popular feeds', err) + } + } + + // helper functions + // = + + _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { + this.feeds = [] + for (const f of res.data.feeds) { + this.feeds.push(new CustomFeedModel(this.rootStore, f)) + } + } +} diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index e92e741dac..610562c9d7 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -53,14 +53,28 @@ export const SavedFeeds = observer( const renderListFooterComponent = useCallback(() => { return ( <> - - - - Change Order - - + + + + + Discover new feeds + + + {!store.me.savedFeeds.isEmpty && ( + + + + Change Order + + + )} + ) - }, [pal]) + }, [pal, store.me.savedFeeds.isEmpty]) const renderItem = useCallback( ({item}) => , @@ -118,14 +132,16 @@ export const SavedFeeds = observer( ) const styles = StyleSheet.create({ + footerLinks: { + marginTop: 8, + borderBottomWidth: 1, + }, footerLink: { flexDirection: 'row', borderTopWidth: 1, - borderBottomWidth: 1, paddingHorizontal: 26, paddingVertical: 18, gap: 18, - marginTop: 8, }, empty: { paddingHorizontal: 18, @@ -134,7 +150,4 @@ const styles = StyleSheet.create({ marginHorizontal: 18, marginTop: 10, }, - feedItem: { - borderTopWidth: 1, - }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 0690a17d81..49798d7583 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -220,7 +220,7 @@ export const CustomFeedScreen = withAuthRequired( )} {isDesktopWeb && ( - +