From da8499c8810eccbb448516adedcbb19a1964c081 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 19 Sep 2023 12:24:58 -0700 Subject: [PATCH] Add thread sort settings (#1475) * Add thread sorting preferences * UI tweaks * Tweak settings * Tune the copy --- src/Navigation.tsx | 14 +- src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/state/models/content/post-thread.ts | 37 +++++- src/state/models/ui/preferences.ts | 30 +++++ src/view/index.ts | 2 + src/view/screens/PreferencesHomeFeed.tsx | 7 +- src/view/screens/PreferencesThreads.tsx | 155 +++++++++++++++++++++++ src/view/screens/Settings.tsx | 22 ++++ 9 files changed, 256 insertions(+), 13 deletions(-) create mode 100644 src/view/screens/PreferencesThreads.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 9bf6ba9818..604fca2b92 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -33,6 +33,10 @@ import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {router} from './routes' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from './state' +import {getRoutingInstrumentation} from 'lib/sentry' +import {bskyTitle} from 'lib/strings/headings' +import {JSX} from 'react/jsx-runtime' +import {timeout} from 'lib/async/timeout' import {HomeScreen} from './view/screens/Home' import {SearchScreen} from './view/screens/Search' @@ -62,11 +66,8 @@ import {AppPasswords} from 'view/screens/AppPasswords' import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {SavedFeeds} from 'view/screens/SavedFeeds' -import {getRoutingInstrumentation} from 'lib/sentry' -import {bskyTitle} from 'lib/strings/headings' -import {JSX} from 'react/jsx-runtime' -import {timeout} from 'lib/async/timeout' import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed' +import {PreferencesThreads} from 'view/screens/PreferencesThreads' const navigationRef = createNavigationContainerRef() @@ -219,6 +220,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { component={PreferencesHomeFeed} options={{title: title('Home Feed Preferences')}} /> + ) } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index cc7a468e92..e2867a707d 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -29,6 +29,7 @@ export type CommonNavigatorParams = { AppPasswords: undefined SavedFeeds: undefined PreferencesHomeFeed: undefined + PreferencesThreads: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 7c356eb1b3..35266d85bb 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -23,6 +23,7 @@ export const router = new Router({ Log: '/sys/log', AppPasswords: '/settings/app-passwords', PreferencesHomeFeed: '/settings/home-feed', + PreferencesThreads: '/settings/threads', SavedFeeds: '/settings/saved-feeds', Support: '/support', PrivacyPolicy: '/support/privacy', diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 7e3650948a..2d3a3d64a1 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -241,7 +241,7 @@ export class PostThreadModel { res.data.thread as AppBskyFeedDefs.ThreadViewPost, thread.uri, ) - sortThread(thread) + sortThread(thread, this.rootStore.preferences) this.thread = thread } } @@ -263,11 +263,16 @@ function pruneReplies(post: MaybePost) { } } +interface SortSettings { + threadDefaultSort: string + threadFollowedUsersFirst: boolean +} + type MaybeThreadItem = | PostThreadItemModel | AppBskyFeedDefs.NotFoundPost | AppBskyFeedDefs.BlockedPost -function sortThread(item: MaybeThreadItem) { +function sortThread(item: MaybeThreadItem, opts: SortSettings) { if ('notFound' in item) { return } @@ -296,13 +301,31 @@ function sortThread(item: MaybeThreadItem) { if (modScore(a.moderation) !== modScore(b.moderation)) { return modScore(a.moderation) - modScore(b.moderation) } - if (a.post.likeCount === b.post.likeCount) { - return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest - } else { - return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes + if (opts.threadFollowedUsersFirst) { + const af = a.post.author.viewer?.following + const bf = b.post.author.viewer?.following + if (af && !bf) { + return -1 + } else if (!af && bf) { + return 1 + } } + if (opts.threadDefaultSort === 'oldest') { + return a.post.indexedAt.localeCompare(b.post.indexedAt) + } else if (opts.threadDefaultSort === 'newest') { + return b.post.indexedAt.localeCompare(a.post.indexedAt) + } else if (opts.threadDefaultSort === 'most-likes') { + if (a.post.likeCount === b.post.likeCount) { + return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest + } else { + return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes + } + } else if (opts.threadDefaultSort === 'random') { + return 0.5 - Math.random() // this is vaguely criminal but we can get away with it + } + return b.post.indexedAt.localeCompare(a.post.indexedAt) }) - item.replies.forEach(reply => sortThread(reply)) + item.replies.forEach(reply => sortThread(reply, opts)) } } diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 3790b3a920..03f08bc1bb 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -25,6 +25,7 @@ const VISIBILITY_VALUES = ['ignore', 'warn', 'hide'] const DEFAULT_LANG_CODES = (deviceLocales || []) .concat(['en', 'ja', 'pt', 'de']) .slice(0, 6) +const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random'] export class LabelPreferencesModel { nsfw: LabelPreference = 'hide' @@ -55,6 +56,8 @@ export class PreferencesModel { homeFeedRepostsEnabled: boolean = true homeFeedQuotePostsEnabled: boolean = true homeFeedMergeFeedEnabled: boolean = false + threadDefaultSort: string = 'oldest' + threadFollowedUsersFirst: boolean = true requireAltTextEnabled: boolean = false // used to linearize async modifications to state @@ -86,6 +89,8 @@ export class PreferencesModel { homeFeedRepostsEnabled: this.homeFeedRepostsEnabled, homeFeedQuotePostsEnabled: this.homeFeedQuotePostsEnabled, homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled, + threadDefaultSort: this.threadDefaultSort, + threadFollowedUsersFirst: this.threadFollowedUsersFirst, requireAltTextEnabled: this.requireAltTextEnabled, } } @@ -189,6 +194,21 @@ export class PreferencesModel { ) { this.homeFeedMergeFeedEnabled = v.homeFeedMergeFeedEnabled } + // check if thread sort order is set in preferences, then hydrate + if ( + hasProp(v, 'threadDefaultSort') && + typeof v.threadDefaultSort === 'string' && + THREAD_SORT_VALUES.includes(v.threadDefaultSort) + ) { + this.threadDefaultSort = v.threadDefaultSort + } + // check if tread followed-users-first is enabled in preferences, then hydrate + if ( + hasProp(v, 'threadFollowedUsersFirst') && + typeof v.threadFollowedUsersFirst === 'boolean' + ) { + this.threadFollowedUsersFirst = v.threadFollowedUsersFirst + } // check if requiring alt text is enabled in preferences, then hydrate if ( hasProp(v, 'requireAltTextEnabled') && @@ -494,6 +514,16 @@ export class PreferencesModel { this.homeFeedMergeFeedEnabled = !this.homeFeedMergeFeedEnabled } + setThreadDefaultSort(v: string) { + if (THREAD_SORT_VALUES.includes(v)) { + this.threadDefaultSort = v + } + } + + toggleThreadFollowedUsersFirst() { + this.threadFollowedUsersFirst = !this.threadFollowedUsersFirst + } + toggleRequireAltTextEnabled() { this.requireAltTextEnabled = !this.requireAltTextEnabled } diff --git a/src/view/index.ts b/src/view/index.ts index 2fdc34e7b5..da1b781463 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -36,6 +36,7 @@ import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' +import {faComments} from '@fortawesome/free-regular-svg-icons/faComments' import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass' import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' @@ -134,6 +135,7 @@ export function setup() { farClone, faComment, faCommentSlash, + faComments, faCompass, faEllipsis, faEnvelope, diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 81bdfc95e2..34139bec14 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -66,7 +66,10 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ ]}> + style={[ + styles.titleSection, + isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20}, + ]}> Fine-tune the content you see on your home screen. @@ -175,7 +178,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ style={[ styles.btnContainer, !isTabletOrDesktop && {borderTopWidth: 1, paddingHorizontal: 20}, - pal.borderDark, + pal.border, ]}> +export const PreferencesThreads = observer(function PreferencesThreadsImpl({ + navigation, +}: Props) { + const pal = usePalette('default') + const store = useStores() + const {isTabletOrDesktop} = useWebMediaQueries() + + return ( + + + + + Fine-tune the discussion threads. + + + + + + + + Sort Replies + + + Sort replies to the same post by: + + + + + + + + + Prioritize Your Follows + + + Show replies by people you follow before all other replies. + + + + + + + + { + navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate('Settings') + }} + style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]} + accessibilityRole="button" + accessibilityLabel="Confirm" + accessibilityHint=""> + Done + + + + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: 90, + }, + desktopContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + paddingBottom: 40, + }, + titleSection: { + paddingBottom: 30, + }, + title: { + textAlign: 'center', + marginBottom: 5, + }, + description: { + textAlign: 'center', + paddingHorizontal: 32, + }, + cardsContainer: { + paddingHorizontal: 20, + }, + card: { + padding: 16, + borderRadius: 10, + marginBottom: 20, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + padding: 14, + backgroundColor: colors.blue3, + }, + btnDesktop: { + marginHorizontal: 'auto', + paddingHorizontal: 80, + }, + btnContainer: { + paddingTop: 20, + }, + dimmed: { + opacity: 0.3, + }, +}) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 761f50d0a3..1ff5f58ff0 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -180,6 +180,10 @@ export const SettingsScreen = withAuthRequired( navigation.navigate('PreferencesHomeFeed') }, [navigation]) + const openThreadsPreferences = React.useCallback(() => { + navigation.navigate('PreferencesThreads') + }, [navigation]) + const onPressAppPasswords = React.useCallback(() => { navigation.navigate('AppPasswords') }, [navigation]) @@ -420,6 +424,24 @@ export const SettingsScreen = withAuthRequired( Home Feed Preferences + + + + + + Thread Preferences + +