selfhostable, read-only reddit client

support expanding/collapsing comments

fix pagination when sorting

Changed files
+161 -55
src
+10
readme.txt
··· 1 1 nix build .#readit 2 + 3 + todo: 4 + - [ ] support crossposts 5 + - [ ] fix gallery thumbnails 6 + - [x] pass query params into templates, add into pagination 7 + - [ ] subscription manager: reorder, mass add 8 + - [ ] styles for info-containers 9 + - [ ] open in reddit/reply in reddit link 10 + - [ ] placeholder for unresolvable thumbnails 11 + - [ ] expand/collapse comments
+33 -16
src/mixins/comment.pug
··· 1 1 include ../utils 2 + 3 + mixin infoContainer(data) 4 + div.comment-info-container 5 + div.info-item #{fmtnum(data.ups)} ↑ 6 + div.info-item u/#{data.author} #{data.is_submitter ? '(OP)' : ''} 7 + if data.collapsed_reason_code == "DELETED" 8 + div.info-item 9 + a(href=`https://undelete.pullpush.io${data.permalink}`) search on undelete 10 + div.info-item #{timeDifference(Date.now(), data.created * 1000)} 11 + 12 + - 13 + function hasReplies(data) { 14 + return data.replies && data.replies.data && data.replies.data.children && data.replies.data.children.length > 0; 15 + } 16 + 2 17 mixin comment(com, isfirst) 3 18 - var data = com.data 4 19 - var kind = com.kind 20 + - var hasReplyData = hasReplies(data) 21 + 5 22 if kind == "more" 6 - div(class=`${isfirst?'first':''}`) 23 + div(class=`${isfirst ? 'first' : ''}`) 7 24 div.more #{data.count} more comments 8 25 else 9 - div(class=`comment ${isfirst?'first':''}`) 10 - div.comment-info-container 11 - div.info-item #{fmtnum(data.ups)} ↑ 12 - div.info-item u/#{data.author} #{data.is_submitter?'(OP)':''} 13 - if data.collapsed_reason_code == "DELETED" 14 - div.info-item 15 - a(href=`https://undelete.pullpush.io${data.permalink}`) search on undelete 16 - div.info-item #{timeDifference(Date.now(), data.created * 1000)} 17 - div.comment-body 18 - != data.body_html 19 - div.replies 20 - if data.replies 21 - if data.replies.data 22 - if data.replies.data.children 26 + div(class=`comment ${isfirst ? 'first' : ''}`) 27 + if hasReplyData 28 + details(id=`${data.id}` open="") 29 + summary.expand-comments 30 + +infoContainer(data) 31 + div.comment-body 32 + != data.body_html 33 + 34 + div.replies 23 35 each reply in data.replies.data.children 24 - +comment(reply,false) 36 + +comment(reply, false) 37 + 38 + else 39 + +infoContainer(data) 40 + div.comment-body 41 + != data.body_html
+4 -4
src/mixins/post.pug
··· 40 40 if p.gallery_data 41 41 if p.gallery_data.items 42 42 details(id=`${p.id}`) 43 - summary expand gallery 43 + summary.expand-post expand gallery 44 44 div.gallery 45 45 - var total = p.gallery_data.items.length 46 46 - var idx = 0 ··· 58 58 button(onclick=`toggleDetails('${p.id}')`) close 59 59 if p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default" 60 60 details(id=`${p.id}`) 61 - summary expand image 61 + summary.expand-post expand image 62 62 a(href=`/media/${p.url}`) 63 63 img(src=p.url loading="lazy").post-media 64 64 button(onclick=`toggleDetails('${p.id}')`) close 65 65 else if p.post_hint == "hosted:video" 66 66 details(id=`${p.id}`) 67 - summary expand video 67 + summary.expand-post expand video 68 68 - var url = p.secure_media.reddit_video.dash_url 69 69 video(src=url controls data-dashjs-player loading="lazy").post-media 70 70 button(onclick=`toggleDetails('${p.id}')`) close 71 71 else if !p.post_hint || (p.post_hint == "link" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default") 72 72 details(id=`${p.id}`) 73 - summary expand link 73 + summary.expand-post expand link 74 74 a(href=`${p.url}`) 75 75 | #{p.url} 76 76 br
+70 -15
src/public/styles.css
··· 1 - @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 1 + @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); 2 2 3 3 :root { 4 4 /* Light mode colors */ ··· 17 17 --bg-color-muted: #333; 18 18 --text-color: white; 19 19 --text-color-muted: #999; 20 - --blockquote-color: green; 20 + --blockquote-color: lightgreen; 21 21 --link-color: lightblue; 22 22 --link-visited-color: violet; 23 23 } ··· 58 58 justify-content: center; 59 59 } 60 60 61 + .sub-title { 62 + display: flex; 63 + } 64 + 65 + #button-container { 66 + margin-left: 10px; 67 + display: flex; 68 + align-items: center; 69 + } 70 + 71 + .sort-opts { 72 + display: flex; 73 + flex-direction: row; 74 + flex-wrap: wrap; 75 + justify-content: space-between; 76 + } 77 + 78 + .sort-opts a { 79 + margin: 10px; 80 + } 81 + 61 82 .footer { 62 83 display: flex; 63 84 flex-direction: row; ··· 90 111 .info-container, 91 112 .comment-info-container, 92 113 .more, 114 + summary.expand-comments::before, 93 115 hr { 94 116 color: var(--text-color-muted) 95 117 } 96 118 97 - .info-container, .comment-info-container, .more { 119 + .info-container, .more { 98 120 font-size: 0.8rem; 99 121 display: flex; 100 122 } 101 123 124 + .comment-info-container { 125 + display: inline-flex; 126 + align-items: center; 127 + font-size: 0.8rem; 128 + } 129 + 102 130 .domain { 103 131 color: var(--text-color-muted); 104 132 font-size: 0.8rem; ··· 124 152 display: none 125 153 } 126 154 155 + .post-media { 156 + display: block; 157 + margin: 0 auto; 158 + max-width: 95%; 159 + padding: 5px; 160 + } 161 + 127 162 @media (min-width: 768px) { 128 163 .post, .comments-container, .hero, .header, .footer { 129 164 flex: 1 1 90%; ··· 135 170 .comments-container { 136 171 font-size: 1.3rem; 137 172 } 138 - .info-container, .comment-info-container, .more { 173 + .info-container, .comment-info-container, .more, summary.expand-comments::before 174 + { 139 175 font-size: 1rem; 140 176 } 141 177 .domain { ··· 150 186 .post-author { 151 187 display: inline 152 188 } 189 + .post-media { 190 + max-width: 50%; 191 + } 153 192 } 154 193 155 194 @media (min-width: 1080px) { ··· 163 202 .comments-container { 164 203 font-size: 1.3rem; 165 204 } 166 - .info-container, .comment-info-container, .more { 205 + .info-container, .comment-info-container, .more, summary.expand-comments::before { 167 206 font-size: 1rem; 168 207 } 169 208 .domain { ··· 177 216 } 178 217 .post-author { 179 218 display: inline 219 + } 220 + .post-media { 221 + max-width: 50%; 180 222 } 181 223 } 182 224 ··· 229 271 flex-direction: row; 230 272 } 231 273 232 - .post-media { 233 - display: block; 234 - margin: 0 auto; 235 - max-width: 95%; 236 - padding: 5px; 237 - } 238 - 239 - .title-container, .comment-info-container { 240 - flex: 1; 274 + .title-container, .comment-info-container, summary.expand-comments::before { 241 275 margin-top: 10px; 242 276 margin-bottom: 10px; 243 277 } ··· 296 330 text-align: left; 297 331 } 298 332 299 - summary { 333 + summary.expand-post { 300 334 display: none; 335 + } 336 + 337 + summary.expand-comments { 338 + list-style: none; 339 + cursor: pointer; 340 + } 341 + 342 + summary.expand-comments::before { 343 + content: "[+] "; 344 + } 345 + 346 + details[open] summary.expand-comments::before { 347 + content: "[-] "; 348 + } 349 + 350 + details:not([open]) summary.expand-comments::before { 351 + content: "[+] "; 352 + } 353 + 354 + .comment-body { 355 + display: block; 301 356 } 302 357 303 358 .footer {
+5 -6
src/routes/index.js
··· 11 11 }); 12 12 13 13 // GET /r/:id 14 - router.get('/r/:subreddit/:sort?', async (req, res) => { 14 + router.get('/r/:subreddit', async (req, res) => { 15 15 var subreddit = req.params.subreddit; 16 - var query = req.query; 17 - var sort = req.params.sort ? req.params.sort : 'hot'; 18 - var options = req.query; 16 + var query = req.query? req.query : {}; 17 + var sort = query.sort? query.sort : 'hot'; 19 18 20 - var postsReq = G.getSubmissions(sort, `${subreddit}`, options); 19 + var postsReq = G.getSubmissions(sort, `${subreddit}`, query); 21 20 var aboutReq = G.getSubreddit(`${subreddit}`); 22 21 23 22 var [posts, about] = await Promise.all([postsReq, aboutReq]); 24 23 25 - res.render('index', { subreddit, posts, about }); 24 + res.render('index', { subreddit, posts, about, query }); 26 25 }); 27 26 28 27 // GET /comments/:id
+7 -1
src/utils.pug
··· 15 15 if (elapsed < msPerMinute) { 16 16 return Math.round(elapsed/1000) + 's'; 17 17 } else if (elapsed < msPerHour) { 18 - return Math.round(elapsed/msPerMinute) + 'min'; 18 + return Math.round(elapsed/msPerMinute) + 'm'; 19 19 } else if (elapsed < msPerDay ) { 20 20 return Math.round(elapsed/msPerHour ) + 'h'; 21 21 } else if (elapsed < msPerMonth) { ··· 26 26 return Math.round(elapsed/msPerYear ) + 'y'; 27 27 } 28 28 } 29 + - 30 + function encodeQueryParams(obj) { 31 + return Object.keys(obj) 32 + .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])) 33 + .join('&'); 34 + }
+9
src/views/comments.pug
··· 1 1 include ../mixins/comment 2 2 include ../mixins/header 3 + include ../utils 3 4 4 5 doctype html 5 6 html ··· 9 10 title reddit 10 11 link(rel='stylesheet', href='/styles.css') 11 12 script(src="https://cdn.dashjs.org/latest/dash.all.min.js") 13 + script. 14 + function toggleDetails(details_id) { 15 + var detailsElement = document.getElementById(details_id); 16 + if (detailsElement) { 17 + detailsElement.open = !detailsElement.open; 18 + } 19 + } 20 + 12 21 body 13 22 main#content 14 23 +header()
+23 -13
src/views/index.pug
··· 1 1 include ../mixins/post 2 2 include ../mixins/sub 3 3 include ../mixins/header 4 + include ../utils 4 5 - var subs = [] 5 6 doctype html 6 7 html ··· 42 43 +header() 43 44 44 45 div.hero 45 - a(href=`/r/${subreddit}`) 46 - h1 r/#{subreddit} 46 + div.sub-title 47 + a(href=`/r/${subreddit}`) 48 + h1 r/#{subreddit} 49 + div#button-container 47 50 if about 48 51 p #{about.public_description} 49 - div#button-container 50 - ul 51 - li 52 - a(href=`/r/${subreddit}/hot`) hot 53 - li 54 - a(href=`/r/${subreddit}/top`) top 55 - li 56 - a(href=`/r/${subreddit}/top?t=all`) top all 52 + details 53 + summary sort by 54 + div.sort-opts 55 + a(href=`/r/${subreddit}?sort=hot`) hot 56 + a(href=`/r/${subreddit}?sort=new`) new 57 + a(href=`/r/${subreddit}?sort=rising`) rising 58 + a(href=`/r/${subreddit}?sort=top`) top 59 + a(href=`/r/${subreddit}?sort=top&t=day`) top day 60 + a(href=`/r/${subreddit}?sort=top&t=week`) top week 61 + a(href=`/r/${subreddit}?sort=top&t=month`) top month 62 + a(href=`/r/${subreddit}?sort=top&t=year`) top year 63 + a(href=`/r/${subreddit}?sort=top&t=all`) top all 57 64 58 65 if posts 59 66 each child in posts.posts 60 67 +post(child.data) 61 - div.footer 62 - div.footer-item 63 - a(href=`/r/${subreddit}?after=${posts.after}`) next → 68 + 69 + if posts.after 70 + div.footer 71 + div.footer-item 72 + - var newQuery = {...query, after: posts.after} 73 + a(href=`/r/${subreddit}?${encodeQueryParams(newQuery)}`) next →