From 6293374bb77af3abc4d73cda5c33a0ea2b75c7f0 Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Sat, 25 Oct 2025 14:57:28 -0700 Subject: [PATCH] feature: watches on the backend, need frontend implementation for notifications and unread indicators --- .zed/settings.json | 30 +- deno.json | 3 +- deno.lock | 9 +- models/watch.ts | 66 ++ public/api/topics/:topic_id/events/index.ts | 30 +- .../users/:user_id/watches/:watch_id/index.ts | 88 ++ public/api/users/:user_id/watches/index.ts | 145 +++ public/api/users/index.ts | 5 +- public/base.css | 182 +++- public/icons/:icon_path/index.ts | 36 + public/icons/favicon-128x128.png | Bin 0 -> 4969 bytes public/icons/favicon-192x192.png | Bin 0 -> 7619 bytes public/index.html | 195 +--- public/js/app.js | 275 ++++++ public/js/eventactions.js | 17 +- public/js/reactions.js | 22 +- public/js/smartfeeds.js | 6 +- public/js/smartforms.js | 4 +- public/sidebar/sidebar.html | 830 +++++++++++------- public/signup_login_wall.html | 9 +- public/tabs/blurbs/blurbs.html | 8 +- public/tabs/blurbs/new_blurb.html | 2 +- public/tabs/chat/chat.html | 21 +- public/tabs/essays/essays.html | 12 +- public/tabs/essays/new_essay.html | 2 +- public/tabs/forum/forum.html | 10 +- public/tabs/forum/new_post.html | 2 +- public/tabs/tabs.html | 4 +- 28 files changed, 1405 insertions(+), 608 deletions(-) create mode 100644 models/watch.ts create mode 100644 public/api/users/:user_id/watches/:watch_id/index.ts create mode 100644 public/api/users/:user_id/watches/index.ts create mode 100644 public/icons/:icon_path/index.ts create mode 100644 public/icons/favicon-128x128.png create mode 100644 public/icons/favicon-192x192.png create mode 100644 public/js/app.js diff --git a/.zed/settings.json b/.zed/settings.json index adf444e..e6dfc03 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -10,12 +10,32 @@ }, "languages": { "TypeScript": { - "language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"], - "formatter": "language_server" + "language_servers": [ + "deno", + "!typescript-language-server", + "!vtsls", + "!eslint", + "..." + ] }, "TSX": { - "language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"], - "formatter": "language_server" + "language_servers": [ + "deno", + "!typescript-language-server", + "!vtsls", + "!eslint", + "..." + ] + }, + "JavaScript": { + "language_servers": [ + "deno", + "!typescript-language-server", + "!vtsls", + "!eslint", + "..." + ] } - } + }, + "formatter": "language_server" } diff --git a/deno.json b/deno.json index 8766abe..812bc80 100644 --- a/deno.json +++ b/deno.json @@ -32,7 +32,7 @@ } }, "imports": { - "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.0.4", + "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.1.0", "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0", "@da/bcrypt": "jsr:@da/bcrypt@^1.0.1", @@ -40,6 +40,7 @@ "@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/fs": "jsr:@std/fs@^1.0.19", "@std/http": "jsr:@std/http@^1.0.21", + "@std/media-types": "jsr:@std/media-types@^1.1.0", "@std/path": "jsr:@std/path@^1.1.2" } } diff --git a/deno.lock b/deno.lock index 6db7fa8..6397343 100644 --- a/deno.lock +++ b/deno.lock @@ -1,7 +1,7 @@ { "version": "5", "specifiers": { - "jsr:@andyburke/fsdb@^1.0.4": "1.0.4", + "jsr:@andyburke/fsdb@^1.1.0": "1.1.0", "jsr:@andyburke/lurid@0.2": "0.2.0", "jsr:@andyburke/serverus@0.13": "0.13.0", "jsr:@da/bcrypt@*": "1.0.1", @@ -31,8 +31,8 @@ "npm:@types/node@*": "22.15.15" }, "jsr": { - "@andyburke/fsdb@1.0.4": { - "integrity": "ce4bf858e6af25bf257726d08b2901c7409f82aa409f435795d5381caffffad4", + "@andyburke/fsdb@1.1.0": { + "integrity": "ad2d062672137ca96df19df032b51f1c7aa3133c973a0b86eb8eaab3b4c2d47b", "dependencies": [ "jsr:@std/cli@^1.0.20", "jsr:@std/fs@^1.0.18", @@ -133,7 +133,7 @@ }, "workspace": { "dependencies": [ - "jsr:@andyburke/fsdb@^1.0.4", + "jsr:@andyburke/fsdb@^1.1.0", "jsr:@andyburke/lurid@0.2", "jsr:@andyburke/serverus@0.13", "jsr:@da/bcrypt@^1.0.1", @@ -141,6 +141,7 @@ "jsr:@std/encoding@^1.0.10", "jsr:@std/fs@^1.0.19", "jsr:@std/http@^1.0.21", + "jsr:@std/media-types@^1.1.0", "jsr:@std/path@^1.1.2" ] } diff --git a/models/watch.ts b/models/watch.ts new file mode 100644 index 0000000..97e13b4 --- /dev/null +++ b/models/watch.ts @@ -0,0 +1,66 @@ +import { FSDB_COLLECTION } from '@andyburke/fsdb'; +import { by_lurid } from '@andyburke/fsdb/organizers'; +import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; + +/** + * @typedef {object} WATCH_TYPE_INFO + * @property {boolean} ignored if true, this type should NOT produce any indications or notifications + * @property {string} last_id_seen the last event id the user has seen for this event type + * @property {string} last_id_notified the last event id the user was notified about for this type + */ + +export type WATCH_TYPE_INFO = { + ignored: boolean; + last_id_seen: string; + last_id_notified: string; +}; + +/** + * @typedef {object} WATCH_TIMESTAMPS + * @property {string} created the created date of the watch + * @property {string} updated the last updated date, usually coinciding with the last seen id being changed + */ + +/** + * WATCH + * + * @property {string} id - lurid (stable) + * @property {string} creator_id - user id of the watch creator + * @property {string} topic_id - the topic_id being watched + * @property {[WATCH_TYPE_INFO]} types - information for types being watched within this topic + * @property {Record} [meta] - optional metadata about the watch + * @property {WATCH_TIMESTAMPS} timestamps - timestamps for the watch + */ + +export type WATCH = { + id: string; + creator_id: string; + topic_id: string; + types: [WATCH_TYPE_INFO]; + meta?: Record; + timestamps: { + created: string; + updated: string; + }; +}; + +export const WATCHES = new FSDB_COLLECTION({ + name: 'watches', + id_field: 'id', + organize: by_lurid, + indexers: { + creator_id: new FSDB_INDEXER_SYMLINKS({ + name: 'creator_id', + field: 'creator_id', + to_many: true, + organize: by_lurid + }), + + topic_id: new FSDB_INDEXER_SYMLINKS({ + name: 'topic_id', + field: 'topic_id', + to_many: true, + organize: by_lurid + }) + } +}); diff --git a/public/api/topics/:topic_id/events/index.ts b/public/api/topics/:topic_id/events/index.ts index 887427e..95f947d 100644 --- a/public/api/topics/:topic_id/events/index.ts +++ b/public/api/topics/:topic_id/events/index.ts @@ -5,7 +5,7 @@ import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; import { EVENT, get_events_collection_for_topic, VALIDATE_EVENT } from '../../../../../models/event.ts'; import parse_body from '../../../../../utils/bodyparser.ts'; import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; -import * as path from '@std/path'; +import { WATCH, WATCHES } from '../../../../../models/watch.ts'; export const PRECHECKS: PRECHECK_TABLE = {}; @@ -58,11 +58,9 @@ export async function GET(request: Request, meta: Record): Promise< sort, filter: (entry: WALK_ENTRY) => { const { - groups: { - event_type, - event_id - } - } = /^.*\/events\/(?.*?)\/.*\/(?[A-Za-z-]+)\.json$/.exec(entry.path) ?? { groups: {} }; + event_type, + event_id + } = /^.*\/events\/(?.*?)\/.*\/(?[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {}; if (meta.query.after_id && event_id <= meta.query.after_id) { return false; @@ -136,6 +134,26 @@ export async function GET(request: Request, meta: Record): Promise< }); } +async function update_watches(topic: TOPIC, event: EVENT) { + const limit = 100; + + let more_to_process; + let offset = 0; + do { + const watches: WATCH[] = (await WATCHES.find({ + topic_id: topic.id + }, { + limit, + offset + })).map((entry) => entry.load()); + + // TODO: look at the watch .types[] and send notifications + + offset += watches.length; + more_to_process = watches.length === limit; + } while (more_to_process); +} + // POST /api/topics/:topic_id/events - Create an event PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; diff --git a/public/api/users/:user_id/watches/:watch_id/index.ts b/public/api/users/:user_id/watches/:watch_id/index.ts new file mode 100644 index 0000000..aa7e7df --- /dev/null +++ b/public/api/users/:user_id/watches/:watch_id/index.ts @@ -0,0 +1,88 @@ +import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts'; +import { WATCH, WATCHES } from '../../../../../../models/watch.ts'; +import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../../utils/prechecks.ts'; +import parse_body from '../../../../../../utils/bodyparser.ts'; + +export const PRECHECKS: PRECHECK_TABLE = {}; + +// PUT /api/users/:user_id/watches/:watch_id - Update topic +PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { + const watch_id: string = meta.params?.watch_id?.toLowerCase().trim() ?? ''; + + // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" + const watch: WATCH | null = watch_id.length === 49 ? await WATCHES.get(watch_id) : null; + + if (!watch) { + return CANNED_RESPONSES.not_found(); + } + + meta.watch = watch; + const user_owns_watch = watch.creator_id === meta.user.id; + + if (!user_owns_watch) { + return CANNED_RESPONSES.permission_denied(); + } +}]; +export async function PUT(req: Request, meta: Record): Promise { + const now = new Date().toISOString(); + + try { + const body = await parse_body(req); + const updated = { + ...meta.watch, + ...body, + id: meta.watch.id, + timestamps: { + created: meta.watch.timestamps.created, + updated: now + } + }; + + await WATCHES.update(updated); + return Response.json(updated, { + status: 200 + }); + } catch (err) { + return Response.json({ + error: { + message: (err as Error)?.message ?? 'Unknown error due to invalid data.', + cause: (err as Error)?.cause ?? 'invalid_data' + } + }, { + status: 400 + }); + } +} + +// DELETE /api/users/:user_id/watches/:watch_id - Delete watch +PRECHECKS.DELETE = [ + get_session, + get_user, + require_user, + async (_req: Request, meta: Record): Promise => { + const watch_id: string = meta.params?.watch_id?.toLowerCase().trim() ?? ''; + + // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" + const watch: WATCH | null = watch_id.length === 49 ? await WATCHES.get(watch_id) : null; + + if (!watch) { + return CANNED_RESPONSES.not_found(); + } + + meta.topic = watch; + const user_owns_watch = watch.creator_id === meta.user.id; + + if (!user_owns_watch) { + return CANNED_RESPONSES.permission_denied(); + } + } +]; +export async function DELETE(_req: Request, meta: Record): Promise { + await WATCHES.delete(meta.watch); + + return Response.json({ + deleted: true + }, { + status: 200 + }); +} diff --git a/public/api/users/:user_id/watches/index.ts b/public/api/users/:user_id/watches/index.ts new file mode 100644 index 0000000..0808a98 --- /dev/null +++ b/public/api/users/:user_id/watches/index.ts @@ -0,0 +1,145 @@ +import { FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; +import { WATCH, WATCHES } from '../../../../../models/watch.ts'; +import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; +import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; +import parse_body from '../../../../../utils/bodyparser.ts'; +import lurid from '@andyburke/lurid'; +import { TOPICS } from '../../../../../models/topic.ts'; + +export const PRECHECKS: PRECHECK_TABLE = {}; + +// GET /api/users/:user_id/watches - get watches this user has created +// query parameters: +// partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted) +PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { + const user_has_read_own_watches_permission = meta.user.permissions.includes('watches.read.own'); + const user_has_read_all_watches_permission = meta.user.permissions.includes('watches.read.all'); + + if (!(user_has_read_all_watches_permission || (user_has_read_own_watches_permission && meta.user.id === meta.params.user_id))) { + return CANNED_RESPONSES.permission_denied(); + } +}]; +export async function GET(_request: Request, meta: Record): Promise { + const sorts = WATCHES.sorts; + const sort_name: string = meta.query.sort ?? 'newest'; + const key = sort_name as keyof typeof sorts; + const sort: any = sorts[key]; + if (!sort) { + return Response.json({ + error: { + message: 'You must specify a sort: newest, oldest, latest, stalest', + cause: 'invalid_sort' + } + }, { + status: 400 + }); + } + + const options: FSDB_SEARCH_OPTIONS = { + ...(meta.query ?? {}), + limit: Math.min(parseInt(meta.query?.limit ?? '100', 10), 1_000), + sort, + filter: (entry: WALK_ENTRY) => { + const { + event_type, + event_id + } = /^.*\/watches\/(?.*?)\/.*\/(?[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {}; + + if (meta.query.after_id && event_id <= meta.query.after_id) { + return false; + } + + if (meta.query.before_id && event_id >= meta.query.before_id) { + return false; + } + + if (meta.query.type && !meta.query.type.split(',').includes(event_type)) { + return false; + } + + return true; + } + }; + + const headers = { + 'Cache-Control': 'no-cache, must-revalidate' + }; + + const results = (await WATCHES.all(options)) + .map((entry: WALK_ENTRY) => entry.load()) + .sort((lhs_item: WATCH, rhs_item: WATCH) => rhs_item.timestamps.created.localeCompare(lhs_item.timestamps.created)); + + return Response.json(results, { + status: 200, + headers + }); +} + +// POST /api/users/:user_id/watches - Create a watch +PRECHECKS.POST = [get_session, get_user, require_user, (_request: Request, meta: Record): Response | undefined => { + const user_has_create_own_watches_permission = meta.user.permissions.includes('watches.create.own'); + const user_has_create_all_watches_permission = meta.user.permissions.includes('watches.create.all'); + + if (!(user_has_create_all_watches_permission || (user_has_create_own_watches_permission && meta.user.id === meta.params.user_id))) { + return CANNED_RESPONSES.permission_denied(); + } +}]; +export async function POST(req: Request, meta: Record): Promise { + try { + const now = new Date().toISOString(); + + const body = await parse_body(req); + const watch: WATCH = { + ...body, + id: lurid(), + creator_id: meta.user.id, + timestamps: { + created: now, + updated: now + } + }; + + const topic = await TOPICS.get(watch.topic_id); + if (!topic) { + return Response.json({ + errors: [{ + cause: 'invalid_topic_id', + message: 'Could not find a topic with id: ' + watch.topic_id + }] + }, { + status: 400 + }); + } + + const existing_watch: WATCH | undefined = (await WATCHES.find({ + creator_id: meta.user.id, + topic_id: topic.id + }, { + limit: 1 + })).shift()?.load(); + + if (existing_watch) { + return Response.json({ + errors: [{ + cause: 'existing_watch', + message: 'You already have a watch for this topic.' + }] + }, { + status: 400 + }); + } + + await WATCHES.create(watch); + + return Response.json(watch, { + status: 201 + }); + } catch (error) { + return Response.json({ + error: { + message: (error as Error).message ?? 'Unknown Error!', + cause: (error as Error).cause ?? 'unknown' + } + }, { status: 500 }); + } +} diff --git a/public/api/users/index.ts b/public/api/users/index.ts index 6e61e4a..0e8cb98 100644 --- a/public/api/users/index.ts +++ b/public/api/users/index.ts @@ -30,7 +30,10 @@ const DEFAULT_USER_PERMISSIONS: string[] = [ 'topics.posts.create', 'topics.posts.write', 'topics.posts.read', - 'users.read' + 'users.read', + 'watches.create.own', + 'watches.read.own', + 'watches.write.own' ]; export const PRECHECKS: PRECHECK_TABLE = {}; diff --git a/public/base.css b/public/base.css index cdc814d..64ae1d2 100644 --- a/public/base.css +++ b/public/base.css @@ -6,6 +6,8 @@ --bg-darker: hsl(from var(--base-color) h 20% 5%); --bg-lighter: hsl(from var(--base-color) h 20% 10%); + --blur-radius: 8px; + --text: hsl(from var(--base-color) h 5% 100%); --accent: hsl(from var(--base-color) h clamp(0, calc(s + 10), 100) clamp(0, calc(l + 20), 100)); @@ -98,7 +100,7 @@ select { font-size: inherit; } -details > summary { +details>summary { display: block; position: relative; padding-left: 2rem; @@ -106,7 +108,7 @@ details > summary { user-select: none; } -details > summary:before { +details>summary:before { content: ""; border-width: 0.6rem; border-style: solid; @@ -119,11 +121,11 @@ details > summary:before { transition: 0.25s transform ease; } -details[open] > summary:before { +details[open]>summary:before { transform: rotate(90deg); } -details > summary::-webkit-details-marker { +details>summary::-webkit-details-marker { display: none; } @@ -163,7 +165,22 @@ body { background-color: var(--bg); display: flex; flex-direction: column; - height: 100vh; /* fixed height? */ + height: 100vh; + /* fixed height? */ +} + +main { + position: relative; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: auto 1fr; +} + +@media screen and (max-width: 1200px) { + main { + grid-template-columns: auto; + } } input[type="text"]:focus, @@ -240,13 +257,11 @@ textarea:focus { left: 0; right: 0; bottom: 0; - background: repeating-linear-gradient( - -55deg, - rgba(0, 0, 0, 0.25) 0px, - rgba(0, 0, 0, 0.25) 20px, - rgba(255, 177, 1, 0.25) 20px, - rgba(255, 177, 1, 0.25) 40px - ); + background: repeating-linear-gradient(-55deg, + rgba(0, 0, 0, 0.25) 0px, + rgba(0, 0, 0, 0.25) 20px, + rgba(255, 177, 1, 0.25) 20px, + rgba(255, 177, 1, 0.25) 40px); } .collapsed { @@ -268,15 +283,15 @@ label:has(input[collapse-toggle]) { margin-bottom: 2rem; } -input[collapse-toggle] + .collapsible, -label:has(input[collapse-toggle]) + .collapsible { +input[collapse-toggle]+.collapsible, +label:has(input[collapse-toggle])+.collapsible { transition: all 0.33s; height: 0; overflow: hidden; } -input[collapse-toggle]:checked + .collapsible, -label:has(input[collapse-toggle]:checked) + .collapsible { +input[collapse-toggle]:checked+.collapsible, +label:has(input[collapse-toggle]:checked)+.collapsible { height: 100%; } @@ -300,8 +315,8 @@ form label.placeholder { font-size 0.2s ease-in-out; } -form input:focus ~ label.placeholder, -form input:valid ~ label.placeholder { +form input:focus~label.placeholder, +form input:valid~label.placeholder { top: -1.6rem; font-size: small; border: 1px solid rgba(128, 128, 128, 0.2); @@ -444,11 +459,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] max-width: 800px; } -.audio-container - .audio-controls-container - .progress-container - .slider-container - input[name="progress"] { +.audio-container .audio-controls-container .progress-container .slider-container input[name="progress"] { width: 100%; } @@ -469,6 +480,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] max-width: 100px; overflow: hidden; } + @media screen and (max-width: 480px) { .audio-container .audio-controls-container .blank { width: auto; @@ -480,13 +492,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] } .audio-container .audio-controls-container input[type="range"] { - --c: var(--accent); /* active color */ - --g: 4px; /* the gap */ - --l: 2px; /* line thickness*/ - --s: 15px; /* thumb size*/ + --c: var(--accent); + /* active color */ + --g: 4px; + /* the gap */ + --l: 2px; + /* line thickness*/ + --s: 15px; + /* thumb size*/ width: 100%; - height: var(--s); /* needed for Firefox*/ + height: var(--s); + /* needed for Firefox*/ --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%)); -webkit-appearance: none; -moz-appearance: none; @@ -495,26 +512,29 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] cursor: pointer; overflow: hidden; } + .audio-container .audio-controls-container input[type="range"]:focus-visible, .audio-container .audio-controls-container input[type="range"]:hover { --p: 25%; } + .audio-container .audio-controls-container input[type="range"]:active, .audio-container .audio-controls-container input[type="range"]:focus-visible { --_b: var(--s); } + /* chromium */ .audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb { height: var(--s); aspect-ratio: 1; border-radius: 50%; box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c); - border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) - 100vw/0 calc(100vw + var(--g)); + border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g)); -webkit-appearance: none; appearance: none; transition: 0.2s; } + /* Firefox */ .audio-container .audio-controls-container input[type="range"]::-moz-range-thumb { height: var(--s); @@ -522,12 +542,12 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] background: none; border-radius: 50%; box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c); - border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) - 100vw/0 calc(100vw + var(--g)); + border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g)); -moz-appearance: none; appearance: none; transition: 0.2s; } + @supports not (color: color-mix(in srgb, red, red)) { .audio-container .audio-controls-container input[type="range"] { --_c: var(--c); @@ -547,6 +567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] opacity: 1; display: block; } + .audio-container .audio-controls-container .audio-control .icon.pause { opacity: 0; display: none; @@ -556,11 +577,24 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] opacity: 0; display: none; } + .audio-container[data-playing] .audio-controls-container .audio-control .icon.pause { opacity: 1; display: block; } +/* === GLOW EFECTO === from: https://codepen.io/andrewuru/pen/Byjdgrb */ +.glow { + position: absolute; + inset: 0; + background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), + hsla(var(--accent), 100%, 60%, 0.4), + transparent 70%); + mix-blend-mode: screen; + pointer-events: none; + filter: blur(calc(2 * var(--blur-radius))); +} + .html-from-markdown { padding: 2em; } @@ -597,6 +631,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] transform: scale(var(--icon-scale, 1)); border-radius: 4px; } + .icon.add::after, .icon.add::before { content: ""; @@ -610,6 +645,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 8px; left: 4px; } + .icon.add::after { width: 2px; height: 10px; @@ -631,6 +667,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] transform: scale(var(--icon-scale, 1)); margin-top: 11px; } + .icon.attachment::after, .icon.attachment::before { content: ""; @@ -640,6 +677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-radius: 3px; border: 2px solid; } + .icon.attachment::after { border-bottom: 0; border-top-left-radius: 100px; @@ -649,6 +687,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 14px; bottom: 8px; } + .icon.attachment::before { width: 6px; height: 12px; @@ -670,6 +709,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 22px; height: 16px; } + .icon.blurb::after, .icon.blurb::before { content: ""; @@ -681,11 +721,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] background: currentColor; bottom: 2px; } + .icon.blurb::before { width: 10px; left: 2px; box-shadow: 4px -4px 0; } + .icon.blurb::after { width: 3px; right: 2px; @@ -698,6 +740,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] display: block; box-sizing: border-box; } + .icon.calendar { position: relative; transform: scale(var(--icon-scale, 1)); @@ -731,6 +774,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 12px; perspective: 24px; } + .icon.camera::after, .icon.camera::before { content: ""; @@ -738,6 +782,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; position: absolute; } + .icon.camera::before { border: 2px solid; border-left-color: transparent; @@ -747,6 +792,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] right: -7px; top: 0; } + .icon.camera::after { width: 10px; height: 5px; @@ -766,6 +812,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 14px; height: 10px; } + .icon.chat::after, .icon.chat::before { content: ""; @@ -776,11 +823,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 2px; background: currentColor; } + .icon.chat::before { width: 10px; opacity: 0.5; box-shadow: 0 4px 0; } + .icon.chat::after { width: 14px; bottom: 0; @@ -809,6 +858,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: var(--border-radius); } + .icon.close::after, .icon.close::before { content: ""; @@ -823,6 +873,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 8px; left: 3px; } + .icon.close::after { transform: rotate(-45deg); } @@ -838,6 +889,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 100px; } + .icon.controller::before { content: ""; display: block; @@ -869,6 +921,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-bottom-right-radius: 2px; margin-top: 8px; } + .icon.download::after { content: ""; display: block; @@ -882,6 +935,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] left: 2px; bottom: 4px; } + .icon.download::before { content: ""; display: block; @@ -907,6 +961,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-radius: 3px; box-shadow: 0 -1px 0; } + .icon.essay::after, .icon.essay::before { content: ""; @@ -916,6 +971,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 6px; top: 2px; } + .icon.essay::before { background: currentColor; left: 2px; @@ -925,6 +981,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-radius: 3px; height: 2px; } + .icon.essay::after { height: 10px; border: 2px solid; @@ -941,6 +998,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 8px; height: 8px; } + .icon.exchange { position: relative; transform: scale(var(--icon-scale, 1)); @@ -948,16 +1006,19 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] -3px 3px 0 -1px, 3px -3px 0 -1px; } + .icon.exchange::after, .icon.exchange::before { content: ""; position: absolute; border: 2px solid; } + .icon.exchange::before { top: -5px; left: -5px; } + .icon.exchange::after { bottom: -5px; right: -5px; @@ -973,6 +1034,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 14px; border-bottom: 2px solid; } + .icon.forum::after, .icon.forum::before { content: ""; @@ -981,6 +1043,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] position: absolute; top: 2px; } + .icon.forum::before { border-left: 4px solid; left: 1px; @@ -989,6 +1052,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-top: 3px solid transparent; border-bottom: 3px solid transparent; } + .icon.forum::after { width: 8px; height: 6px; @@ -1007,6 +1071,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 16px; box-shadow: 6px -6px 0 -4px; } + .icon.forward-copy::before { content: ""; display: block; @@ -1019,6 +1084,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] left: 0; bottom: 0; } + .icon.forward-copy::after { content: ""; display: block; @@ -1053,6 +1119,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-bottom-left-radius: 0; margin-bottom: -2px; } + .icon.home::after, .icon.home::before { content: ""; @@ -1060,6 +1127,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; position: absolute; } + .icon.home::before { border-top: 2px solid; border-left: 2px solid; @@ -1071,6 +1139,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 14px; left: 0; } + .icon.home::after { width: 8px; height: 10px; @@ -1087,6 +1156,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] .icon.live { transform: scale(var(--icon-scale, 1)); } + .icon.live, .icon.live::after, .icon.live::before { @@ -1099,6 +1169,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-bottom-color: transparent; border-radius: 50%; } + .icon.live::after, .icon.live::before { content: ""; @@ -1108,6 +1179,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 2px; left: 2px; } + .icon.live::after { width: 22px; height: 22px; @@ -1138,6 +1210,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 3px; } + .icon.more::before { content: ""; position: absolute; @@ -1158,6 +1231,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] .icon.more-borderless { transform: scale(var(--icon-scale, 1)); } + .icon.more-borderless, .icon.more-borderless::after, .icon.more-borderless::before { @@ -1169,15 +1243,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] background: currentColor; border-radius: 100%; } + .icon.more-borderless::after, .icon.more-borderless::before { content: ""; position: absolute; top: 0; } + .icon.more-borderless::after { left: -6px; } + .icon.more-borderless::before { right: -6px; } @@ -1193,6 +1270,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 24px; } + .icon.more-circle::before { content: ""; position: absolute; @@ -1218,6 +1296,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 22px; transform: scale(var(--icon-scale, 1)); } + .icon.phone::after, .icon.phone::before { content: ""; @@ -1225,6 +1304,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; position: absolute; } + .icon.phone::after { width: 18px; height: 18px; @@ -1239,6 +1319,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] linear-gradient(to left, currentColor 10px, transparent 0) no-repeat right 11px/6px 4px, linear-gradient(to left, currentColor 10px, transparent 0) no-repeat -1px 0/4px 6px; } + .icon.phone::before { width: 20px; height: 20px; @@ -1259,17 +1340,20 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] display: block; box-sizing: border-box; } + .icon.plus::after, .icon.plus::before { border-radius: 10px; background: currentColor; } + .icon.plus { position: relative; transform: scale(var(--icon-scale, 1)); width: 16px; height: 16px; } + .icon.plus::after { content: ""; position: absolute; @@ -1278,6 +1362,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 0; left: 7px; } + .icon.plus::before { content: ""; position: absolute; @@ -1297,6 +1382,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 16px; box-shadow: -6px -6px 0 -4px; } + .icon.reply::before { content: ""; display: block; @@ -1309,6 +1395,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] right: 0; bottom: 0; } + .icon.reply::after { content: ""; display: block; @@ -1330,6 +1417,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; border-radius: 22px; } + .icon.resources { position: relative; transform: scale(var(--icon-scale, 1)); @@ -1337,6 +1425,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 20px; border: 2px solid transparent; } + .icon.resources::after { content: ""; position: absolute; @@ -1363,6 +1452,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 4px; } + .icon.send::after, .icon.send::before { content: ""; @@ -1375,6 +1465,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 5px; right: 5px; } + .icon.send::after { width: 6px; height: 6px; @@ -1388,6 +1479,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] .icon.talk { transform: scale(var(--icon-scale, 1)); } + .icon.talk, .icon.talk::after { box-sizing: border-box; @@ -1398,6 +1490,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-radius: 100px; border: 2px dotted currentColor; } + .icon.talk::after { content: ""; position: absolute; @@ -1428,6 +1521,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border-bottom-right-radius: 1px; margin-top: 4px; } + .icon.trash::after, .icon.trash::before { content: ""; @@ -1435,6 +1529,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; position: absolute; } + .icon.trash::after { background: currentColor; border-radius: 3px; @@ -1443,6 +1538,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: -4px; left: -5px; } + .icon.trash::before { width: 10px; height: 4px; @@ -1463,6 +1559,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 100px; } + .icon.user { overflow: hidden; transform: scale(var(--icon-scale, 1)); @@ -1470,6 +1567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 22px; position: relative; } + .icon.user::after, .icon.user::before { content: ""; @@ -1479,6 +1577,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 8px; height: 8px; } + .icon.user::after { border-radius: 200px; top: 11px; @@ -1498,6 +1597,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 22px; } + .icon.work::after, .icon.work::before { content: ""; @@ -1505,6 +1605,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] box-sizing: border-box; position: absolute; } + .icon.work::before { width: 12px; height: 6px; @@ -1515,6 +1616,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] left: 2px; border-bottom: 0; } + .icon.work::after { width: 18px; height: 2px; @@ -1532,6 +1634,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 22px; height: 22px; } + .icon.right::after, .icon.right::before { content: ""; @@ -1546,6 +1649,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 7px; right: 6px; } + .icon.right::after { right: 11px; } @@ -1558,6 +1662,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 22px; height: 22px; } + .icon.left::after, .icon.left::before { content: ""; @@ -1572,6 +1677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 7px; left: 6px; } + .icon.left::after { left: 11px; } @@ -1587,6 +1693,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 4px; } + .icon.skip-back::after, .icon.skip-back::before { content: ""; @@ -1596,12 +1703,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 8px; top: 5px; } + .icon.skip-back::before { width: 2px; border-radius: 2px; right: 11px; background: currentColor; } + .icon.skip-back::after { width: 0; border-top: 4px solid transparent; @@ -1620,6 +1729,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 22px; height: 22px; } + .icon.rewind::after, .icon.rewind::before { content: ""; @@ -1634,6 +1744,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 6px; left: 5px; } + .icon.rewind::after { left: 9px; } @@ -1648,6 +1759,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 4px; } + .icon.play::before { content: ""; display: block; @@ -1672,6 +1784,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 4px; } + .icon.pause::before { content: ""; display: block; @@ -1695,6 +1808,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] width: 22px; height: 22px; } + .icon.fastforward::after, .icon.fastforward::before { content: ""; @@ -1709,6 +1823,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] top: 6px; right: 5px; } + .icon.fastforward::after { right: 9px; } @@ -1723,6 +1838,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] border: 2px solid; border-radius: 4px; } + .icon.skip-forward::after, .icon.skip-forward::before { content: ""; @@ -1732,12 +1848,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] height: 8px; top: 5px; } + .icon.skip-forward::before { width: 2px; border-radius: 2px; left: 11px; background: currentColor; } + .icon.skip-forward::after { width: 0; border-top: 4px solid transparent; diff --git a/public/icons/:icon_path/index.ts b/public/icons/:icon_path/index.ts new file mode 100644 index 0000000..aa4bf53 --- /dev/null +++ b/public/icons/:icon_path/index.ts @@ -0,0 +1,36 @@ +import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; +import * as fs from '@std/fs'; +import * as path from '@std/path'; +import * as media_types from '@std/media-types'; + +// GET /icons/:icon_path - get an icon from settings or from defaults +export async function GET(_request: Request, meta: Record): Promise { + const filename = meta.params.icon_path; + if (!filename || filename.indexOf('..') !== -1) { + return CANNED_RESPONSES.not_found(); + } + + const settings_version_exists = fs.existsSync(`./files/settings/icons/${filename}`); + if (settings_version_exists) { + return new Response(null, { + status: 301, + headers: { + Location: `/files/settings/icons/${filename}` + } + }); + } + + const default_version_exists = fs.existsSync(`./icons/${filename}`); + if (default_version_exists) { + const extension = path.extname(filename)?.slice(1)?.toLowerCase() ?? ''; + + const content_type = media_types.contentType(extension) ?? 'application/octet-stream'; + return new Response(await Deno.readFile(`./icons/${filename}`), { + headers: { + 'Content-Type': content_type + } + }); + } + + return CANNED_RESPONSES.not_found(); +} diff --git a/public/icons/favicon-128x128.png b/public/icons/favicon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a70a53a09bdfbb8d33ad38fa5ddf544e42f68f GIT binary patch literal 4969 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 literal 0 HcmV?d00001 diff --git a/public/icons/favicon-192x192.png b/public/icons/favicon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..26aa0e7ae6a5fa1d8e2b15252c267658b9defbf9 GIT binary patch literal 7619 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 a&mcUN>x8 - - + + + + + + @@ -49,191 +53,4 @@ - diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..1b9fd8d --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,275 @@ +const HASH_EXTRACTOR = /^\#\/topic\/(?[A-Za-z\-]+)\/?(?\w+)?/gm; +const UPDATE_TOPICS_FREQUENCY = 60_000; + +const APP = { + user_servers: [], + user_watches: [], + + _event_callbacks: {}, + + on: function( event_name, callback ) { + this._event_callbacks[ event_name ] = this._event_callbacks[ event_name ] ?? new Set(); + this._event_callbacks[event_name ].add( callback ); + return true; + }, + + off: function( event_name, callback ) { + return this._event_callbacks[ event_name ]?.delete( callback ); + }, + + _emit: function( event_name, event_data ) { + const event_callbacks = this._event_callbacks[ event_name ]; + event_callbacks?.forEach( ( callback ) => { + callback( event_data ); + }); + }, + + check_if_logged_in: async function () { + try { + const session_response = await api.fetch("/api/users/me"); + + if (!session_response.ok) { + const error_body = await session_response.json(); + const error = error_body?.error; + + console.dir({ + error_body, + error, + }); + return; + } + + const user = await session_response.json(); + this.login( user ); + } catch (error) { + console.dir({ + error, + }); + } + }, + + extract_url_hash_info: async function () { + HASH_EXTRACTOR.lastIndex = 0; // ugh, need this to have this work on multiple exec calls + const { + groups: { topic_id, view }, + } = HASH_EXTRACTOR.exec(window.location.hash ?? "") ?? { + groups: {}, + }; + + console.dir({ + url: window.location.href, + hash: window.location.hash, + topic_id, + view, + }); + + if (!document.body.dataset.topic || document.body.dataset.topic !== topic_id) { + const previous = document.body.dataset.topic; + + console.dir({ + topic_changed: { + detail: { + previous, + topic_id, + }, + }, + }); + + document.body.dataset.topic = topic_id; + + this._emit( 'topic_changed', { + previous, + topic_id + }); + + if (!topic_id) { + const first_topic_id = this.TOPICS.TOPIC_LIST[0]?.id; + if (first_topic_id) { + window.location.hash = `/topic/${first_topic_id}/chat`; // TODO: allow a different default than chat + } + } + } + + if (!document.body.dataset.view || document.body.dataset.view !== view) { + const previous = document.body.dataset.view; + document.body.dataset.view = view; + + console.dir({ + view_changed: { + detail: { + previous, + view, + }, + }, + }); + + this._emit( 'view_changed', { + previous, + view + }); + } + }, + + load: async function() { + this.server = {}; + this.suggested_servers = []; + try { + const server_info_response = await api.fetch( '/files/settings/settings.json' ); + if ( !server_info_response.ok ) { + throw new Error( 'Could not get server info.' ); + } + + const this_server_info = await server_info_response.json(); + + this.server = { + name: this_server_info?.name ?? document.title, + url: this_server_info?.url ?? window.location.origin ?? window.location.href, + icon: this_server_info?.icon ?? '/icons/favicon-128x128.png', + icon_background: this_server_info?.icon_background ?? undefined + }; + + const suggested_servers = await (await api.fetch( '/files/settings/suggested_servers.json' )).json(); + + } + catch( error ) { + console.error( error ); + } + + window.addEventListener("locationchange", this.extract_url_hash_info.bind( this )); + window.addEventListener("locationchange", this.TOPICS.update ); + + this.check_if_logged_in(); + this.extract_url_hash_info(); + this._emit( 'load', this ); + }, + + update_user: async function( updated_user ) { + const user = this.user = updated_user; + document.body.dataset.user = JSON.stringify(user); + document.body.dataset.perms = user.permissions.join(":"); + + this.TOPICS.update(); + + this.user_servers = []; + try { + const user_server_response = await api.fetch( `/files/users/${ user.id }/settings/servers.json` ); + this.user_servers = user_server_response.ok ? await user_server_response.json() : []; + } + catch( error ) { + console.error( error ); + } + + this.user_watches = []; + try { + const user_watches_response = await api.fetch( `/api/users/${ user.id }/watches` ); + this.user_watches = user_watches_response.ok ? await user_watches_response.json() : []; + } + catch( error ) { + console.error( error ); + } + + // TODO: show unread indicators based on watches + }, + + login: async function( user ) { + await this.update_user( user ); + this._emit( 'user_logged_in', { user } ); + }, + + logout: function() { + delete document.body.dataset.user; + delete document.body.dataset.perms; + window.location = "/"; + + this._emit( "user_logged_out", {}); + }, + + USERS: { + _evict_timeouts: {}, + _update_timeouts: {}, + get: async (id, force) => { + if (force || !APP.USERS[id]) { + APP.USERS[id] = await (await api.fetch(`/api/users/${id}`)).json(); + } + + if (!APP.USERS._update_timeouts[id]) { + APP.USERS._update_timeouts[id] = setInterval(() => { + APP.USERS.get(id, true); + }, 1 * 60_000); + } + + if (!force) { + if (APP.USERS._evict_timeouts[id]) { + clearTimeout(APP.USERS._evict_timeouts[id]); + } + + APP.USERS._evict_timeouts[id] = setTimeout(() => { + if (APP.USERS._update_timeouts[id]) { + clearTimeout(APP.USERS._update_timeouts[id]); + delete APP.USERS._update_timeouts[id]; + } + + delete APP.USERS[id]; + }, 10 * 60_000); + } + + return APP.USERS[id]; + }, + }, + + TOPICS: { + _last_topic_update: undefined, + _update_topics_timeout: undefined, + TOPIC_LIST: [], + + update: async () => { + const now = new Date(); + const time_since_last_update = now - (APP.TOPICS._last_topic_update ?? 0); + if (time_since_last_update < UPDATE_TOPICS_FREQUENCY / 2) { + return; + } + + if (APP.TOPICS._update_topics_timeout) { + clearTimeout(APP.TOPICS._update_topics_timeout); + APP.TOPICS._update_topics_timeout = undefined; + } + + try { + const topics_response = await api.fetch("/api/topics"); + if (topics_response.ok) { + const new_topics = await topics_response.json(); + const has_differences = + APP.TOPICS.TOPIC_LIST.length !== new_topics.length || + new_topics.some((topic, index) => { + return ( + APP.TOPICS.TOPIC_LIST[index]?.id !== topic.id || + APP.TOPICS.TOPIC_LIST[index]?.name !== topic.name + ); + }); + + if (has_differences) { + APP.TOPICS.TOPIC_LIST = [...new_topics]; + + APP._emit( 'topics_updated', { + topics: APP.TOPICS.TOPIC_LIST + }); + } + + APP.TOPICS._last_topic_update = now; + } + } catch (error) { + console.error(error); + } + + APP.TOPICS._update_topics_timeout = setTimeout( + APP.TOPICS.update, + UPDATE_TOPICS_FREQUENCY, + ); + + // now that we have topics, make sure our url is all good + APP.extract_url_hash_info(); + }, + }, +}; + +document.addEventListener("DOMContentLoaded", APP.load.bind( APP )); diff --git a/public/js/eventactions.js b/public/js/eventactions.js index e957342..2cd9039 100644 --- a/public/js/eventactions.js +++ b/public/js/eventactions.js @@ -11,6 +11,15 @@ const event_actions_popup_styling = ` overflow: hidden; border: 1px solid var(--border-normal); padding: 0.5rem; + visibility: hidden; + display: none; + opacity: 0; +} + +#eventactionspopup[data-shown] { + visibility: visible; + display: block; + opacity: 1; } #eventactionspopup .icon.close { @@ -61,9 +70,7 @@ function open_event_actions_popup(event) { event_actions_popup.style.left = position.x + "px"; event_actions_popup.style.top = position.y + "px"; - event_actions_popup.style.visibility = "visible"; - event_actions_popup.style.opacity = "1"; - event_actions_popup.style.display = "block"; + event_actions_popup.dataset.shown = true; } function clear_event_actions_popup() { @@ -71,9 +78,7 @@ function clear_event_actions_popup() { return; } - event_actions_popup.style.visibility = "hidden"; - event_actions_popup.style.opacity = "0"; - event_actions_popup.style.display = "none"; + delete event_actions_popup.dataset.shown; } document.addEventListener("DOMContentLoaded", () => { diff --git a/public/js/reactions.js b/public/js/reactions.js index e46d664..230be01 100644 --- a/public/js/reactions.js +++ b/public/js/reactions.js @@ -12,6 +12,15 @@ const reactions_popup_styling = ` border: 1px solid var(--border-normal); padding: 0.5rem; text-align: center; + visibility: hidden; + display: none; + opacity: 0; +} + +#reactionspopup[data-shown] { + visibility: visible; + display: block; + opacity: 1; } #reactionspopup .icon.close { @@ -103,10 +112,7 @@ function open_reactions_popup(event) { reactions_popup.style.left = position.x + "px"; reactions_popup.style.top = position.y + "px"; - reactions_popup.style.visibility = "visible"; - reactions_popup.style.opacity = "1"; - reactions_popup.style.display = "block"; - + reactions_popup.dataset.shown = true; reactions_popup_search_input.focus(); } @@ -115,9 +121,7 @@ function clear_reactions_popup() { return; } - reactions_popup.style.visibility = "hidden"; - reactions_popup.style.opacity = "0"; - reactions_popup.style.display = "none"; + delete reactions_popup.dataset.shown; } document.addEventListener("DOMContentLoaded", () => { @@ -160,7 +164,7 @@ document.addEventListener("DOMContentLoaded", () => { { document.body.appendChild(reactions_popup); reactions_popup_form = document.getElementById("reactions-selection-form"); - document.addEventListener("topic_changed", ({ detail: { topic_id } }) => { + APP.on("topic_changed", ({ topic_id }) => { const reaction_topic_id = topic_id ?? document.body.dataset.topic; reactions_popup_form.action = reaction_topic_id ? `/api/topics/${reaction_topic_id}/events` diff --git a/public/js/smartfeeds.js b/public/js/smartfeeds.js index 8dea899..4d36942 100644 --- a/public/js/smartfeeds.js +++ b/public/js/smartfeeds.js @@ -228,8 +228,12 @@ function smarten_feeds() { return; } + if ( error.name === 'TypeError' && error.message === 'NetworkError when attempting to fetch resource.' ) { + console.log( error.message ); + return; + } + feed.dataset.error = JSON.stringify(error); - console.trace(error); }) .finally(() => { if (feed.__started && feed.dataset.longpolling) { diff --git a/public/js/smartforms.js b/public/js/smartforms.js index e7610c2..d2f10bd 100644 --- a/public/js/smartforms.js +++ b/public/js/smartforms.js @@ -41,9 +41,7 @@ function smarten_forms() { form.uploaded = []; form.errors = []; - const user = document.body.dataset.user - ? JSON.parse(document.body.dataset.user) - : undefined; + const user = APP.user; if (!user) { throw new Error("You must be logged in to upload files here."); } diff --git a/public/sidebar/sidebar.html b/public/sidebar/sidebar.html index 481c405..be0024e 100644 --- a/public/sidebar/sidebar.html +++ b/public/sidebar/sidebar.html @@ -1,5 +1,5 @@