Thread viewer for Bluesky

added folding/unfolding of thread sections

icons/add-square.png

This is a binary file and will not be displayed.

icons/subtract-square.png

This is a binary file and will not be displayed.

+1 -1
index.html
··· 9 <link href="style.css" rel="stylesheet"> 10 </head> 11 <body> 12 - <div id="loader"><img src="sunny.png"></div> 13 14 <div id="search"> 15 <form method="get">
··· 9 <link href="style.css" rel="stylesheet"> 10 </head> 11 <body> 12 + <div id="loader"><img src="icons/sunny.png"></div> 13 14 <div id="search"> 15 <form method="get">
+46 -7
post.js
··· 23 let header = this.buildPostHeader(); 24 div.appendChild(header); 25 26 let p = document.createElement('p'); 27 p.innerText = this.post.text; 28 - div.appendChild(p); 29 30 if (this.post.embed) { 31 let embed = document.createElement('p'); 32 embed.innerText = `[${this.post.embed.$type}]`; 33 - div.appendChild(embed); 34 } 35 36 let stats = this.buildStatsFooter(); 37 - div.appendChild(stats); 38 39 if (this.post.replies.length == 1 && this.post.replies[0].author.did == this.post.author.did) { 40 let component = new PostComponent(this.post.replies[0], this.root); 41 let element = component.buildElement(); 42 element.classList.add('flat'); 43 - div.appendChild(element); 44 } else { 45 for (let reply of this.post.replies) { 46 let component = new PostComponent(reply, this.root); 47 - div.appendChild(component.buildElement()); 48 } 49 } 50 51 if (this.post.replyCount != this.post.replies.length) { 52 let loadMore = this.buildLoadMoreLink() 53 - div.appendChild(loadMore); 54 } 55 56 return div; 57 } 58 59 timeFormatForTimestamp() { ··· 142 143 link.addEventListener('click', (e) => { 144 e.preventDefault(); 145 - link.innerHTML = `<img class="loader" src="sunny.png">`; 146 loadThread(this.post.author.handle, this.post.id, loadMore.parentNode.parentNode); 147 }); 148
··· 23 let header = this.buildPostHeader(); 24 div.appendChild(header); 25 26 + let content = document.createElement('div'); 27 + content.className = 'content'; 28 + 29 + if (!this.isRoot) { 30 + let edge = document.createElement('div'); 31 + edge.className = 'edge'; 32 + div.appendChild(edge); 33 + 34 + let line = document.createElement('div'); 35 + line.className = 'line'; 36 + edge.appendChild(line); 37 + 38 + let plus = document.createElement('img'); 39 + plus.className = 'plus'; 40 + plus.src = 'icons/subtract-square.png'; 41 + div.appendChild(plus); 42 + 43 + for (let element of [edge, plus]) { 44 + element.addEventListener('click', (e) => { 45 + e.preventDefault(); 46 + this.toggleSectionFold(div); 47 + }); 48 + } 49 + } 50 + 51 let p = document.createElement('p'); 52 p.innerText = this.post.text; 53 + content.appendChild(p); 54 55 if (this.post.embed) { 56 let embed = document.createElement('p'); 57 embed.innerText = `[${this.post.embed.$type}]`; 58 + content.appendChild(embed); 59 } 60 61 let stats = this.buildStatsFooter(); 62 + content.appendChild(stats); 63 64 if (this.post.replies.length == 1 && this.post.replies[0].author.did == this.post.author.did) { 65 let component = new PostComponent(this.post.replies[0], this.root); 66 let element = component.buildElement(); 67 element.classList.add('flat'); 68 + content.appendChild(element); 69 } else { 70 for (let reply of this.post.replies) { 71 let component = new PostComponent(reply, this.root); 72 + content.appendChild(component.buildElement()); 73 } 74 } 75 76 if (this.post.replyCount != this.post.replies.length) { 77 let loadMore = this.buildLoadMoreLink() 78 + content.appendChild(loadMore); 79 } 80 + 81 + div.appendChild(content); 82 83 return div; 84 + } 85 + 86 + toggleSectionFold(div) { 87 + let plus = div.querySelector('.plus'); 88 + 89 + if (div.classList.contains('collapsed')) { 90 + div.classList.remove('collapsed'); 91 + plus.src = 'icons/subtract-square.png' 92 + } else { 93 + div.classList.add('collapsed'); 94 + plus.src = 'icons/add-square.png' 95 + } 96 } 97 98 timeFormatForTimestamp() { ··· 181 182 link.addEventListener('click', (e) => { 183 e.preventDefault(); 184 + link.innerHTML = `<img class="loader" src="icons/sunny.png">`; 185 loadThread(this.post.author.handle, this.post.id, loadMore.parentNode.parentNode); 186 }); 187
+38 -4
style.css
··· 184 } 185 186 .post { 187 - padding-left: 20px; 188 margin-top: 30px; 189 border-left: 1px solid #aaa; 190 } 191 192 - body > .post { 193 - border-left: 0px; 194 } 195 196 .post.flat { 197 padding-left: 0px; 198 - border-left: 0px; 199 margin-top: 25px; 200 } 201 202 .post .avatar { 203 width: 32px; 204 height: 32px; ··· 214 border-radius: 16px; 215 vertical-align: middle; 216 margin-right: 8px; 217 } 218 219 .post h2 {
··· 184 } 185 186 .post { 187 + position: relative; 188 + padding-left: 21px; 189 margin-top: 30px; 190 + } 191 + 192 + .post .edge { 193 + position: absolute; 194 + left: -2px; 195 + top: 30px; 196 + bottom: 0px; 197 + width: 6px; 198 + } 199 + 200 + .post .edge .line { 201 + position: absolute; 202 + left: 2px; 203 + top: 0px; 204 + bottom: 0px; 205 border-left: 1px solid #aaa; 206 } 207 208 + .post .edge:hover .line { 209 + border-left: 2px solid #888; 210 + } 211 + 212 + .post.collapsed .line { 213 + display: none; 214 + } 215 + 216 + .post.collapsed .content { 217 + display: none; 218 } 219 220 .post.flat { 221 padding-left: 0px; 222 margin-top: 25px; 223 } 224 225 + .post.flat .line { 226 + display: none; 227 + } 228 + 229 .post .avatar { 230 width: 32px; 231 height: 32px; ··· 241 border-radius: 16px; 242 vertical-align: middle; 243 margin-right: 8px; 244 + } 245 + 246 + .post .plus { 247 + position: absolute; 248 + top: 8px; 249 + left: -6px; 250 + width: 14px; 251 } 252 253 .post h2 {
sunny.png icons/sunny.png