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 0000000..d8a70a5 Binary files /dev/null and b/public/icons/favicon-128x128.png differ diff --git a/public/icons/favicon-192x192.png b/public/icons/favicon-192x192.png new file mode 100644 index 0000000..26aa0e7 Binary files /dev/null and b/public/icons/favicon-192x192.png differ diff --git a/public/index.html b/public/index.html index bda0418..9b02c9a 100644 --- a/public/index.html +++ b/public/index.html @@ -11,8 +11,12 @@ - - + + + + + + @@ -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 @@