diff options
Diffstat (limited to 'app.js')
-rw-r--r-- | app.js | 328 |
1 files changed, 328 insertions, 0 deletions
@@ -0,0 +1,328 @@ +import { Mastodon } from "./masto.js"; + +function strftime(s, date) { + if (!(date instanceof Date)) { + date = new Date(date); + } + let pad = (n, width) => String(n).padStart(width, '0'); + return s.replaceAll(/%./g, (match) => {switch (match[1]) { + case 'Y': return pad(date.getFullYear(), 4); + case 'm': return pad(date.getMonth()+1, 2); + case 'd': return pad(date.getDate(), 2); + case 'H': return pad(date.getHours(), 2); + case 'M': return pad(date.getMinutes(), 2); + default: return match; + }}); +} + +function date_add(date, days) { + date = new Date(date); + date.setDate(date.getDate() + days); + return strftime("%Y-%m-%d", date); +} + +let post_tree = {}; +let posts_raw = []; + +let active_day = date_add(new Date(), -1); + +let Post = { + oninit: function(vnode) { + let post = vnode.attrs.post; + vnode.state.chuj = post.spoiler_text; + vnode.state.collapsed = post.spoiler_text.length > 0; + }, + view: function(vnode) { + let post = vnode.attrs.post; + let original = post; + if (post.reblog) post = post.reblog; + + let time = (original == post) + ? strftime("%H:%M", post.created_at) + : strftime("%Y-%m-%d %H:%M", post.created_at); + + let spoiler_el = + post.spoiler_text + ? m("span.toot__spoiler.flex-grow", { + "class": vnode.state.collapsed ? "" : "toot__spoiler_open", + onclick: () => {vnode.state.collapsed = !vnode.state.collapsed} + }, post.spoiler_text) + : m("span.flex-grow"); + + let buttons = [ + // m("span.material-icons.fg2.cursor-pointer", { + // onclick: () => {console.log(original, vnode);}, + // }, "pest_control"), + m("span.material-icons.fg2", { + title: post.visibility + }, { + "public": "public", + "unlisted": "bedtime", + "private": "lock", + "direct": "alternate_email", + }[post.visibility]), + ]; + + let header = null; + if (original.reblog) { + header = m("div.flex.flex-align-center.border-bottom.px-1em", [ + m("img.avatar.avatar-reblog", {src: post.account.avatar_static}), + + m("div.flex.flex-grow.flex-column.py-025em", [ + + m("div.flex", [ + m("span.flex-grow", [ + m("span.fg2", "RT "), + m("a", {href: post.account.url}, post.account.display_name) + ]), + buttons + ]), + + m("div.flex", [ + m("a.fg2.link-stealth", + {href: post.url}, + strftime("%Y-%m-%d %H:%M", post.created_at) + ), + spoiler_el, + ]), + + ]), + ]); + } else { + header = m("div.flex.border-bottom.border-sunk", [ + m("div.flex.flex-grow.flex-column.px-1em", [ + m("div.flex.py-025em", [ + m("a.fg2.link-stealth", + {href: post.url}, + strftime("%H:%M", post.created_at) + ), + spoiler_el, + buttons + ]), + ]), + ]); + } + + let media = post.media_attachments; + + return m("div.toot", [ + // TODO: mark replies + + header, + + vnode.state.collapsed || m("div.px-1em.py-025em", [ + post.in_reply_to_id != null && m("div.fg2", [ + `(reply to something. dunno what)`, + ]), + + m.trust(post.content), + + (media.length > 0) && m("div.pt-1em", + // TODO properly handle non-image attachments + post.media_attachments.map(ma => + m("details.toot__media", [ + m("summary", [ + m("span.material-icons.fg2", "image"), + ma.description || "no alt text :(", + ]), + m("img[loading=lazy]", {src: ma.preview_url}) + ]) + ) + ), + ]), + ]); + } +} + +let DayDigest = { + view: function() { + let day_tree = post_tree[active_day] || {}; + + let users = Object.keys(day_tree); + + // Put local users first, then sort alphabetically. + users.sort((a, b) => { + let isLocal = (nick) => nick.indexOf('@') == -1; + if (isLocal(a) && !isLocal(b)) return -1; + if (!isLocal(a) && isLocal(b)) return 1; + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + + return users.map((user) => { + let posts = Object.values(day_tree[user]); + + // Sort by own toots first, then by time. + // TODO: allow configuring this. + posts.sort((a, b) => { + if (a.reblog && !b.reblog) return 1; + if (!a.reblog && b.reblog) return -1; + if (a.created_at < b.created_at) return -1; + if (a.created_at > b.created_at) return 1; + return 0; + }); + + let own_posts = 0, reblogs = 0, replies = 0; + for (let post of posts) { + if (post.reblog) reblogs++; + else if (post.in_reply_to_id) replies++; + else own_posts++; + } + + if (posts.length == 0) return; + + let account = posts[0].account; + + return m("details.acct", {key: user}, [ + m("summary.acct__header", [ + m("img.avatar", {src: account.avatar_static}), + m("span.acct__username", account.display_name || account.acct), // TODO figure out where to show acct + m("span.acct__counts.fg2", `${own_posts}o | ${replies}r | ${reblogs}b`), + ]), + posts.map(post => m(Post, {post, key: post.id})) + ]) + }); + } +} + +let Main = { + view: function() { + return [ + m("header", [ + m("button", { + onclick: () => { + if (confirm("Are you sure you want to logout?")) { + localStorage.removeItem("oauth_app"); + localStorage.removeItem("access_token"); + window.location.reload(); + } + } + }, "Logout"), + m("span.px-1em", `${posts_raw.length} posts loaded...`), + m("div.flex", [ + m("a.flex-grow.flex-basis-0.link-stealth.text-right", { + onclick: () => { + // TODO ensure that day is "sensible" + active_day = date_add(active_day, 1); + }, + href: "javascript:" + }, "newer"), + m("span.px-1em", active_day), + m("a.flex-grow.flex-basis-0.link-stealth", { + onclick: () => { + // TODO ensure that day is "sensible" + active_day = date_add(active_day, -1); + }, + href: "javascript:" + }, "earlier"), + ]), + m("hr"), + ]), + m("main", m(DayDigest)), + m("footer", [ + m("hr"), + m("p.fg2", [ + "(a very early version of) treed 2. ", + m("a", {href: "#"}, "go to top") + ]), + ]) + ]; + } +}; + +let Login = { + view: function() { + return [ + m("h1.text-center", "Treed"), + m("form.mw-40ch", { + onsubmit: async (e) => { + e.preventDefault(); + + let redir = window.location.href.split('#')[0]; + + let instance = document.getElementById("instance").value; + let masto = new Mastodon(instance, null); + localStorage.instance = instance; + + let res = await masto.post('/api/v1/apps', { + // TODO change + client_name: 'Treed 2', + redirect_uris: [redir], + scopes: 'read', + }); + localStorage.oauth_app = JSON.stringify(res); + // TODO reuse creds if possible + //let res = JSON.parse(localStorage.oauth_app); + // TODO error handling + window.location = masto.instance + '/oauth/authorize?' + new URLSearchParams({ + client_id: res.client_id, + scope: 'read', + redirect_uri: redir, + response_type: 'code' + }); + } + }, [ + m("label.form-label", "Your instance"), + m("input#instance.w-100.mb-05em[autofocus]", {placeholder: "mastodon.example"}), + m("button[type=submit]", "Login"), + m("p", "note that i'm not 100% sure if i'm doing oauth correctly. your token might leak. sorry.") + ]) + ]; + } +}; + + +let oauth_code = new URLSearchParams(window.location.search).get('code'); +if (oauth_code) { + // TODO error handling + let oauth = JSON.parse(localStorage.oauth_app); + // TODO setter for token + let masto = new Mastodon(localStorage.instance, null); + let res = await masto.post('/oauth/token', { + client_id: oauth.client_id, + client_secret: oauth.client_secret, + redirect_uri: oauth.redirect_uri, + grant_type: 'authorization_code', + code: oauth_code, + scope: 'read', + }); + localStorage.access_token = res.access_token; + + window.location.search = ''; +} + +m.route(document.body, "/home", { + "/home": Main, + "/login": Login, +}); + +if (!localStorage.instance || !localStorage.access_token) { + m.route.set("/login"); + // TODO +} else { + m.route.set("/home"); + let masto = new Mastodon(localStorage.instance, localStorage.access_token); + let max_id = null; + for (let i = 0; i < 20; i++) { + let opts = {limit: 100}; + if (max_id !== null) opts.max_id = max_id; + let posts = await masto.get("/api/v1/timelines/home", opts); + + if (posts.length == 0) break; + + for (const post of posts) { + const date = strftime("%Y-%m-%d", post.created_at); + const acct = post.account.acct; + if (!post_tree[date]) post_tree[date] = {}; + if (!post_tree[date][acct]) post_tree[date][acct] = {}; + post_tree[date][acct][post.id] = post; + + if (max_id === null || post.id < max_id) { + max_id = post.id; + } + posts_raw.push(post); + } + m.redraw(); + } +} |