diff --git a/src/formatters.nim b/src/formatters.nim index ee80206..4936ee9 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -6,7 +6,7 @@ import types, utils, query const cards = "cards.twitter.com/cards" tco = "https://t.co" - twitter = parseUri("https://twitter.com") + twitter = parseUri("https://x.com") let twRegex = re"(?<=(? 0 and "youtu" in result: - result = result.replace(ytRegex, prefs.replaceYouTube) + let youtubeHost = strip(prefs.replaceYouTube, chars={'/'}) + result = result.replace(ytRegex, youtubeHost) if prefs.replaceTwitter.len > 0: + let twitterHost = strip(prefs.replaceTwitter, chars={'/'}) if tco in result: - result = result.replace(tco, https & prefs.replaceTwitter & "/t.co") + result = result.replace(tco, https & twitterHost & "/t.co") if "x.com" in result: - result = result.replace(xRegex, prefs.replaceTwitter) + result = result.replace(xRegex, twitterHost) result = result.replacef(xLinkRegex, a( - prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1")) + twitterHost & "$2", href = https & twitterHost & "$1")) if "twitter.com" in result: - result = result.replace(cards, prefs.replaceTwitter & "/cards") - result = result.replace(twRegex, prefs.replaceTwitter) + result = result.replace(cards, twitterHost & "/cards") + result = result.replace(twRegex, twitterHost) result = result.replacef(twLinkRegex, a( prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1")) if "imgur.com" in result: @@ -96,9 +98,10 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string = prefs.replaceSoundCloud & "$4", href = https & prefs.replaceSoundCloud & "$2")) if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result): - result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/") - result = result.replace(rdRegex, prefs.replaceReddit) - if prefs.replaceReddit in result and "/gallery/" in result: + let redditHost = strip(prefs.replaceReddit, chars={'/'}) + result = result.replace(rdShortRegex, redditHost & "/comments/") + result = result.replace(rdRegex, redditHost) + if redditHost in result and "/gallery/" in result: result = result.replace("/gallery/", "/comments/") if absolute.len > 0 and "href" in result: diff --git a/src/parser.nim b/src/parser.nim index 2e140ee..e7971c1 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -487,6 +487,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = result.before.content.add tweet elif entryId.startsWith("cursor-bottom"): var cursorValue = select( + e{"content", "value"}, e{"content", "content", "value"}, e{"content", "itemContent", "value"} ) diff --git a/src/query.nim b/src/query.nim index bf3f00a..eae9cde 100644 --- a/src/query.nim +++ b/src/query.nim @@ -6,10 +6,9 @@ import types const validFilters* = @[ "media", "images", "twimg", "videos", - "native_video", "consumer_video", "pro_video", + "native_video", "consumer_video", "spaces", "links", "news", "quote", "mentions", - "replies", "retweets", "nativeretweets", - "verified", "safe" + "replies", "retweets", "nativeretweets" ] emptyQuery* = "include:nativeretweets" @@ -18,6 +17,11 @@ template `@`(param: string): untyped = if param in pms: pms[param] else: "" +proc validateNumber(value: string): string = + if value.anyIt(not it.isDigit): + return "" + return value + proc initQuery*(pms: Table[string, string]; name=""): Query = result = Query( kind: parseEnum[QueryKind](@"f", tweets), @@ -26,7 +30,7 @@ proc initQuery*(pms: Table[string, string]; name=""): Query = excludes: validFilters.filterIt("e-" & it in pms), since: @"since", until: @"until", - near: @"near" + minLikes: validateNumber(@"min_faves") ) if name.len > 0: @@ -81,8 +85,8 @@ proc genQueryParam*(query: Query): string = result &= " since:" & query.since if query.until.len > 0: result &= " until:" & query.until - if query.near.len > 0: - result &= &" near:\"{query.near}\" within:15mi" + if query.minLikes.len > 0: + result &= " min_faves:" & query.minLikes if query.text.len > 0: if result.len > 0: result &= " " & query.text @@ -106,8 +110,8 @@ proc genQueryUrl*(query: Query): string = params.add "since=" & query.since if query.until.len > 0: params.add "until=" & query.until - if query.near.len > 0: - params.add "near=" & query.near + if query.minLikes.len > 0: + params.add "min_faves=" & query.minLikes if params.len > 0: result &= params.join("&") diff --git a/src/routes/status.nim b/src/routes/status.nim index 96f5a37..bd2e229 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -31,8 +31,6 @@ proc createStatusRouter*(cfg: Config) = resp $renderReplies(replies, prefs, getPath()) let conv = await getTweet(id, getCursor()) - if conv == nil: - warn "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id == 0: var error = "Tweet not found" @@ -68,7 +66,7 @@ proc createStatusRouter*(cfg: Config) = get "/@name/@s/@id/@m/?@i?": cond @"s" in ["status", "statuses"] - cond @"m" in ["video", "photo"] + cond @"m" in ["video", "photo", "history"] redirect("/$1/status/$2" % [@"name", @"id"]) get "/@name/statuses/@id/?": @@ -76,6 +74,6 @@ proc createStatusRouter*(cfg: Config) = get "/i/web/status/@id": redirect("/i/status/" & @"id") - + get "/@name/thread/@id/?": redirect("/$1/status/$2" % [@"name", @"id"]) diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index 7a10e91..49c7ce2 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -105,6 +105,12 @@ proc createTimelineRouter*(cfg: Config) = get "/intent/user": respUserId() + get "/intent/follow/?": + let username = request.params.getOrDefault("screen_name") + if username.len == 0: + resp Http400, showError("Missing screen_name parameter", cfg) + redirect("/" & username) + get "/@name/?@tab?/?": cond '.' notin @"name" cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"] diff --git a/src/routes/unsupported.nim b/src/routes/unsupported.nim index 0c085d4..362b36b 100644 --- a/src/routes/unsupported.nim +++ b/src/routes/unsupported.nim @@ -17,7 +17,7 @@ proc createUnsupportedRouter*(cfg: Config) = get "/@name/lists/?": feature() get "/intent/?@i?": - cond @"i" notin ["user"] + cond @"i" notin ["user", "follow"] feature() get "/i/@i?/?@j?": diff --git a/src/sass/index.scss b/src/sass/index.scss index 94e3331..027e3ca 100644 --- a/src/sass/index.scss +++ b/src/sass/index.scss @@ -52,7 +52,7 @@ body { background-color: var(--bg_color); color: var(--fg_color); font-family: $font_stack; - font-size: 14px; + font-size: 15px; line-height: 1.3; margin: 0; } diff --git a/src/sass/inputs.scss b/src/sass/inputs.scss index f4d48a1..6c964b8 100644 --- a/src/sass/inputs.scss +++ b/src/sass/inputs.scss @@ -14,6 +14,7 @@ button { input[type="text"], input[type="date"], +input[type="number"], select { @include input-colors; background-color: var(--bg_elements); @@ -24,7 +25,12 @@ select { font-size: 14px; } -input[type="text"] { +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="text"], +input[type="number"] { height: 16px; } @@ -38,6 +44,17 @@ input[type="date"]::-webkit-inner-spin-button { display: none; } +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + display: none; + -webkit-appearance: none; + margin: 0; +} + input[type="date"]::-webkit-clear-button { margin-left: 17px; filter: grayscale(100%); @@ -176,7 +193,8 @@ input::-webkit-datetime-edit-year-field:focus { color: var(--fg_color); } - input[type="text"] { + input[type="text"], + input[type="number"] { position: absolute; right: 0; max-width: 140px; diff --git a/src/sass/search.scss b/src/sass/search.scss index 8afc930..909284e 100644 --- a/src/sass/search.scss +++ b/src/sass/search.scss @@ -27,7 +27,8 @@ min-width: 0; } - input[type="text"] { + input[type="text"], + input[type="number"] { height: calc(100% - 4px); width: calc(100% - 8px); } @@ -155,15 +156,14 @@ .profile-tabs { @include search-resize(820px, 5); - @include search-resize(725px, 4); - @include search-resize(600px, 6); - @include search-resize(560px, 5); - @include search-resize(480px, 4); + @include search-resize(715px, 4); + @include search-resize(700px, 5); + @include search-resize(485px, 4); @include search-resize(410px, 3); } -@include search-resize(560px, 5); -@include search-resize(480px, 4); +@include search-resize(700px, 5); +@include search-resize(485px, 4); @include search-resize(410px, 3); @media(max-width: 600px) { diff --git a/src/types.nim b/src/types.nim index 0d196cc..42cef1f 100644 --- a/src/types.nim +++ b/src/types.nim @@ -141,7 +141,7 @@ type fromUser*: seq[string] since*: string until*: string - near*: string + minLikes*: string sep*: string Gif* = object diff --git a/src/views/general.nim b/src/views/general.nim index 11444ca..3352f8f 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -33,14 +33,14 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode = tdiv(class="nav-item right"): if cfg.enableRss and rss.len > 0: icon "rss", title="RSS Feed", href=rss - icon "bird", title="Open in Twitter", href=canonical + icon "bird", title="Open in X", href=canonical a(href="https://buymeacoffee.com/kuu7o"): verbatim lp icon "info", title="About", href="/about" icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path)) proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; video=""; images: seq[string] = @[]; banner=""; ogTitle=""; - rss=""; canonical=""): VNode = + rss=""; alternate=""): VNode = var theme = prefs.theme.toTheme if "theme" in req.params: theme = req.params["theme"].toTheme @@ -54,7 +54,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=20") + link(rel="stylesheet", type="text/css", href="/css/style.css?v=21") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=3") if theme.len > 0: @@ -68,8 +68,8 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; link(rel="search", type="application/opensearchdescription+xml", title=cfg.title, href=opensearchUrl) - if canonical.len > 0: - link(rel="canonical", href=canonical) + if alternate.len > 0: + link(rel="alternate", href=alternate, title="View on X") if cfg.enableRss and rss.len > 0: link(rel="alternate", type="application/rss+xml", href=rss, title="RSS feed") @@ -131,16 +131,16 @@ proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; titleText=""; desc=""; ogTitle=""; rss=""; video=""; images: seq[string] = @[]; banner=""): string = - let canonical = getTwitterLink(req.path, req.params) + let twitterLink = getTwitterLink(req.path, req.params) let node = buildHtml(html(lang="en")): renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle, - rss, canonical) + rss, twitterLink) body(class=if prefs.verifiedBadge == "Hide all": "hide-verified-all" elif prefs.verifiedBadge == "Show official only": "hide-verified-blue" else: ""): - renderNavbar(cfg, req, rss, canonical) + renderNavbar(cfg, req, rss, twitterLink) tdiv(class="container"): body diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index ee9bed4..48beb62 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -89,6 +89,13 @@ proc genDate*(pref, state: string): VNode = input(name=pref, `type`="date", value=state) icon "calendar" +proc genNumberInput*(pref, label, state, placeholder: string; class=""; autofocus=true; min="0"): VNode = + let p = placeholder + buildHtml(tdiv(class=("pref-group pref-input " & class))): + if label.len > 0: + label(`for`=pref): text label + input(name=pref, `type`="number", placeholder=p, value=state, autofocus=(autofocus and state.len == 0), min=min, step="1") + proc genImg*(url: string; class=""): VNode = buildHtml(): img(src=getPicUrl(url), class=class, alt="", loading="lazy") diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 1ccc0ca..51ec5e8 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -57,7 +57,7 @@ proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode = proc renderUser*(user: User; prefs: Prefs; path: string): VNode = let class = if user.sensitive: "timeline-item nsfw" else: "timeline-item" - buildHtml(tdiv(class=class)): + buildHtml(tdiv(class=class, data-username=user.username)): a(class="tweet-link", href=("/" & user.username)) tdiv(class="tweet-body profile-result"): tdiv(class="tweet-header"): diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 5e8c166..237f81b 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -275,7 +275,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; divClass.add " nsfw" if not tweet.available: - return buildHtml(tdiv(class=divClass & "unavailable timeline-item")): + return buildHtml(tdiv(class=divClass & "unavailable timeline-item", data-username=tweet.user.username)): tdiv(class="unavailable-box"): if tweet.tombstone.len > 0: text tweet.tombstone @@ -297,7 +297,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; tweet = tweet.retweet.get retweet = fullTweet.user.fullname - buildHtml(tdiv(class=("timeline-item " & divClass))): + buildHtml(tdiv(class=("timeline-item " & divClass), data-username=tweet.user.username)): if not mainTweet: a(class="tweet-link", href=getLink(tweet))