diff --git a/package.json b/package.json index 3ba944db77..91b091a1b5 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.13.11", + "@atproto/api": "^0.13.18", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@emoji-mart/react": "^1.1.1", diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx index c27cea7dec..d29daa58b8 100644 --- a/src/screens/Settings/ThreadPreferences.tsx +++ b/src/screens/Settings/ThreadPreferences.tsx @@ -56,6 +56,12 @@ export function ThreadPreferencesScreen({}: Props) { values={sortReplies ? [sortReplies] : []} onChange={values => setThreadViewPrefs({sort: values[0]})}> + + + + Hot replies first + + diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index 93e3a5c3bc..4784a9d759 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -237,7 +237,11 @@ export function sortThread( } } - if (opts.sort === 'oldest') { + if (opts.sort === 'hotness') { + const aHotness = getHotness(a.post) + const bHotness = getHotness(b.post) + return bHotness - aHotness + } else if (opts.sort === 'oldest') { return a.post.indexedAt.localeCompare(b.post.indexedAt) } else if (opts.sort === 'newest') { return b.post.indexedAt.localeCompare(a.post.indexedAt) @@ -269,6 +273,21 @@ export function sortThread( // internal methods // = +// Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html +// We want to give recent comments a real chance (and not bury them deep below the fold) +// while also surfacing well-liked comments from the past. In the future, we can explore +// something more sophisticated, but we don't have much data on the client right now. +function getHotness(post: AppBskyFeedDefs.PostView) { + const hoursAgo = + (new Date().getTime() - new Date(post.indexedAt).getTime()) / + (1000 * 60 * 60) + const likeCount = post.likeCount ?? 0 + const likeOrder = Math.log(3 + likeCount) + const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount)) + const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent) + return likeOrder / timePenalty +} + function responseToThreadNodes( node: ThreadViewNode, depth = 0, diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index e07f40ec52..549f7ce291 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -15,7 +15,7 @@ export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs } export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = { - sort: 'newest', + sort: 'hotness', prioritizeFollowedUsers: true, lab_treeViewEnabled: false, } diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index 928bb90da8..8f523fcf22 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -22,6 +22,6 @@ export type ThreadViewPreferences = Pick< BskyThreadViewPreference, 'prioritizeFollowedUsers' > & { - sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string + sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string lab_treeViewEnabled?: boolean } diff --git a/yarn.lock b/yarn.lock index acc9e0fc53..daa9a3de9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,6 +72,20 @@ tlds "^1.234.0" zod "^3.23.8" +"@atproto/api@^0.13.18": + version "0.13.18" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.18.tgz#cc537cc3b4c8d03f258a373f4d893fea11a77cdd" + integrity sha512-rrl5HhzGYWZ7fiC965TPBUOVItq9M4dxMb6qz8IvAVQliSkrJrKc7UD0QWL89QiiXaOBuX8w+4i5r4wrfBGddg== + dependencies: + "@atproto/common-web" "^0.3.1" + "@atproto/lexicon" "^0.4.3" + "@atproto/syntax" "^0.3.1" + "@atproto/xrpc" "^0.6.4" + await-lock "^2.2.2" + multiformats "^9.9.0" + tlds "^1.234.0" + zod "^3.23.8" + "@atproto/aws@^0.2.7": version "0.2.7" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.7.tgz#2f3e05c897ef49b4c46452ec8c36870193952998" @@ -269,6 +283,17 @@ multiformats "^9.9.0" zod "^3.23.8" +"@atproto/lexicon@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.3.tgz#d69f6bb363a6326df7766c48132bfa30e22622d9" + integrity sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg== + dependencies: + "@atproto/common-web" "^0.3.1" + "@atproto/syntax" "^0.3.1" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + zod "^3.23.8" + "@atproto/oauth-provider@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.5.tgz#7358398125f840404bcc02c966fd2f333869ad2f" @@ -411,6 +436,11 @@ resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== +"@atproto/syntax@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.1.tgz#4346418728f9643d783d2ffcf7c77e132e1f53d4" + integrity sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw== + "@atproto/xrpc-server@^0.7.1": version "0.7.1" resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.1.tgz#e9750aab7bb531c3a82dc6048d47179de18dbdd9" @@ -437,6 +467,14 @@ "@atproto/lexicon" "^0.4.2" zod "^3.23.8" +"@atproto/xrpc@^0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.4.tgz#4cf59774f7c72e5bc821bc5f1d57f0a6ae2014db" + integrity sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA== + dependencies: + "@atproto/lexicon" "^0.4.3" + zod "^3.23.8" + "@aws-crypto/crc32@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"