forked from andyburke/autonomous.contact
feature: watches on the backend, need frontend implementation for
notifications and unread indicators
This commit is contained in:
parent
7046bb0389
commit
6293374bb7
28 changed files with 1405 additions and 608 deletions
|
|
@ -10,12 +10,32 @@
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"TypeScript": {
|
"TypeScript": {
|
||||||
"language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"],
|
"language_servers": [
|
||||||
"formatter": "language_server"
|
"deno",
|
||||||
|
"!typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!eslint",
|
||||||
|
"..."
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"TSX": {
|
"TSX": {
|
||||||
"language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"],
|
"language_servers": [
|
||||||
|
"deno",
|
||||||
|
"!typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!eslint",
|
||||||
|
"..."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"JavaScript": {
|
||||||
|
"language_servers": [
|
||||||
|
"deno",
|
||||||
|
"!typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!eslint",
|
||||||
|
"..."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"formatter": "language_server"
|
"formatter": "language_server"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": {
|
"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/lurid": "jsr:@andyburke/lurid@^0.2.0",
|
||||||
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0",
|
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0",
|
||||||
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
|
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.19",
|
"@std/fs": "jsr:@std/fs@^1.0.19",
|
||||||
"@std/http": "jsr:@std/http@^1.0.21",
|
"@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"
|
"@std/path": "jsr:@std/path@^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
deno.lock
generated
9
deno.lock
generated
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"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/lurid@0.2": "0.2.0",
|
||||||
"jsr:@andyburke/serverus@0.13": "0.13.0",
|
"jsr:@andyburke/serverus@0.13": "0.13.0",
|
||||||
"jsr:@da/bcrypt@*": "1.0.1",
|
"jsr:@da/bcrypt@*": "1.0.1",
|
||||||
|
|
@ -31,8 +31,8 @@
|
||||||
"npm:@types/node@*": "22.15.15"
|
"npm:@types/node@*": "22.15.15"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@andyburke/fsdb@1.0.4": {
|
"@andyburke/fsdb@1.1.0": {
|
||||||
"integrity": "ce4bf858e6af25bf257726d08b2901c7409f82aa409f435795d5381caffffad4",
|
"integrity": "ad2d062672137ca96df19df032b51f1c7aa3133c973a0b86eb8eaab3b4c2d47b",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/cli@^1.0.20",
|
"jsr:@std/cli@^1.0.20",
|
||||||
"jsr:@std/fs@^1.0.18",
|
"jsr:@std/fs@^1.0.18",
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@andyburke/fsdb@^1.0.4",
|
"jsr:@andyburke/fsdb@^1.1.0",
|
||||||
"jsr:@andyburke/lurid@0.2",
|
"jsr:@andyburke/lurid@0.2",
|
||||||
"jsr:@andyburke/serverus@0.13",
|
"jsr:@andyburke/serverus@0.13",
|
||||||
"jsr:@da/bcrypt@^1.0.1",
|
"jsr:@da/bcrypt@^1.0.1",
|
||||||
|
|
@ -141,6 +141,7 @@
|
||||||
"jsr:@std/encoding@^1.0.10",
|
"jsr:@std/encoding@^1.0.10",
|
||||||
"jsr:@std/fs@^1.0.19",
|
"jsr:@std/fs@^1.0.19",
|
||||||
"jsr:@std/http@^1.0.21",
|
"jsr:@std/http@^1.0.21",
|
||||||
|
"jsr:@std/media-types@^1.1.0",
|
||||||
"jsr:@std/path@^1.1.2"
|
"jsr:@std/path@^1.1.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
models/watch.ts
Normal file
66
models/watch.ts
Normal file
|
|
@ -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<string,any>} [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<string, any>;
|
||||||
|
timestamps: {
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WATCHES = new FSDB_COLLECTION<WATCH>({
|
||||||
|
name: 'watches',
|
||||||
|
id_field: 'id',
|
||||||
|
organize: by_lurid,
|
||||||
|
indexers: {
|
||||||
|
creator_id: new FSDB_INDEXER_SYMLINKS<WATCH>({
|
||||||
|
name: 'creator_id',
|
||||||
|
field: 'creator_id',
|
||||||
|
to_many: true,
|
||||||
|
organize: by_lurid
|
||||||
|
}),
|
||||||
|
|
||||||
|
topic_id: new FSDB_INDEXER_SYMLINKS<WATCH>({
|
||||||
|
name: 'topic_id',
|
||||||
|
field: 'topic_id',
|
||||||
|
to_many: true,
|
||||||
|
organize: by_lurid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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 { EVENT, get_events_collection_for_topic, VALIDATE_EVENT } from '../../../../../models/event.ts';
|
||||||
import parse_body from '../../../../../utils/bodyparser.ts';
|
import parse_body from '../../../../../utils/bodyparser.ts';
|
||||||
import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb';
|
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 = {};
|
export const PRECHECKS: PRECHECK_TABLE = {};
|
||||||
|
|
||||||
|
|
@ -58,11 +58,9 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
|
||||||
sort,
|
sort,
|
||||||
filter: (entry: WALK_ENTRY<EVENT>) => {
|
filter: (entry: WALK_ENTRY<EVENT>) => {
|
||||||
const {
|
const {
|
||||||
groups: {
|
|
||||||
event_type,
|
event_type,
|
||||||
event_id
|
event_id
|
||||||
}
|
} = /^.*\/events\/(?<event_type>.*?)\/.*\/(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {};
|
||||||
} = /^.*\/events\/(?<event_type>.*?)\/.*\/(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path) ?? { groups: {} };
|
|
||||||
|
|
||||||
if (meta.query.after_id && event_id <= meta.query.after_id) {
|
if (meta.query.after_id && event_id <= meta.query.after_id) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -136,6 +134,26 @@ export async function GET(request: Request, meta: Record<string, any>): 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
|
// POST /api/topics/:topic_id/events - Create an event
|
||||||
PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
|
PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
|
||||||
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
|
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
|
||||||
|
|
|
||||||
88
public/api/users/:user_id/watches/:watch_id/index.ts
Normal file
88
public/api/users/:user_id/watches/:watch_id/index.ts
Normal file
|
|
@ -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<string, any>): Promise<Response | undefined> => {
|
||||||
|
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<string, any>): Promise<Response> {
|
||||||
|
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<string, any>): Promise<Response | undefined> => {
|
||||||
|
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<string, any>): Promise<Response> {
|
||||||
|
await WATCHES.delete(meta.watch);
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
deleted: true
|
||||||
|
}, {
|
||||||
|
status: 200
|
||||||
|
});
|
||||||
|
}
|
||||||
145
public/api/users/:user_id/watches/index.ts
Normal file
145
public/api/users/:user_id/watches/index.ts
Normal file
|
|
@ -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<string, any>): 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<string, any>): Promise<Response> {
|
||||||
|
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<WATCH> = {
|
||||||
|
...(meta.query ?? {}),
|
||||||
|
limit: Math.min(parseInt(meta.query?.limit ?? '100', 10), 1_000),
|
||||||
|
sort,
|
||||||
|
filter: (entry: WALK_ENTRY<WATCH>) => {
|
||||||
|
const {
|
||||||
|
event_type,
|
||||||
|
event_id
|
||||||
|
} = /^.*\/watches\/(?<event_type>.*?)\/.*\/(?<event_id>[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<WATCH>) => 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<string, any>): 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<string, any>): Promise<Response> {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,10 @@ const DEFAULT_USER_PERMISSIONS: string[] = [
|
||||||
'topics.posts.create',
|
'topics.posts.create',
|
||||||
'topics.posts.write',
|
'topics.posts.write',
|
||||||
'topics.posts.read',
|
'topics.posts.read',
|
||||||
'users.read'
|
'users.read',
|
||||||
|
'watches.create.own',
|
||||||
|
'watches.read.own',
|
||||||
|
'watches.write.own'
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PRECHECKS: PRECHECK_TABLE = {};
|
export const PRECHECKS: PRECHECK_TABLE = {};
|
||||||
|
|
|
||||||
176
public/base.css
176
public/base.css
|
|
@ -6,6 +6,8 @@
|
||||||
--bg-darker: hsl(from var(--base-color) h 20% 5%);
|
--bg-darker: hsl(from var(--base-color) h 20% 5%);
|
||||||
--bg-lighter: hsl(from var(--base-color) h 20% 10%);
|
--bg-lighter: hsl(from var(--base-color) h 20% 10%);
|
||||||
|
|
||||||
|
--blur-radius: 8px;
|
||||||
|
|
||||||
--text: hsl(from var(--base-color) h 5% 100%);
|
--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));
|
--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;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary {
|
details>summary {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
|
|
@ -106,7 +108,7 @@ details > summary {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary:before {
|
details>summary:before {
|
||||||
content: "";
|
content: "";
|
||||||
border-width: 0.6rem;
|
border-width: 0.6rem;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|
@ -119,11 +121,11 @@ details > summary:before {
|
||||||
transition: 0.25s transform ease;
|
transition: 0.25s transform ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] > summary:before {
|
details[open]>summary:before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary::-webkit-details-marker {
|
details>summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +165,22 @@ body {
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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,
|
input[type="text"]:focus,
|
||||||
|
|
@ -240,13 +257,11 @@ textarea:focus {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(-55deg,
|
||||||
-55deg,
|
|
||||||
rgba(0, 0, 0, 0.25) 0px,
|
rgba(0, 0, 0, 0.25) 0px,
|
||||||
rgba(0, 0, 0, 0.25) 20px,
|
rgba(0, 0, 0, 0.25) 20px,
|
||||||
rgba(255, 177, 1, 0.25) 20px,
|
rgba(255, 177, 1, 0.25) 20px,
|
||||||
rgba(255, 177, 1, 0.25) 40px
|
rgba(255, 177, 1, 0.25) 40px);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed {
|
.collapsed {
|
||||||
|
|
@ -268,15 +283,15 @@ label:has(input[collapse-toggle]) {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[collapse-toggle] + .collapsible,
|
input[collapse-toggle]+.collapsible,
|
||||||
label:has(input[collapse-toggle]) + .collapsible {
|
label:has(input[collapse-toggle])+.collapsible {
|
||||||
transition: all 0.33s;
|
transition: all 0.33s;
|
||||||
height: 0;
|
height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[collapse-toggle]:checked + .collapsible,
|
input[collapse-toggle]:checked+.collapsible,
|
||||||
label:has(input[collapse-toggle]:checked) + .collapsible {
|
label:has(input[collapse-toggle]:checked)+.collapsible {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,8 +315,8 @@ form label.placeholder {
|
||||||
font-size 0.2s ease-in-out;
|
font-size 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input:focus ~ label.placeholder,
|
form input:focus~label.placeholder,
|
||||||
form input:valid ~ label.placeholder {
|
form input:valid~label.placeholder {
|
||||||
top: -1.6rem;
|
top: -1.6rem;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
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;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-container
|
.audio-container .audio-controls-container .progress-container .slider-container input[name="progress"] {
|
||||||
.audio-controls-container
|
|
||||||
.progress-container
|
|
||||||
.slider-container
|
|
||||||
input[name="progress"] {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,6 +480,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
.audio-container .audio-controls-container .blank {
|
.audio-container .audio-controls-container .blank {
|
||||||
width: auto;
|
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"] {
|
.audio-container .audio-controls-container input[type="range"] {
|
||||||
--c: var(--accent); /* active color */
|
--c: var(--accent);
|
||||||
--g: 4px; /* the gap */
|
/* active color */
|
||||||
--l: 2px; /* line thickness*/
|
--g: 4px;
|
||||||
--s: 15px; /* thumb size*/
|
/* the gap */
|
||||||
|
--l: 2px;
|
||||||
|
/* line thickness*/
|
||||||
|
--s: 15px;
|
||||||
|
/* thumb size*/
|
||||||
|
|
||||||
width: 100%;
|
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%));
|
--_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
|
@ -495,26 +512,29 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-container .audio-controls-container input[type="range"]:focus-visible,
|
.audio-container .audio-controls-container input[type="range"]:focus-visible,
|
||||||
.audio-container .audio-controls-container input[type="range"]:hover {
|
.audio-container .audio-controls-container input[type="range"]:hover {
|
||||||
--p: 25%;
|
--p: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-container .audio-controls-container input[type="range"]:active,
|
.audio-container .audio-controls-container input[type="range"]:active,
|
||||||
.audio-container .audio-controls-container input[type="range"]:focus-visible {
|
.audio-container .audio-controls-container input[type="range"]:focus-visible {
|
||||||
--_b: var(--s);
|
--_b: var(--s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* chromium */
|
/* chromium */
|
||||||
.audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb {
|
.audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb {
|
||||||
height: var(--s);
|
height: var(--s);
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
|
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)
|
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g));
|
||||||
100vw/0 calc(100vw + var(--g));
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
.audio-container .audio-controls-container input[type="range"]::-moz-range-thumb {
|
.audio-container .audio-controls-container input[type="range"]::-moz-range-thumb {
|
||||||
height: var(--s);
|
height: var(--s);
|
||||||
|
|
@ -522,12 +542,12 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
|
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)
|
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g));
|
||||||
100vw/0 calc(100vw + var(--g));
|
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports not (color: color-mix(in srgb, red, red)) {
|
@supports not (color: color-mix(in srgb, red, red)) {
|
||||||
.audio-container .audio-controls-container input[type="range"] {
|
.audio-container .audio-controls-container input[type="range"] {
|
||||||
--_c: var(--c);
|
--_c: var(--c);
|
||||||
|
|
@ -547,6 +567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-container .audio-controls-container .audio-control .icon.pause {
|
.audio-container .audio-controls-container .audio-control .icon.pause {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -556,11 +577,24 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-container[data-playing] .audio-controls-container .audio-control .icon.pause {
|
.audio-container[data-playing] .audio-controls-container .audio-control .icon.pause {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
display: block;
|
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 {
|
.html-from-markdown {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
@ -597,6 +631,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.add::after,
|
.icon.add::after,
|
||||||
.icon.add::before {
|
.icon.add::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -610,6 +645,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 8px;
|
top: 8px;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.add::after {
|
.icon.add::after {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
@ -631,6 +667,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
margin-top: 11px;
|
margin-top: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.attachment::after,
|
.icon.attachment::after,
|
||||||
.icon.attachment::before {
|
.icon.attachment::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -640,6 +677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.attachment::after {
|
.icon.attachment::after {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-top-left-radius: 100px;
|
border-top-left-radius: 100px;
|
||||||
|
|
@ -649,6 +687,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 14px;
|
height: 14px;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.attachment::before {
|
.icon.attachment::before {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
|
|
@ -670,6 +709,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.blurb::after,
|
.icon.blurb::after,
|
||||||
.icon.blurb::before {
|
.icon.blurb::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -681,11 +721,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.blurb::before {
|
.icon.blurb::before {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
box-shadow: 4px -4px 0;
|
box-shadow: 4px -4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.blurb::after {
|
.icon.blurb::after {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
|
|
@ -698,6 +740,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.calendar {
|
.icon.calendar {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
|
|
@ -731,6 +774,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 12px;
|
height: 12px;
|
||||||
perspective: 24px;
|
perspective: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.camera::after,
|
.icon.camera::after,
|
||||||
.icon.camera::before {
|
.icon.camera::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -738,6 +782,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.camera::before {
|
.icon.camera::before {
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
|
|
@ -747,6 +792,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
right: -7px;
|
right: -7px;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.camera::after {
|
.icon.camera::after {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
|
@ -766,6 +812,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.chat::after,
|
.icon.chat::after,
|
||||||
.icon.chat::before {
|
.icon.chat::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -776,11 +823,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.chat::before {
|
.icon.chat::before {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
box-shadow: 0 4px 0;
|
box-shadow: 0 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.chat::after {
|
.icon.chat::after {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -809,6 +858,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.close::after,
|
.icon.close::after,
|
||||||
.icon.close::before {
|
.icon.close::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -823,6 +873,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 8px;
|
top: 8px;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.close::after {
|
.icon.close::after {
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
}
|
}
|
||||||
|
|
@ -838,6 +889,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.controller::before {
|
.icon.controller::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -869,6 +921,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-bottom-right-radius: 2px;
|
border-bottom-right-radius: 2px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.download::after {
|
.icon.download::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -882,6 +935,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
left: 2px;
|
left: 2px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.download::before {
|
.icon.download::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -907,6 +961,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 -1px 0;
|
box-shadow: 0 -1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.essay::after,
|
.icon.essay::after,
|
||||||
.icon.essay::before {
|
.icon.essay::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -916,6 +971,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 6px;
|
width: 6px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.essay::before {
|
.icon.essay::before {
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
|
@ -925,6 +981,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.essay::after {
|
.icon.essay::after {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
|
|
@ -941,6 +998,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.exchange {
|
.icon.exchange {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: scale(var(--icon-scale, 1));
|
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,
|
||||||
3px -3px 0 -1px;
|
3px -3px 0 -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.exchange::after,
|
.icon.exchange::after,
|
||||||
.icon.exchange::before {
|
.icon.exchange::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.exchange::before {
|
.icon.exchange::before {
|
||||||
top: -5px;
|
top: -5px;
|
||||||
left: -5px;
|
left: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.exchange::after {
|
.icon.exchange::after {
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
right: -5px;
|
right: -5px;
|
||||||
|
|
@ -973,6 +1034,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 14px;
|
height: 14px;
|
||||||
border-bottom: 2px solid;
|
border-bottom: 2px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.forum::after,
|
.icon.forum::after,
|
||||||
.icon.forum::before {
|
.icon.forum::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -981,6 +1043,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.forum::before {
|
.icon.forum::before {
|
||||||
border-left: 4px solid;
|
border-left: 4px solid;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
|
|
@ -989,6 +1052,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-top: 3px solid transparent;
|
border-top: 3px solid transparent;
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.forum::after {
|
.icon.forum::after {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|
@ -1007,6 +1071,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 16px;
|
height: 16px;
|
||||||
box-shadow: 6px -6px 0 -4px;
|
box-shadow: 6px -6px 0 -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.forward-copy::before {
|
.icon.forward-copy::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1019,6 +1084,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.forward-copy::after {
|
.icon.forward-copy::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1053,6 +1119,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.home::after,
|
.icon.home::after,
|
||||||
.icon.home::before {
|
.icon.home::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1060,6 +1127,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.home::before {
|
.icon.home::before {
|
||||||
border-top: 2px solid;
|
border-top: 2px solid;
|
||||||
border-left: 2px solid;
|
border-left: 2px solid;
|
||||||
|
|
@ -1071,6 +1139,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 14px;
|
height: 14px;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.home::after {
|
.icon.home::after {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
@ -1087,6 +1156,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
.icon.live {
|
.icon.live {
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.live,
|
.icon.live,
|
||||||
.icon.live::after,
|
.icon.live::after,
|
||||||
.icon.live::before {
|
.icon.live::before {
|
||||||
|
|
@ -1099,6 +1169,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.live::after,
|
.icon.live::after,
|
||||||
.icon.live::before {
|
.icon.live::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1108,6 +1179,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.live::after {
|
.icon.live::after {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
|
@ -1138,6 +1210,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more::before {
|
.icon.more::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1158,6 +1231,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
.icon.more-borderless {
|
.icon.more-borderless {
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more-borderless,
|
.icon.more-borderless,
|
||||||
.icon.more-borderless::after,
|
.icon.more-borderless::after,
|
||||||
.icon.more-borderless::before {
|
.icon.more-borderless::before {
|
||||||
|
|
@ -1169,15 +1243,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more-borderless::after,
|
.icon.more-borderless::after,
|
||||||
.icon.more-borderless::before {
|
.icon.more-borderless::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more-borderless::after {
|
.icon.more-borderless::after {
|
||||||
left: -6px;
|
left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more-borderless::before {
|
.icon.more-borderless::before {
|
||||||
right: -6px;
|
right: -6px;
|
||||||
}
|
}
|
||||||
|
|
@ -1193,6 +1270,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.more-circle::before {
|
.icon.more-circle::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1218,6 +1296,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 22px;
|
height: 22px;
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.phone::after,
|
.icon.phone::after,
|
||||||
.icon.phone::before {
|
.icon.phone::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1225,6 +1304,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.phone::after {
|
.icon.phone::after {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 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 right 11px/6px 4px,
|
||||||
linear-gradient(to left, currentColor 10px, transparent 0) no-repeat -1px 0/4px 6px;
|
linear-gradient(to left, currentColor 10px, transparent 0) no-repeat -1px 0/4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.phone::before {
|
.icon.phone::before {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
@ -1259,17 +1340,20 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.plus::after,
|
.icon.plus::after,
|
||||||
.icon.plus::before {
|
.icon.plus::before {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.plus {
|
.icon.plus {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.plus::after {
|
.icon.plus::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1278,6 +1362,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 7px;
|
left: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.plus::before {
|
.icon.plus::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1297,6 +1382,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 16px;
|
height: 16px;
|
||||||
box-shadow: -6px -6px 0 -4px;
|
box-shadow: -6px -6px 0 -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.reply::before {
|
.icon.reply::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1309,6 +1395,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.reply::after {
|
.icon.reply::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1330,6 +1417,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.resources {
|
.icon.resources {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
|
|
@ -1337,6 +1425,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.resources::after {
|
.icon.resources::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1363,6 +1452,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.send::after,
|
.icon.send::after,
|
||||||
.icon.send::before {
|
.icon.send::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1375,6 +1465,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.send::after {
|
.icon.send::after {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|
@ -1388,6 +1479,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
.icon.talk {
|
.icon.talk {
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.talk,
|
.icon.talk,
|
||||||
.icon.talk::after {
|
.icon.talk::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -1398,6 +1490,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
border: 2px dotted currentColor;
|
border: 2px dotted currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.talk::after {
|
.icon.talk::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1428,6 +1521,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border-bottom-right-radius: 1px;
|
border-bottom-right-radius: 1px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.trash::after,
|
.icon.trash::after,
|
||||||
.icon.trash::before {
|
.icon.trash::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1435,6 +1529,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.trash::after {
|
.icon.trash::after {
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -1443,6 +1538,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: -4px;
|
top: -4px;
|
||||||
left: -5px;
|
left: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.trash::before {
|
.icon.trash::before {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
|
@ -1463,6 +1559,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.user {
|
.icon.user {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: scale(var(--icon-scale, 1));
|
transform: scale(var(--icon-scale, 1));
|
||||||
|
|
@ -1470,6 +1567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 22px;
|
height: 22px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.user::after,
|
.icon.user::after,
|
||||||
.icon.user::before {
|
.icon.user::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1479,6 +1577,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.user::after {
|
.icon.user::after {
|
||||||
border-radius: 200px;
|
border-radius: 200px;
|
||||||
top: 11px;
|
top: 11px;
|
||||||
|
|
@ -1498,6 +1597,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.work::after,
|
.icon.work::after,
|
||||||
.icon.work::before {
|
.icon.work::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1505,6 +1605,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.work::before {
|
.icon.work::before {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|
@ -1515,6 +1616,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
left: 2px;
|
left: 2px;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.work::after {
|
.icon.work::after {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
|
|
@ -1532,6 +1634,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.right::after,
|
.icon.right::after,
|
||||||
.icon.right::before {
|
.icon.right::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1546,6 +1649,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 7px;
|
top: 7px;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.right::after {
|
.icon.right::after {
|
||||||
right: 11px;
|
right: 11px;
|
||||||
}
|
}
|
||||||
|
|
@ -1558,6 +1662,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.left::after,
|
.icon.left::after,
|
||||||
.icon.left::before {
|
.icon.left::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1572,6 +1677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 7px;
|
top: 7px;
|
||||||
left: 6px;
|
left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.left::after {
|
.icon.left::after {
|
||||||
left: 11px;
|
left: 11px;
|
||||||
}
|
}
|
||||||
|
|
@ -1587,6 +1693,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-back::after,
|
.icon.skip-back::after,
|
||||||
.icon.skip-back::before {
|
.icon.skip-back::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1596,12 +1703,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 8px;
|
height: 8px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-back::before {
|
.icon.skip-back::before {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
right: 11px;
|
right: 11px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-back::after {
|
.icon.skip-back::after {
|
||||||
width: 0;
|
width: 0;
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
|
|
@ -1620,6 +1729,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.rewind::after,
|
.icon.rewind::after,
|
||||||
.icon.rewind::before {
|
.icon.rewind::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1634,6 +1744,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 6px;
|
top: 6px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.rewind::after {
|
.icon.rewind::after {
|
||||||
left: 9px;
|
left: 9px;
|
||||||
}
|
}
|
||||||
|
|
@ -1648,6 +1759,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.play::before {
|
.icon.play::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1672,6 +1784,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.pause::before {
|
.icon.pause::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -1695,6 +1808,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.fastforward::after,
|
.icon.fastforward::after,
|
||||||
.icon.fastforward::before {
|
.icon.fastforward::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1709,6 +1823,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.fastforward::after {
|
.icon.fastforward::after {
|
||||||
right: 9px;
|
right: 9px;
|
||||||
}
|
}
|
||||||
|
|
@ -1723,6 +1838,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-forward::after,
|
.icon.skip-forward::after,
|
||||||
.icon.skip-forward::before {
|
.icon.skip-forward::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -1732,12 +1848,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
||||||
height: 8px;
|
height: 8px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-forward::before {
|
.icon.skip-forward::before {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
left: 11px;
|
left: 11px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.skip-forward::after {
|
.icon.skip-forward::after {
|
||||||
width: 0;
|
width: 0;
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
|
|
|
||||||
36
public/icons/:icon_path/index.ts
Normal file
36
public/icons/:icon_path/index.ts
Normal file
|
|
@ -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<string, any>): Promise<Response> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
BIN
public/icons/favicon-128x128.png
Normal file
BIN
public/icons/favicon-128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/icons/favicon-192x192.png
Normal file
BIN
public/icons/favicon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
|
|
@ -11,8 +11,12 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="./base.css"></link>
|
<link rel="stylesheet" href="./base.css"></link>
|
||||||
|
|
||||||
<script src="./js/_utils.js" type="text/javascript"></script>
|
<!-- inlining these to force them to be scoped for everything else -->
|
||||||
<script src="./js/api.js" type="text/javascript"></script>
|
<script type="text/javascript"><!-- #include file="./js/_utils.js" --></script>
|
||||||
|
<script type="text/javascript"><!-- #include file="./js/api.js" --></script>
|
||||||
|
<script type="text/javascript"><!-- #include file="./js/app.js" --></script>
|
||||||
|
|
||||||
|
<!-- everything else -->
|
||||||
<script src="./js/audioplayer.js" type="text/javascript"></script>
|
<script src="./js/audioplayer.js" type="text/javascript"></script>
|
||||||
<script src="./js/datetimeutils.js" type="text/javascript"></script>
|
<script src="./js/datetimeutils.js" type="text/javascript"></script>
|
||||||
<script src="./js/debounce.js" type="text/javascript"></script>
|
<script src="./js/debounce.js" type="text/javascript"></script>
|
||||||
|
|
@ -49,191 +53,4 @@
|
||||||
<!-- #include file="./tabs/tabs.html" -->
|
<!-- #include file="./tabs/tabs.html" -->
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
|
||||||
/* globals - sue me */
|
|
||||||
const USERS = {
|
|
||||||
_evict_timeouts: {},
|
|
||||||
_update_timeouts: {},
|
|
||||||
get: async (id, force) => {
|
|
||||||
if ( force || !USERS[ id ] ) {
|
|
||||||
USERS[ id ] = (await (await api.fetch(`/api/users/${id}`)).json());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !USERS._update_timeouts[ id ] ) {
|
|
||||||
USERS._update_timeouts[ id ] = setInterval( () => {
|
|
||||||
USERS.get( id, true );
|
|
||||||
}, 1 * 60_000 );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !force ) {
|
|
||||||
if ( USERS._evict_timeouts[ id ] ) {
|
|
||||||
clearTimeout( USERS._evict_timeouts[ id ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
USERS._evict_timeouts[id] = setTimeout( () => {
|
|
||||||
if ( USERS._update_timeouts[ id ] ) {
|
|
||||||
clearTimeout( USERS._update_timeouts[ id ] );
|
|
||||||
delete USERS._update_timeouts[ id ];
|
|
||||||
}
|
|
||||||
|
|
||||||
delete USERS[ id ];
|
|
||||||
}, 10 * 60_000 );
|
|
||||||
}
|
|
||||||
|
|
||||||
return USERS[ id ];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const HASH_EXTRACTOR = /^\#\/topic\/(?<topic_id>[A-Za-z\-]+)\/?(?<view>\w+)?/gm;
|
|
||||||
|
|
||||||
function extract_url_hash_info() {
|
|
||||||
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.dispatchEvent(new CustomEvent( "topic_changed", {
|
|
||||||
detail: {
|
|
||||||
previous,
|
|
||||||
topic_id
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent( "view_changed", {
|
|
||||||
detail: {
|
|
||||||
previous,
|
|
||||||
view
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener("locationchange", extract_url_hash_info);
|
|
||||||
document.addEventListener( 'topic_changed', ( {detail: { topic_id }}) => {
|
|
||||||
if ( !topic_id ) {
|
|
||||||
const first_topic_id = TOPICS?.[0]?.id;
|
|
||||||
if ( first_topic_id ) {
|
|
||||||
window.location.hash = `/topic/${ first_topic_id }/chat`;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.dataset.topic = topic_id;
|
|
||||||
});
|
|
||||||
|
|
||||||
let TOPICS = [];
|
|
||||||
let last_topic_update = undefined;
|
|
||||||
let update_topics_timeout = undefined;
|
|
||||||
const UPDATE_TOPICS_FREQUENCY = 60_000;
|
|
||||||
async function update_topics() {
|
|
||||||
const now = new Date();
|
|
||||||
const time_since_last_update = now - (last_topic_update ?? 0);
|
|
||||||
if (time_since_last_update < UPDATE_TOPICS_FREQUENCY / 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( update_topics_timeout ) {
|
|
||||||
clearTimeout( update_topics_timeout );
|
|
||||||
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 = TOPICS.length !== new_topics.length || new_topics.some( (topic, index) => {
|
|
||||||
return ( TOPICS[ index ]?.id !== topic.id || TOPICS[ index ]?.name !== topic.name );
|
|
||||||
});
|
|
||||||
|
|
||||||
if ( has_differences ) {
|
|
||||||
TOPICS = new_topics;
|
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent("topics_updated", { detail: { topics: TOPICS } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
last_topic_update = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( error ) {
|
|
||||||
console.error( error );
|
|
||||||
}
|
|
||||||
|
|
||||||
update_topics_timeout = setTimeout( update_topics, UPDATE_TOPICS_FREQUENCY);
|
|
||||||
|
|
||||||
// now that we have topics, make sure our url is all good
|
|
||||||
extract_url_hash_info();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
|
||||||
window.addEventListener( 'locationchange', update_topics);
|
|
||||||
document.addEventListener( 'user_logged_in', update_topics );
|
|
||||||
|
|
||||||
/* check if we are logged in */
|
|
||||||
(async () => {
|
|
||||||
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();
|
|
||||||
|
|
||||||
document.body.dataset.user = JSON.stringify( user );
|
|
||||||
document.body.dataset.perms = user.permissions.join(":");
|
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent("user_logged_in", { detail: { user } }));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.dir({
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
extract_url_hash_info();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
275
public/js/app.js
Normal file
275
public/js/app.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
const HASH_EXTRACTOR = /^\#\/topic\/(?<topic_id>[A-Za-z\-]+)\/?(?<view>\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 ));
|
||||||
|
|
@ -11,6 +11,15 @@ const event_actions_popup_styling = `
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-normal);
|
border: 1px solid var(--border-normal);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
visibility: hidden;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eventactionspopup[data-shown] {
|
||||||
|
visibility: visible;
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eventactionspopup .icon.close {
|
#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.left = position.x + "px";
|
||||||
event_actions_popup.style.top = position.y + "px";
|
event_actions_popup.style.top = position.y + "px";
|
||||||
|
|
||||||
event_actions_popup.style.visibility = "visible";
|
event_actions_popup.dataset.shown = true;
|
||||||
event_actions_popup.style.opacity = "1";
|
|
||||||
event_actions_popup.style.display = "block";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear_event_actions_popup() {
|
function clear_event_actions_popup() {
|
||||||
|
|
@ -71,9 +78,7 @@ function clear_event_actions_popup() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event_actions_popup.style.visibility = "hidden";
|
delete event_actions_popup.dataset.shown;
|
||||||
event_actions_popup.style.opacity = "0";
|
|
||||||
event_actions_popup.style.display = "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,15 @@ const reactions_popup_styling = `
|
||||||
border: 1px solid var(--border-normal);
|
border: 1px solid var(--border-normal);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
visibility: hidden;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reactionspopup[data-shown] {
|
||||||
|
visibility: visible;
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#reactionspopup .icon.close {
|
#reactionspopup .icon.close {
|
||||||
|
|
@ -103,10 +112,7 @@ function open_reactions_popup(event) {
|
||||||
reactions_popup.style.left = position.x + "px";
|
reactions_popup.style.left = position.x + "px";
|
||||||
reactions_popup.style.top = position.y + "px";
|
reactions_popup.style.top = position.y + "px";
|
||||||
|
|
||||||
reactions_popup.style.visibility = "visible";
|
reactions_popup.dataset.shown = true;
|
||||||
reactions_popup.style.opacity = "1";
|
|
||||||
reactions_popup.style.display = "block";
|
|
||||||
|
|
||||||
reactions_popup_search_input.focus();
|
reactions_popup_search_input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,9 +121,7 @@ function clear_reactions_popup() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions_popup.style.visibility = "hidden";
|
delete reactions_popup.dataset.shown;
|
||||||
reactions_popup.style.opacity = "0";
|
|
||||||
reactions_popup.style.display = "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
@ -160,7 +164,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="creator_id"
|
name="creator_id"
|
||||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
generator="() => { return APP.user?.id; }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
@ -194,7 +198,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.body.appendChild(reactions_popup);
|
document.body.appendChild(reactions_popup);
|
||||||
|
|
||||||
reactions_popup_form = document.getElementById("reactions-selection-form");
|
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;
|
const reaction_topic_id = topic_id ?? document.body.dataset.topic;
|
||||||
reactions_popup_form.action = reaction_topic_id
|
reactions_popup_form.action = reaction_topic_id
|
||||||
? `/api/topics/${reaction_topic_id}/events`
|
? `/api/topics/${reaction_topic_id}/events`
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,12 @@ function smarten_feeds() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( error.name === 'TypeError' && error.message === 'NetworkError when attempting to fetch resource.' ) {
|
||||||
|
console.log( error.message );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
feed.dataset.error = JSON.stringify(error);
|
feed.dataset.error = JSON.stringify(error);
|
||||||
console.trace(error);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (feed.__started && feed.dataset.longpolling) {
|
if (feed.__started && feed.dataset.longpolling) {
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ function smarten_forms() {
|
||||||
form.uploaded = [];
|
form.uploaded = [];
|
||||||
form.errors = [];
|
form.errors = [];
|
||||||
|
|
||||||
const user = document.body.dataset.user
|
const user = APP.user;
|
||||||
? JSON.parse(document.body.dataset.user)
|
|
||||||
: undefined;
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("You must be logged in to upload files here.");
|
throw new Error("You must be logged in to upload files here.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("topics_updated", ({ detail: { topics } }) => {
|
APP.on("topics_updated", ({ topics }) => {
|
||||||
const topic_list = document.getElementById("topic-list");
|
const topic_list = document.getElementById("topic-list");
|
||||||
topic_list.innerHTML = "";
|
topic_list.innerHTML = "";
|
||||||
for (const topic of topics.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) {
|
for (const topic of topics.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) {
|
||||||
|
|
@ -16,19 +16,22 @@
|
||||||
.forEach((element) => element.classList.remove("active"));
|
.forEach((element) => element.classList.remove("active"));
|
||||||
|
|
||||||
const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic;
|
const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic;
|
||||||
|
if (new_topic_id) {
|
||||||
if (!new_topic_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document
|
document
|
||||||
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
||||||
.forEach((element) => element.classList.add("active"));
|
.forEach((element) => element.classList.add("active"));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("topics_updated", update_topic_indicators);
|
for ( const watch of APP.user_watches ) {
|
||||||
document.addEventListener("topic_changed", update_topic_indicators);
|
// find the topic indicator for this watch
|
||||||
document.addEventListener("user_logged_in", update_topic_indicators);
|
// if there is new stuff - TODO implement a HEAD for getting latest event id?
|
||||||
|
// add a class of 'new-content'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
APP.on("topics_updated", update_topic_indicators);
|
||||||
|
APP.on("topic_changed", update_topic_indicators);
|
||||||
|
APP.on("user_logged_in", update_topic_indicators);
|
||||||
|
|
||||||
function clear_invite_popup() {
|
function clear_invite_popup() {
|
||||||
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
|
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
|
||||||
|
|
@ -64,7 +67,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = document.body.dataset.user && JSON.parse(document.body.dataset.user);
|
const user = APP.user;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
alert("You must be logged in.");
|
alert("You must be logged in.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -103,22 +106,53 @@
|
||||||
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
|
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
(event) => {
|
||||||
|
if (!event.target?.closest("#sidebar")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic_selector = event.target.closest("li.topic");
|
||||||
|
if (!topic_selector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const context_menu = document.getElementById("sidebar-context-menu");
|
||||||
|
context_menu.dataset.prepare = true;
|
||||||
|
|
||||||
|
const position = get_best_coords_for_popup({
|
||||||
|
target_element: topic_selector,
|
||||||
|
popup: {
|
||||||
|
width: context_menu.getBoundingClientRect().width,
|
||||||
|
height: context_menu.getBoundingClientRect().height,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
x: 4,
|
||||||
|
y: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
context_menu.style.left = position.x + "px";
|
||||||
|
context_menu.style.top = position.y + "px";
|
||||||
|
context_menu.dataset.show = true;
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target?.closest("#sidebar-context-menu")) {
|
||||||
|
const context_menu = document.getElementById("sidebar-context-menu");
|
||||||
|
delete context_menu.dataset.show;
|
||||||
|
delete context_menu.dataset.prepare;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
@ -126,15 +160,86 @@
|
||||||
width: auto;
|
width: auto;
|
||||||
left: 0;
|
left: 0;
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
padding: 0.5rem;
|
padding-left: 6rem;
|
||||||
transition: all ease-in-out 0.33s;
|
transition: all ease-in-out 0.33s;
|
||||||
border-right: 1px solid var(--border-subtle);
|
border-right: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu {
|
||||||
|
display: none;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid var(--border-highlight);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow:
|
||||||
|
0 0 25px hsla(var(--accent), 100%, 70%, 0.4),
|
||||||
|
inset 0 0 10px hsla(var(--accent), 100%, 60%, 0.2);
|
||||||
|
backdrop-filter: blur(var(--blur-radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu[data-prepare] {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu[data-show] {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu button {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition:
|
||||||
|
background 0.25s,
|
||||||
|
color 0.25s;
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
#sidebar #sidebar-context-menu button:last-of-type {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu button::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: hsla(var(--accent), 100%, 75%, 0.8);
|
||||||
|
transition: height 0.25s;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu button:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar #sidebar-context-menu button:hover::before {
|
||||||
|
height: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar #sidebar-toggle,
|
#sidebar #sidebar-toggle,
|
||||||
#sidebar #sidebar-toggle-icon {
|
#sidebar #sidebar-toggle-icon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
display: none;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 1200px) {
|
||||||
|
|
@ -285,19 +390,137 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
<div id="sidebar-context-menu">
|
||||||
|
<form
|
||||||
|
data-smart="true"
|
||||||
|
data-method="POST"
|
||||||
|
action="/api/users/${ APP.user?.id }/watches"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="target" value="" />
|
||||||
|
|
||||||
|
<button data-sidebar-context-menu-item="true">👁️🗨️ Watch</button>
|
||||||
|
<button data-sidebar-context-menu-item="true">🕳️ Hide</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="checkbox" id="sidebar-toggle" />
|
<input type="checkbox" id="sidebar-toggle" />
|
||||||
<label id="sidebar-toggle-icon" for="sidebar-toggle">
|
<label id="sidebar-toggle-icon" for="sidebar-toggle">
|
||||||
<div class="icon right"></div>
|
<div class="icon right"></div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function update_servers_list() {
|
||||||
|
const template = document.getElementById( 'server-list-entry-template');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const server = APP.server;
|
||||||
|
const entry = eval("`" + template.innerHTML.trim() + "`");
|
||||||
|
document.getElementById('this-server-container').innerHTML = entry;
|
||||||
|
}
|
||||||
|
catch( error ) {
|
||||||
|
console.error( error );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.getElementById( 'suggested-servers-container').innerHTML = '';
|
||||||
|
|
||||||
|
for ( const server of APP.suggested_servers ) {
|
||||||
|
const entry = eval( "`" + template.innerHTML.trim() + "`");
|
||||||
|
document.getElementById( 'suggested-servers-container').insertAdjacentHTML( 'beforeend', entry );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( error ) {
|
||||||
|
console.error( error );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.getElementById( 'user-servers-container').innerHTML = '';
|
||||||
|
|
||||||
|
for ( const server of APP.user_servers ) {
|
||||||
|
const entry = eval( "`" + template.innerHTML.trim() + "`");
|
||||||
|
document.getElementById( 'user-servers-container').insertAdjacentHTML( 'beforeend', entry );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( error ) {
|
||||||
|
console.error( error );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
APP.on( 'load', update_servers_list );
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#server-list-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
width: 6rem;
|
||||||
|
border-right: 1px solid var(--border-subtle);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-entry {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-entry a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-entry img {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin: 0 auto 0.75rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
object-fit: scale-down;
|
||||||
|
align-content: center;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--icon-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-entry .server-name {
|
||||||
|
font-size: x-small;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-weight: bold;
|
||||||
|
max-height: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="server-list-container">
|
||||||
|
<template id="server-list-entry-template">
|
||||||
|
<div class="server-list-entry">
|
||||||
|
<a href="${ server.url }">
|
||||||
|
<img class="server-icon" src="${ server.icon ?? ( server.url + '/favicon.ico' ) }" alt="${ server.name ?? server.url } icon" style="${ server.icon_background ? `--icon-background: ${ server.icon_background };` : '' }" />
|
||||||
|
<div class="server-name">${ server.name ?? server.url }</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div id="this-server-container"></div>
|
||||||
|
<div id="suggested-servers-container"></div>
|
||||||
|
<div id="user-servers-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#server-info {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="server-info">
|
||||||
<script>
|
<script>
|
||||||
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
const user = document.body.dataset.user
|
const user = APP.user;
|
||||||
? JSON.parse(document.body.dataset.user)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
||||||
for (const user_bound_element of user_bound_elements) {
|
for (const user_bound_element of user_bound_elements) {
|
||||||
|
|
@ -382,7 +605,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = JSON.parse(document.body.dataset.user);
|
const user = APP.user;
|
||||||
|
|
||||||
const updated_user = { ...user };
|
const updated_user = { ...user };
|
||||||
|
|
||||||
|
|
@ -448,9 +671,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const saved_user = await saved_user_response.json();
|
const saved_user = await saved_user_response.json();
|
||||||
|
APP.update_user( saved_user );
|
||||||
document.body.dataset.user = JSON.stringify(saved_user);
|
|
||||||
document.body.dataset.perms = saved_user.permissions.join(":");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
|
|
@ -531,11 +752,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete document.body.dataset.user;
|
APP.logout();
|
||||||
delete document.body.dataset.perms;
|
|
||||||
window.location = "/";
|
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -605,4 +822,5 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,7 @@
|
||||||
const form = document.currentScript.closest("form");
|
const form = document.currentScript.closest("form");
|
||||||
form.on_reply = (response) => {
|
form.on_reply = (response) => {
|
||||||
const user = response.user;
|
const user = response.user;
|
||||||
document.body.dataset.user = JSON.stringify(user);
|
APP.login( user );
|
||||||
document.body.dataset.perms = user.permissions.join(":");
|
|
||||||
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent("user_logged_in", { detail: { user } }),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -127,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
APP.on( 'load', () => {
|
||||||
const query = new URL(document.location.toString())
|
const query = new URL(document.location.toString())
|
||||||
.searchParams;
|
.searchParams;
|
||||||
const invite_code = query.get("invite_code");
|
const invite_code = query.get("invite_code");
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
name="top-level-tabs"
|
name="top-level-tabs"
|
||||||
id="blurb-tab-input"
|
id="blurb-tab-input"
|
||||||
class="tab-switch"
|
class="tab-switch"
|
||||||
data-view="blurb"
|
data-view="blurbs"
|
||||||
/>
|
/>
|
||||||
<label for="blurb-tab-input" class="tab-label"
|
<label for="blurb-tab-input" class="tab-label"
|
||||||
><div class="icon blurb"></div>
|
><div class="icon blurb"></div>
|
||||||
|
|
@ -159,8 +159,8 @@
|
||||||
{
|
{
|
||||||
const feed = document.currentScript.closest("[data-feed]");
|
const feed = document.currentScript.closest("[data-feed]");
|
||||||
|
|
||||||
document.addEventListener("topic_changed", () => { feed.__reset && feed.__reset(); });
|
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||||
document.addEventListener("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||||
|
|
||||||
feed.__target_element = (item) => {
|
feed.__target_element = (item) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -195,7 +195,7 @@
|
||||||
return {
|
return {
|
||||||
event: item,
|
event: item,
|
||||||
blurb: item,
|
blurb: item,
|
||||||
creator: await USERS.get(item.creator_id),
|
creator: await APP.USERS.get(item.creator_id),
|
||||||
blurb_datetime
|
blurb_datetime
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="creator_id"
|
name="creator_id"
|
||||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
generator="() => { return APP.user?.id; }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,8 @@
|
||||||
{
|
{
|
||||||
const feed = document.currentScript.closest("[data-feed]");
|
const feed = document.currentScript.closest("[data-feed]");
|
||||||
|
|
||||||
document.addEventListener("topic_changed", () => {
|
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||||
feed.__reset && feed.__reset();
|
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||||
});
|
|
||||||
document.addEventListener("user_logged_in", () => {
|
|
||||||
feed.__reset && feed.__reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
const time_tick_tock_timeout = 60_000;
|
const time_tick_tock_timeout = 60_000;
|
||||||
|
|
||||||
|
|
@ -71,7 +67,7 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event: item,
|
event: item,
|
||||||
creator: await USERS.get(item.creator_id),
|
creator: await APP.USERS.get(item.creator_id),
|
||||||
event_datetime,
|
event_datetime,
|
||||||
time_tick_tock_class,
|
time_tick_tock_class,
|
||||||
user_tick_tock_class,
|
user_tick_tock_class,
|
||||||
|
|
@ -165,12 +161,9 @@
|
||||||
<script>
|
<script>
|
||||||
{
|
{
|
||||||
const form = document.currentScript.closest("form");
|
const form = document.currentScript.closest("form");
|
||||||
document.addEventListener(
|
APP.on( "topic_changed", ({ topic_id }) => {
|
||||||
"topic_changed",
|
|
||||||
({ detail: { topic_id } }) => {
|
|
||||||
form.action = topic_id ? `/api/topics/${topic_id}/events` : "";
|
form.action = topic_id ? `/api/topics/${topic_id}/events` : "";
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -192,7 +185,7 @@
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="creator_id"
|
name="creator_id"
|
||||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
generator="() => { return APP.user?.id; }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
name="top-level-tabs"
|
name="top-level-tabs"
|
||||||
id="essay-tab-input"
|
id="essay-tab-input"
|
||||||
class="tab-switch"
|
class="tab-switch"
|
||||||
data-view="essay"
|
data-view="essays"
|
||||||
/>
|
/>
|
||||||
<label for="essay-tab-input" class="tab-label"
|
<label for="essay-tab-input" class="tab-label"
|
||||||
><div class="icon essay"></div>
|
><div class="icon essay"></div>
|
||||||
|
|
@ -126,12 +126,8 @@
|
||||||
{
|
{
|
||||||
const feed = document.currentScript.closest("[data-feed]");
|
const feed = document.currentScript.closest("[data-feed]");
|
||||||
|
|
||||||
document.addEventListener("topic_changed", () => {
|
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||||
feed.__reset && feed.__reset();
|
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||||
});
|
|
||||||
document.addEventListener("user_logged_in", () => {
|
|
||||||
feed.__reset && feed.__reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
feed.__target_element = (item) => {
|
feed.__target_element = (item) => {
|
||||||
let target = feed;
|
let target = feed;
|
||||||
|
|
@ -156,7 +152,7 @@
|
||||||
return {
|
return {
|
||||||
event: item,
|
event: item,
|
||||||
essay: item,
|
essay: item,
|
||||||
creator: await USERS.get(item.creator_id),
|
creator: await APP.USERS.get(item.creator_id),
|
||||||
essay_datetime,
|
essay_datetime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="creator_id"
|
name="creator_id"
|
||||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
generator="() => { return APP.user?.id; }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -156,12 +156,8 @@
|
||||||
{
|
{
|
||||||
const feed = document.currentScript.closest("[data-feed]");
|
const feed = document.currentScript.closest("[data-feed]");
|
||||||
|
|
||||||
document.addEventListener("topic_changed", () => {
|
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||||
feed.__reset && feed.__reset();
|
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||||
});
|
|
||||||
document.addEventListener("user_logged_in", () => {
|
|
||||||
feed.__reset && feed.__reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
feed.__target_element = (item) => {
|
feed.__target_element = (item) => {
|
||||||
let target = feed;
|
let target = feed;
|
||||||
|
|
@ -189,7 +185,7 @@
|
||||||
return {
|
return {
|
||||||
event: item,
|
event: item,
|
||||||
post: item,
|
post: item,
|
||||||
creator: await USERS.get(item.creator_id),
|
creator: await APP.USERS.get(item.creator_id),
|
||||||
post_datetime,
|
post_datetime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="creator_id"
|
name="creator_id"
|
||||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
generator="() => { return APP.user?.id; }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("view_changed", ({ detail: { view } }) => {
|
APP.on( "view_changed", ({ view }) => {
|
||||||
const target_tab = document.querySelector(`.tab-switch[data-view="${view}"]`);
|
const target_tab = document.querySelector(`.tab-switch[data-view="${view}"]`);
|
||||||
|
|
||||||
if (target_tab) {
|
if (target_tab) {
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
APP.on( 'load', () => {
|
||||||
const tab_switchers = document.querySelectorAll(".tab-switch");
|
const tab_switchers = document.querySelectorAll(".tab-switch");
|
||||||
for (const tab_switch of tab_switchers) {
|
for (const tab_switch of tab_switchers) {
|
||||||
tab_switch.addEventListener("input", (event) => {
|
tab_switch.addEventListener("input", (event) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue