diff --git a/nitter.nimble b/nitter.nimble
index 3d2eaea..039b7d0 100644
--- a/nitter.nimble
+++ b/nitter.nimble
@@ -16,3 +16,4 @@ requires "jester >= 0.4.1"
requires "regex >= 0.11.2"
requires "q >= 0.0.7"
requires "nimcrypto >= 0.3.9"
+requires "karax#b99a543"
diff --git a/public/style.css b/public/style.css
index f4c5375..d3c5799 100644
--- a/public/style.css
+++ b/public/style.css
@@ -63,20 +63,20 @@ a:hover {
margin-left: 58px;
}
-.media-heading {
+.tweet-header {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: .2em;
}
-.media-heading a {
+.tweet-header a {
display: inline-block;
word-break: break-all;
max-width: 100%;
}
-.heading-name-row {
+.tweet-name-row {
padding: 0;
display: flex;
justify-content: space-between;
@@ -132,7 +132,7 @@ a:hover {
font-weight: bold;
}
-.heading-right {
+.tweet-date {
display: flex;
flex-shrink: 0;
margin-left: 4px;
@@ -247,7 +247,7 @@ nav {
overflow: hidden;
}
-.gallery-row .image-attachment, .attachments .image-attachment {
+.gallery-row .still-image, .attachments .image-attachment {
width: 100%;
}
@@ -348,6 +348,7 @@ video {
}
.show-more {
+ background-color: #161616;
text-align: center;
padding: .75em 0;
display: block;
@@ -508,7 +509,7 @@ video {
margin-top: 5px;
}
-.photo-rail-heading {
+.photo-rail-header {
padding: 5px 12px 0px 12px;
}
@@ -627,8 +628,8 @@ video {
}
.thread-line .unavailable::before {
- top: 40px;
- margin-bottom: 19px;
+ top: 48px;
+ margin-bottom: 28px;
}
.thread-last .status-el::before {
@@ -641,7 +642,7 @@ video {
.thread-line .more-replies::before {
content: '...';
background: unset;
- color: #b94e46;
+ color: #ad433b;
font-weight: bold;
font-size: 22px;
line-height: 0.25em;
@@ -750,18 +751,26 @@ video {
}
.timeline-footer, .timeline-header {
- max-width: 550px;
- margin: 0 auto;
+ background-color: #161616;
padding: 6px 0px;
}
-.timeline-none, .timeline-protected {
+.timeline-protected {
+ padding-left: 12px;
+}
+
+.timeline-protected p {
+ margin: 8px 0px;
+}
+
+.timeline-none, .timeline-protected h2 {
color: #ff6c60;
font-size: 21px;
font-weight: 600;
}
.timeline-end {
+ background-color: #161616;
text-align: center;
font-size: 16px;
color: #ff6c60;
@@ -771,14 +780,14 @@ video {
.unavailable-box {
width: 100%;
height: 100%;
- padding: 8px;
+ padding: 12px;
border: solid 1px #404040;
border-radius: 10px;
background-color: #121212;
}
.unavailable-quote {
- padding: 8px;
+ padding: 6px;
}
.quote {
@@ -786,16 +795,17 @@ video {
border: solid 1px #404040;
border-radius: 10px;
background-color: #121212;
+ overflow: auto;
+ padding: 6px;
+ position: relative;
}
.quote:hover {
border-color: #808080;
}
-.quote-container {
- position: relative;
- overflow: auto;
- padding: 6px;
+.quote.unavailable:hover {
+ border-color: #404040;
}
.quote-link {
diff --git a/src/api.nim b/src/api.nim
index 5c334fa..4f36dd8 100644
--- a/src/api.nim
+++ b/src/api.nim
@@ -238,14 +238,15 @@ proc getTweet*(username, id: string): Future[Conversation] {.async.} =
let pollFut = getConversationPolls(result)
await all(vidsFut, pollFut)
-proc finishTimeline(json: JsonNode; query: Option[Query]): Future[Timeline] {.async.} =
+proc finishTimeline(json: JsonNode; query: Option[Query]; after: string): Future[Timeline] {.async.} =
if json == nil: return Timeline()
result = Timeline(
hasMore: json["has_more_items"].to(bool),
maxId: json.getOrDefault("max_position").getStr(""),
minId: json.getOrDefault("min_position").getStr("").cleanPos(),
- query: query
+ query: query,
+ beginning: after.len == 0
)
if json["new_latent_count"].to(int) == 0: return
@@ -281,7 +282,7 @@ proc getTimeline*(username, after: string): Future[Timeline] {.async.} =
params.add {"max_position": after}
let json = await fetchJson(base / (timelineUrl % username) ? params, headers)
- result = await finishTimeline(json, none(Query))
+ result = await finishTimeline(json, none(Query), after)
proc getTimelineSearch*(username, after: string; query: Query): Future[Timeline] {.async.} =
let queryParam = genQueryParam(query)
@@ -308,4 +309,4 @@ proc getTimelineSearch*(username, after: string; query: Query): Future[Timeline]
}
let json = await fetchJson(base / timelineSearchUrl ? params, headers)
- result = await finishTimeline(json, some(query))
+ result = await finishTimeline(json, some(query), after)
diff --git a/src/formatters.nim b/src/formatters.nim
index fe78d69..56b366f 100644
--- a/src/formatters.nim
+++ b/src/formatters.nim
@@ -70,23 +70,6 @@ proc getUserpic*(userpic: string; style=""): string =
proc getUserpic*(profile: Profile; style=""): string =
getUserPic(profile.userpic, style)
-proc genImg*(url: string; class=""): string =
- result = img(src = url.getSigUrl("pic"), class = class, alt = "Image")
-
-proc linkUser*(profile: Profile; class=""): string =
- let
- username = "username" in class
- href = &"/{profile.username}"
- text = if username: "@" & profile.username
- else: xmltree.escape(profile.fullname)
-
- result = a(text, href = href, class = class, title = text)
-
- if not username and profile.verified:
- result &= span("β", class="icon verified-icon", title="Verified account")
- if not username and profile.protected:
- result &= span("π", class="icon protected-icon", title="Protected account")
-
proc pageTitle*(profile: Profile): string =
&"{profile.fullname} (@{profile.username}) | Nitter"
diff --git a/src/nitter.nim b/src/nitter.nim
index cdfa536..71ba075 100644
--- a/src/nitter.nim
+++ b/src/nitter.nim
@@ -3,8 +3,7 @@ import jester, regex
import api, utils, types, cache, formatters, search
-include views/"user.nimf"
-include views/"general.nimf"
+import views/[general, profile, status]
const cacheDir {.strdefine.} = "/tmp/nitter"
@@ -24,7 +23,7 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a
if profile.username.len == 0:
return ""
- let profileHtml = renderProfile(profile, await timelineFut, await railFut, after.len == 0)
+ let profileHtml = renderProfile(profile, await timelineFut, await railFut)
return renderMain(profileHtml, title=pageTitle(profile))
template respTimeline(timeline: typed) =
@@ -34,7 +33,7 @@ template respTimeline(timeline: typed) =
routes:
get "/":
- resp renderMain(renderSearchPanel(), title=pageTitle("Search"))
+ resp renderMain(renderSearch(), title=pageTitle("Search"))
post "/search":
if @"query".len == 0:
diff --git a/src/parser.nim b/src/parser.nim
index 0dea171..5deb817 100644
--- a/src/parser.nim
+++ b/src/parser.nim
@@ -176,5 +176,5 @@ proc parsePhotoRail*(node: XmlNode): seq[GalleryPhoto] =
result.add GalleryPhoto(
url: img.attr("data-image-url"),
tweetId: img.attr("data-tweet-id"),
- color: img.attr("background-color").replace("style", "background-color")
+ color: img.attr("background-color").replace("style: ", "")
)
diff --git a/src/parserutils.nim b/src/parserutils.nim
index 5d808f9..35a086c 100644
--- a/src/parserutils.nim
+++ b/src/parserutils.nim
@@ -91,9 +91,10 @@ proc getBanner*(tweet: XmlNode): string =
result = url.replace("600x200", "1500x500")
else:
result = tweet.selectAttr(".ProfileCard-bg", "style")
+ result = result.replace("background-color: ", "")
if result.len == 0:
- result = "background-color: #161616"
+ result = "#161616"
proc getPopupStats*(profile: var Profile; node: XmlNode) =
for s in node.selectAll( ".ProfileCardStats-statLink"):
diff --git a/src/search.nim b/src/search.nim
index a178a07..89109e0 100644
--- a/src/search.nim
+++ b/src/search.nim
@@ -81,12 +81,3 @@ proc cleanPos*(pos: string): string =
proc genPos*(pos: string): string =
posPrefix & pos & posSuffix
-
-proc tabClass*(timeline: Timeline; tab: string): string =
- result = '"' & "tab-item"
- if timeline.query.isNone:
- if tab == "tweets":
- result &= " active"
- elif $timeline.query.get().queryType == tab:
- result &= " active"
- result &= '"'
diff --git a/src/types.nim b/src/types.nim
index cc9e45e..4aac3b5 100644
--- a/src/types.nim
+++ b/src/types.nim
@@ -124,6 +124,7 @@ type
minId*: string
maxId*: string
hasMore*: bool
+ beginning*: bool
query*: Option[Query]
proc contains*(thread: Thread; tweet: Tweet): bool =
diff --git a/src/views/general.nim b/src/views/general.nim
new file mode 100644
index 0000000..1c5165a
--- /dev/null
+++ b/src/views/general.nim
@@ -0,0 +1,35 @@
+import karax/[karaxdsl, vdom]
+
+const doctype = "\n"
+
+proc renderMain*(body: VNode; title="Nitter"): string =
+ let node = buildHtml(html(lang="en")):
+ head:
+ title: text title
+ link(rel="stylesheet", `type`="text/css", href="/style.css")
+
+ body:
+ nav(id="nav", class="nav-bar container"):
+ tdiv(class="inner-nav"):
+ tdiv(class="item"):
+ a(href="/", class="site-name"): text "nitter"
+
+ tdiv(id="content", class="container"):
+ body
+
+ result = doctype & $node
+
+proc renderSearch*(): VNode =
+ buildHtml(tdiv(class="panel")):
+ tdiv(class="search-panel"):
+ form(`method`="post", action="search"):
+ input(`type`="text", name="query", placeholder="Enter username...")
+ button(`type`="submit"): text "π"
+
+proc renderError*(error: string): VNode =
+ buildHtml(tdiv(class="panel")):
+ tdiv(class="error-panel"):
+ span: text error
+
+proc showError*(error: string): string =
+ renderMain(renderError(error), title = "Error | Nitter")
diff --git a/src/views/general.nimf b/src/views/general.nimf
deleted file mode 100644
index 3f3fee4..0000000
--- a/src/views/general.nimf
+++ /dev/null
@@ -1,49 +0,0 @@
-#? stdtmpl(subsChar = '$', metaChar = '#')
-#import xmltree
-#
-#proc renderMain*(body: string; title="Nitter"): string =
-
-
-
- ${xmltree.escape(title)}
-
-
-
-
-
-
-
-
-
- ${body}
-
-
-
-#end proc
-#
-#proc renderSearchPanel*(): string =
-
-#end proc
-#
-#proc renderError*(error: string): string =
-
-#end proc
-#
-#proc showError*(error: string): string =
-#renderMain(renderError(error), title="Error | Nitter")
-#end proc
diff --git a/src/views/profile.nim b/src/views/profile.nim
new file mode 100644
index 0000000..9255acc
--- /dev/null
+++ b/src/views/profile.nim
@@ -0,0 +1,66 @@
+import strutils, strformat
+import karax/[karaxdsl, vdom, vstyles]
+
+import ../types, ../utils, ../formatters
+import tweet, timeline, renderutils
+
+proc renderStat(stat, text: string): VNode =
+ buildHtml(li(class=text)):
+ span(class="profile-stat-header"): text capitalizeAscii(text)
+ span(class="profile-stat-num"): text stat
+
+proc renderProfileCard*(profile: Profile): VNode =
+ buildHtml(tdiv(class="profile-card")):
+ a(class="profile-card-avatar", href=profile.getUserPic().getSigUrl("pic")):
+ genImg(profile.getUserpic("_200x200"))
+
+ tdiv(class="profile-card-tabs"):
+ tdiv(class="profile-card-tabs-name"):
+ linkUser(profile, class="profile-card-fullname")
+ linkUser(profile, class="profile-card-username")
+
+ tdiv(class="profile-card-extra"):
+ if profile.bio.len > 0:
+ tdiv(class="profile-bio"):
+ p: verbatim linkifyText(profile.bio)
+
+ tdiv(class="profile-card-extra-links"):
+ ul(class="profile-statlist"):
+ renderStat(profile.tweets, "tweets")
+ renderStat(profile.followers, "followers")
+ renderStat(profile.following, "following")
+
+proc renderPhotoRail(username: string; photoRail: seq[GalleryPhoto]): VNode =
+ buildHtml(tdiv(class="photo-rail-card")):
+ tdiv(class="photo-rail-header"):
+ a(href=(&"/{username}/media")):
+ text "πΌ Photos and videos"
+
+ tdiv(class="photo-rail-grid"):
+ for i, photo in photoRail:
+ if i == 16: break
+ a(href=(&"/{username}/status/{photo.tweetId}"),
+ style={backgroundColor: photo.color}):
+ genImg(photo.url & ":thumb")
+
+proc renderBanner(profile: Profile): VNode =
+ buildHtml():
+ if "#" in profile.banner:
+ tdiv(class="profile-banner-color", style={backgroundColor: profile.banner})
+ else:
+ a(href=getSigUrl(profile.banner, "pic")):
+ genImg(profile.banner)
+
+proc renderProfile*(profile: Profile; timeline: Timeline;
+ photoRail: seq[GalleryPhoto]): VNode =
+ buildHtml(tdiv(class="profile-tabs")):
+ tdiv(class="profile-banner"):
+ renderBanner(profile)
+
+ tdiv(class="profile-tab"):
+ renderProfileCard(profile)
+ if photoRail.len > 0:
+ renderPhotoRail(profile.username, photoRail)
+
+ tdiv(class="timeline-tab"):
+ renderTimeline(timeline, profile)
diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim
new file mode 100644
index 0000000..2f501f0
--- /dev/null
+++ b/src/views/renderutils.nim
@@ -0,0 +1,21 @@
+import karax/[karaxdsl, vdom, vstyles]
+
+import ../types, ../utils
+
+proc linkUser*(profile: Profile, class=""): VNode =
+ let
+ isName = "username" notin class
+ href = "/" & profile.username
+ nameText = if isName: profile.fullname
+ else: "@" & profile.username
+
+ buildHtml(a(href=href, class=class, title=nameText)):
+ text nameText
+ if isName and profile.verified:
+ span(class="icon verified-icon", title="Verified account"): text "β"
+ if isName and profile.protected:
+ span(class="icon protected-icon", title="Protected account"): text "π"
+
+proc genImg*(url: string; class=""): VNode =
+ buildHtml():
+ img(src=url.getSigUrl("pic"), class=class, alt="Image")
diff --git a/src/views/status.nim b/src/views/status.nim
new file mode 100644
index 0000000..a36e9f9
--- /dev/null
+++ b/src/views/status.nim
@@ -0,0 +1,42 @@
+import strutils, strformat
+import karax/[karaxdsl, vdom]
+
+import ../types
+import tweet, renderutils
+
+proc renderReplyThread(thread: Thread): VNode =
+ buildHtml(tdiv(class="reply thread thread-line")):
+ for i, tweet in thread.tweets:
+ let last = (i == thread.tweets.high and thread.more == 0)
+ renderTweet(tweet, index=i, last=last)
+
+ if thread.more != 0:
+ let num = if thread.more != -1: $thread.more & " " else: ""
+ let reply = if thread.more == 1: "reply" else: "replies"
+ tdiv(class="status-el more-replies"):
+ a(class="more-replies-text", title="Not implemented yet"):
+ text $num & "more " & reply
+
+proc renderConversation*(conversation: Conversation): VNode =
+ let hasAfter = conversation.after != nil
+ buildHtml(tdiv(class="conversation", id="tweets")):
+ tdiv(class="main-thread"):
+ if conversation.before != nil:
+ tdiv(class="before-tweet thread-line"):
+ for i, tweet in conversation.before.tweets:
+ renderTweet(tweet, index=i)
+
+ tdiv(class="main-tweet"):
+ let afterClass = if hasAfter: "thread thread-line" else: ""
+ renderTweet(conversation.tweet, class=afterClass)
+
+ if hasAfter:
+ tdiv(class="after-tweet thread-line"):
+ let total = conversation.after.tweets.high
+ for i, tweet in conversation.after.tweets:
+ renderTweet(tweet, index=i, total=total)
+
+ if conversation.replies.len > 0:
+ tdiv(class="replies"):
+ for thread in conversation.replies:
+ renderReplyThread(thread)
diff --git a/src/views/timeline.nim b/src/views/timeline.nim
new file mode 100644
index 0000000..cb1d32e
--- /dev/null
+++ b/src/views/timeline.nim
@@ -0,0 +1,93 @@
+import strutils, strformat, algorithm, times
+import karax/[karaxdsl, vdom, vstyles]
+
+import ../types, ../search
+import tweet, renderutils
+
+proc getQuery(timeline: Timeline): string =
+ if timeline.query.isNone: "?"
+ else: genQueryUrl(get(timeline.query))
+
+proc getTabClass(timeline: Timeline; tab: string): string =
+ var classes = @["tab-item"]
+
+ if timeline.query.isNone:
+ if tab == "tweets":
+ classes.add "active"
+ elif $timeline.query.get().queryType == tab:
+ classes.add "active"
+
+ return classes.join(" ")
+
+proc renderSearchTabs(timeline: Timeline; profile: Profile): VNode =
+ let link = "/" & profile.username
+ buildHtml(ul(class="tab")):
+ li(class=timeline.getTabClass("tweets")):
+ a(href=link): text "Tweets"
+ li(class=timeline.getTabClass("replies")):
+ a(href=(link & "/replies")): text "Tweets & Replies"
+ li(class=timeline.getTabClass("media")):
+ a(href=(link & "/media")): text "Media"
+
+proc renderNewer(timeline: Timeline; profile: Profile): VNode =
+ buildHtml(tdiv(class="status-el show-more")):
+ a(href=("/" & profile.username & getQuery(timeline).strip(chars={'?'}))):
+ text "Load newest tweets"
+
+proc renderOlder(timeline: Timeline; profile: Profile): VNode =
+ buildHtml(tdiv(class="show-more")):
+ a(href=(&"/{profile.username}{getQuery(timeline)}after={timeline.minId}")):
+ text "Load older tweets"
+
+proc renderNoMore(): VNode =
+ buildHtml(tdiv(class="timeline-footer")):
+ h2(class="timeline-end", style={textAlign: "center"}):
+ text "No more tweets."
+
+proc renderNoneFound(): VNode =
+ buildHtml(tdiv(class="timeline-header")):
+ h2(class="timeline-none", style={textAlign: "center"}):
+ text "No tweets found."
+
+proc renderProtected(username: string): VNode =
+ buildHtml(tdiv(class="timeline-header timeline-protected")):
+ h2: text "This account's tweets are protected."
+ p: text &"Only confirmed followers have access to @{username}'s tweets."
+
+proc renderThread(thread: seq[Tweet]): VNode =
+ buildHtml(tdiv(class="timeline-tweet thread-line")):
+ for i, threadTweet in thread.sortedByIt(it.time):
+ renderTweet(threadTweet, "thread", index=i, total=thread.high)
+
+proc threadFilter(it: Tweet; tweetThread: string): bool =
+ it.retweet.isNone and it.reply.len == 0 and it.threadId == tweetThread
+
+proc renderTweets(timeline: Timeline): VNode =
+ buildHtml(tdiv(id="tweets")):
+ var threads: seq[string]
+ for tweet in timeline.tweets:
+ if tweet.threadId in threads: continue
+ let thread = timeline.tweets.filterIt(threadFilter(it, tweet.threadId))
+ if thread.len < 2:
+ renderTweet(tweet, "timeline-tweet")
+ else:
+ renderThread(thread)
+ threads &= tweet.threadId
+
+proc renderTimeline*(timeline: Timeline; profile: Profile): VNode =
+ buildHtml(tdiv):
+ renderSearchTabs(timeline, profile)
+
+ if not profile.protected and not timeline.beginning:
+ renderNewer(timeline, profile)
+
+ if profile.protected:
+ renderProtected(profile.username)
+ elif timeline.tweets.len == 0:
+ renderNoneFound()
+ else:
+ renderTweets(timeline)
+ if timeline.hasMore or timeline.query.isSome:
+ renderOlder(timeline, profile)
+ else:
+ renderNoMore()
diff --git a/src/views/tweet.nim b/src/views/tweet.nim
new file mode 100644
index 0000000..3ffd50e
--- /dev/null
+++ b/src/views/tweet.nim
@@ -0,0 +1,176 @@
+import strutils
+import karax/[karaxdsl, vdom, vstyles]
+
+import ../types, ../utils, ../formatters
+import renderutils
+
+proc renderHeader(tweet: Tweet): VNode =
+ buildHtml(tdiv):
+ if tweet.retweet.isSome:
+ tdiv(class="retweet"):
+ span: text "π " & get(tweet.retweet).by & " retweeted"
+ if tweet.pinned:
+ tdiv(class="pinned"):
+ span: text "π Pinned Tweet"
+
+ tdiv(class="tweet-header"):
+ tdiv(class="tweet-name-row"):
+ a(class="tweet-avatar", href=("/" & tweet.profile.username)):
+ genImg(tweet.profile.getUserpic("_bigger"), class="avatar")
+
+ tdiv(class="fullname-and-username"):
+ linkUser(tweet.profile, class="fullname")
+ linkUser(tweet.profile, class="username")
+
+ span(class="tweet-date"):
+ a(href=getLink(tweet), title=tweet.getTime()):
+ text tweet.shortTime
+
+proc renderAlbum(tweet: Tweet): VNode =
+ let
+ groups = if tweet.photos.len < 3: @[tweet.photos]
+ else: tweet.photos.distribute(2)
+ class = if groups.len == 1 and groups[0].len == 1: "single-image"
+ else: ""
+
+ buildHtml(tdiv(class=("attachments " & class))):
+ for i, photos in groups:
+ let margin = if i > 0: ".25em" else: ""
+ let flex = if photos.len > 1 or groups.len > 1: "flex" else: "block"
+ tdiv(class="gallery-row", style={marginTop: margin}):
+ for photo in photos:
+ tdiv(class="attachment image"):
+ a(href=getSigUrl(photo & "?name=orig", "pic"), class="still-image",
+ target="_blank", style={display: flex}):
+ genImg(photo)
+
+proc renderVideo(video: Video): VNode =
+ buildHtml(tdiv(class="attachments")):
+ tdiv(class="gallery-video"):
+ tdiv(class="attachment video-container"):
+ case video.playbackType
+ of mp4:
+ video(poster=video.thumb.getSigUrl("pic"), controls=""):
+ source(src=video.url.getSigUrl("video"), `type`="video/mp4")
+ of m3u8, vmap:
+ video(poster=video.thumb.getSigUrl("pic"))
+ tdiv(class="video-overlay"):
+ p: text "Video playback not supported"
+
+proc renderGif(gif: Gif): VNode =
+ buildHtml(tdiv(class="attachments media-gif")):
+ tdiv(class="gallery-gif", style=style(maxHeight, "unset")):
+ tdiv(class="attachment"):
+ video(class="gif", poster=gif.thumb.getSigUrl("pic"),
+ autoplay="", muted="", loop=""):
+ source(src=gif.url.getSigUrl("video"), `type`="video/mp4")
+
+proc renderPoll(poll: Poll): VNode =
+ buildHtml(tdiv(class="poll")):
+ for i in 0 ..< poll.options.len:
+ let leader = if poll.leader == i: " leader" else: ""
+ let perc = $poll.values[i] & "%"
+ tdiv(class=("poll-meter" & leader)):
+ span(class="poll-choice-bar", style=style(width, perc))
+ span(class="poll-choice-value"): text perc
+ span(class="poll-choice-option"): text poll.options[i]
+ span(class="poll-info"):
+ text $poll.votes & " votes β’ " & poll.status
+
+proc renderStats(stats: TweetStats): VNode =
+ buildHtml(tdiv(class="tweet-stats")):
+ span(class="tweet-stat"): text "π¬ " & $stats.replies
+ span(class="tweet-stat"): text "π " & $stats.retweets
+ span(class="tweet-stat"): text "π " & $stats.likes
+
+proc renderReply(tweet: Tweet): VNode =
+ buildHtml(tdiv(class="replying-to")):
+ text "Replying to "
+ for i, u in tweet.reply:
+ if i > 0: text " "
+ a(href=("/" & u)): text "@" & u
+
+proc renderReply(quote: Quote): VNode =
+ buildHtml(tdiv(class="replying-to")):
+ text "Replying to "
+ for i, u in quote.reply:
+ if i > 0: text " "
+ a(href=("/" & u)): text "@" & u
+
+proc renderQuoteMedia(quote: Quote): VNode =
+ buildHtml(tdiv(class="quote-media-container")):
+ if quote.thumb.len > 0:
+ tdiv(class="quote-media"):
+ genImg(quote.thumb)
+ if quote.badge.len > 0:
+ tdiv(class="quote-badge"):
+ tdiv(class="quote-badge-text"): text quote.badge
+ elif quote.sensitive:
+ tdiv(class="quote-sensitive"):
+ span(class="icon quote-sensitive-icon"): text "β"
+
+proc renderQuote(quote: Quote): VNode =
+ if not quote.available:
+ return buildHtml(tdiv(class="quote unavailable")):
+ tdiv(class="unavailable-quote"):
+ text "This tweet is unavailable"
+
+ buildHtml(tdiv(class="quote")):
+ a(class="quote-link", href=getLink(quote))
+
+ if quote.thumb.len > 0 or quote.sensitive:
+ renderQuoteMedia(quote)
+
+ tdiv(class="fullname-and-username"):
+ linkUser(quote.profile, class="fullname")
+ linkUser(quote.profile, class="username")
+
+ if quote.reply.len > 0:
+ renderReply(quote)
+
+ tdiv(class="quote-text"):
+ verbatim linkifyText(quote.text)
+
+ if quote.hasThread:
+ a(href=getLink(quote)):
+ text "Show this thread"
+
+proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNode =
+ var divClass = class
+ if index == total or last:
+ divClass = "thread-last " & class
+
+ if not tweet.available:
+ return buildHtml(tdiv(class=divClass)):
+ tdiv(class="status-el unavailable"):
+ tdiv(class="unavailable-box"):
+ text "This tweet is unavailable"
+
+ buildHtml(tdiv(class=divClass)):
+ tdiv(class="status-el"):
+ tdiv(class="status-body"):
+ renderHeader(tweet)
+
+ if index == 0 and tweet.reply.len > 0:
+ renderReply(tweet)
+
+ tdiv(class="status-content media-body"):
+ verbatim linkifyText(tweet.text)
+
+ if tweet.quote.isSome:
+ renderQuote(tweet.quote.get())
+
+ if tweet.photos.len > 0:
+ renderAlbum(tweet)
+ elif tweet.video.isSome:
+ renderVideo(tweet.video.get())
+ elif tweet.gif.isSome:
+ renderGif(tweet.gif.get())
+ elif tweet.poll.isSome:
+ renderPoll(tweet.poll.get())
+
+ renderStats(tweet.stats)
+
+ if tweet.hasThread and "timeline" in class:
+ a(href=getLink(tweet)):
+ text "Show this thread"
diff --git a/src/views/tweet.nimf b/src/views/tweet.nimf
deleted file mode 100644
index 630bc09..0000000
--- a/src/views/tweet.nimf
+++ /dev/null
@@ -1,204 +0,0 @@
-#? stdtmpl(subsChar = '$', metaChar = '#')
-#import xmltree, strutils, strformat, sequtils, times, uri
-#import ../types, ../formatters, ../utils
-#
-#proc renderHeading(tweet: Tweet): string =
-#if tweet.retweet.isSome:
-
-#end if
-#if tweet.pinned:
-
- π Pinned Tweet
-
-#end if
-
-#end proc
-#
-#proc renderMediaGroup(tweet: Tweet): string =
-#let groups = if tweet.photos.len > 2: tweet.photos.distribute(2) else: @[tweet.photos]
-#let class = if groups.len == 1 and groups[0].len == 1: "single-image" else: ""
-#var first = true
-
-#for photos in groups:
- #let margin = if not first: "margin-top: .25em;" else: ""
- #let flex = if photos.len > 1 or groups.len > 1: "display: flex;" else: ""
-
- #for photo in photos:
-
- #end for
-
- #first = false
-#end for
-
-#end proc
-#
-#proc renderVideo(video: Video): string =
-
-
-
- #case video.playbackType
- #of mp4:
-
-
-
- #of m3u8, vmap:
-
-
-
Video playback not supported
-
- #end case
-
-
-
-#end proc
-#
-#proc renderGif(gif: Gif): string =
-
-#end proc
-#
-#proc renderPoll(poll: Poll): string =
-
- #for i in 0 ..< poll.options.len:
- #let leader = if poll.leader == i: " leader" else: ""
-
-
- ${poll.values[i]}%
- ${poll.options[i]}
-
- #end for
-
${poll.votes} votes β’ ${poll.status}
-
-#end proc
-#
-#proc renderStats(stats: TweetStats): string =
-
-#end proc
-#
-#proc renderShowThread(tweet: Tweet | Quote): string =
-Show this thread
-#end proc
-#
-#proc renderReply(tweet: Tweet | Quote): string =
-#let usernames = tweet.reply.mapIt(&"""@{it} """)
-Replying to ${usernames.join(" ")}
-#end proc
-#
-#proc renderQuote(quote: Quote): string =
-#let hasMedia = quote.thumb.len > 0 or quote.sensitive
-#if not quote.available:
-
-
This tweet is unavailable
-
-#return
-#end if
-
-
-
- #if hasMedia:
-
- #end if
-
- ${linkUser(quote.profile, class="fullname")}
- ${linkUser(quote.profile, class="username")}
-
- #if quote.reply.len > 0:
- ${renderReply(quote)}
- #end if
-
${linkifyText(quote.text)}
- #if quote.hasThread:
- ${renderShowThread(quote)}
- #end if
-
-
-#end proc
-#
-#proc renderTweet*(tweet: Tweet; class=""; first=true; last=false): string =
-#var divClass = if last: "thread-last " & class else: class
-#if divClass.len > 0:
-
-#end if
-#if tweet.available:
-
-
- ${renderHeading(tweet)}
- #if first and tweet.reply.len > 0:
- ${renderReply(tweet)}
- #end if
-
-
${linkifyText(tweet.text)}
-
- #if tweet.photos.len > 0:
- ${renderMediaGroup(tweet)}
- #elif tweet.video.isSome:
- ${renderVideo(tweet.video.get())}
- #elif tweet.gif.isSome:
- ${renderGif(tweet.gif.get())}
- #elif tweet.quote.isSome:
- ${renderQuote(tweet.quote.get())}
- #elif tweet.poll.isSome:
- ${renderPoll(tweet.poll.get())}
- #end if
- ${renderStats(tweet.stats)}
- #if tweet.hasThread and "timeline" in class:
- ${renderShowThread(tweet)}
- #end if
-
-
-#else:
-
-
This tweet is unavailable
-
-#end if
-#if divClass.len > 0:
-
-#end if
-#end proc
diff --git a/src/views/user.nimf b/src/views/user.nimf
deleted file mode 100644
index 1c50f32..0000000
--- a/src/views/user.nimf
+++ /dev/null
@@ -1,192 +0,0 @@
-#? stdtmpl(subsChar = '$', metaChar = '#')
-#import xmltree, strutils, uri, algorithm
-#import ../types, ../formatters, ../utils, ../search
-#include "tweet.nimf"
-#
-#proc renderProfileCard*(profile: Profile): string =
-
-#end proc
-#
-#proc renderPhotoRail(username: string; photoRail: seq[GalleryPhoto]): string =
-
-
-
- #for i, photo in photoRail:
- #if i == 20: break
- #end if
-
-
-
- #end for
-
-
-#end proc
-#
-#proc renderBanner(profile: Profile): string =
-#if "#" in profile.banner:
-
-#else:
-#let url = getSigUrl(profile.banner, "pic")
-${genImg(profile.banner)}
-#end if
-#end proc
-#
-#proc renderTimeline*(timeline: Timeline; profile: Profile; beginning: bool): string =
-#var threads: seq[string]
-#var query = "?"
-#if timeline.query.isSome: query = genQueryUrl(get(timeline.query))
-#end if
-
-#end proc
-#
-#proc renderProfile*(profile: Profile; timeline: Timeline;
-# photoRail: seq[GalleryPhoto]; beginning: bool): string =
-
-
- ${renderBanner(profile)}
-
-
- ${renderProfileCard(profile)}
- #if photoRail.len > 0:
- ${renderPhotoRail(profile.username, photoRail)}
- #end if
-
-
- #let link = "/" & profile.username
-
- ${renderTimeline(timeline, profile, beginning)}
-
-
-#end proc
-#
-#proc renderConversation*(conversation: Conversation): string =
-
-
-#end proc
diff --git a/tests/base.py b/tests/base.py
index 9d84916..6538492 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -3,11 +3,11 @@ from seleniumbase import BaseCase
class Tweet(object):
def __init__(self, tweet=''):
- namerow = tweet + 'div.media-heading '
+ namerow = tweet + '.tweet-header '
self.fullname = namerow + '.fullname'
self.username = namerow + '.username'
- self.date = tweet + 'div.media-heading .heading-right'
- self.text = tweet + '.status-content-wrapper .status-content.media-body'
+ self.date = namerow + '.tweet-date'
+ self.text = tweet + '.status-content.media-body'
self.retweet = tweet = '.retweet'
@@ -21,7 +21,7 @@ class Profile(object):
class Timeline(object):
- newest = 'div[class="show-more status-el"]'
+ newest = 'div[class="status-el show-more"]'
older = 'div[class="show-more"]'
end = '.timeline-end'
none = '.timeline-none'
diff --git a/tests/test_profile.py b/tests/test_profile.py
index bf080f6..8d3ebae 100644
--- a/tests/test_profile.py
+++ b/tests/test_profile.py
@@ -10,8 +10,8 @@ profiles = [
verified = [['jack'], ['elonmusk']]
protected = [
- ['mobile_test_7', 'mobile test 7', ''],
- ['Poop', 'Randy', 'Social media fanatic.']
+ ['mobile_test_7', 'mobile test 7π', ''],
+ ['Poop', 'Randyπ', 'Social media fanatic.']
]
invalid = [['thisprofiledoesntexist'], ['%']]
diff --git a/tests/test_tweet.py b/tests/test_tweet.py
index 6671ecb..c62f1c8 100644
--- a/tests/test_tweet.py
+++ b/tests/test_tweet.py
@@ -16,7 +16,7 @@ timeline = [
]
status = [
- [20, 'jack πππ', 'jack', '21 Mar 2006', 'just setting up my twttr'],
+ [20, 'jack πππβ', 'jack', '21 Mar 2006', 'just setting up my twttr'],
[134849778302464000, 'The Twoffice', 'TheTwoffice', '10 Nov 2011', 'test'],
[105685475985080322, 'The Twoffice', 'TheTwoffice', '22 Aug 2011', 'regular tweet'],
[572593440719912960, 'Test account', 'mobile_test', '2 Mar 2015', 'testing test']
@@ -77,7 +77,7 @@ emoji = [
retweet = [
[7, 'mobile_test_2', 'mobile test 2', 'Test account', '@mobile_test', '1234'],
- [3, 'mobile_test_8', 'mobile test 8', 'jack πππ', '@jack', 'twttr']
+ [3, 'mobile_test_8', 'mobile test 8', 'jack πππβ', '@jack', 'twttr']
]