newsfeed
This commit is contained in:
parent
d64cc3fbf2
commit
a1464bb865
20 changed files with 636 additions and 193 deletions
|
@ -369,6 +369,16 @@ th {
|
|||
--recursive-casl: 0.5;
|
||||
}
|
||||
|
||||
/* Horizontal rules */
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-1);
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
/* Style the noscript box a little more prettily. */
|
||||
|
||||
.noscript {
|
||||
|
@ -413,6 +423,7 @@ nav {
|
|||
|
||||
nav .nav-page {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
@ -435,8 +446,49 @@ h1.page-title {
|
|||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Style the footer */
|
||||
/* Style the `new` link on the homepage */
|
||||
a[is="th-new"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-color);
|
||||
opacity: 50%;
|
||||
|
||||
&.has-news {
|
||||
opacity: 100%;
|
||||
text-decoration: none;
|
||||
|
||||
& .new-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
& .badge {
|
||||
margin-left: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Style new badges */
|
||||
span.badge {
|
||||
--recursive-wght: 800;
|
||||
--recursive-slnt: 0;
|
||||
--recursive-mono: 1.0;
|
||||
--recursive-casl: 0;
|
||||
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&.red {
|
||||
color: white;
|
||||
background-color: #d01243;
|
||||
}
|
||||
|
||||
&.before-content {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Style the footer */
|
||||
footer {
|
||||
margin-top: 4rem;
|
||||
text-align: right;
|
||||
|
@ -561,6 +613,9 @@ th-literate-program[data-mode="output"] {
|
|||
border-style: none;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& img.placeholder.js {
|
||||
transition: opacity var(--transition-duration);
|
||||
}
|
||||
|
||||
|
@ -709,3 +764,11 @@ th-literate-program[data-mode="output"] {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Style settings sections */
|
||||
|
||||
section[is="th-settings"] {
|
||||
/* Don't display settings when JavaScript is disabled.
|
||||
JS overrides this value on the element itself. */
|
||||
display: none;
|
||||
}
|
||||
|
|
103
static/css/new.css
Normal file
103
static/css/new.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
/* Give the intro and outro some breathing room. */
|
||||
section {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Style all links in the last paragraph as big buttons. */
|
||||
.tree th-bc>p:last-child {
|
||||
--transition-duration: 0.2s;
|
||||
|
||||
margin-top: 12px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&>a {
|
||||
padding: 0.5em 1.5em;
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 2em;
|
||||
text-decoration: none;
|
||||
|
||||
transition:
|
||||
color var(--transition-duration),
|
||||
background-color var(--transition-duration),
|
||||
border-color var(--transition-duration);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: white;
|
||||
background-color: #058ef0;
|
||||
border-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section[is="th-settings"] {
|
||||
& h3 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& details>summary {
|
||||
--recursive-wght: 700;
|
||||
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
|
||||
opacity: 50%;
|
||||
transition: opacity var(--transition-duration);
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
--recursive-casl: 0.0;
|
||||
--recursive-mono: 1.0;
|
||||
--recursive-slnt: 0.0;
|
||||
|
||||
content: '+';
|
||||
margin-right: 0.3em;
|
||||
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& details[open]>summary {
|
||||
opacity: 100%;
|
||||
|
||||
&::before {
|
||||
content: '-';
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
& button {
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
background: none;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition:
|
||||
color var(--transition-duration),
|
||||
background-color var(--transition-duration),
|
||||
border-color var(--transition-duration);
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #058ef0;
|
||||
border-color: white;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -184,6 +184,7 @@ class OutputMode {
|
|||
});
|
||||
|
||||
if (this.frame.placeholderImage != null) {
|
||||
this.frame.placeholderImage.classList.add("js");
|
||||
this.frame.placeholderImage.classList.add("loading");
|
||||
}
|
||||
|
||||
|
|
70
static/js/news.js
Normal file
70
static/js/news.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
// news.js because new.js makes the TypeScript language server flip out.
|
||||
// Likely because `new` is a keyword, but also, what the fuck.
|
||||
|
||||
import { getSettingValue } from "./settings.js";
|
||||
import { Branch } from "./tree.js";
|
||||
|
||||
const seenStatesKey = "treehouse.news.seenBranches";
|
||||
const seenStates = new Set(JSON.parse(localStorage.getItem(seenStatesKey)) || []);
|
||||
|
||||
let seenCount = seenStates.size;
|
||||
let unseenCount = TREEHOUSE_NEWS_COUNT - seenCount;
|
||||
|
||||
function saveSeenStates() {
|
||||
localStorage.setItem(seenStatesKey, JSON.stringify(Array.from(seenStates)));
|
||||
}
|
||||
|
||||
function markAsRead(branch) {
|
||||
if (!seenStates.has(branch.namedID) && seenCount > 0) {
|
||||
let badge = document.createElement("span");
|
||||
badge.classList.add("badge", "red", "before-content");
|
||||
badge.textContent = "new";
|
||||
|
||||
branch.branchContent.firstChild.insertBefore(badge, branch.branchContent.firstChild.firstChild);
|
||||
}
|
||||
|
||||
seenStates.add(branch.namedID);
|
||||
}
|
||||
|
||||
export function initNewsPage() {
|
||||
for (let [_, branch] of Branch.branchesByNamedID) {
|
||||
markAsRead(branch);
|
||||
}
|
||||
saveSeenStates();
|
||||
|
||||
// If any branches are added past the initial load, add them to the seen set too.
|
||||
Branch.onAdded.push(branch => {
|
||||
markAsRead(branch);
|
||||
saveSeenStates();
|
||||
})
|
||||
}
|
||||
|
||||
export function markAllAsUnread() {
|
||||
localStorage.removeItem(seenStatesKey);
|
||||
}
|
||||
|
||||
class New extends HTMLAnchorElement {
|
||||
connectedCallback() {
|
||||
// Do not show the badge to people who have never seen any news.
|
||||
// It's just annoying in that case.
|
||||
// In case you do not wish to see the badge anymore, go to the news page and uncheck the
|
||||
// checkbox at the bottom.
|
||||
let userSawNews = seenCount > 0;
|
||||
let userWantsToSeeNews = getSettingValue("showNewPostIndicator");
|
||||
if (userSawNews && userWantsToSeeNews && unseenCount > 0) {
|
||||
this.newText = document.createElement("span");
|
||||
this.newText.classList.add("new-text");
|
||||
this.newText.textContent = this.textContent;
|
||||
this.textContent = "";
|
||||
this.appendChild(this.newText);
|
||||
|
||||
this.badge = document.createElement("span");
|
||||
this.badge.classList.add("badge", "red");
|
||||
this.badge.textContent = unseenCount.toString();
|
||||
this.appendChild(this.badge);
|
||||
this.classList.add("has-news");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-new", New, { extends: "a" });
|
35
static/js/settings.js
Normal file
35
static/js/settings.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const settingsKey = "treehouse.settings";
|
||||
const settings = JSON.parse(localStorage.getItem(settingsKey)) || {};
|
||||
|
||||
const defaultSettingValues = {
|
||||
showNewPostIndicator: true,
|
||||
};
|
||||
|
||||
function saveSettings() {
|
||||
localStorage.setItem(settingsKey, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
export function getSettingValue(setting) {
|
||||
return settings[setting] ?? defaultSettingValues[setting];
|
||||
}
|
||||
|
||||
class SettingCheckbox extends HTMLInputElement {
|
||||
connectedCallback() {
|
||||
this.checked = getSettingValue(this.id);
|
||||
|
||||
this.addEventListener("change", () => {
|
||||
settings[this.id] = this.checked;
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-setting-checkbox", SettingCheckbox, { extends: "input" });
|
||||
|
||||
class Settings extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-settings", Settings, { extends: "section" });
|
|
@ -1,5 +1,5 @@
|
|||
// Detect if we can have crucial functionality (ie. custom elements call constructors).
|
||||
// This doesn't seem to happen in Epiphany, and possibly also other Webkit-based browsers.
|
||||
// This doesn't seem to happen in Epiphany, and also other Webkit-based browsers.
|
||||
let works = false;
|
||||
class WebkitMoment extends HTMLLIElement {
|
||||
constructor() {
|
||||
|
|
|
@ -17,12 +17,11 @@ function branchIsOpen(branchID) {
|
|||
return branchState[branchID];
|
||||
}
|
||||
|
||||
class Branch extends HTMLLIElement {
|
||||
export class Branch extends HTMLLIElement {
|
||||
static branchesByNamedID = new Map();
|
||||
static onAdded = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
connectedCallback() {
|
||||
this.isLeaf = this.classList.contains("leaf");
|
||||
|
||||
this.details = this.childNodes[0];
|
||||
|
@ -48,16 +47,20 @@ class Branch extends HTMLLIElement {
|
|||
});
|
||||
}
|
||||
|
||||
let namedID = this.id.split(':')[1];
|
||||
Branch.branchesByNamedID.set(namedID, this);
|
||||
this.namedID = this.id.split(':')[1];
|
||||
Branch.branchesByNamedID.set(this.namedID, this);
|
||||
|
||||
if (ulid.isCanonicalUlid(namedID)) {
|
||||
let timestamp = ulid.getTimestamp(namedID);
|
||||
if (ulid.isCanonicalUlid(this.namedID)) {
|
||||
let timestamp = ulid.getTimestamp(this.namedID);
|
||||
let date = document.createElement("span");
|
||||
date.classList.add("branch-date");
|
||||
date.innerText = timestamp.toLocaleDateString();
|
||||
this.buttonBar.insertBefore(date, this.buttonBar.firstChild);
|
||||
}
|
||||
|
||||
for (let callback of Branch.onAdded) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,8 +71,8 @@ customElements.define("th-b", Branch, { extends: "li" });
|
|||
class LinkedBranch extends Branch {
|
||||
static byLink = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.linkedTree = this.getAttribute("data-th-link");
|
||||
LinkedBranch.byLink.set(this.linkedTree, this);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue