Sort replies by relevance, recency, or likes

Fixes #1336
This commit is contained in:
Zed 2026-07-08 00:11:52 +02:00
commit 2f07fa6151
10 changed files with 117 additions and 21 deletions

View file

@ -35,8 +35,8 @@ proc userTweetsUrl(id: string; cursor: string): ApiReq =
proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq =
return apiReq(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles, skipTid=true)
proc tweetDetailUrl(id: string; cursor: string): ApiReq =
return apiReq(graphTweet, tweetVars % [id, cursor])
proc tweetDetailUrl(id, cursor: string; mode = Relevance): ApiReq =
return apiReq(graphTweet, tweetVars % [id, cursor, $mode])
# let cookieVars = tweetDetailVars % [id, cursor]
# result = ApiReq(
# cookie: apiUrl(graphTweetDetail, cookieVars, tweetDetailFieldToggles),
@ -230,21 +230,21 @@ proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
js = await fetch(url)
result = parseGraphTweetResult(js)
proc getGraphTweet(id: string; after=""): Future[Conversation] {.async.} =
proc getGraphTweet(id: string; after=""; mode = Relevance): Future[Conversation] {.async.} =
if id.len == 0: return
let
cursor = cursorParam(after)
js = await fetch(tweetDetailUrl(id, cursor))
js = await fetch(tweetDetailUrl(id, cursor, mode))
result = parseGraphConversation(js, id)
proc getReplies*(id, after: string): Future[Result[Chain]] {.async.} =
result = (await getGraphTweet(id, after)).replies
proc getReplies*(id, after: string; mode = Relevance): Future[Result[Chain]] {.async.} =
result = (await getGraphTweet(id, after, mode)).replies
result.beginning = after.len == 0
proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
result = await getGraphTweet(id)
proc getTweet*(id: string; after=""; mode = Relevance): Future[Conversation] {.async.} =
result = await getGraphTweet(id, mode=mode)
if after.len > 0:
result.replies = await getReplies(id, after)
result.replies = await getReplies(id, after, mode)
proc getGraphEditHistory*(id: string): Future[EditHistory] {.async.} =
if id.len == 0: return

View file

@ -92,6 +92,7 @@ const
tweetVars* = """{
"postId": "$1",
$2
"ranking_mode": "$3",
"includeHasBirdwatchNotes": false,
"includePromotedContent": false,
"withBirdwatchNotes": true,

View file

@ -21,16 +21,18 @@ proc createStatusRouter*(cfg: Config) =
if id.len > 19 or id.any(c => not c.isDigit):
resp Http404, showError("Invalid tweet ID", cfg)
let prefs = requestPrefs()
let
prefs = requestPrefs()
sort = parseEnum[RankingMode](@"sort".toLowerAscii.capitalizeAscii, Relevance)
# used for the infinite scroll feature
if @"scroll".len > 0:
let replies = await getReplies(id, getCursor())
let replies = await getReplies(id, getCursor(), sort)
if replies.content.len == 0:
resp Http204
resp $renderReplies(replies, prefs, getPath())
resp $renderReplies(replies, prefs, getPath(), sort=sort)
let conv = await getTweet(id, getCursor())
let conv = await getTweet(id, getCursor(), sort)
if conv == nil or conv.tweet == nil or conv.tweet.id == 0:
var error = "Tweet not found"
@ -64,7 +66,7 @@ proc createStatusRouter*(cfg: Config) =
elif card.video.isSome():
images = @[card.video.get().thumb]
let html = renderConversation(conv, prefs, getPath() & "#m")
let html = renderConversation(conv, prefs, getPath() & "#m", sort)
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
images=images, video=video)

View file

@ -19,6 +19,39 @@
margin-bottom: 10px;
}
.reply-sort {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px 14px;
margin-bottom: 10px;
padding: 8px 12px;
background-color: var(--bg_panel);
font-size: 14px;
}
.reply-sort-label {
color: var(--fg_faded);
margin-right: 2px;
}
.reply-sort-option {
color: var(--tab);
font-weight: bold;
text-decoration: none;
border-bottom: 0.1rem solid transparent;
&:hover {
color: var(--fg_color);
text-decoration: none;
}
&.active {
color: var(--tab_selected);
border-bottom-color: var(--tab_selected);
}
}
.main-tweet,
.replies,
.edit-history > div {

View file

@ -176,6 +176,9 @@ type
QueryKind* = enum
posts, replies, media, users, tweets, userList, followers, following
RankingMode* = enum
Relevance, Recency, Likes
Query* = object
kind*: QueryKind
view*: string

View file

@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=45")
link(rel="stylesheet", type="text/css", href="/css/style.css?v=46")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=7")
if theme.len > 0:

View file

@ -28,7 +28,22 @@ proc renderReplyThread(thread: Chain; prefs: Prefs; path: string): VNode =
if thread.hasMore:
renderMoreReplies(thread)
proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string; tweet: Tweet = nil): VNode =
proc renderReplySort(sort: RankingMode): VNode =
buildHtml(tdiv(class="reply-sort")):
span(class="reply-sort-label"): text "Sort replies:"
for mode in RankingMode:
let
cls = if mode == sort: "reply-sort-option active"
else: "reply-sort-option"
label = case mode
of Relevance: "Relevant"
of Recency: "Recent"
of Likes: "Liked"
a(class=cls, href=("?sort=" & $mode & "#r")):
text label
proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string;
tweet: Tweet = nil; sort = Relevance): VNode =
buildHtml(tdiv(class="replies", id="r")):
var hasReplies = false
var replyCount = 0
@ -40,9 +55,11 @@ proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string; tweet: T
if hasReplies and replies.bottom.len > 0:
if tweet == nil or not replies.beginning or replyCount < tweet.stats.replies:
renderMore(Query(), replies.bottom, focus="#r")
let extra = if sort == Relevance: "" else: "sort=" & $sort & "&"
renderMore(Query(), replies.bottom, focus="#r", extra=extra)
proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode =
proc renderConversation*(conv: Conversation; prefs: Prefs; path: string;
sort = Relevance): VNode =
let hasAfter = conv.after.content.len > 0
let threadId = conv.tweet.threadId
buildHtml(tdiv(class="conversation")):
@ -75,7 +92,8 @@ proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode
if not conv.replies.beginning:
renderNewer(Query(), getLink(conv.tweet), focus="#r")
if conv.replies.content.len > 0 or conv.replies.bottom.len > 0:
renderReplies(conv.replies, prefs, path, conv.tweet)
renderReplySort(sort)
renderReplies(conv.replies, prefs, path, conv.tweet, sort)
renderToTop(focus="#m")

View file

@ -50,9 +50,9 @@ proc renderNewer*(query: Query; path: string; focus=""): VNode =
a(href=(p & url)):
text "Load newest"
proc renderMore*(query: Query; cursor: string; focus=""): VNode =
proc renderMore*(query: Query; cursor: string; focus=""; extra=""): VNode =
buildHtml(tdiv(class="show-more")):
a(href=(&"?{getQuery(query)}cursor={encodeUrl(cursor, usePlus=false)}{focus}")):
a(href=(&"?{extra}{getQuery(query)}cursor={encodeUrl(cursor, usePlus=false)}{focus}")):
text "Load more"
proc renderNoMore(): VNode =