From 85af5be925fccdc75f06189feabc246a5a8a95f1 Mon Sep 17 00:00:00 2001 From: Christopher Gurnee Date: Fri, 13 May 2022 22:04:49 +0000 Subject: [PATCH] Add context query param: shows permalink ancestors Appending `?context=#` to a permalink will highlight the permalinked comment and display above it up to 8 ancestors, similar to Reddit's behaviour. To keep things fast, this update required that the Pushshift comments endpoint work with `ids` queries which was fixed around May 11. --- dist/main.css | 2 +- src/api/pushshift/index.js | 41 ++++- src/api/reddit/index.js | 18 ++ src/pages/thread/Comment.js | 6 +- src/pages/thread/CommentSection.js | 21 ++- src/pages/thread/LoadMore.js | 15 +- src/pages/thread/index.js | 278 ++++++++++++++++++----------- src/sass/comment.sass | 5 + 8 files changed, 272 insertions(+), 114 deletions(-) diff --git a/dist/main.css b/dist/main.css index 1e1da1c..388a6c9 100644 --- a/dist/main.css +++ b/dist/main.css @@ -1,2 +1,2 @@ -:root{--background: #262626;--scrolltrack: black;--scrollthumb: #888;--scrollbar: #888 black;--light: #ddd;--white: white;--border: #767676;--border-lt: #808080;--deleted-bg: #00007d;--removed-bg: #840c09;--link: #8cb3d9;--author: #6a98af;--dim: #828282;--score: #b4b4b4;--rank: #505050;--odd-bg: #121212;--even-bg: #161616;--comment-brd: #333;--tooltip-bg: #121212;--thread-title: #a6a6a6;--thread-score: #646464;--thread-domain: #888;--thread-flair: #c8c8c8;--thread-flr-bg: #404040;--thread-flr-brd: #4d4d4d;--thread-brd: #666;--thread-rest: #ccc;--thread-rest-bg: #264d73;--thread-rest-brd: #2966a3;--about-bg: #161616;--about-q: #add8e6;--l-background: #262626;--l-scrolltrack: black;--l-scrollthumb: #888;--l-scrollbar: #888 black;--l-light: #ddd;--l-white: white;--l-border: #767676;--l-border-lt: #808080;--l-deleted-bg: #00007d;--l-removed-bg: #840c09;--l-link: #8cb3d9;--l-author: #6a98af;--l-dim: #828282;--l-score: #b4b4b4;--l-rank: #505050;--l-odd-bg: #121212;--l-even-bg: #161616;--l-comment-brd: #333;--l-tooltip-bg: #121212;--l-thread-title: #a6a6a6;--l-thread-score: #646464;--l-thread-domain: #888;--l-thread-flair: #c8c8c8;--l-thread-flr-bg: #404040;--l-thread-flr-brd: #4d4d4d;--l-thread-brd: #666;--l-thread-rest: #ccc;--l-thread-rest-bg: #264d73;--l-thread-rest-brd: #2966a3;--l-about-bg: #161616;--l-about-q: #add8e6}[data-theme=LIGHT]{--background: #f1f1f1;--scrolltrack: #e7e7e7;--scrollthumb: #eee;--scrollbar: auto;--light: #111;--white: black;--border: #898989;--border-lt: #7f7f7f;--deleted-bg: #a3c2ff;--removed-bg: #ff9494;--link: #00e;--author: #369;--dim: #737373;--score: #4b4b4b;--rank: #afafaf;--odd-bg: #f6f6f6;--even-bg: white;--comment-brd: #ccc;--tooltip-bg: white;--thread-title: #4d4d4d;--thread-score: #878787;--thread-domain: #777;--thread-flair: black;--thread-flr-bg: #c9c9c9;--thread-flr-brd: #999;--thread-brd: #999;--thread-rest: #333;--thread-rest-bg: #d9b28c;--thread-rest-brd: #d6995c;--about-bg: white;--about-q: #522719}[data-theme=LIGHT],[data-theme=SYSTEM]{--l-background: #f1f1f1;--l-scrolltrack: #e7e7e7;--l-scrollthumb: #e7e7e7;--l-scrollbar: auto;--l-light: #111;--l-white: black;--l-border: #898989;--l-border-lt: #7f7f7f;--l-deleted-bg: #a3c2ff;--l-removed-bg: #ff9494;--l-link: #00e;--l-author: #369;--l-dim: #737373;--l-score: #4b4b4b;--l-rank: #afafaf;--l-odd-bg: #f6f6f6;--l-even-bg: white;--l-comment-brd: #ccc;--l-tooltip-bg: white;--l-thread-title: #4d4d4d;--l-thread-score: #878787;--l-thread-domain: #777;--l-thread-flair: black;--l-thread-flr-bg: #c9c9c9;--l-thread-flr-brd: #999;--l-thread-brd: #999;--l-thread-rest: #333;--l-thread-rest-bg: #d9b28c;--l-thread-rest-brd: #d6995c;--l-about-bg: white;--l-about-q: #522719}html{background-color:var(--background);font-family:verdana,arial,helvetica,sans-serif;scrollbar-color:var(--scrollbar);transition-property:background-color;transition-timing-function:ease-out;transition-duration:0s}@media(prefers-color-scheme: light){html{background-color:var(--l-background)}}html ::-webkit-scrollbar{background-color:var(--scrolltrack)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar{background-color:var(--l-scrolltrack)}}html ::-webkit-scrollbar-thumb{-webkit-box-shadow:inset 0 0 8px rgba(0,0,0,.8);background-color:var(--scrollthumb)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar-thumb{background-color:var(--l-scrollthumb)}}html ::-webkit-scrollbar-corner{-webkit-box-shadow:inset 0 0 8px rgba(0,0,0,.8);background-color:var(--scrollthumb)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar-corner{background-color:var(--l-scrollthumb)}}@media(prefers-color-scheme: light){html{scrollbar-color:var(--l-scrollbar)}}div,input{transition-property:background-color,border-color;transition-timing-function:ease-out;transition-duration:.4s}body{margin:0}.wait{cursor:wait}.main{margin:8px 15px 15px 15px;color:var(--white)}@media(prefers-color-scheme: light){.main{color:var(--l-white)}}.removed{background-color:var(--removed-bg)}@media(prefers-color-scheme: light){.removed{background-color:var(--l-removed-bg)}}.deleted{background-color:var(--deleted-bg)}@media(prefers-color-scheme: light){.deleted{background-color:var(--l-deleted-bg)}}.removed-text{color:#c70300}.deleted-text{color:blue}a{color:var(--link)}@media(prefers-color-scheme: light){a{color:var(--l-link)}}a:link,a:visited,a:active{text-decoration:none}a:hover{text-decoration:underline}.author{color:var(--author)}@media(prefers-color-scheme: light){.author{color:var(--l-author)}}.author:not([href]){color:inherit;text-decoration:none}.nowrap{display:inline-block !important}.space{margin-left:5px}select{background:rgba(0,0,0,0);border:0;color:var(--dim)}@media(prefers-color-scheme: light){select{color:var(--l-dim)}}@media(pointer: coarse),(hover: none){span[title],a[title]{position:relative;display:inline-flex;justify-content:center}span[title]:hover::after,a[title]:hover::after{content:attr(title);position:absolute;top:95%;z-index:1;padding:3px;min-width:125px;font-weight:normal;text-align:center;color:var(--white);background-color:var(--tooltip-bg);border:1px solid var(--thread-brd);border-radius:3px}}@media(pointer: coarse)and (prefers-color-scheme: light),(hover: none)and (prefers-color-scheme: light){span[title]:hover::after,a[title]:hover::after{color:var(--l-white);background-color:var(--l-tooltip-bg);border-color:var(--l-thread-brd)}}header{background-color:#c70300;color:#fff;display:flex;justify-content:space-between;align-items:center;margin:7px;padding:7px}header #header h1{margin:0 0 7px;text-align:center}header #header .switch-toggle{margin-right:14px;float:left;font-size:14px}header #header .switch-toggle label{cursor:pointer}header #header a[href]{color:#fff;font-size:20px}header #status{display:flex;justify-content:space-between;align-items:center}header #status #status-text,header #status #status-helpurl{margin:0 20px 0 0;text-align:center}header #status #status-helpurl{color:#fff;font-size:20px}header #status #status-image{height:64px;width:64px;border-radius:32px}#main-box{margin:0 auto;padding:20px;border-radius:5px;color:var(--light);word-wrap:break-word;max-width:800px;background-color:var(--about-bg)}@media(prefers-color-scheme: light){#main-box{color:var(--l-light);background-color:var(--l-about-bg)}}#main-box h2{margin:20px 0px 12px}#main-box h2.about{margin:0;color:#c70300}#main-box h2.todo{color:#239f2b}#main-box h2.contact{color:#1b767a}#main-box .highlighted{animation:highlighted-anim 3s ease-out}@keyframes highlighted-anim{0%{background-color:var(--about-q)}}@media(prefers-color-scheme: light){@keyframes highlighted-anim{0%{background-color:var(--l-about-q)}}}#main-box .question{font-size:18px;color:var(--about-q)}@media(prefers-color-scheme: light){#main-box .question{color:var(--l-about-q)}}#main-box ul{padding-left:30px;margin:0}#main-box .bookmarklet{background:#c70300;color:#fff;padding:9px;font-size:large;font-weight:bold;display:inline-block;border-radius:5px;margin:0 7px}.comment{min-width:240px;margin:0 0 8px 0;padding:5px 8px 5px 20px;border:1px solid var(--comment-brd);border-radius:3px;color:var(--light);font-size:14px}@media(prefers-color-scheme: light){.comment{border-color:var(--l-comment-brd);color:var(--l-light)}}.comment .comment-head{font-size:10px}.comment .comment-collapse{cursor:pointer}.comment .comment-collapsed span,.comment .comment-collapsed a:not(.comment-collapse){color:var(--dim);font-style:oblique}@media(prefers-color-scheme: light){.comment .comment-collapsed span,.comment .comment-collapsed a:not(.comment-collapse){color:var(--l-dim)}}.comment .comment-author{font-weight:bold}.comment .comment-author:not([href]){color:var(--dim)}@media(prefers-color-scheme: light){.comment .comment-author:not([href]){color:var(--l-dim)}}.comment .comment-poster{color:#ddd;background-color:#0055df;padding:0 2px;border-radius:3px}.comment .comment-score{color:var(--score);font-weight:bold}@media(prefers-color-scheme: light){.comment .comment-score{color:var(--l-score)}}.comment .comment-time{color:var(--dim)}@media(prefers-color-scheme: light){.comment .comment-time{color:var(--l-dim)}}.comment .comment-body{max-width:840px;line-height:20px}.comment .comment-body p{margin:5px 0;line-height:20px}.comment .comment-body a:hover{text-decoration:none}.comment .comment-links{margin-bottom:6px}.comment .comment-links a{color:var(--dim);font-weight:bold;font-size:10px;margin-right:4px;cursor:pointer}@media(prefers-color-scheme: light){.comment .comment-links a{color:var(--l-dim)}}.comment .comment-links a.wait{cursor:wait}.comment .comment-links a.wait:hover{text-decoration:none}.comment-odd{background-color:var(--odd-bg)}@media(prefers-color-scheme: light){.comment-odd{background-color:var(--l-odd-bg)}}.comment-even{background-color:var(--even-bg)}@media(prefers-color-scheme: light){.comment-even{background-color:var(--l-even-bg)}}#comment-info{margin-top:10px;font-weight:bold;padding-bottom:5px;border-bottom:1px dotted var(--border-lt);font-size:16px}@media(prefers-color-scheme: light){#comment-info{border-bottom-color:var(--l-border-lt)}}#comment-info span{margin-right:8px}#comment-sort,#comment-sort input{color:var(--border-lt);font-size:12px;margin:5px 0;background-color:var(--background)}@media(prefers-color-scheme: light){#comment-sort,#comment-sort input{color:var(--l-border-lt);background-color:var(--l-background)}}#comment-sort input{border-color:var(--border)}@media(prefers-color-scheme: light){#comment-sort input{border-color:var(--l-border)}}#comment-sort input[type=number]{width:54px;appearance:textfield}#comment-sort input[type=number]:hover,#comment-sort input[type=number]:focus{appearance:none}#comment-sort input:hover,#comment-sort input:focus,#comment-sort select:hover,#comment-sort select:focus{color:var(--light)}@media(prefers-color-scheme: light){#comment-sort input:hover,#comment-sort input:focus,#comment-sort select:hover,#comment-sort select:focus{color:var(--l-light)}}#comment-sort option{color:var(--light);background-color:var(--background)}@media(prefers-color-scheme: light){#comment-sort option{color:var(--l-light);background-color:var(--l-background)}}#comment-sort .attention{outline:3px solid #c70300;border-radius:2px;animation:attention-anim 1s steps(2, jump-none) 2}@keyframes attention-anim{100%{outline-style:none}}.thread{display:flex;flex-grow:1;margin-bottom:7px;border-radius:1px;padding-top:6px}.thread .thumbnail{width:70px;margin-right:3px;margin-top:-2px}.thread .thumbnail img{border-radius:1px}.thread .thumbnail-default{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1099px;background-repeat:no-repeat;height:50px}.thread .thumbnail-self{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1267px;background-repeat:no-repeat;height:50px}.thread .thumbnail-image{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1043px;background-repeat:no-repeat;height:50px}.thread .thumbnail-nsfw{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1155px;background-repeat:no-repeat;height:50px}.thread .thread-score-box{margin:0 7px;width:43px}.thread .vote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAcCAMAAADC4sagAAABFFBMVEUjJCQiIiIjIyMkIiIkJCQlJSUlJicmJiYnIyEnJycnKCsoKCgpLC8rIyErKyssMDUvLy8wJCAwMDAxMTEyMjI0NDQ2NjY3JR83Nzc5OTk7Ozs8PDw9PT0+Pj4/Pz9AQEBBQUFCQkJDQ0NGRkZISEhKSkpKV2tLS0tMTExOTk5PT09QYHdSUlJTU1NVVVVWVlZXV1dXaINYWFhaWlpabYlbW1tecpBgYGBhYWFid5dkZGRle5xlfJ1pgKRrhKhshKlshapuLhZwirFyjbV0j7h4lL97mcZ9MBR/nsyCo9OOMxGcNQ+eNg+kNw6sOA2vOAy0OQy7Ogq/OwrBOwrDPAnLPQjPPQfTPgfbPwXjQQTrQgPzQwLKxGgxAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlJREFUOMu9lF1TwjAQRTcMvmeQOi1QPmxFBQTxW1ERUUFEUVRU/P//w9yGmBTFyZPnpXNmbju76W6IJM8Uw49rLa6Tz4mpq/mCqY3Gvqnj6XQ61lp0mGd8favCwx2tow8wUlpwGWOZrNJKjXO+UVc6fJcMZzV7IswSOU/qej0l0rwyq33wphhAM1kWkcy70GB7mUfUNqG9V02PyPMTMs2Wig5RaTctwzxVXyPqvJh0VkJBIKJ4huncCRAvHOEZyGKegO4a6UDrIdLGCT4C6/QDsE7fA+v0HdAadWqk0SH9D3sgXkm4uJIDYKTFnwwXd3kMrNOnwDp9BmzS5QuTshN1iDRwfkxV9VJTJXLzSWYM+NzEEjWvFE2oGvCE7/2yDeL8riWzM8xmorRazLlNI2rdgNb3ZYLF1Es/t8VE7X6/39Yqlt4tLLwhiLq3XVNLfumP24fo3Opm+wLGCVa252Y8tQAAAABJRU5ErkJggg==);background-repeat:no-repeat;margin-left:auto;margin-right:auto;width:15px;height:14px;cursor:pointer}.thread .upvote{background-position:-15px 0px;margin-top:2px}.thread .downvote{background-position:-15px -14px;margin-top:2px}.thread .thread-score{color:var(--thread-score);font-size:13px;font-weight:bold;text-align:center}@media(prefers-color-scheme: light){.thread .thread-score{color:var(--l-thread-score)}}.thread .link-flair{display:inline-block;color:var(--thread-flair);background-color:var(--thread-flr-bg);margin-right:5px;border:1px solid var(--thread-flr-brd);border-radius:2px;padding:0 2px;font-size:10px}@media(prefers-color-scheme: light){.thread .link-flair{color:var(--l-thread-flair);background-color:var(--l-thread-flr-bg);border-color:var(--l-thread-flr-brd)}}.thread .thread-title{color:var(--thread-title);margin-right:5px}@media(prefers-color-scheme: light){.thread .thread-title{color:var(--l-thread-title)}}.thread .thread-title:hover{text-decoration:none}.thread .domain{color:var(--thread-domain);font-size:10px}@media(prefers-color-scheme: light){.thread .domain{color:var(--l-thread-domain)}}.thread .thread-image{max-width:768px;max-height:768px}.thread .thread-info{color:var(--dim);font-size:10px;margin-top:2px}@media(prefers-color-scheme: light){.thread .thread-info{color:var(--l-dim)}}.thread-content{margin:0 7px}.thread-content .thread-selftext{border:1px solid var(--thread-brd);border-radius:7px;margin:5px 0 7px;padding:5px 10px;max-width:840px;color:var(--light);font-size:14px}@media(prefers-color-scheme: light){.thread-content .thread-selftext{border-color:var(--l-thread-brd);color:var(--l-light)}}.thread-content .thread-selftext p{margin:5px 0;line-height:20px}.thread-content .thread-selftext a:hover{text-decoration:none}.thread-content .thread-selftext p:first-child{margin-top:0px}.thread-content .thread-selftext p:last-child{margin-bottom:0px}.thread-content .total-comments{font-size:10px;font-weight:bold;margin-top:4px;padding-bottom:7px}.thread-content .total-comments a,.thread-content .total-comments span{color:var(--dim);margin-right:4px}@media(prefers-color-scheme: light){.thread-content .total-comments a,.thread-content .total-comments span{color:var(--l-dim)}}.thread-content .total-comments a{cursor:pointer}.view-rest-of-comment{background-color:var(--thread-rest-bg);border-color:var(--thread-rest-brd);font-size:13px;padding:10px;color:var(--thread-rest);margin-bottom:10px}@media(prefers-color-scheme: light){.view-rest-of-comment{background-color:var(--l-thread-rest-bg);border-color:var(--l-thread-rest-brd);color:var(--l-thread-rest)}}.view-rest-of-comment .faux-link{color:var(--link)}@media(prefers-color-scheme: light){.view-rest-of-comment .faux-link{color:var(--l-link)}}.load-more{font-weight:bold;font-size:10px}.load-more a,.load-more span{margin-left:16px}.load-more a{cursor:pointer}.load-more .fade{animation:fade-anim 3s 5s ease-in-out forwards}@keyframes fade-anim{100%{color:var(--background)}}@media(prefers-color-scheme: light){@keyframes fade-anim{100%{color:var(--l-background)}}}.post-rank{min-width:20px;color:var(--rank);font-size:16px;text-align:right;margin:14px 4px 0 0}@media(prefers-color-scheme: light){.post-rank{color:var(--l-rank)}} +:root{--background: #262626;--scrolltrack: black;--scrollthumb: #888;--scrollbar: #888 black;--light: #ddd;--white: white;--border: #767676;--border-lt: #808080;--deleted-bg: #00007d;--removed-bg: #840c09;--link: #8cb3d9;--author: #6a98af;--dim: #828282;--score: #b4b4b4;--rank: #505050;--odd-bg: #121212;--even-bg: #161616;--comment-brd: #333;--tooltip-bg: #121212;--thread-title: #a6a6a6;--thread-score: #646464;--thread-domain: #888;--thread-flair: #c8c8c8;--thread-flr-bg: #404040;--thread-flr-brd: #4d4d4d;--thread-brd: #666;--thread-rest: #ccc;--thread-rest-bg: #264d73;--thread-rest-brd: #2966a3;--about-bg: #161616;--about-q: #add8e6;--l-background: #262626;--l-scrolltrack: black;--l-scrollthumb: #888;--l-scrollbar: #888 black;--l-light: #ddd;--l-white: white;--l-border: #767676;--l-border-lt: #808080;--l-deleted-bg: #00007d;--l-removed-bg: #840c09;--l-link: #8cb3d9;--l-author: #6a98af;--l-dim: #828282;--l-score: #b4b4b4;--l-rank: #505050;--l-odd-bg: #121212;--l-even-bg: #161616;--l-comment-brd: #333;--l-tooltip-bg: #121212;--l-thread-title: #a6a6a6;--l-thread-score: #646464;--l-thread-domain: #888;--l-thread-flair: #c8c8c8;--l-thread-flr-bg: #404040;--l-thread-flr-brd: #4d4d4d;--l-thread-brd: #666;--l-thread-rest: #ccc;--l-thread-rest-bg: #264d73;--l-thread-rest-brd: #2966a3;--l-about-bg: #161616;--l-about-q: #add8e6}[data-theme=LIGHT]{--background: #f1f1f1;--scrolltrack: #e7e7e7;--scrollthumb: #eee;--scrollbar: auto;--light: #111;--white: black;--border: #898989;--border-lt: #7f7f7f;--deleted-bg: #a3c2ff;--removed-bg: #ff9494;--link: #00e;--author: #369;--dim: #737373;--score: #4b4b4b;--rank: #afafaf;--odd-bg: #f6f6f6;--even-bg: white;--comment-brd: #ccc;--tooltip-bg: white;--thread-title: #4d4d4d;--thread-score: #878787;--thread-domain: #777;--thread-flair: black;--thread-flr-bg: #c9c9c9;--thread-flr-brd: #999;--thread-brd: #999;--thread-rest: #333;--thread-rest-bg: #d9b28c;--thread-rest-brd: #d6995c;--about-bg: white;--about-q: #522719}[data-theme=LIGHT],[data-theme=SYSTEM]{--l-background: #f1f1f1;--l-scrolltrack: #e7e7e7;--l-scrollthumb: #e7e7e7;--l-scrollbar: auto;--l-light: #111;--l-white: black;--l-border: #898989;--l-border-lt: #7f7f7f;--l-deleted-bg: #a3c2ff;--l-removed-bg: #ff9494;--l-link: #00e;--l-author: #369;--l-dim: #737373;--l-score: #4b4b4b;--l-rank: #afafaf;--l-odd-bg: #f6f6f6;--l-even-bg: white;--l-comment-brd: #ccc;--l-tooltip-bg: white;--l-thread-title: #4d4d4d;--l-thread-score: #878787;--l-thread-domain: #777;--l-thread-flair: black;--l-thread-flr-bg: #c9c9c9;--l-thread-flr-brd: #999;--l-thread-brd: #999;--l-thread-rest: #333;--l-thread-rest-bg: #d9b28c;--l-thread-rest-brd: #d6995c;--l-about-bg: white;--l-about-q: #522719}html{background-color:var(--background);font-family:verdana,arial,helvetica,sans-serif;scrollbar-color:var(--scrollbar);transition-property:background-color;transition-timing-function:ease-out;transition-duration:0s}@media(prefers-color-scheme: light){html{background-color:var(--l-background)}}html ::-webkit-scrollbar{background-color:var(--scrolltrack)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar{background-color:var(--l-scrolltrack)}}html ::-webkit-scrollbar-thumb{-webkit-box-shadow:inset 0 0 8px rgba(0,0,0,.8);background-color:var(--scrollthumb)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar-thumb{background-color:var(--l-scrollthumb)}}html ::-webkit-scrollbar-corner{-webkit-box-shadow:inset 0 0 8px rgba(0,0,0,.8);background-color:var(--scrollthumb)}@media(prefers-color-scheme: light){html ::-webkit-scrollbar-corner{background-color:var(--l-scrollthumb)}}@media(prefers-color-scheme: light){html{scrollbar-color:var(--l-scrollbar)}}div,input{transition-property:background-color,border-color;transition-timing-function:ease-out;transition-duration:.4s}body{margin:0}.wait{cursor:wait}.main{margin:8px 15px 15px 15px;color:var(--white)}@media(prefers-color-scheme: light){.main{color:var(--l-white)}}.removed{background-color:var(--removed-bg)}@media(prefers-color-scheme: light){.removed{background-color:var(--l-removed-bg)}}.deleted{background-color:var(--deleted-bg)}@media(prefers-color-scheme: light){.deleted{background-color:var(--l-deleted-bg)}}.removed-text{color:#c70300}.deleted-text{color:blue}a{color:var(--link)}@media(prefers-color-scheme: light){a{color:var(--l-link)}}a:link,a:visited,a:active{text-decoration:none}a:hover{text-decoration:underline}.author{color:var(--author)}@media(prefers-color-scheme: light){.author{color:var(--l-author)}}.author:not([href]){color:inherit;text-decoration:none}.nowrap{display:inline-block !important}.space{margin-left:5px}select{background:rgba(0,0,0,0);border:0;color:var(--dim)}@media(prefers-color-scheme: light){select{color:var(--l-dim)}}@media(pointer: coarse),(hover: none){span[title],a[title]{position:relative;display:inline-flex;justify-content:center}span[title]:hover::after,a[title]:hover::after{content:attr(title);position:absolute;top:95%;z-index:1;padding:3px;min-width:125px;font-weight:normal;text-align:center;color:var(--white);background-color:var(--tooltip-bg);border:1px solid var(--thread-brd);border-radius:3px}}@media(pointer: coarse)and (prefers-color-scheme: light),(hover: none)and (prefers-color-scheme: light){span[title]:hover::after,a[title]:hover::after{color:var(--l-white);background-color:var(--l-tooltip-bg);border-color:var(--l-thread-brd)}}header{background-color:#c70300;color:#fff;display:flex;justify-content:space-between;align-items:center;margin:7px;padding:7px}header #header h1{margin:0 0 7px;text-align:center}header #header .switch-toggle{margin-right:14px;float:left;font-size:14px}header #header .switch-toggle label{cursor:pointer}header #header a[href]{color:#fff;font-size:20px}header #status{display:flex;justify-content:space-between;align-items:center}header #status #status-text,header #status #status-helpurl{margin:0 20px 0 0;text-align:center}header #status #status-helpurl{color:#fff;font-size:20px}header #status #status-image{height:64px;width:64px;border-radius:32px}#main-box{margin:0 auto;padding:20px;border-radius:5px;color:var(--light);word-wrap:break-word;max-width:800px;background-color:var(--about-bg)}@media(prefers-color-scheme: light){#main-box{color:var(--l-light);background-color:var(--l-about-bg)}}#main-box h2{margin:20px 0px 12px}#main-box h2.about{margin:0;color:#c70300}#main-box h2.todo{color:#239f2b}#main-box h2.contact{color:#1b767a}#main-box .highlighted{animation:highlighted-anim 3s ease-out}@keyframes highlighted-anim{0%{background-color:var(--about-q)}}@media(prefers-color-scheme: light){@keyframes highlighted-anim{0%{background-color:var(--l-about-q)}}}#main-box .question{font-size:18px;color:var(--about-q)}@media(prefers-color-scheme: light){#main-box .question{color:var(--l-about-q)}}#main-box ul{padding-left:30px;margin:0}#main-box .bookmarklet{background:#c70300;color:#fff;padding:9px;font-size:large;font-weight:bold;display:inline-block;border-radius:5px;margin:0 7px}.comment{min-width:240px;margin:0 0 8px 0;padding:5px 8px 5px 20px;border:1px solid var(--comment-brd);border-radius:3px;color:var(--light);font-size:14px}@media(prefers-color-scheme: light){.comment{border-color:var(--l-comment-brd);color:var(--l-light)}}.comment .highlighted{border:2px solid var(--thread-rest-brd)}@media(prefers-color-scheme: light){.comment .highlighted{border-color:var(--l-thread-rest-brd)}}.comment .comment-head{font-size:10px}.comment .comment-collapse{cursor:pointer}.comment .comment-collapsed span,.comment .comment-collapsed a:not(.comment-collapse){color:var(--dim);font-style:oblique}@media(prefers-color-scheme: light){.comment .comment-collapsed span,.comment .comment-collapsed a:not(.comment-collapse){color:var(--l-dim)}}.comment .comment-author{font-weight:bold}.comment .comment-author:not([href]){color:var(--dim)}@media(prefers-color-scheme: light){.comment .comment-author:not([href]){color:var(--l-dim)}}.comment .comment-poster{color:#ddd;background-color:#0055df;padding:0 2px;border-radius:3px}.comment .comment-score{color:var(--score);font-weight:bold}@media(prefers-color-scheme: light){.comment .comment-score{color:var(--l-score)}}.comment .comment-time{color:var(--dim)}@media(prefers-color-scheme: light){.comment .comment-time{color:var(--l-dim)}}.comment .comment-body{max-width:840px;line-height:20px}.comment .comment-body p{margin:5px 0;line-height:20px}.comment .comment-body a:hover{text-decoration:none}.comment .comment-links{margin-bottom:6px}.comment .comment-links a{color:var(--dim);font-weight:bold;font-size:10px;margin-right:4px;cursor:pointer}@media(prefers-color-scheme: light){.comment .comment-links a{color:var(--l-dim)}}.comment .comment-links a.wait{cursor:wait}.comment .comment-links a.wait:hover{text-decoration:none}.comment-odd{background-color:var(--odd-bg)}@media(prefers-color-scheme: light){.comment-odd{background-color:var(--l-odd-bg)}}.comment-even{background-color:var(--even-bg)}@media(prefers-color-scheme: light){.comment-even{background-color:var(--l-even-bg)}}#comment-info{margin-top:10px;font-weight:bold;padding-bottom:5px;border-bottom:1px dotted var(--border-lt);font-size:16px}@media(prefers-color-scheme: light){#comment-info{border-bottom-color:var(--l-border-lt)}}#comment-info span{margin-right:8px}#comment-sort,#comment-sort input{color:var(--border-lt);font-size:12px;margin:5px 0;background-color:var(--background)}@media(prefers-color-scheme: light){#comment-sort,#comment-sort input{color:var(--l-border-lt);background-color:var(--l-background)}}#comment-sort input{border-color:var(--border)}@media(prefers-color-scheme: light){#comment-sort input{border-color:var(--l-border)}}#comment-sort input[type=number]{width:54px;appearance:textfield}#comment-sort input[type=number]:hover,#comment-sort input[type=number]:focus{appearance:none}#comment-sort input:hover,#comment-sort input:focus,#comment-sort select:hover,#comment-sort select:focus{color:var(--light)}@media(prefers-color-scheme: light){#comment-sort input:hover,#comment-sort input:focus,#comment-sort select:hover,#comment-sort select:focus{color:var(--l-light)}}#comment-sort option{color:var(--light);background-color:var(--background)}@media(prefers-color-scheme: light){#comment-sort option{color:var(--l-light);background-color:var(--l-background)}}#comment-sort .attention{outline:3px solid #c70300;border-radius:2px;animation:attention-anim 1s steps(2, jump-none) 2}@keyframes attention-anim{100%{outline-style:none}}.thread{display:flex;flex-grow:1;margin-bottom:7px;border-radius:1px;padding-top:6px}.thread .thumbnail{width:70px;margin-right:3px;margin-top:-2px}.thread .thumbnail img{border-radius:1px}.thread .thumbnail-default{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1099px;background-repeat:no-repeat;height:50px}.thread .thumbnail-self{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1267px;background-repeat:no-repeat;height:50px}.thread .thumbnail-image{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1043px;background-repeat:no-repeat;height:50px}.thread .thumbnail-nsfw{background-image:url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");background-position:0px -1155px;background-repeat:no-repeat;height:50px}.thread .thread-score-box{margin:0 7px;width:43px}.thread .vote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAcCAMAAADC4sagAAABFFBMVEUjJCQiIiIjIyMkIiIkJCQlJSUlJicmJiYnIyEnJycnKCsoKCgpLC8rIyErKyssMDUvLy8wJCAwMDAxMTEyMjI0NDQ2NjY3JR83Nzc5OTk7Ozs8PDw9PT0+Pj4/Pz9AQEBBQUFCQkJDQ0NGRkZISEhKSkpKV2tLS0tMTExOTk5PT09QYHdSUlJTU1NVVVVWVlZXV1dXaINYWFhaWlpabYlbW1tecpBgYGBhYWFid5dkZGRle5xlfJ1pgKRrhKhshKlshapuLhZwirFyjbV0j7h4lL97mcZ9MBR/nsyCo9OOMxGcNQ+eNg+kNw6sOA2vOAy0OQy7Ogq/OwrBOwrDPAnLPQjPPQfTPgfbPwXjQQTrQgPzQwLKxGgxAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlJREFUOMu9lF1TwjAQRTcMvmeQOi1QPmxFBQTxW1ERUUFEUVRU/P//w9yGmBTFyZPnpXNmbju76W6IJM8Uw49rLa6Tz4mpq/mCqY3Gvqnj6XQ61lp0mGd8favCwx2tow8wUlpwGWOZrNJKjXO+UVc6fJcMZzV7IswSOU/qej0l0rwyq33wphhAM1kWkcy70GB7mUfUNqG9V02PyPMTMs2Wig5RaTctwzxVXyPqvJh0VkJBIKJ4huncCRAvHOEZyGKegO4a6UDrIdLGCT4C6/QDsE7fA+v0HdAadWqk0SH9D3sgXkm4uJIDYKTFnwwXd3kMrNOnwDp9BmzS5QuTshN1iDRwfkxV9VJTJXLzSWYM+NzEEjWvFE2oGvCE7/2yDeL8riWzM8xmorRazLlNI2rdgNb3ZYLF1Es/t8VE7X6/39Yqlt4tLLwhiLq3XVNLfumP24fo3Opm+wLGCVa252Y8tQAAAABJRU5ErkJggg==);background-repeat:no-repeat;margin-left:auto;margin-right:auto;width:15px;height:14px;cursor:pointer}.thread .upvote{background-position:-15px 0px;margin-top:2px}.thread .downvote{background-position:-15px -14px;margin-top:2px}.thread .thread-score{color:var(--thread-score);font-size:13px;font-weight:bold;text-align:center}@media(prefers-color-scheme: light){.thread .thread-score{color:var(--l-thread-score)}}.thread .link-flair{display:inline-block;color:var(--thread-flair);background-color:var(--thread-flr-bg);margin-right:5px;border:1px solid var(--thread-flr-brd);border-radius:2px;padding:0 2px;font-size:10px}@media(prefers-color-scheme: light){.thread .link-flair{color:var(--l-thread-flair);background-color:var(--l-thread-flr-bg);border-color:var(--l-thread-flr-brd)}}.thread .thread-title{color:var(--thread-title);margin-right:5px}@media(prefers-color-scheme: light){.thread .thread-title{color:var(--l-thread-title)}}.thread .thread-title:hover{text-decoration:none}.thread .domain{color:var(--thread-domain);font-size:10px}@media(prefers-color-scheme: light){.thread .domain{color:var(--l-thread-domain)}}.thread .thread-image{max-width:768px;max-height:768px}.thread .thread-info{color:var(--dim);font-size:10px;margin-top:2px}@media(prefers-color-scheme: light){.thread .thread-info{color:var(--l-dim)}}.thread-content{margin:0 7px}.thread-content .thread-selftext{border:1px solid var(--thread-brd);border-radius:7px;margin:5px 0 7px;padding:5px 10px;max-width:840px;color:var(--light);font-size:14px}@media(prefers-color-scheme: light){.thread-content .thread-selftext{border-color:var(--l-thread-brd);color:var(--l-light)}}.thread-content .thread-selftext p{margin:5px 0;line-height:20px}.thread-content .thread-selftext a:hover{text-decoration:none}.thread-content .thread-selftext p:first-child{margin-top:0px}.thread-content .thread-selftext p:last-child{margin-bottom:0px}.thread-content .total-comments{font-size:10px;font-weight:bold;margin-top:4px;padding-bottom:7px}.thread-content .total-comments a,.thread-content .total-comments span{color:var(--dim);margin-right:4px}@media(prefers-color-scheme: light){.thread-content .total-comments a,.thread-content .total-comments span{color:var(--l-dim)}}.thread-content .total-comments a{cursor:pointer}.view-rest-of-comment{background-color:var(--thread-rest-bg);border-color:var(--thread-rest-brd);font-size:13px;padding:10px;color:var(--thread-rest);margin-bottom:10px}@media(prefers-color-scheme: light){.view-rest-of-comment{background-color:var(--l-thread-rest-bg);border-color:var(--l-thread-rest-brd);color:var(--l-thread-rest)}}.view-rest-of-comment .faux-link{color:var(--link)}@media(prefers-color-scheme: light){.view-rest-of-comment .faux-link{color:var(--l-link)}}.load-more{font-weight:bold;font-size:10px}.load-more a,.load-more span{margin-left:16px}.load-more a{cursor:pointer}.load-more .fade{animation:fade-anim 3s 5s ease-in-out forwards}@keyframes fade-anim{100%{color:var(--background)}}@media(prefers-color-scheme: light){@keyframes fade-anim{100%{color:var(--l-background)}}}.post-rank{min-width:20px;color:var(--rank);font-size:16px;text-align:right;margin:14px 4px 0 0}@media(prefers-color-scheme: light){.post-rank{color:var(--l-rank)}} .switch-toggle a,.switch-light span span{display:none}@media only screen{.switch-light{position:relative;display:block}.switch-light::after{clear:both;content:"";display:table}.switch-light *,.switch-light *:before,.switch-light *:after{box-sizing:border-box}.switch-light a{display:block;transition:all .2s ease-out}.switch-light label,.switch-light>span{line-height:2em}.switch-light input:focus~span a,.switch-light input:focus+label{outline-width:2px;outline-style:solid;outline-color:Highlight}}@media only screen and (-webkit-min-device-pixel-ratio: 0){.switch-light input:focus~span a,.switch-light input:focus+label{outline-color:-webkit-focus-ring-color;outline-style:auto}}@media only screen{.switch-light input{position:absolute;opacity:0;z-index:3}.switch-light input:checked~span a{right:0%}.switch-light strong{font-weight:inherit}.switch-light>span{position:relative;overflow:hidden;display:block;min-height:2em;padding:0;text-align:left}.switch-light span span{position:relative;z-index:2;display:block;float:left;width:50%;text-align:center;user-select:none}.switch-light a{position:absolute;right:50%;top:0;z-index:1;display:block;width:50%;height:100%;padding:0}.switch-light.row{display:flex}.switch-light .alert-light{color:#333}.switch-toggle{position:relative;display:block;padding:0 !important}.switch-toggle::after{clear:both;content:"";display:table}.switch-toggle *,.switch-toggle *:before,.switch-toggle *:after{box-sizing:border-box}.switch-toggle a{display:block;transition:all .2s ease-out}.switch-toggle label,.switch-toggle>span{line-height:2em}.switch-toggle input:focus~span a,.switch-toggle input:focus+label{outline-width:2px;outline-style:solid;outline-color:Highlight}}@media only screen and (-webkit-min-device-pixel-ratio: 0){.switch-toggle input:focus~span a,.switch-toggle input:focus+label{outline-color:-webkit-focus-ring-color;outline-style:auto}}@media only screen{.switch-toggle input{position:absolute;left:0;opacity:0}.switch-toggle input+label{position:relative;z-index:2;display:block;float:left;padding:0 8px;margin:0;text-align:center}.switch-toggle a{position:absolute;top:0;left:0;padding:0;z-index:1;width:10px;height:100%}.switch-toggle label:nth-child(2):nth-last-child(4),.switch-toggle label:nth-child(2):nth-last-child(4)~label,.switch-toggle label:nth-child(2):nth-last-child(4)~a{width:50%}.switch-toggle label:nth-child(2):nth-last-child(4)~input:checked:nth-child(3)+label~a{left:50%}.switch-toggle label:nth-child(2):nth-last-child(6),.switch-toggle label:nth-child(2):nth-last-child(6)~label,.switch-toggle label:nth-child(2):nth-last-child(6)~a{width:33.33%}.switch-toggle label:nth-child(2):nth-last-child(6)~input:checked:nth-child(3)+label~a{left:33.33%}.switch-toggle label:nth-child(2):nth-last-child(6)~input:checked:nth-child(5)+label~a{left:66.66%}.switch-toggle label:nth-child(2):nth-last-child(8),.switch-toggle label:nth-child(2):nth-last-child(8)~label,.switch-toggle label:nth-child(2):nth-last-child(8)~a{width:25%}.switch-toggle label:nth-child(2):nth-last-child(8)~input:checked:nth-child(3)+label~a{left:25%}.switch-toggle label:nth-child(2):nth-last-child(8)~input:checked:nth-child(5)+label~a{left:50%}.switch-toggle label:nth-child(2):nth-last-child(8)~input:checked:nth-child(7)+label~a{left:75%}.switch-toggle label:nth-child(2):nth-last-child(10),.switch-toggle label:nth-child(2):nth-last-child(10)~label,.switch-toggle label:nth-child(2):nth-last-child(10)~a{width:20%}.switch-toggle label:nth-child(2):nth-last-child(10)~input:checked:nth-child(3)+label~a{left:20%}.switch-toggle label:nth-child(2):nth-last-child(10)~input:checked:nth-child(5)+label~a{left:40%}.switch-toggle label:nth-child(2):nth-last-child(10)~input:checked:nth-child(7)+label~a{left:60%}.switch-toggle label:nth-child(2):nth-last-child(10)~input:checked:nth-child(9)+label~a{left:80%}.switch-toggle label:nth-child(2):nth-last-child(12),.switch-toggle label:nth-child(2):nth-last-child(12)~label,.switch-toggle label:nth-child(2):nth-last-child(12)~a{width:16.6%}.switch-toggle label:nth-child(2):nth-last-child(12)~input:checked:nth-child(3)+label~a{left:16.6%}.switch-toggle label:nth-child(2):nth-last-child(12)~input:checked:nth-child(5)+label~a{left:33.2%}.switch-toggle label:nth-child(2):nth-last-child(12)~input:checked:nth-child(7)+label~a{left:49.8%}.switch-toggle label:nth-child(2):nth-last-child(12)~input:checked:nth-child(9)+label~a{left:66.4%}.switch-toggle label:nth-child(2):nth-last-child(12)~input:checked:nth-child(11)+label~a{left:83%}.switch-toggle.switch-candy,.switch-light.switch-candy>span{background-color:#2d3035;border-radius:3px;box-shadow:inset 0 2px 6px rgba(0, 0, 0, 0.3),0 1px 0 rgba(255, 255, 255, 0.2)}.switch-light.switch-candy span span,.switch-light.switch-candy input:checked~span span:first-child,.switch-toggle.switch-candy label{color:#fff;font-weight:bold;text-align:center;text-shadow:1px 1px 1px #191b1e}.switch-light.switch-candy input~span span:first-child,.switch-light.switch-candy input:checked~span span:nth-child(2),.switch-candy input:checked+label{color:#333;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5)}.switch-candy a{border:1px solid #333;border-radius:3px;box-shadow:0 1px 1px rgba(0, 0, 0, 0.2),inset 0 1px 1px rgba(255, 255, 255, 0.45);background-color:#70c66b;background-image:linear-gradient(rgba(255, 255, 255, 0.2), transparent)}.switch-candy-blue a{background-color:#38a3d4}.switch-candy-yellow a{background-color:#f5e560}.switch-ios.switch-light span span{color:#888b92}.switch-ios.switch-light a{left:0;top:0;width:32px;height:32px;background-color:#fff;border-radius:100%;border:4px solid #d8d9db;transition:all .2s ease-out}.switch-ios.switch-light>span{display:block;width:100%;height:32px;background-color:#d8d9db;border-radius:28px;transition:all .4s ease-out}.switch-ios.switch-light>span span{position:absolute;top:0;left:0;width:100%;opacity:0;line-height:30px;vertical-align:middle;transition:all .2s ease-out}.switch-ios.switch-light>span span:first-of-type{opacity:1;padding-left:30px}.switch-ios.switch-light>span span:last-of-type{padding-right:30px}.switch-ios.switch-light input:checked~span a{left:100%;border-color:#4bd865;margin-left:-32px}.switch-ios.switch-light input:checked~span{border-color:#4bd865;box-shadow:inset 0 0 0 30px #4bd865}.switch-ios.switch-light input:checked~span span:first-of-type{opacity:0}.switch-ios.switch-light input:checked~span span:last-of-type{opacity:1;color:#fff}.switch-ios.switch-toggle{background-color:#d8d9db;border-radius:30px;box-shadow:inset rgba(0, 0, 0, 0.1) 0 1px 0}.switch-ios.switch-toggle a{background-color:#4bd865;border:2px solid #d8d9db;border-radius:28px;transition:all .12s ease-out}.switch-ios.switch-toggle label{height:2.4em;color:#888b92;line-height:2.4em;vertical-align:middle}.switch-ios input:checked+label{color:#3e4043}.switch-toggle.switch-holo,.switch-light.switch-holo>span{background-color:#464747;border-radius:1px;box-shadow:inset rgba(0, 0, 0, 0.1) 0 1px 0;color:#fff;text-transform:uppercase}.switch-holo label{color:#fff}.switch-holo>span span{opacity:0;transition:all .1s}.switch-holo>span span:first-of-type{opacity:1}.switch-holo>span span,.switch-holo label{font-size:85%;line-height:34.5px}.switch-holo a{background-color:#666;border-radius:1px;box-shadow:inset rgba(255, 255, 255, 0.2) 0 1px 0,inset rgba(0, 0, 0, 0.3) 0 -1px 0}.switch-holo.switch-light input:checked~span a{background-color:#0e88b1}.switch-holo.switch-light input:checked~span span:first-of-type{opacity:0}.switch-holo.switch-light input:checked~span span:last-of-type{opacity:1}.switch-light.switch-material a{top:-3px;width:28px;height:28px;border-radius:50%;background:#fafafa;box-shadow:0 2px 2px 0 rgba(0, 0, 0, 0.14),0 3px 2px -2px rgba(0, 0, 0, 0.2),0 2px 4px 0 rgba(0, 0, 0, 0.12);transition:right .28s cubic-bezier(0.4, 0, 0.2, 1)}.switch-material.switch-light{overflow:visible}.switch-material.switch-light::after{clear:both;content:"";display:table}.switch-material.switch-light>span{overflow:visible;position:relative;top:3px;width:52px;height:24px;min-height:auto;border-radius:16px;background:rgba(0, 0, 0, 0.26)}.switch-material.switch-light span span{position:absolute;clip:rect(0 0 0 0)}.switch-material.switch-light input:checked~span a{right:0;background:#3f51b5;box-shadow:0 3px 4px 0 rgba(0, 0, 0, 0.14),0 3px 3px -2px rgba(0, 0, 0, 0.2),0 1px 6px 0 rgba(0, 0, 0, 0.12)}.switch-material.switch-light input:checked~span{background:rgba(63, 81, 181, 0.5)}.switch-toggle.switch-material{overflow:visible}.switch-toggle.switch-material::after{clear:both;content:"";display:table}.switch-toggle.switch-material a{top:48%;width:6px !important;height:6px;margin-left:4px;background:#3f51b5;border-radius:100%;transform:translateY(-50%);transition:transform .4s ease-in}.switch-toggle.switch-material label{color:rgba(0, 0, 0, 0.54);font-size:1em}.switch-toggle.switch-material label:before{content:"";position:absolute;top:48%;left:0;display:block;width:14px;height:14px;border-radius:100%;border:2px solid rgba(0, 0, 0, 0.54);transform:translateY(-50%)}.switch-toggle.switch-material input:checked+label:before{border-color:#3f51b5}.switch-light.switch-material>span:before,.switch-light.switch-material>span:after,.switch-toggle.switch-material label:after{content:"";position:absolute;top:0;left:0;z-index:3;display:block;width:64px;height:64px;border-radius:100%;background:#3f51b5;opacity:.4;margin-left:-20px;margin-top:-20px;transform:scale(0);transition:opacity .4s ease-in}.switch-light.switch-material>span:after{left:auto;right:0;margin-left:0;margin-right:-20px}.switch-toggle.switch-material label:after{width:52px;height:52px;margin-top:-12px}@keyframes materialRipple{0%{transform:scale(0)}20%{transform:scale(1)}100%{opacity:0;transform:scale(1)}}.switch-material.switch-light input:not(:checked)~span:after,.switch-material.switch-light input:checked~span:before,.switch-toggle.switch-material input:checked+label:after{animation:materialRipple .4s ease-in}.switch-light.switch-material.switch-light input~span:before,.switch-light.switch-material.switch-light input~span:after,.switch-material.switch-toggle input+label:after{visibility:hidden}.switch-light.switch-material.switch-light input:focus:checked~span:before,.switch-light.switch-material.switch-light input:focus:not(:checked)~span:after,.switch-material.switch-toggle input:focus:checked+label:after{visibility:visible}}@media only screen and (-webkit-max-device-pixel-ratio: 2)and (max-device-width: 1280px){.switch-light,.switch-toggle{-webkit-animation:webkitSiblingBugfix infinite 1s}}@-webkit-keyframes webkitSiblingBugfix{from{-webkit-transform:translate3d(0, 0, 0)}to{-webkit-transform:translate3d(0, 0, 0)}} diff --git a/src/api/pushshift/index.js b/src/api/pushshift/index.js index 66e0d94..8f84b22 100644 --- a/src/api/pushshift/index.js +++ b/src/api/pushshift/index.js @@ -2,7 +2,9 @@ import { fetchJson, sleep } from '../../utils' export const chunkSize = 100; const postURL = 'https://api.pushshift.io/reddit/submission/search/?fields=author,created_utc,domain,edited,id,link_flair_text,num_comments,permalink,position,removed_by_category,retrieved_on,retrieved_utc,score,selftext,subreddit,thumbnail,thumbnail_height,thumbnail_width,title,url&ids=' -const commentURL = `https://api.pushshift.io/reddit/comment/search/?metadata=true&size=${chunkSize}&sort=asc&fields=author,body,created_utc,id,link_id,parent_id,retrieved_on,retrieved_utc,score,subreddit&link_id=` +const commentURL = 'https://api.pushshift.io/reddit/comment/search/?fields=author,body,created_utc,id,link_id,parent_id,retrieved_on,retrieved_utc,score,subreddit&' +const commentURLbyIDs = `${commentURL}ids=` +const commentURLbyLink = `${commentURL}metadata=true&size=${chunkSize}&sort=asc&link_id=` const errorHandler = (msg, origError, from) => { console.error(from + ': ' + origError) @@ -71,6 +73,41 @@ export const getPost = async threadID => { } } +// Starting sometime around May/6/2022, queries using the `ids` parameter +// started returning decimal IDs for the `_id` members instead of fullnames. +const toBase36 = id => { + if (!id) + return id + if (typeof id == 'number') + return id.toString(36) + else + return id[2] == '_' ? id.substring(3) : id +} + +export const getCommentsFromIds = async commentIDs => { + if (commentIDs.length == 0) + return [] + let response, delay = 0 + while (true) { + await pushshiftTokenBucket.waitForToken() + try { + response = await fetchJson(`${commentURLbyIDs}${commentIDs.join()}`) + break + } catch (error) { + if (delay >= 2000) // after ~4s of consecutive failures + errorHandler('Could not get removed comments', error, 'pushshift.getCommentsFromIds') // rethrows + delay = delay * 2 || 125 + pushshiftTokenBucket.setNextAvail(delay) + console.log('pushshift.getCommentsFromIds delay: ' + delay) + } + } + return response.data.map(c => { + c.link_id = toBase36(c.link_id) + c.parent_id = toBase36(c.parent_id) || c.link_id + return c + }) +} + // Comments w/a created_utc in the ranges below must be queried *without* the faster `q=*` parameter: // 1504224000 - 1506815999 (Sep/1/2017 0:00 - Sep/30/2017 23:59:59 UTC) // 1517443200 - 1522540799 (Feb/1/2018 0:00 - Mar/31/2018 23:59:59 UTC) @@ -88,7 +125,7 @@ export const getComments = async (callback, threadID, maxComments, after = 0, be let delay = 0 while (true) { - let query = commentURL + threadID + let query = commentURLbyLink + threadID if (!inBrokenRange(after)) query += '&q=*' if (after) diff --git a/src/api/reddit/index.js b/src/api/reddit/index.js index 76f4967..a025cdc 100644 --- a/src/api/reddit/index.js +++ b/src/api/reddit/index.js @@ -43,3 +43,21 @@ export const getComments = commentIDs => ( .then(results => results.data.children.map(({data}) => data)) .catch(error => errorHandler(error, 'reddit.getComments')) ) + +// Fetch up to 8 of a comment's parents +export const getParentComments = (threadID, commentID, parents) => { + parents = Math.min(parents, 8) + return fetchJson( + `${baseURL}/comments/${threadID}?comment=${commentID}&context=${parents}&limit=${parents}&threaded=false&showmore=false`, + requestSettings + ) + .then(results => { + const { children } = results[1].data + // If there are fewer parents than requested, remove the comments which aren't parents + const idx = children.findIndex(c => c.data.id == commentID) + if (idx >= 0) + children.splice(idx + 1) + return children.map(({data}) => data) + }) + .catch(error => errorHandler(error, 'reddit.getParentComments')) +} diff --git a/src/pages/thread/Comment.js b/src/pages/thread/Comment.js index ca6ef66..108abab 100644 --- a/src/pages/thread/Comment.js +++ b/src/pages/thread/Comment.js @@ -12,6 +12,9 @@ const Comment = (props) => { } else { commentStyle += props.depth % 2 === 0 ? 'comment-even' : 'comment-odd' } + if (props.id == props.highlightedID) { + commentStyle += ' highlighted' + } let innerHTML, editedInnerHTML; if (props.removed && isRemoved(props.body)) { @@ -46,7 +49,7 @@ const Comment = (props) => { >parent : // Use a function, not just an object--state is recreated and scrollBehavior is always initialized - ({hash: `#${props.parent_id}`, state: {scrollBehavior: 'smooth'}})}>parent + ({...loc, hash: `#${props.parent_id}`, state: {scrollBehavior: 'smooth'}})}>parent ) return ( @@ -94,6 +97,7 @@ const Comment = (props) => { {...comment} depth={props.depth + 1} postAuthor={props.postAuthor} + highlightedID={props.highlightedID} /> ))} diff --git a/src/pages/thread/CommentSection.js b/src/pages/thread/CommentSection.js index 59ce4d5..efb9f70 100644 --- a/src/pages/thread/CommentSection.js +++ b/src/pages/thread/CommentSection.js @@ -6,7 +6,7 @@ import { showRemovedAndDeleted, showRemoved, showDeleted } from '../../utils' -const unflatten = (commentMap, rootID, postID) => { +const unflatten = (commentMap, rootID, context, postID) => { const commentTree = [] commentMap.forEach(comment => { @@ -58,6 +58,12 @@ const unflatten = (commentMap, rootID, postID) => { replies: missingRootReplies } } + let newRoot + while (context && rootComment.parent_id && (newRoot = commentMap.get(rootComment.parent_id))) { + newRoot.replies = [rootComment] + rootComment = newRoot + context-- + } return [rootComment] } } @@ -95,13 +101,13 @@ const filterCommentTree = (comments, filterFunction) => { return hasOkComment } -let commentTree, lastTotal, lastRoot, lastFilter, lastSort, lengthBeforeFiltering +let commentTree, lastTotal, lastRoot, lastContext, lastFilter, lastSort, lengthBeforeFiltering const commentSection = (props) => { console.time('Build comment tree') - const {total, root, commentFilter, commentSort} = props + const {total, root, context, commentFilter, commentSort} = props - const needsRebuild = !(total === lastTotal && root === lastRoot && ( + const needsRebuild = !(total === lastTotal && root === lastRoot && context === lastContext && ( commentFilter === lastFilter || lastFilter === filter.all || lastFilter === filter.removedDeleted && ( @@ -110,7 +116,7 @@ const commentSection = (props) => { ) )) if (needsRebuild) { - commentTree = unflatten(props.comments, root, props.postID) + commentTree = unflatten(props.comments, root, context, props.postID) lengthBeforeFiltering = commentTree.length } @@ -142,6 +148,7 @@ const commentSection = (props) => { lastSort = commentSort console.timeEnd('Build comment tree') + props.setMoreContextAvail(commentTree.length > 0 && commentTree[0].parent_id != commentTree[0].link_id) props.setAllCommentsFiltered(commentTree.length == 0 && lengthBeforeFiltering > 0) console.time('Build html tree') @@ -153,6 +160,7 @@ const commentSection = (props) => { {...comment} depth={0} postAuthor={props.postAuthor} + highlightedID={context ? root : null} /> )) :

No comments found

@@ -168,7 +176,8 @@ const areEqual = (prevProps, nextProps) => { return false if (nextProps.reloadingComments) return true - return prevProps.total === nextProps.total && + return prevProps.total === nextProps.total && + prevProps.context === nextProps.context && prevProps.postAuthor === nextProps.postAuthor } diff --git a/src/pages/thread/LoadMore.js b/src/pages/thread/LoadMore.js index c6d214f..e8d5d0f 100644 --- a/src/pages/thread/LoadMore.js +++ b/src/pages/thread/LoadMore.js @@ -1,7 +1,7 @@ import React from 'react' import {connect, maxCommentsDefault} from '../../state' -let loadingStarted = false, showLoadedCount = false, lastTotal, newComments +let loadingStarted = false, showLoadedCount = false, lastTotal, lastContext, newComments const loadMoreComments = (props, maxComments) => { loadingStarted = true props.global.loadMoreComments(maxComments) @@ -26,15 +26,22 @@ const loadMore = (props) => { if (maxCommentsPreferred >= maxCommentsDefault * 2) loadElements.push( loadMoreComments(props, maxCommentsPreferred)}>load {maxCommentsPreferred} more comments) } + + // If loadMoreComments() was called and has completed (because reloadingComments is now false), if (loadingStarted) { - showLoadedCount = true + showLoadedCount = true // then showLoadedCount below newComments = props.total - lastTotal lastTotal = props.total + lastContext = props.context loadingStarted = false - } else if (lastTotal !== props.total) { - showLoadedCount = false + + // Otherwise if the comment total or context changed w/o calling loadMoreComments(), + } else if (lastTotal !== props.total || lastContext !== props.context) { + showLoadedCount = false // then don't showLoadedCount below lastTotal = props.total + lastContext = props.context } + if (showLoadedCount) { loadElements.push( {newComments > 0 ? `loaded ${newComments} more comments` : 'no new comments found'} diff --git a/src/pages/thread/index.js b/src/pages/thread/index.js index cb5f72f..f0337cb 100644 --- a/src/pages/thread/index.js +++ b/src/pages/thread/index.js @@ -3,11 +3,13 @@ import { Link } from 'react-router-dom' import { getPost, getComments as getRedditComments, + getParentComments, chunkSize as redditChunkSize } from '../../api/reddit' import { getPost as getPushshiftPost, getComments as getPushshiftComments, + getCommentsFromIds, chunkSize as pushshiftChunkSize } from '../../api/pushshift' import { isDeleted, isRemoved, sleep } from '../../utils' @@ -57,11 +59,14 @@ class Thread extends React.Component { pushshiftCommentLookup: new Map(), removed: 0, deleted: 0, + context: 0, + moreContextAvail: true, allCommentsFiltered: false, loadedAllComments: false, loadingComments: true, reloadingComments: false } + nextMoreContextAvail = true nextAllCommentsFiltered = false // A 'contig' is an object representing a contiguous block of comments currently being downloaded or already @@ -212,8 +217,9 @@ class Thread extends React.Component { }) // The max_comments query parameter can increase the initial comments-to-download - const maxComments = Math.max(this.props.global.maxComments, constrainMaxComments( - parseInt((new URLSearchParams(this.props.location.search)).get('max_comments')))) + const searchParams = new URLSearchParams(this.props.location.search) + const maxComments = Math.max(this.props.global.maxComments, + constrainMaxComments(parseInt(searchParams.get('max_comments')))) // Get comments starting from the earliest available (not a permalink) if (commentID === undefined) { @@ -234,6 +240,9 @@ class Thread extends React.Component { console.error('link_id mismatch:', comment) return } + const context = parseInt(searchParams.get('context')) + if (context > 0) + this.contextPromise = this.getContext(context) this.contigs.unshift({firstCreated: comment?.created_utc || EARLIEST_CREATED}) this.getComments(maxComments, false, comment) }) @@ -278,6 +287,7 @@ class Thread extends React.Component { componentDidUpdate () { let { loadingComments } = this.state + const { commentID } = this.props.match.params // If the max-to-download Reload button or 'load more comments' was clicked const { loadingMoreComments } = this.props.global.state @@ -289,71 +299,92 @@ class Thread extends React.Component { this.updateCurContig() this.getComments(loadingMoreComments, true) - // Otherwise if we're loading a comment tree we haven't downloaded yet - } else if (!loadingComments && !this.state.reloadingComments && !this.updateCurContig()) { + // Otherwise if we're not already downloading comments, check to see if we need to start + } else if (!loadingComments && !this.state.reloadingComments) { - // If we haven't downloaded from the earliest available yet (not a permalink) - const { commentID } = this.props.match.params - if (commentID === undefined) { - loadingComments = true - this.setState({loadingComments}) - this.props.global.setLoading('Loading comments...') - console.time('Load comments') - this.contigs.unshift({firstCreated: EARLIEST_CREATED}) - this.setCurContig(0) - this.getComments(this.props.global.maxComments) + // If we're loading a comment tree we haven't downloaded yet + if (!this.updateCurContig()) { - // If we haven't downloaded this permalink yet - } else if (!this.commentIdAttempts.has(commentID)) { - this.commentIdAttempts.add(commentID) - this.setState({reloadingComments: true}) - this.props.global.setLoading('Loading comments...') - console.time('Load comments') - let createdUtcNotFound // true if Reddit doesn't have the comment's created_utc - getRedditComments([commentID]) - .then(([comment]) => { - const created_utc = comment?.created_utc - if (created_utc > EARLIEST_CREATED) { - let insertBefore = this.contigs.findIndex(contig => created_utc < contig.firstCreated) - if (insertBefore == -1) - insertBefore = this.contigs.length + // If we haven't downloaded from the earliest available yet (not a permalink) + if (commentID === undefined) { + loadingComments = true + this.setState({loadingComments}) + this.props.global.setLoading('Loading comments...') + console.time('Load comments') + this.contigs.unshift({firstCreated: EARLIEST_CREATED}) + this.setCurContig(0) + this.getComments(this.props.global.maxComments) - // If comment isn't inside an existing contig, create a new one and start downloading - if (insertBefore == 0 || created_utc >= this.contigs[insertBefore - 1].lastCreated) { - this.contigs.splice(insertBefore, 0, {firstCreated: created_utc}) - this.setCurContig(insertBefore) - this.fullnamesToShortIDs(comment) - this.getComments(this.props.global.maxComments, false, comment) + // Otherwise if we haven't downloaded this permalink yet + } else if (!this.commentIdAttempts.has(commentID)) { + this.commentIdAttempts.add(commentID) + this.setState({reloadingComments: true}) + this.props.global.setLoading('Loading comments...') + console.time('Load comments') + let createdUtcNotFound // true if Reddit doesn't have the comment's created_utc + getRedditComments([commentID]) + .then(([comment]) => { + const created_utc = comment?.created_utc + if (created_utc > EARLIEST_CREATED) { + let insertBefore = this.contigs.findIndex(contig => created_utc < contig.firstCreated) + if (insertBefore == -1) + insertBefore = this.contigs.length - // Otherwise an earlier attempt to download it from Pushshift turned up nothing, - } else { - const { pushshiftCommentLookup } = this.state - this.fullnamesToShortIDs(comment) - this.useRedditComment(comment) // so use the Reddit comment instead - this.setCurContig(insertBefore - 1) // (this was the failed earlier attempt) - console.timeEnd('Load comments') - this.props.global.setSuccess() - this.setState({pushshiftCommentLookup, loadingComments: false, reloadingComments: false}) + // If comment isn't inside an existing contig, create a new one and start downloading + if (insertBefore == 0 || created_utc >= this.contigs[insertBefore - 1].lastCreated) { + this.contigs.splice(insertBefore, 0, {firstCreated: created_utc}) + this.setCurContig(insertBefore) + this.fullnamesToShortIDs(comment) + this.getComments(this.props.global.maxComments, false, comment) + + // Otherwise an earlier attempt to download it from Pushshift turned up nothing, + } else { + this.fullnamesToShortIDs(comment) + this.useRedditComment(comment) // so use the Reddit comment instead + this.setCurContig(insertBefore - 1) // (this was the failed earlier attempt) + console.timeEnd('Load comments') + this.props.global.setSuccess() + this.setState({loadingComments: false, reloadingComments: false}) + } + } else + createdUtcNotFound = true + }) + .catch(() => createdUtcNotFound = true) + .finally(() => { + if (createdUtcNotFound) { + // As a last resort, try to download starting from the previous contig; + // this only occurs once per commentID due to the commentIdAttempts Set. + if (this.curContigIdx > 0) + this.setCurContig(this.curContigIdx - 1) + // If there is no previous, create one + else if (this.curContig().firstCreated != EARLIEST_CREATED) + this.contigs.unshift({firstCreated: EARLIEST_CREATED}) + this.getComments(this.props.global.maxComments) } - } else - createdUtcNotFound = true - }) - .catch(() => createdUtcNotFound = true) - .finally(() => { - if (createdUtcNotFound) { - // As a last resort, try to download starting from the previous contig; - // this only occurs once per commentID due to the commentIdAttempts Set. - if (this.curContigIdx > 0) - this.setCurContig(this.curContigIdx - 1) - // If there is no previous, create one - else if (this.curContig().firstCreated != EARLIEST_CREATED) - this.contigs.unshift({firstCreated: EARLIEST_CREATED}) - this.getComments(this.props.global.maxComments) - } - }) - } - } + }) + } + } // end of "If we're loading a comment tree we haven't downloaded yet" + // Check if the context query parameter has changed + if (commentID) { + const context = parseInt((new URLSearchParams(this.props.location.search)).get('context')) || 0 + if (context > this.state.context) { + this.setState({reloadingComments: true}) + this.props.global.setLoading('Loading comments...') + console.time('Load comments') + this.getContext(context) // also updates state.context + .then(commentCount => { + console.log('Reddit:', commentCount, 'comments') + console.timeEnd('Load comments') + this.props.global.setSuccess() + this.setState({loadingComments: false, reloadingComments: false}) + }) + } else if (context >= 0 && context != this.state.context) + this.setState({ context }) + } + } // end of "If we're not already downloading comments, check to see if we need to start" + + // Handle any requested scrolling const { location } = this.props if (location.state?.scrollBehavior && location.hash.length > 1 && !loadingComments && !this.props.global.isErrored()) { @@ -364,10 +395,54 @@ class Thread extends React.Component { } } + if (this.nextMoreContextAvail != this.state.moreContextAvail) + this.setState({moreContextAvail: this.nextMoreContextAvail}) if (this.nextAllCommentsFiltered != this.state.allCommentsFiltered) this.setState({allCommentsFiltered: this.nextAllCommentsFiltered}) } + // Before calling, state.pushshiftCommentLookup must be populated. + // Compares an array of Reddit comments with those in pushshiftCommentLookup, + // updating them to reflect their removed/deleted/edited status and then + // updates the state's removed/deleted count. Returns the number processed. + compareAndUpdateComments (redditComments) { + if (redditComments.length == 0) + return 0 + const { pushshiftCommentLookup } = this.state + let removed = 0, deleted = 0 + redditComments.forEach(redditComment => { + let pushshiftComment = pushshiftCommentLookup.get(redditComment.id) + if (pushshiftComment === undefined) { + // When a parent comment is missing from pushshift, use the redditComment instead + pushshiftComment = this.fullnamesToShortIDs(redditComment) + pushshiftCommentLookup.set(pushshiftComment.id, pushshiftComment) + } else { + // Replace pushshift score with reddit (it's usually more accurate) + pushshiftComment.score = redditComment.score + } + + // Check what is removed / deleted according to reddit + if (isRemoved(redditComment.body)) { + removed++ + pushshiftComment.removed = true + } else if (isDeleted(redditComment.body)) { + deleted++ + pushshiftComment.deleted = true + } else if (pushshiftComment !== redditComment) { + if (isRemoved(pushshiftComment.body)) { + // If it's deleted in pushshift, but later restored by a mod, use the restored + this.fullnamesToShortIDs(redditComment) + pushshiftCommentLookup.set(redditComment.id, redditComment) + } else if (pushshiftComment.body != redditComment.body) { + pushshiftComment.edited_body = redditComment.body + pushshiftComment.edited = redditComment.edited + } + } + }) + this.setState({ removed: this.state.removed + removed, deleted: this.state.deleted + deleted }) + return redditComments.length + } + // Before calling, either create (and set to current) a new contig to begin downloading // after a new time, or set the current contig to begin adding to the end of that contig. // persistent: if true, will try to continue downloading after the current contig has @@ -410,40 +485,7 @@ class Thread extends React.Component { // Download a list of comments by id from Reddit, and process them doRedditComments = ids => redditPromises.push(getRedditComments(ids) - .then(comments => { - let removed = 0, deleted = 0 - comments.forEach(comment => { - let pushshiftComment = pushshiftCommentLookup.get(comment.id) - if (pushshiftComment === undefined) { - // When a parent comment is missing from pushshift, use the reddit comment instead - pushshiftComment = this.redditIdsToPushshift(comment) - pushshiftCommentLookup.set(comment.id, pushshiftComment) - } else { - // Replace pushshift score with reddit (it's usually more accurate) - pushshiftComment.score = comment.score - } - - // Check what is removed / deleted according to reddit - if (isRemoved(comment.body)) { - removed++ - pushshiftComment.removed = true - } else if (isDeleted(comment.body)) { - deleted++ - pushshiftComment.deleted = true - } else if (pushshiftComment !== comment) { - if (isRemoved(pushshiftComment.body)) { - // If it's deleted in pushshift, but later restored by a mod, use the restored - this.redditIdsToPushshift(comment) - pushshiftCommentLookup.set(comment.id, comment) - } else if (pushshiftComment.body != comment.body) { - pushshiftComment.edited_body = comment.body - pushshiftComment.edited = comment.edited - } - } - }) - this.setState({ removed: this.state.removed + removed, deleted: this.state.deleted + deleted }) - return comments.length - }) + .then(comments => this.compareAndUpdateComments(comments)) .catch(error => { console.timeEnd('Load comments') this.props.global.setError(error, error.helpUrl) @@ -489,7 +531,10 @@ class Thread extends React.Component { // All comments from Pushshift have been processed; wait for Reddit to finish while (!redditIdQueue.isEmpty()) doRedditComments(redditIdQueue.shiftChunk()) + if (this.contextPromise) + redditPromises.push(this.contextPromise) Promise.all(redditPromises).then(lengths => { + this.contextPromise = undefined console.log('Reddit:', lengths.reduce((a,b) => a+b, 0), 'comments') if (!this.stopLoading) { @@ -524,6 +569,26 @@ class Thread extends React.Component { }) } + // Makes a best-effort attempt to retrieve context# ancestors of the current commentID. + // Returns a Promise which resolves with the number retrieved, or rejects with undefined. + getContext (context) { + const { params } = this.props.match + const { pushshiftCommentLookup } = this.state + return getParentComments(params.threadID, params.commentID, context) + .then(redditComments => { + const ids = redditComments.map(c => c.id).filter(id => !pushshiftCommentLookup.has(id)) + return getCommentsFromIds(ids) + .then(pushshiftComments => { + if (ids.length) + console.log('Pushshift:', pushshiftComments.length, 'comments') + this.setState({ context }) + pushshiftComments.forEach(comment => pushshiftCommentLookup.set(comment.id, comment)) + return this.compareAndUpdateComments(redditComments) + }) + }) + .catch(e => console.error(e)) + } + componentWillUnmount () { this.stopLoading = true } @@ -557,19 +622,30 @@ class Thread extends React.Component { <> {isSingleComment &&
-
you are viewing a single comment's thread.
+
you are viewing a single comment's thread.
{this.state.reloadingComments ? -
view the rest of the comments
: - ({ + view the rest of the comments → : + ({ pathname: `/r/${subreddit}/comments/${id}/_/`, hash: '#comment-info', state: {scrollBehavior: 'smooth'}} - )}>view the rest of the comments + )}>view the rest of the comments → } -
+ {this.state.moreContextAvail && this.state.context < 8 && <> + + {this.state.reloadingComments ? + view more context → : + ({ + pathname: `/r/${subreddit}/comments/${id}/_/${commentID}/`, + search: `?context=${this.state.context < 4 ? 4 : 8}`} + )}>view more context → + } + } +
} this.nextMoreContextAvail = avail} setAllCommentsFiltered={filtered => this.nextAllCommentsFiltered = filtered} /> } diff --git a/src/sass/comment.sass b/src/sass/comment.sass index f9d801d..25a9776 100644 --- a/src/sass/comment.sass +++ b/src/sass/comment.sass @@ -10,6 +10,11 @@ border-color: var(--l-comment-brd) color: var(--l-light) + .highlighted + border: 2px solid var(--thread-rest-brd) + @media (prefers-color-scheme: light) + border-color: var(--l-thread-rest-brd) + .comment-head font-size: 10px