Use Karax html rendering instead of source filters (#2)
* Use Karax html rendering instead of source filters
このコミットが含まれているのは:
コミット
ab36664ad2
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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: ", "")
|
||||
)
|
||||
|
@ -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"):
|
||||
|
@ -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 &= '"'
|
||||
|
@ -124,6 +124,7 @@ type
|
||||
minId*: string
|
||||
maxId*: string
|
||||
hasMore*: bool
|
||||
beginning*: bool
|
||||
query*: Option[Query]
|
||||
|
||||
proc contains*(thread: Thread; tweet: Tweet): bool =
|
||||
|
35
src/views/general.nim
ノーマルファイル
35
src/views/general.nim
ノーマルファイル
@ -0,0 +1,35 @@
|
||||
import karax/[karaxdsl, vdom]
|
||||
|
||||
const doctype = "<!DOCTYPE html>\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")
|
@ -1,49 +0,0 @@
|
||||
#? stdtmpl(subsChar = '$', metaChar = '#')
|
||||
#import xmltree
|
||||
#
|
||||
#proc renderMain*(body: string; title="Nitter"): string =
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>${xmltree.escape(title)}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav id="nav" class="nav-bar container">
|
||||
<div class="inner-nav">
|
||||
<div class="item">
|
||||
<a href="/" class="site-name">nitter</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="content" class="container">
|
||||
${body}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
#end proc
|
||||
#
|
||||
#proc renderSearchPanel*(): string =
|
||||
<div class="panel">
|
||||
<div class="search-panel">
|
||||
<form action="search" method="post">
|
||||
<input type="text" name="query" placeholder="Enter username...">
|
||||
<button type="submit" name="button">🔎</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderError*(error: string): string =
|
||||
<div class="panel">
|
||||
<div class="error-panel">
|
||||
<span>${error}</span>
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc showError*(error: string): string =
|
||||
#renderMain(renderError(error), title="Error | Nitter")
|
||||
#end proc
|
66
src/views/profile.nim
ノーマルファイル
66
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)
|
21
src/views/renderutils.nim
ノーマルファイル
21
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")
|
42
src/views/status.nim
ノーマルファイル
42
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)
|
93
src/views/timeline.nim
ノーマルファイル
93
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()
|
176
src/views/tweet.nim
ノーマルファイル
176
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"
|
@ -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:
|
||||
<div class="retweet">
|
||||
<span>🔄 ${get(tweet.retweet).by} retweeted</span>
|
||||
</div>
|
||||
#end if
|
||||
#if tweet.pinned:
|
||||
<div class="pinned">
|
||||
<span>📌 Pinned Tweet</span>
|
||||
</div>
|
||||
#end if
|
||||
<div class="media-heading">
|
||||
<div class="heading-name-row">
|
||||
<a class="tweet-avatar" href="/${tweet.profile.username}">
|
||||
${genImg(tweet.profile.getUserpic("_bigger"), "avatar")}
|
||||
</a>
|
||||
<div class="fullname-and-username">
|
||||
${linkUser(tweet.profile, class="fullname")}
|
||||
${linkUser(tweet.profile, class="username")}
|
||||
</div>
|
||||
<span class="heading-right">
|
||||
<a href="${getLink(tweet)}" title="${tweet.getTime()}">${tweet.shortTime}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
#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
|
||||
<div class="attachments ${class}">
|
||||
#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: ""
|
||||
<div class="gallery-row cover-fit" style="${margin}">
|
||||
#for photo in photos:
|
||||
<div class="attachment image">
|
||||
##TODO: why doesn't this work?
|
||||
<a href=${getSigUrl(photo & "?name=orig", "pic")} target="_blank" class="image-attachment">
|
||||
<div class="still-image" style="${flex}">
|
||||
${genImg(photo)}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
#end for
|
||||
</div>
|
||||
#first = false
|
||||
#end for
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderVideo(video: Video): string =
|
||||
<div class="attachments">
|
||||
<div class="gallery-video">
|
||||
<div 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">
|
||||
</video>
|
||||
#of m3u8, vmap:
|
||||
<video poster=${video.thumb.getSigUrl("pic")} autoplay muted loop></video>
|
||||
<div class="video-overlay">
|
||||
<p>Video playback not supported</p>
|
||||
</div>
|
||||
#end case
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderGif(gif: Gif): string =
|
||||
<div class="attachments media-gif">
|
||||
<div class="gallery-gif" style="max-height: unset;">
|
||||
<div class="attachment">
|
||||
<video class="gif" poster=${gif.thumb.getSigUrl("pic")} autoplay muted loop>
|
||||
<source src=${gif.url.getSigUrl("video")} type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderPoll(poll: Poll): string =
|
||||
<div class="poll">
|
||||
#for i in 0 ..< poll.options.len:
|
||||
#let leader = if poll.leader == i: " leader" else: ""
|
||||
<div class="poll-meter${leader}">
|
||||
<span class="poll-choice-bar" style="width: ${poll.values[i]}%"></span>
|
||||
<span class="poll-choice-value">${poll.values[i]}%</span>
|
||||
<span class="poll-choice-option">${poll.options[i]}</span>
|
||||
</div>
|
||||
#end for
|
||||
<span class="poll-info">${poll.votes} votes • ${poll.status}</span>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderStats(stats: TweetStats): string =
|
||||
<div class="tweet-stats">
|
||||
<span class="tweet-stat">💬 ${$stats.replies}</span>
|
||||
<span class="tweet-stat">🔄 ${$stats.retweets}</span>
|
||||
<span class="tweet-stat">👍 ${$stats.likes}</span>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderShowThread(tweet: Tweet | Quote): string =
|
||||
<a href="${getLink(tweet)}">Show this thread</a>
|
||||
#end proc
|
||||
#
|
||||
#proc renderReply(tweet: Tweet | Quote): string =
|
||||
#let usernames = tweet.reply.mapIt(&"""<a href="/{it}">@{it}</a>""")
|
||||
<div class="replying-to">Replying to ${usernames.join(" ")}</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderQuote(quote: Quote): string =
|
||||
#let hasMedia = quote.thumb.len > 0 or quote.sensitive
|
||||
#if not quote.available:
|
||||
<div class="quote unavailable">
|
||||
<div class="unavailable-quote">This tweet is unavailable</div>
|
||||
</div>
|
||||
#return
|
||||
#end if
|
||||
<div class="quote">
|
||||
<div class="quote-container">
|
||||
<a class="quote-link" href="${getLink(quote)}"></a>
|
||||
#if hasMedia:
|
||||
<div class="quote-media-container">
|
||||
<div class="quote-media">
|
||||
#if quote.thumb.len > 0:
|
||||
${genImg(quote.thumb)}
|
||||
#if quote.badge.len > 0:
|
||||
<div class="quote-badge">
|
||||
<div class="quote-badge-text">${quote.badge}</div>
|
||||
</div>
|
||||
#end if
|
||||
#elif quote.sensitive:
|
||||
<div class="quote-sensitive">
|
||||
<span class="icon quote-sensitive-icon">❗</span>
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
#end if
|
||||
<div class="fullname-and-username">
|
||||
${linkUser(quote.profile, class="fullname")}
|
||||
${linkUser(quote.profile, class="username")}
|
||||
</div>
|
||||
#if quote.reply.len > 0:
|
||||
${renderReply(quote)}
|
||||
#end if
|
||||
<div class="quote-text">${linkifyText(quote.text)}</div>
|
||||
#if quote.hasThread:
|
||||
${renderShowThread(quote)}
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
#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:
|
||||
<div class="${divClass}">
|
||||
#end if
|
||||
#if tweet.available:
|
||||
<div class="status-el">
|
||||
<div class="status-body">
|
||||
${renderHeading(tweet)}
|
||||
#if first and tweet.reply.len > 0:
|
||||
${renderReply(tweet)}
|
||||
#end if
|
||||
<div class="status-content-wrapper">
|
||||
<div class="status-content media-body">${linkifyText(tweet.text)}</div>
|
||||
</div>
|
||||
#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
|
||||
</div>
|
||||
</div>
|
||||
#else:
|
||||
<div class="status-el unavailable">
|
||||
<div class="unavailable-box">This tweet is unavailable</div>
|
||||
</div>
|
||||
#end if
|
||||
#if divClass.len > 0:
|
||||
</div>
|
||||
#end if
|
||||
#end proc
|
@ -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 =
|
||||
<div class="profile-card">
|
||||
<a class="profile-card-avatar" href="${profile.getUserPic().getSigUrl("pic")}">
|
||||
${genImg(profile.getUserpic("_200x200"))}
|
||||
</a>
|
||||
<div class="profile-card-tabs">
|
||||
<div class="profile-card-tabs-name">
|
||||
${linkUser(profile, class="profile-card-fullname")}
|
||||
${linkUser(profile, class="profile-card-username")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-card-extra">
|
||||
#if profile.bio.len > 0:
|
||||
<div class="profile-bio">
|
||||
<p>${linkifyText(profile.bio)}</p>
|
||||
</div>
|
||||
#end if
|
||||
|
||||
<div class="profile-card-extra-links">
|
||||
<ul class="profile-statlist">
|
||||
<li class="tweets">
|
||||
<span class="profile-stat-header">Tweets</span>
|
||||
<span class="profile-stat-num">${$profile.tweets}</span>
|
||||
</li>
|
||||
<li class="followers">
|
||||
<span class="profile-stat-header">Followers</span>
|
||||
<span class="profile-stat-num">${$profile.followers}</span>
|
||||
</li>
|
||||
<li class="following">
|
||||
<span class="profile-stat-header">Following</span>
|
||||
<span class="profile-stat-num">${$profile.following}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderPhotoRail(username: string; photoRail: seq[GalleryPhoto]): string =
|
||||
<div class="photo-rail-card">
|
||||
<div class="photo-rail-heading">
|
||||
<a href="/${username}/media">🖼 Photos and videos</a>
|
||||
</div>
|
||||
<div class="photo-rail-grid">
|
||||
#for i, photo in photoRail:
|
||||
#if i == 20: break
|
||||
#end if
|
||||
<a href="/${username}/status/${photo.tweetId}" style="${photo.color}">
|
||||
<img src=${getSigUrl(photo.url & ":thumb", "pic")}></img>
|
||||
</a>
|
||||
#end for
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderBanner(profile: Profile): string =
|
||||
#if "#" in profile.banner:
|
||||
<div style="${profile.banner}" class="profile-banner-color"></div>
|
||||
#else:
|
||||
#let url = getSigUrl(profile.banner, "pic")
|
||||
<a href="${url}">${genImg(profile.banner)}</a>
|
||||
#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
|
||||
<div id="tweets">
|
||||
#if not beginning:
|
||||
<div class="show-more status-el">
|
||||
<a href="/${profile.username}${query.strip(chars={'?'})}">Load newest tweets</a>
|
||||
</div>
|
||||
#end if
|
||||
#
|
||||
#for tweet in timeline.tweets:
|
||||
#if tweet.threadId in threads: continue
|
||||
#end if
|
||||
#proc threadFilter(it: Tweet): bool =
|
||||
#it.retweet.isNone and it.reply.len == 0 and it.threadId == tweet.threadId
|
||||
#end proc
|
||||
#let thread = timeline.tweets.filter(threadFilter)
|
||||
#if thread.len < 2:
|
||||
${renderTweet(tweet, "timeline-tweet")}
|
||||
#else:
|
||||
<div class="thread-line">
|
||||
#for i, threadTweet in thread.sortedByIt(it.time):
|
||||
#let last = (i == thread.high)
|
||||
#let class = if last: "timeline-tweet" else: "thread"
|
||||
${renderTweet(threadTweet, class, first=(i == 0), last=last)}
|
||||
#end for
|
||||
</div>
|
||||
#threads.add tweet.threadId
|
||||
#end if
|
||||
#end for
|
||||
#
|
||||
#if timeline.hasMore or timeline.query.isSome and timeline.tweets.len > 0:
|
||||
<div class="show-more">
|
||||
<a href="/${profile.username}${query}after=${timeline.minId}">Load older tweets</a>
|
||||
</div>
|
||||
#elif timeline.tweets.len > 0:
|
||||
<div class="timeline-footer">
|
||||
<h2 class="timeline-end" style="text-align: center;">No more tweets.</h2>
|
||||
</div>
|
||||
#else:
|
||||
<div class="timeline-header">
|
||||
#if profile.protected:
|
||||
<h2 class="timeline-protected">This account's tweets are protected.</h2>
|
||||
<p>Only confirmed followers have access to @${profile.username}'s tweets.</p>
|
||||
#else:
|
||||
<h2 class="timeline-none" style="text-align: center;">No tweets found.</h2>
|
||||
#end if
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderProfile*(profile: Profile; timeline: Timeline;
|
||||
# photoRail: seq[GalleryPhoto]; beginning: bool): string =
|
||||
<div class="profile-tabs">
|
||||
<div class="profile-banner">
|
||||
${renderBanner(profile)}
|
||||
</div>
|
||||
<div class="profile-tab">
|
||||
${renderProfileCard(profile)}
|
||||
#if photoRail.len > 0:
|
||||
${renderPhotoRail(profile.username, photoRail)}
|
||||
#end if
|
||||
</div>
|
||||
<div class="timeline-tab">
|
||||
#let link = "/" & profile.username
|
||||
<ul class="tab">
|
||||
<li class=${timeline.tabClass("tweets")}><a href="${link}">Tweets</a></li>
|
||||
<li class=${timeline.tabClass("replies")}><a href="${link}/replies">Tweets & Replies</a></li>
|
||||
<li class=${timeline.tabClass("media")}><a href="${link}/media">Media</a></li>
|
||||
#discard "<li class=tab-item><a href=${link}/search>Custom</a></li>"
|
||||
</ul>
|
||||
${renderTimeline(timeline, profile, beginning)}
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderConversation*(conversation: Conversation): string =
|
||||
<div class="conversation" id="tweets">
|
||||
<div class="main-thread">
|
||||
#if conversation.before != nil:
|
||||
<div class="before-tweet thread-line">
|
||||
#for i, tweet in conversation.before.tweets:
|
||||
${renderTweet(tweet, first=(i == 0))}
|
||||
#end for
|
||||
</div>
|
||||
#end if
|
||||
<div class="main-tweet">
|
||||
#let afterClass = if conversation.after != nil: "thread thread-line" else: ""
|
||||
${renderTweet(conversation.tweet, class=afterClass)}
|
||||
</div>
|
||||
#if conversation.after != nil:
|
||||
<div class="after-tweet thread-line">
|
||||
#for i, tweet in conversation.after.tweets:
|
||||
${renderTweet(tweet, first=(i == 0), last=(i == conversation.after.tweets.high))}
|
||||
#end for
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
#if conversation.replies.len > 0:
|
||||
<div class="replies">
|
||||
#for thread in conversation.replies:
|
||||
<div class="reply thread thread-line">
|
||||
#for i, tweet in thread.tweets:
|
||||
#let last = (i == thread.tweets.high and thread.more == 0)
|
||||
${renderTweet(tweet, first=(i == 0), last=last)}
|
||||
#end for
|
||||
#if thread.more != 0:
|
||||
#let num = if thread.more != -1: $thread.more & " " else: ""
|
||||
<div class="status-el more-replies">
|
||||
#let reply = if thread.more == 1: "reply" else: "replies"
|
||||
<a class="more-replies-text" title="Not implemented yet">${num}more ${reply}</a>
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
#end for
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
@ -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'
|
||||
|
@ -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'], ['%']]
|
||||
|
@ -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']
|
||||
]
|
||||
|
||||
|
||||
|
新しいイシューから参照
ユーザーをブロックする