mirror of
https://github.com/zedeus/nitter.git
synced 2026-04-13 17:22:11 -04:00
16
src/api.nim
16
src/api.nim
@@ -18,16 +18,16 @@ proc apiReq(endpoint, variables: string; fieldToggles = ""): ApiReq =
|
||||
let url = apiUrl(endpoint, variables, fieldToggles)
|
||||
return ApiReq(cookie: url, oauth: url)
|
||||
|
||||
proc mediaUrl(id: string; cursor: string): ApiReq =
|
||||
proc mediaUrl(id, cursor: string; count=20): ApiReq =
|
||||
result = ApiReq(
|
||||
cookie: apiUrl(graphUserMedia, userMediaVars % [id, cursor]),
|
||||
oauth: apiUrl(graphUserMediaV2, restIdVars % [id, cursor])
|
||||
cookie: apiUrl(graphUserMedia, userMediaVars % [id, cursor, $count]),
|
||||
oauth: apiUrl(graphUserMediaV2, restIdVars % [id, cursor, $count])
|
||||
)
|
||||
|
||||
proc userTweetsUrl(id: string; cursor: string): ApiReq =
|
||||
result = ApiReq(
|
||||
# cookie: apiUrl(graphUserTweets, userTweetsVars % [id, cursor], userTweetsFieldToggles),
|
||||
oauth: apiUrl(graphUserTweetsV2, restIdVars % [id, cursor])
|
||||
oauth: apiUrl(graphUserTweetsV2, restIdVars % [id, cursor, "20"])
|
||||
)
|
||||
# might change this in the future pending testing
|
||||
result.cookie = result.oauth
|
||||
@@ -36,7 +36,7 @@ proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq =
|
||||
let cookieVars = userTweetsAndRepliesVars % [id, cursor]
|
||||
result = ApiReq(
|
||||
cookie: apiUrl(graphUserTweetsAndReplies, cookieVars, userTweetsFieldToggles),
|
||||
oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor])
|
||||
oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor, "20"])
|
||||
)
|
||||
|
||||
proc tweetDetailUrl(id: string; cursor: string): ApiReq =
|
||||
@@ -73,7 +73,7 @@ proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profi
|
||||
url = case kind
|
||||
of TimelineKind.tweets: userTweetsUrl(id, cursor)
|
||||
of TimelineKind.replies: userTweetsAndRepliesUrl(id, cursor)
|
||||
of TimelineKind.media: mediaUrl(id, cursor)
|
||||
of TimelineKind.media: mediaUrl(id, cursor, 100)
|
||||
js = await fetch(url)
|
||||
result = parseGraphTimeline(js, after)
|
||||
|
||||
@@ -81,7 +81,7 @@ proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
|
||||
if id.len == 0: return
|
||||
let
|
||||
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||
url = apiReq(graphListTweets, restIdVars % [id, cursor])
|
||||
url = apiReq(graphListTweets, restIdVars % [id, cursor, "20"])
|
||||
js = await fetch(url)
|
||||
result = parseGraphTimeline(js, after).tweets
|
||||
|
||||
@@ -205,7 +205,7 @@ proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.}
|
||||
|
||||
proc getPhotoRail*(id: string): Future[PhotoRail] {.async.} =
|
||||
if id.len == 0: return
|
||||
let js = await fetch(mediaUrl(id, ""))
|
||||
let js = await fetch(mediaUrl(id, "", 30))
|
||||
result = parseGraphPhotoRail(js)
|
||||
|
||||
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
|
||||
|
||||
@@ -138,12 +138,12 @@ const
|
||||
|
||||
restIdVars* = """{
|
||||
"rest_id": "$1", $2
|
||||
"count": 20
|
||||
"count": $3
|
||||
}"""
|
||||
|
||||
userMediaVars* = """{
|
||||
"userId": "$1", $2
|
||||
"count": 20,
|
||||
"count": $3,
|
||||
"includePromotedContent": false,
|
||||
"withClientEventToken": false,
|
||||
"withBirdwatchNotes": false,
|
||||
|
||||
@@ -100,6 +100,13 @@ genPrefs:
|
||||
autoplayGifs(checkbox, true):
|
||||
"Autoplay gifs"
|
||||
|
||||
compactGallery(checkbox, false):
|
||||
"Compact media gallery (no profile info or text)"
|
||||
|
||||
mediaView(select, "Timeline"):
|
||||
"Default media view"
|
||||
options: @["Timeline", "Grid", "Gallery"]
|
||||
|
||||
"Link replacements (blank to disable)":
|
||||
replaceTwitter(input, ""):
|
||||
"Twitter -> Nitter"
|
||||
|
||||
@@ -20,6 +20,7 @@ template `@`(param: string): untyped =
|
||||
proc initQuery*(pms: Table[string, string]; name=""): Query =
|
||||
result = Query(
|
||||
kind: parseEnum[QueryKind](@"f", tweets),
|
||||
view: @"view",
|
||||
text: @"q",
|
||||
filters: validFilters.filterIt("f-" & it in pms),
|
||||
excludes: validFilters.filterIt("e-" & it in pms),
|
||||
@@ -98,24 +99,28 @@ proc genQueryParam*(query: Query; maxId=""): string =
|
||||
result &= " max_id:" & maxId
|
||||
|
||||
proc genQueryUrl*(query: Query): string =
|
||||
if query.kind notin {tweets, users}: return
|
||||
var params: seq[string]
|
||||
|
||||
var params = @[&"f={query.kind}"]
|
||||
if query.text.len > 0:
|
||||
params.add "q=" & encodeUrl(query.text)
|
||||
for f in query.filters:
|
||||
params.add &"f-{f}=on"
|
||||
for e in query.excludes:
|
||||
params.add &"e-{e}=on"
|
||||
for i in query.includes.filterIt(it != "nativeretweets"):
|
||||
params.add &"i-{i}=on"
|
||||
if query.view.len > 0:
|
||||
params.add "view=" & encodeUrl(query.view)
|
||||
|
||||
if query.since.len > 0:
|
||||
params.add "since=" & query.since
|
||||
if query.until.len > 0:
|
||||
params.add "until=" & query.until
|
||||
if query.minLikes.len > 0:
|
||||
params.add "min_faves=" & query.minLikes
|
||||
if query.kind in {tweets, users}:
|
||||
params.add &"f={query.kind}"
|
||||
if query.text.len > 0:
|
||||
params.add "q=" & encodeUrl(query.text)
|
||||
for f in query.filters:
|
||||
params.add &"f-{f}=on"
|
||||
for e in query.excludes:
|
||||
params.add &"e-{e}=on"
|
||||
for i in query.includes.filterIt(it != "nativeretweets"):
|
||||
params.add &"i-{i}=on"
|
||||
|
||||
if query.since.len > 0:
|
||||
params.add "since=" & query.since
|
||||
if query.until.len > 0:
|
||||
params.add "until=" & query.until
|
||||
if query.minLikes.len > 0:
|
||||
params.add "min_faves=" & query.minLikes
|
||||
|
||||
if params.len > 0:
|
||||
result &= params.join("&")
|
||||
|
||||
@@ -12,12 +12,20 @@ export router_utils
|
||||
export redis_cache, formatters, query, api
|
||||
export profile, timeline, status
|
||||
|
||||
proc getQuery*(request: Request; tab, name: string): Query =
|
||||
proc getQuery*(request: Request; tab, name: string; prefs: Prefs): Query =
|
||||
let view = request.params.getOrDefault("view")
|
||||
case tab
|
||||
of "with_replies": getReplyQuery(name)
|
||||
of "media": getMediaQuery(name)
|
||||
of "search": initQuery(params(request), name=name)
|
||||
else: Query(fromUser: @[name])
|
||||
of "with_replies":
|
||||
result = getReplyQuery(name)
|
||||
of "media":
|
||||
result = getMediaQuery(name)
|
||||
result.view =
|
||||
if view in ["timeline", "grid", "gallery"]: view
|
||||
else: prefs.mediaView.toLowerAscii
|
||||
of "search":
|
||||
result = initQuery(params(request), name=name)
|
||||
else:
|
||||
result = Query(fromUser: @[name])
|
||||
|
||||
template skipIf[T](cond: bool; default; body: Future[T]): Future[T] =
|
||||
if cond:
|
||||
@@ -121,7 +129,7 @@ proc createTimelineRouter*(cfg: Config) =
|
||||
after = getCursor()
|
||||
names = getNames(@"name")
|
||||
|
||||
var query = request.getQuery(@"tab", @"name")
|
||||
var query = request.getQuery(@"tab", @"name", prefs)
|
||||
if names.len != 1:
|
||||
query.fromUser = names
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ input::-webkit-datetime-edit-year-field:focus {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
|
||||
@@ -1,87 +1,116 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import "_variables";
|
||||
@import "_mixins";
|
||||
|
||||
@import 'card';
|
||||
@import 'photo-rail';
|
||||
@import "card";
|
||||
@import "photo-rail";
|
||||
|
||||
.profile-tabs {
|
||||
@include panel(auto, 900px);
|
||||
@include panel(auto, 900px);
|
||||
|
||||
.timeline-container {
|
||||
float: right;
|
||||
width: 68% !important;
|
||||
max-width: unset;
|
||||
}
|
||||
.timeline-container {
|
||||
float: right;
|
||||
width: 68% !important;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
margin-bottom: 4px;
|
||||
background-color: var(--bg_panel);
|
||||
margin-bottom: 4px;
|
||||
background-color: var(--bg_panel);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 33.34% 0 0 0;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 33.34% 0 0 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
padding: 0 4px 0 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
max-width: 32%;
|
||||
top: 0;
|
||||
padding: 0 4px 0 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
max-width: 32%;
|
||||
top: 0;
|
||||
|
||||
body.fixed-nav & {
|
||||
top: 50px;
|
||||
}
|
||||
body.fixed-nav & {
|
||||
top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-result {
|
||||
min-height: 54px;
|
||||
min-height: 54px;
|
||||
|
||||
.username {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.username {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.tweet-header {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
.tweet-header {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 700px) {
|
||||
.profile-tabs {
|
||||
width: 100vw;
|
||||
max-width: 600px;
|
||||
.profile-tabs.media-only {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
|
||||
.timeline-container {
|
||||
width: 100% !important;
|
||||
.timeline-container {
|
||||
float: none;
|
||||
width: 100% !important;
|
||||
max-width: none;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-item wide {
|
||||
flex-grow: 1.4;
|
||||
}
|
||||
}
|
||||
.timeline-container > .tab {
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.profile-tabs {
|
||||
width: 100vw;
|
||||
max-width: 600px;
|
||||
|
||||
.timeline-container {
|
||||
width: 100% !important;
|
||||
|
||||
.tab-item wide {
|
||||
flex-grow: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
position: initial !important;
|
||||
padding: 0;
|
||||
.profile-tabs.media-only {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
|
||||
.timeline-container {
|
||||
width: 100vw !important;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-tab {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
position: initial !important;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 900px) {
|
||||
.profile-tab.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
.profile-tab.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
padding: 8px;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
button {
|
||||
@@ -36,7 +36,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
margin: 0 0 5px 0;
|
||||
margin: 0 0 4px 0;
|
||||
background-color: var(--bg_panel);
|
||||
padding: 0;
|
||||
}
|
||||
@@ -157,3 +157,340 @@
|
||||
position: relative;
|
||||
background-color: var(--bg_panel);
|
||||
}
|
||||
|
||||
.timeline.media-grid-view,
|
||||
.timeline.media-gallery-view {
|
||||
> div:not(:first-child) {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline.media-grid-view,
|
||||
.timeline.media-gallery-view .gallery-masonry.compact {
|
||||
.tweet-header,
|
||||
.replying-to,
|
||||
.retweet-header,
|
||||
.pinned,
|
||||
.tweet-stats,
|
||||
.attribution,
|
||||
.poll,
|
||||
.quote,
|
||||
.community-note,
|
||||
.media-tag-block,
|
||||
.tweet-content,
|
||||
.card-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: unset;
|
||||
|
||||
.card-container {
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
|
||||
.card-image-container {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.card-content-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeline.media-grid-view {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
> div:not(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tweet-link {
|
||||
z-index: 1000;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
> .show-more,
|
||||
> .top-ref,
|
||||
> .timeline-footer,
|
||||
> .timeline-header {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.tweet-body {
|
||||
height: 100%;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.gallery-row + .gallery-row {
|
||||
margin-top: 0.25em !important;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
background-color: var(--darkest_grey);
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.attachments,
|
||||
.gallery-row,
|
||||
.still-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.still-image img,
|
||||
.attachment > video,
|
||||
.attachment > img {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gallery-video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media-gif {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.timeline-item:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.alt-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline.media-gallery-view {
|
||||
.gallery-masonry {
|
||||
margin: 10px 0;
|
||||
column-gap: 10px;
|
||||
column-width: 350px;
|
||||
|
||||
&.masonry-active {
|
||||
column-width: unset;
|
||||
column-gap: unset;
|
||||
position: relative;
|
||||
|
||||
.timeline-item {
|
||||
animation: none;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
.tweet-body {
|
||||
padding: 0;
|
||||
|
||||
> .attachments {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image-container img {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes masonry-init {
|
||||
to {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Start hidden. CSS animation reveals after a delay as a no-JS fallback.
|
||||
// With JS, masonry-active cancels the animation and masonry-visible reveals.
|
||||
.gallery-masonry .timeline-item,
|
||||
> .show-more,
|
||||
> .top-ref,
|
||||
> .timeline-footer {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
animation: masonry-init 0.2s 0.3s forwards;
|
||||
}
|
||||
|
||||
.gallery-masonry.masonry-active .timeline-item.masonry-visible,
|
||||
> .show-more.masonry-visible,
|
||||
> .top-ref.masonry-visible,
|
||||
> .timeline-footer.masonry-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.15s ease;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
margin-bottom: 10px;
|
||||
break-inside: avoid;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> .show-more,
|
||||
> .top-ref,
|
||||
> .timeline-footer,
|
||||
> .timeline-header {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
> .show-more {
|
||||
padding: 0;
|
||||
margin-top: 8px;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.tweet-content {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.tweet-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin-left: 0;
|
||||
padding: 10px;
|
||||
|
||||
> .attachments {
|
||||
align-self: stretch;
|
||||
border-radius: 0;
|
||||
margin: -10px -10px 10px;
|
||||
max-height: none;
|
||||
order: -1;
|
||||
width: auto;
|
||||
background-color: var(--bg_elements);
|
||||
|
||||
.gallery-row {
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.still-image img,
|
||||
.attachment > video,
|
||||
.attachment > img {
|
||||
max-height: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment:last-child {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet-stat {
|
||||
padding-top: unset;
|
||||
}
|
||||
|
||||
.quote {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.replying-to {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet-header {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 0.75em;
|
||||
margin-bottom: 0;
|
||||
|
||||
.tweet-avatar {
|
||||
img {
|
||||
float: none;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet-name-row {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fullname-and-username {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fullname {
|
||||
max-width: calc(100% - 18px);
|
||||
}
|
||||
|
||||
.verified-icon {
|
||||
margin-left: 4px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
flex-basis: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.timeline.media-grid-view {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.timeline.media-grid-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.timeline.media-gallery-view {
|
||||
padding: 8px 0;
|
||||
|
||||
.gallery-masonry {
|
||||
columns: 1;
|
||||
column-gap: 0;
|
||||
|
||||
&.masonry-active {
|
||||
columns: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
.avatar {
|
||||
&.round {
|
||||
border-radius: 50%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
@@ -204,6 +205,7 @@
|
||||
|
||||
.tweet-stats {
|
||||
margin-bottom: -3px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
@@ -236,6 +238,7 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -123,6 +123,7 @@ type
|
||||
|
||||
Query* = object
|
||||
kind*: QueryKind
|
||||
view*: string
|
||||
text*: string
|
||||
filters*: seq[string]
|
||||
includes*: seq[string]
|
||||
|
||||
@@ -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=29")
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=30")
|
||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||
|
||||
if theme.len > 0:
|
||||
|
||||
@@ -102,17 +102,22 @@ proc renderProtected(username: string): VNode =
|
||||
|
||||
proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode =
|
||||
profile.tweets.query.fromUser = @[profile.user.username]
|
||||
let
|
||||
isGalleryView = profile.tweets.query.kind == media and
|
||||
profile.tweets.query.view == "gallery"
|
||||
viewClass = if isGalleryView: " media-only" else: ""
|
||||
|
||||
buildHtml(tdiv(class="profile-tabs")):
|
||||
if not prefs.hideBanner:
|
||||
buildHtml(tdiv(class=("profile-tabs" & viewClass))):
|
||||
if not isGalleryView and not prefs.hideBanner:
|
||||
tdiv(class="profile-banner"):
|
||||
renderBanner(profile.user.banner)
|
||||
|
||||
let sticky = if prefs.stickyProfile: " sticky" else: ""
|
||||
tdiv(class=("profile-tab" & sticky)):
|
||||
renderUserCard(profile.user, prefs)
|
||||
if profile.photoRail.len > 0:
|
||||
renderPhotoRail(profile)
|
||||
if not isGalleryView:
|
||||
let sticky = if prefs.stickyProfile: " sticky" else: ""
|
||||
tdiv(class=("profile-tab" & sticky)):
|
||||
renderUserCard(profile.user, prefs)
|
||||
if profile.photoRail.len > 0:
|
||||
renderPhotoRail(profile)
|
||||
|
||||
if profile.user.protected:
|
||||
renderProtected(profile.user.username)
|
||||
|
||||
@@ -39,6 +39,19 @@ proc renderProfileTabs*(query: Query; username: string): VNode =
|
||||
li(class=query.getTabClass(tweets)):
|
||||
a(href=(link & "/search")): text "Search"
|
||||
|
||||
proc renderMediaViewTabs*(query: Query; username: string): VNode =
|
||||
let currentView = if query.view.len > 0: query.view else: "timeline"
|
||||
let base = "/" & username & "/media?view="
|
||||
func cls(view: string): string =
|
||||
if currentView == view: "tab-item active" else: "tab-item"
|
||||
buildHtml(ul(class="tab media-view-tabs")):
|
||||
li(class=cls("timeline")):
|
||||
a(href=(base & "timeline")): text "Timeline"
|
||||
li(class=cls("grid")):
|
||||
a(href=(base & "grid")): text "Grid"
|
||||
li(class=cls("gallery")):
|
||||
a(href=(base & "gallery")): text "Gallery"
|
||||
|
||||
proc renderSearchTabs*(query: Query): VNode =
|
||||
var q = query
|
||||
buildHtml(ul(class="tab")):
|
||||
@@ -95,7 +108,10 @@ proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string;
|
||||
text query.fromUser.join(" | ")
|
||||
|
||||
if query.fromUser.len > 0:
|
||||
renderProfileTabs(query, query.fromUser.join(","))
|
||||
if query.kind != media or query.view != "gallery":
|
||||
renderProfileTabs(query, query.fromUser.join(","))
|
||||
if query.kind == media and query.fromUser.len == 1:
|
||||
renderMediaViewTabs(query, query.fromUser[0])
|
||||
|
||||
if query.fromUser.len == 0 or query.kind == tweets:
|
||||
tdiv(class="timeline-header"):
|
||||
|
||||
@@ -5,6 +5,15 @@ import karax/[karaxdsl, vdom]
|
||||
import ".."/[types, query, formatters]
|
||||
import tweet, renderutils
|
||||
|
||||
proc timelineViewClass(query: Query): string =
|
||||
if query.kind != media:
|
||||
return "timeline"
|
||||
|
||||
case query.view
|
||||
of "grid": "timeline media-grid-view"
|
||||
of "gallery": "timeline media-gallery-view"
|
||||
else: "timeline"
|
||||
|
||||
proc getQuery(query: Query): string =
|
||||
if query.kind != posts:
|
||||
result = genQueryUrl(query)
|
||||
@@ -105,9 +114,22 @@ proc renderTimelineUsers*(results: Result[User]; prefs: Prefs; path=""): VNode =
|
||||
else:
|
||||
renderNoMore()
|
||||
|
||||
proc filterThreads(threads: seq[Tweets]; prefs: Prefs): seq[Tweets] =
|
||||
var retweets: seq[int64]
|
||||
for thread in threads:
|
||||
if thread.len == 1:
|
||||
let tweet = thread[0]
|
||||
let retweetId = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
|
||||
if retweetId in retweets or tweet.id in retweets or
|
||||
tweet.pinned and prefs.hidePins:
|
||||
continue
|
||||
if retweetId != 0 and tweet.retweet.isSome:
|
||||
retweets &= retweetId
|
||||
result.add(thread)
|
||||
|
||||
proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string;
|
||||
pinned=none(Tweet)): VNode =
|
||||
buildHtml(tdiv(class="timeline")):
|
||||
buildHtml(tdiv(class=results.query.timelineViewClass)):
|
||||
if not results.beginning:
|
||||
renderNewer(results.query, parseUri(path).path)
|
||||
|
||||
@@ -121,23 +143,17 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string;
|
||||
else:
|
||||
renderNoneFound()
|
||||
else:
|
||||
var retweets: seq[int64]
|
||||
let filtered = filterThreads(results.content, prefs)
|
||||
|
||||
for thread in results.content:
|
||||
if thread.len == 1:
|
||||
let
|
||||
tweet = thread[0]
|
||||
retweetId = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
|
||||
|
||||
if retweetId in retweets or tweet.id in retweets or
|
||||
tweet.pinned and prefs.hidePins:
|
||||
continue
|
||||
|
||||
if retweetId != 0 and tweet.retweet.isSome:
|
||||
retweets &= retweetId
|
||||
renderTweet(tweet, prefs, path)
|
||||
else:
|
||||
renderThread(thread, prefs, path)
|
||||
if results.query.view == "gallery":
|
||||
tdiv(class=if prefs.compactGallery: "gallery-masonry compact" else: "gallery-masonry"):
|
||||
for thread in filtered:
|
||||
if thread.len == 1: renderTweet(thread[0], prefs, path)
|
||||
else: renderThread(thread, prefs, path)
|
||||
else:
|
||||
for thread in filtered:
|
||||
if thread.len == 1: renderTweet(thread[0], prefs, path)
|
||||
else: renderThread(thread, prefs, path)
|
||||
|
||||
var cursor = getSearchMaxId(results, path)
|
||||
if cursor.len > 0:
|
||||
|
||||
Reference in New Issue
Block a user