Zed 6 роки тому
батько
коміт
2950c0de35

+ 2 - 1
src/sass/profile/card.scss

@@ -42,7 +42,8 @@
 
     img {
         display: block;
-        width: calc(100% - 8px);
+        box-sizing: border-box;
+        width: 100%;
         height: 100%;
         margin: 0;
         border: 4px solid var(--darker_grey);

+ 3 - 2
src/sass/profile/photo-rail.scss

@@ -14,9 +14,10 @@
     }
 
     &-header-mobile {
-        padding: 5px 12px 0;
         display: none;
-        width: calc(100% - 24px);
+        box-sizing: border-box;
+        padding: 5px 12px 0;
+        width: 100%;
         float: unset;
         color: var(--accent);
         justify-content: space-between;

+ 10 - 0
src/sass/timeline.scss

@@ -13,18 +13,28 @@
 }
 
 .timeline-header {
+    width: 100%;
     background-color: var(--bg_panel);
     text-align: center;
     padding: 8px;
     display: block;
     font-weight: bold;
     margin-bottom: 5px;
+    box-sizing: border-box;
 
     button {
         float: unset;
     }
 }
 
+.timeline-banner img {
+    width: 100%;
+}
+
+.timeline-description {
+    font-weight: normal;
+}
+
 .tab {
     align-items: center;
     display: flex;

+ 1 - 1
src/sass/tweet/card.scss

@@ -76,7 +76,7 @@
     left: 0;
     bottom: 0;
     right: 0;
-    background-color: var(--fg_color);
+    background-color: var(--bg_overlays);
 
     img {
         width: 100%;

+ 11 - 11
src/sass/tweet/media.scss

@@ -13,7 +13,7 @@
 
     .still-image {
         width: 100%;
-        display: block;
+        display: flex;
     }
 }
 
@@ -67,17 +67,17 @@
     display: inline-block;
 }
 
-.single-image {
-    display: inline-block;
-    width: 100%;
-    max-height: 600px;
+// .single-image {
+//     display: inline-block;
+//     width: 100%;
+//     max-height: 600px;
 
-    .attachments {
-        width: unset;
-        max-height: unset;
-        display: inherit;
-    }
-}
+//     .attachments {
+//         width: unset;
+//         max-height: unset;
+//         display: inherit;
+//     }
+// }
 
 .overlay-circle {
     border-radius: 50%;

+ 48 - 66
src/sass/tweet/quote.scss

@@ -5,10 +5,10 @@
     border: solid 1px var(--dark_grey);
     border-radius: 10px;
     background-color: var(--bg_elements);
-    overflow: auto;
-    padding: 6px;
-    position: relative;
+    overflow: hidden;
     pointer-events: all;
+    position: relative;
+    width: 100%;
 
     &:hover {
         border-color: var(--grey);
@@ -17,91 +17,73 @@
     &.unavailable:hover {
         border-color: var(--dark_grey);
     }
+
+    .tweet-name-row {
+        padding: 6px 6px 0px 6px;
+    }
+
+    .quote-text {
+        overflow: hidden;
+        white-space: pre-wrap;
+        word-wrap: break-word;
+        padding: 3px 6px 6px 6px;
+    }
+
+    .show-thread {
+        padding: 0px 6px 3px 6px;
+        margin-top: -3px;
+    }
+
+    .replying-to {
+        padding: 0px 6px;
+        margin: unset;
+    }
 }
 
 .unavailable-quote {
-    padding: 6px;
+    padding: 12px;
 }
 
 .quote-link {
-    height: 100%;
     width: 100%;
+    height: 100%;
     left: 0;
     top: 0;
     position: absolute;
 }
 
-.quote .quote-link {
-    z-index: 1;
-}
-
-.quote-text {
-    overflow: hidden;
-    white-space: pre-wrap;
-    word-wrap: break-word;
-}
-
 .quote-media-container {
+    max-height: 300px;
     display: flex;
-    flex-direction: column;
-    align-items: center;
-    overflow: hidden;
-    max-height: 102px;
-    width: 102px;
-    float: left;
-    margin-right: 7px;
-    border-radius: 7px;
-    position: relative;
-}
 
-.quote-media {
-    display: flex;
-    justify-content: center;
-    pointer-events: none;
+    .card {
+        margin: unset;
+    }
 
-    img {
+    .media-gif {
         width: 100%;
-        height: 100%;
-        align-self: center;
     }
-}
 
-.quote-badge {
-    left: 0;
-    bottom: 0;
-    position: absolute;
-    z-index: 1;
-    align-self: flex-end;
-}
+    .attachments {
+        border-radius: 0;
+    }
 
-.quote-badge-text {
-    margin: 4px;
-    background: $shadow;
-    border-radius: 4px;
-    color: #fffffff0;
-    padding: 1px 3px;
-    font-size: 12px;
-    font-weight: bold;
-}
+    .gallery-gif .attachment {
+        display: flex;
+        justify-content: center;
+        background-color: var(--bg_color);
 
-.quote-sensitive {
-    background: var(--darker_grey);
-    width: 102px;
-    height: 102px;
-    border-radius: 12px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-}
+        video {
+            height: unset;
+            width: unset;
+        }
+    }
 
-.quote-sensitive-icon {
-    font-size: 40px;
-    color: var(--grey);
-}
+    .gallery-video, .gallery-gif {
+        max-height: 300px;
+    }
 
-@media(max-width: 600px) {
-    .quote-media-container {
-        width: 70px;
-        max-height: 70px;
+    .still-image img {
+        max-height: 250px
     }
 }

+ 12 - 4
src/views/list.nim

@@ -2,7 +2,7 @@ import strformat
 import karax/[karaxdsl, vdom]
 
 import renderutils
-import ".."/[types]
+import ".."/[types, utils]
 
 proc renderListTabs*(query: Query; path: string): VNode =
   buildHtml(ul(class="tab")):
@@ -11,10 +11,18 @@ proc renderListTabs*(query: Query; path: string): VNode =
     li(class=query.getTabClass(userList)):
       a(href=(path & "/members")): text "Members"
 
-proc renderList*(body: VNode; query: Query; name, list: string): VNode =
+proc renderList*(body: VNode; query: Query; list: List): VNode =
   buildHtml(tdiv(class="timeline-container")):
+    if list.banner.len > 0:
+      tdiv(class="timeline-banner"):
+        a(href=getPicUrl(list.banner), target="_blank"):
+          genImg(list.banner)
+
     tdiv(class="timeline-header"):
-      text &"\"{list}\" by @{name}"
+      text &"\"{list.name}\" by @{list.username}"
+
+      tdiv(class="timeline-description"):
+        text list.description
 
-    renderListTabs(query, &"/{name}/lists/{list}")
+    renderListTabs(query, &"/{list.username}/lists/{list.name}")
     body

+ 5 - 4
src/views/profile.nim

@@ -58,14 +58,15 @@ proc renderProfileCard*(profile: Profile; prefs: Prefs): VNode =
           renderStat(profile.likes, "likes")
 
 proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode =
+  let count = insertSep($profile.media, ',')
   buildHtml(tdiv(class="photo-rail-card")):
     tdiv(class="photo-rail-header"):
       a(href=(&"/{profile.username}/media")):
-        icon "picture", $profile.media & " Photos and videos"
+        icon "picture", count & " Photos and videos"
 
     input(id="photo-rail-grid-toggle", `type`="checkbox")
     label(`for`="photo-rail-grid-toggle", class="photo-rail-header-mobile"):
-      icon "picture", $profile.media & " Photos and videos"
+      icon "picture", count & " Photos and videos"
       icon "down"
 
     tdiv(class="photo-rail-grid"):
@@ -73,7 +74,7 @@ proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode =
         if i == 16: break
         a(href=(&"/{profile.username}/status/{photo.tweetId}#m"),
           style={backgroundColor: photo.color}):
-          genImg(photo.url & ":thumb")
+          genImg(photo.url & (if "format" in photo.url: "" else: ":thumb"))
 
 proc renderBanner(profile: Profile): VNode =
   buildHtml():
@@ -89,7 +90,7 @@ proc renderProtected(username: string): VNode =
       h2: text "This account's tweets are protected."
       p: text &"Only confirmed followers have access to @{username}'s tweets."
 
-proc renderProfile*(profile: Profile; timeline: Timeline;
+proc renderProfile*(profile: Profile; timeline: var Timeline;
                     photoRail: PhotoRail; prefs: Prefs; path: string): VNode =
   timeline.query.fromUser = @[profile.username]
   buildHtml(tdiv(class="profile-tabs")):

+ 2 - 3
src/views/renderutils.nim

@@ -1,7 +1,6 @@
-import strutils, xmltree
+import strutils
 import karax/[karaxdsl, vdom, vstyles]
-
-import ../types, ../utils
+import ".."/[types, utils]
 
 proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode =
   var c = "icon-" & icon

+ 5 - 5
src/views/rss.nimf

@@ -82,24 +82,24 @@
       <width>128</width>
       <height>128</height>
     </image>
-    #if timeline != nil:
+    # if timeline.content.len > 0:
     ${renderRssTweets(timeline.content, prefs, hostname)}
     #end if
   </channel>
 </rss>
 #end proc
 #
-#proc renderListRss*(tweets: seq[Tweet]; name, list, hostname: string): string =
+#proc renderListRss*(tweets: seq[Tweet]; list: List; hostname: string): string =
 #let prefs = Prefs(replaceTwitter: hostname, replaceYouTube: "invidio.us")
-#let link = &"https://{hostname}/{name}/lists/{list}"
+#let link = &"https://{hostname}/{list.username}/lists/{list.name}"
 #result = ""
 <?xml version="1.0" encoding="UTF-8"?>
 <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
   <channel>
     <atom:link href="${link}" rel="self" type="application/rss+xml" />
-    <title>${list} / @${name}</title>
+    <title>${list.name} / @${list.username}</title>
     <link>${link}</link>
-    <description>Twitter feed for: ${list} by @${name}. Generated by ${hostname}</description>
+    <description>Twitter feed for: ${list.name} by @${list.username}. Generated by ${hostname}</description>
     <language>en-us</language>
     <ttl>40</ttl>
     ${renderRssTweets(tweets, prefs, hostname)}

+ 7 - 5
src/views/search.nim

@@ -1,8 +1,8 @@
 import strutils, strformat, sequtils, unicode, tables
-import karax/[karaxdsl, vdom, vstyles]
+import karax/[karaxdsl, vdom]
 
 import renderutils, timeline
-import ".."/[types, formatters, query]
+import ".."/[types, query]
 
 let toggles = {
   "nativeretweets": "Retweets",
@@ -93,13 +93,15 @@ proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string): VNo
     if query.fromUser.len > 1:
       tdiv(class="timeline-header"):
         text query.fromUser.join(" | ")
+
+    if query.fromUser.len > 0:
+      renderProfileTabs(query, query.fromUser.join(","))
+
     if query.fromUser.len == 0 or query.kind == tweets:
       tdiv(class="timeline-header"):
         renderSearchPanel(query)
 
-    if query.fromUser.len > 0:
-      renderProfileTabs(query, query.fromUser.join(","))
-    else:
+    if query.fromUser.len == 0:
       renderSearchTabs(query)
 
     renderTimelineTweets(results, prefs, path)

+ 28 - 23
src/views/status.nim

@@ -1,3 +1,4 @@
+import uri
 import karax/[karaxdsl, vdom]
 
 import ".."/[types, formatters]
@@ -13,7 +14,7 @@ proc renderMoreReplies(thread: Chain): VNode =
   let reply = if thread.more == 1: "reply" else: "replies"
   let link = getLink(thread.content[^1])
   buildHtml(tdiv(class="timeline-item more-replies")):
-    if link.len > 0:
+    if thread.content[^1].available:
       a(class="more-replies-text", href=link):
         text $num & "more " & reply
     else:
@@ -32,41 +33,45 @@ proc renderReplyThread(thread: Chain; prefs: Prefs; path: string): VNode =
 proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string): VNode =
   buildHtml(tdiv(class="replies", id="r")):
     for thread in replies.content:
-      if thread == nil: continue
+      if thread.content.len == 0: continue
       renderReplyThread(thread, prefs, path)
 
-    if replies.hasMore:
-      renderMore(Query(), replies.minId, focus="#r")
+    if replies.bottom.len > 0:
+      renderMore(Query(), encodeUrl(replies.bottom), focus="#r")
 
-proc renderConversation*(conversation: Conversation; prefs: Prefs; path: string): VNode =
-  let hasAfter = conversation.after != nil
-  let showReplies = not prefs.hideReplies
+proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode =
+  let hasAfter = conv.after.content.len > 0
+  let threadId = conv.tweet.threadId
   buildHtml(tdiv(class="conversation")):
     tdiv(class="main-thread"):
-      if conversation.before != nil:
+      if conv.before.content.len > 0:
         tdiv(class="before-tweet thread-line"):
-          if conversation.before.more == -1:
-            renderEarlier(conversation.before)
-          for i, tweet in conversation.before.content:
+          let first = conv.before.content[0]
+          if threadId != first.id and (first.replyId > 0 or not first.available):
+            renderEarlier(conv.before)
+          for i, tweet in conv.before.content:
             renderTweet(tweet, prefs, path, index=i)
 
       tdiv(class="main-tweet", id="m"):
         let afterClass = if hasAfter: "thread thread-line" else: ""
-        renderTweet(conversation.tweet, prefs, path, class=afterClass,
-                    mainTweet=true)
+        renderTweet(conv.tweet, prefs, path, class=afterClass, mainTweet=true)
 
       if hasAfter:
         tdiv(class="after-tweet thread-line"):
-          let total = conversation.after.content.high
-          let more = conversation.after.more
-          for i, tweet in conversation.after.content:
-            renderTweet(tweet, prefs, path, index=i, last=(i == total and more == 0))
+          let
+            total = conv.after.content.high
+            more = conv.after.more
+          for i, tweet in conv.after.content:
+            renderTweet(tweet, prefs, path, index=i,
+                        last=(i == total and more == 0), afterTweet=true)
 
           if more != 0:
-            renderMoreReplies(conversation.after)
+            renderMoreReplies(conv.after)
 
-    if conversation.replies != nil and showReplies:
-      if not conversation.replies.beginning:
-        renderNewer(Query(), getLink(conversation.tweet))
-      if conversation.replies.content.len > 0:
-        renderReplies(conversation.replies, prefs, path)
+    if not prefs.hideReplies:
+      if not conv.replies.beginning:
+        renderNewer(Query(), getLink(conv.tweet), focus="#r")
+      if conv.replies.content.len > 0 or conv.replies.bottom.len > 0:
+        renderReplies(conv.replies, prefs, path)
+
+    renderToTop(focus="#m")

+ 44 - 29
src/views/timeline.nim

@@ -10,16 +10,22 @@ proc getQuery(query: Query): string =
   if result.len > 0:
     result &= "&"
 
-proc renderNewer*(query: Query; path: string): VNode =
-  let q = genQueryUrl(query)
-  let url = if q.len > 0: "?" & q else: ""
+proc renderToTop*(focus="#"): VNode =
+  buildHtml(tdiv(class="top-ref")):
+    icon "down", href=focus
+
+proc renderNewer*(query: Query; path: string; focus=""): VNode =
+  let
+    q = genQueryUrl(query)
+    url = if q.len > 0: "?" & q else: ""
+    p = if focus.len > 0: path.replace("#m", focus) else: path
   buildHtml(tdiv(class="timeline-item show-more")):
-    a(href=(path & url)):
+    a(href=(p & url)):
       text "Load newest"
 
-proc renderMore*(query: Query; minId: string; focus=""): VNode =
+proc renderMore*(query: Query; cursor: string; focus=""): VNode =
   buildHtml(tdiv(class="show-more")):
-    a(href=(&"?{getQuery(query)}max_position={minId}{focus}")):
+    a(href=(&"?{getQuery(query)}cursor={encodeUrl(cursor)}{focus}")):
       text "Load more"
 
 proc renderNoMore(): VNode =
@@ -32,10 +38,6 @@ proc renderNoneFound(): VNode =
     h2(class="timeline-none"):
       text "No items found"
 
-proc renderToTop(): VNode =
-  buildHtml(tdiv(class="top-ref")):
-    icon "down", href="#"
-
 proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode =
   buildHtml(tdiv(class="thread-line")):
     let sortedThread = thread.sortedByIt(it.id)
@@ -43,10 +45,16 @@ proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode =
       let show = i == thread.high and sortedThread[0].id != tweet.threadId
       let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: ""
       renderTweet(tweet, prefs, path, class=(header & "thread"),
-                  index=i, total=thread.high, showThread=show)
+                  index=i, last=(i == thread.high), showThread=show)
 
-proc threadFilter(it: Tweet; thread: int64): bool =
-  it.retweet.isNone and it.reply.len == 0 and it.threadId == thread
+proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet): seq[Tweet] =
+  result = @[it]
+  if it.retweet.isSome or it.replyId in threads: return
+  for t in tweets:
+    if t.id == result[0].replyId:
+      result.insert t
+    elif t.replyId == result[0].id:
+      result.add t
 
 proc renderUser(user: Profile; prefs: Prefs): VNode =
   buildHtml(tdiv(class="timeline-item")):
@@ -72,8 +80,8 @@ proc renderTimelineUsers*(results: Result[Profile]; prefs: Prefs; path=""): VNod
     if results.content.len > 0:
       for user in results.content:
         renderUser(user, prefs)
-      if results.minId != "0":
-        renderMore(results.query, results.minId)
+      if results.bottom.len > 0:
+        renderMore(results.query, results.bottom)
       renderToTop()
     elif results.beginning:
       renderNoneFound()
@@ -86,24 +94,31 @@ proc renderTimelineTweets*(results: Result[Tweet]; prefs: Prefs; path: string):
       renderNewer(results.query, parseUri(path).path)
 
     if results.content.len == 0:
-      renderNoneFound()
+      if not results.beginning:
+        renderNoMore()
+      else:
+        renderNoneFound()
     else:
-      var threads: seq[int64]
-      var retweets: seq[int64]
+      var
+        threads: seq[int64]
+        retweets: seq[int64]
+
       for tweet in results.content:
-        if tweet.threadId in threads or tweet.id in retweets: continue
-        if tweet.pinned and prefs.hidePins: continue
-        let thread = results.content.filterIt(threadFilter(it, tweet.threadId))
+        let rt = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
+
+        if tweet.id in threads or rt in retweets or
+           tweet.pinned and prefs.hidePins: continue
+
+        let thread = results.content.threadFilter(threads, tweet)
         if thread.len < 2:
-          if tweet.retweet.isSome:
-            retweets &= tweet.id
-          renderTweet(tweet, prefs, path, showThread=tweet.hasThread)
+          var hasThread = tweet.hasThread
+          if rt != 0:
+            retweets &= rt
+            hasThread = get(tweet.retweet).hasThread
+          renderTweet(tweet, prefs, path, showThread=hasThread)
         else:
           renderThread(thread, prefs, path)
-          threads &= tweet.threadId
+          threads &= thread.mapIt(it.id)
 
-      if results.hasMore or results.query.kind != posts:
-        renderMore(results.query, results.minId)
-      else:
-        renderNoMore()
+      renderMore(results.query, results.bottom)
       renderToTop()

+ 62 - 64
src/views/tweet.nim

@@ -4,11 +4,11 @@ import karax/[karaxdsl, vdom, vstyles]
 import renderutils
 import ".."/[types, utils, formatters]
 
-proc renderHeader(tweet: Tweet): VNode =
+proc renderHeader(tweet: Tweet; retweet=""): VNode =
   buildHtml(tdiv):
-    if tweet.retweet.isSome:
+    if retweet.len > 0:
       tdiv(class="retweet-header"):
-        span: icon "retweet", get(tweet.retweet).by & " retweeted"
+        span: icon "retweet", retweet & " retweeted"
 
     if tweet.pinned:
       tdiv(class="pinned"):
@@ -24,31 +24,24 @@ proc renderHeader(tweet: Tweet): VNode =
           linkUser(tweet.profile, class="username")
 
         span(class="tweet-date"):
-          a(href=getLink(tweet), title=tweet.getTime()):
-            text tweet.shortTime
+          a(href=getLink(tweet), title=tweet.getTime):
+            text tweet.getShortTime
 
 proc renderAlbum(tweet: Tweet): VNode =
   let
     groups = if tweet.photos.len < 3: @[tweet.photos]
              else: tweet.photos.distribute(2)
 
-  if groups.len == 1 and groups[0].len == 1:
-    buildHtml(tdiv(class="single-image")):
-      tdiv(class="attachments gallery-row"):
-        a(href=getPicUrl(groups[0][0] & "?name=orig"), class="still-image",
-          target="_blank"):
-            genImg(groups[0][0])
-  else:
-    buildHtml(tdiv(class="attachments")):
-      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=getPicUrl(photo & "?name=orig"), class="still-image",
-                target="_blank", style={display: flex}):
-                genImg(photo)
+  buildHtml(tdiv(class="attachments")):
+    for i, photos in groups:
+      let margin = if i > 0: ".25em" else: ""
+      tdiv(class="gallery-row", style={marginTop: margin}):
+        for photo in photos:
+          tdiv(class="attachment image"):
+            var url = photo
+            if "=orig" notin url: url &= "?name=orig"
+            a(href=getPicUrl(url), class="still-image", target="_blank"):
+              genImg(photo)
 
 proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
   case video.playbackType
@@ -88,7 +81,8 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
         elif not prefs.isPlaybackEnabled(video):
           renderVideoDisabled(video, path)
         else:
-          let source = getVidUrl(video.url)
+          let vid = video.variants.filterIt(it.videoType == video.playbackType)
+          let source = getVidUrl(vid[0].url)
           case video.playbackType
           of mp4:
             if prefs.muteVideos:
@@ -138,7 +132,7 @@ proc renderPoll(poll: Poll): VNode =
 proc renderCardImage(card: Card): VNode =
   buildHtml(tdiv(class="card-image-container")):
     tdiv(class="card-image"):
-      img(src=getPicUrl(get(card.image)))
+      img(src=getPicUrl(card.image), alt="")
       if card.kind == player:
         tdiv(class="card-overlay"):
           tdiv(class="overlay-circle"):
@@ -151,9 +145,8 @@ proc renderCardContent(card: Card): VNode =
     span(class="card-destination"): text card.dest
 
 proc renderCard(card: Card; prefs: Prefs; path: string): VNode =
-  const largeCards = {summaryLarge, liveEvent, promoWebsite,
-                      promoVideo, promoVideoConvo}
-  let large = if card.kind in largeCards: " large" else: ""
+  const smallCards = {player, summary}
+  let large = if card.kind notin smallCards: " large" else: ""
   let url = replaceUrl(card.url, prefs)
 
   buildHtml(tdiv(class=("card" & large))):
@@ -164,7 +157,7 @@ proc renderCard(card: Card; prefs: Prefs; path: string): VNode =
           renderCardContent(card)
     else:
       a(class="card-container", href=url):
-        if card.image.isSome:
+        if card.image.len > 0:
           renderCardImage(card)
         tdiv(class="card-content-container"):
           renderCardContent(card)
@@ -184,13 +177,6 @@ proc renderReply(tweet: Tweet): VNode =
       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 renderAttribution(profile: Profile): VNode =
   let avatarUrl = getPicUrl(profile.getUserpic("_200x200"))
   buildHtml(a(class="attribution", href=("/" & profile.username))):
@@ -206,19 +192,17 @@ proc renderMediaTags(tags: seq[Profile]): VNode =
       if i < tags.high:
         text ", "
 
-proc renderQuoteMedia(quote: Quote): VNode =
+proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): 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"):
-        icon "attention", class="quote-sensitive-icon"
-
-proc renderQuote(quote: Quote; prefs: Prefs): VNode =
+    if quote.photos.len > 0:
+      renderAlbum(quote)
+      # genImg(quote.photos[0])
+    elif quote.video.isSome:
+      renderVideo(quote.video.get(), prefs, path)
+    elif quote.gif.isSome:
+      renderGif(quote.gif.get(), prefs)
+
+proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
   if not quote.available:
     return buildHtml(tdiv(class="quote unavailable")):
       tdiv(class="unavailable-quote"):
@@ -227,26 +211,32 @@ proc renderQuote(quote: Quote; prefs: Prefs): VNode =
         else:
           text "This tweet is unavailable"
 
-  buildHtml(tdiv(class="quote")):
+  buildHtml(tdiv(class="quote quote-big")):
     a(class="quote-link", href=getLink(quote))
 
-    if quote.thumb.len > 0 or quote.sensitive:
-      renderQuoteMedia(quote)
+    tdiv(class="tweet-name-row"):
+      tdiv(class="fullname-and-username"):
+        linkUser(quote.profile, class="fullname")
+        linkUser(quote.profile, class="username")
 
-    tdiv(class="fullname-and-username"):
-      linkUser(quote.profile, class="fullname")
-      linkUser(quote.profile, class="username")
+      span(class="tweet-date"):
+        a(href=getLink(quote), title=quote.getTime):
+          text quote.getShortTime
 
     if quote.reply.len > 0:
       renderReply(quote)
 
-    tdiv(class="quote-text"):
-      verbatim replaceUrl(quote.text, prefs)
+    if quote.text.len > 0:
+      tdiv(class="quote-text"):
+        verbatim replaceUrl(quote.text, prefs)
 
     if quote.hasThread:
       a(class="show-thread", href=getLink(quote)):
         text "Show this thread"
 
+    if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
+      renderQuoteMedia(quote, prefs, path)
+
 proc renderLocation*(tweet: Tweet): string =
   let (place, url) = tweet.getLocation()
   if place.len == 0: return
@@ -258,11 +248,10 @@ proc renderLocation*(tweet: Tweet): string =
       text place
   return $node
 
-proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class="";
-                  index=0; total=(-1); last=false; showThread=false;
-                  mainTweet=false): VNode =
+proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
+                  last=false; showThread=false; mainTweet=false; afterTweet=false): VNode =
   var divClass = class
-  if index == total or last:
+  if index == -1 or last:
     divClass = "thread-last " & class
 
   if not tweet.available:
@@ -273,15 +262,22 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class="";
         else:
           text "This tweet is unavailable"
 
+  let fullTweet = tweet
+  var retweet: string
+  var tweet = fullTweet
+  if tweet.retweet.isSome:
+    tweet = tweet.retweet.get
+    retweet = fullTweet.profile.fullname
+
   buildHtml(tdiv(class=("timeline-item " & divClass))):
     if not mainTweet:
       a(class="tweet-link", href=getLink(tweet))
 
     tdiv(class="tweet-body"):
       var views = ""
-      renderHeader(tweet)
+      renderHeader(tweet, retweet)
 
-      if index == 0 and tweet.reply.len > 0 and
+      if not afterTweet and index == 0 and tweet.reply.len > 0 and
          (tweet.reply.len > 1 or tweet.reply[0] != tweet.profile.username):
         renderReply(tweet)
 
@@ -293,7 +289,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class="";
 
       if tweet.card.isSome:
         renderCard(tweet.card.get(), prefs, path)
-      elif tweet.photos.len > 0:
+
+      if tweet.photos.len > 0:
         renderAlbum(tweet)
       elif tweet.video.isSome:
         renderVideo(tweet.video.get(), prefs, path)
@@ -301,11 +298,12 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class="";
       elif tweet.gif.isSome:
         renderGif(tweet.gif.get(), prefs)
         views = "GIF"
-      elif tweet.poll.isSome:
+
+      if tweet.poll.isSome:
         renderPoll(tweet.poll.get())
 
       if tweet.quote.isSome:
-        renderQuote(tweet.quote.get(), prefs)
+        renderQuote(tweet.quote.get(), prefs, path)
 
       if mainTweet:
         p(class="tweet-published"): text getTweetTime(tweet)