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
|
|
@ -5,7 +5,7 @@ import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts';
|
|||
import { EVENT, get_events_collection_for_topic, VALIDATE_EVENT } from '../../../../../models/event.ts';
|
||||
import parse_body from '../../../../../utils/bodyparser.ts';
|
||||
import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb';
|
||||
import * as path from '@std/path';
|
||||
import { WATCH, WATCHES } from '../../../../../models/watch.ts';
|
||||
|
||||
export const PRECHECKS: PRECHECK_TABLE = {};
|
||||
|
||||
|
|
@ -58,11 +58,9 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
|
|||
sort,
|
||||
filter: (entry: WALK_ENTRY<EVENT>) => {
|
||||
const {
|
||||
groups: {
|
||||
event_type,
|
||||
event_id
|
||||
}
|
||||
} = /^.*\/events\/(?<event_type>.*?)\/.*\/(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path) ?? { groups: {} };
|
||||
event_type,
|
||||
event_id
|
||||
} = /^.*\/events\/(?<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;
|
||||
|
|
@ -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
|
||||
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() ?? '';
|
||||
|
|
|
|||
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.write',
|
||||
'topics.posts.read',
|
||||
'users.read'
|
||||
'users.read',
|
||||
'watches.create.own',
|
||||
'watches.read.own',
|
||||
'watches.write.own'
|
||||
];
|
||||
|
||||
export const PRECHECKS: PRECHECK_TABLE = {};
|
||||
|
|
|
|||
182
public/base.css
182
public/base.css
|
|
@ -6,6 +6,8 @@
|
|||
--bg-darker: hsl(from var(--base-color) h 20% 5%);
|
||||
--bg-lighter: hsl(from var(--base-color) h 20% 10%);
|
||||
|
||||
--blur-radius: 8px;
|
||||
|
||||
--text: hsl(from var(--base-color) h 5% 100%);
|
||||
--accent: hsl(from var(--base-color) h clamp(0, calc(s + 10), 100) clamp(0, calc(l + 20), 100));
|
||||
|
||||
|
|
@ -98,7 +100,7 @@ select {
|
|||
font-size: inherit;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
details>summary {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
|
|
@ -106,7 +108,7 @@ details > summary {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
details > summary:before {
|
||||
details>summary:before {
|
||||
content: "";
|
||||
border-width: 0.6rem;
|
||||
border-style: solid;
|
||||
|
|
@ -119,11 +121,11 @@ details > summary:before {
|
|||
transition: 0.25s transform ease;
|
||||
}
|
||||
|
||||
details[open] > summary:before {
|
||||
details[open]>summary:before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
details > summary::-webkit-details-marker {
|
||||
details>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +165,22 @@ body {
|
|||
background-color: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh; /* fixed height? */
|
||||
height: 100vh;
|
||||
/* fixed height? */
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
main {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
|
|
@ -240,13 +257,11 @@ textarea:focus {
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
rgba(0, 0, 0, 0.25) 0px,
|
||||
rgba(0, 0, 0, 0.25) 20px,
|
||||
rgba(255, 177, 1, 0.25) 20px,
|
||||
rgba(255, 177, 1, 0.25) 40px
|
||||
);
|
||||
background: repeating-linear-gradient(-55deg,
|
||||
rgba(0, 0, 0, 0.25) 0px,
|
||||
rgba(0, 0, 0, 0.25) 20px,
|
||||
rgba(255, 177, 1, 0.25) 20px,
|
||||
rgba(255, 177, 1, 0.25) 40px);
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
|
|
@ -268,15 +283,15 @@ label:has(input[collapse-toggle]) {
|
|||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
input[collapse-toggle] + .collapsible,
|
||||
label:has(input[collapse-toggle]) + .collapsible {
|
||||
input[collapse-toggle]+.collapsible,
|
||||
label:has(input[collapse-toggle])+.collapsible {
|
||||
transition: all 0.33s;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input[collapse-toggle]:checked + .collapsible,
|
||||
label:has(input[collapse-toggle]:checked) + .collapsible {
|
||||
input[collapse-toggle]:checked+.collapsible,
|
||||
label:has(input[collapse-toggle]:checked)+.collapsible {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -300,8 +315,8 @@ form label.placeholder {
|
|||
font-size 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
form input:focus ~ label.placeholder,
|
||||
form input:valid ~ label.placeholder {
|
||||
form input:focus~label.placeholder,
|
||||
form input:valid~label.placeholder {
|
||||
top: -1.6rem;
|
||||
font-size: small;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
|
|
@ -444,11 +459,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
max-width: 800px;
|
||||
}
|
||||
|
||||
.audio-container
|
||||
.audio-controls-container
|
||||
.progress-container
|
||||
.slider-container
|
||||
input[name="progress"] {
|
||||
.audio-container .audio-controls-container .progress-container .slider-container input[name="progress"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -469,6 +480,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.audio-container .audio-controls-container .blank {
|
||||
width: auto;
|
||||
|
|
@ -480,13 +492,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
}
|
||||
|
||||
.audio-container .audio-controls-container input[type="range"] {
|
||||
--c: var(--accent); /* active color */
|
||||
--g: 4px; /* the gap */
|
||||
--l: 2px; /* line thickness*/
|
||||
--s: 15px; /* thumb size*/
|
||||
--c: var(--accent);
|
||||
/* active color */
|
||||
--g: 4px;
|
||||
/* the gap */
|
||||
--l: 2px;
|
||||
/* line thickness*/
|
||||
--s: 15px;
|
||||
/* thumb size*/
|
||||
|
||||
width: 100%;
|
||||
height: var(--s); /* needed for Firefox*/
|
||||
height: var(--s);
|
||||
/* needed for Firefox*/
|
||||
--_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
|
@ -495,26 +512,29 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.audio-container .audio-controls-container input[type="range"]:focus-visible,
|
||||
.audio-container .audio-controls-container input[type="range"]:hover {
|
||||
--p: 25%;
|
||||
}
|
||||
|
||||
.audio-container .audio-controls-container input[type="range"]:active,
|
||||
.audio-container .audio-controls-container input[type="range"]:focus-visible {
|
||||
--_b: var(--s);
|
||||
}
|
||||
|
||||
/* chromium */
|
||||
.audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb {
|
||||
height: var(--s);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
|
||||
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2)
|
||||
100vw/0 calc(100vw + var(--g));
|
||||
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g));
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
.audio-container .audio-controls-container input[type="range"]::-moz-range-thumb {
|
||||
height: var(--s);
|
||||
|
|
@ -522,12 +542,12 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
background: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
|
||||
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2)
|
||||
100vw/0 calc(100vw + var(--g));
|
||||
border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2) 100vw/0 calc(100vw + var(--g));
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
@supports not (color: color-mix(in srgb, red, red)) {
|
||||
.audio-container .audio-controls-container input[type="range"] {
|
||||
--_c: var(--c);
|
||||
|
|
@ -547,6 +567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
opacity: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.audio-container .audio-controls-container .audio-control .icon.pause {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
|
|
@ -556,11 +577,24 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audio-container[data-playing] .audio-controls-container .audio-control .icon.pause {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* === GLOW EFECTO === from: https://codepen.io/andrewuru/pen/Byjdgrb */
|
||||
.glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
hsla(var(--accent), 100%, 60%, 0.4),
|
||||
transparent 70%);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
filter: blur(calc(2 * var(--blur-radius)));
|
||||
}
|
||||
|
||||
.html-from-markdown {
|
||||
padding: 2em;
|
||||
}
|
||||
|
|
@ -597,6 +631,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
transform: scale(var(--icon-scale, 1));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.add::after,
|
||||
.icon.add::before {
|
||||
content: "";
|
||||
|
|
@ -610,6 +645,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 8px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.icon.add::after {
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
|
|
@ -631,6 +667,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
transform: scale(var(--icon-scale, 1));
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.icon.attachment::after,
|
||||
.icon.attachment::before {
|
||||
content: "";
|
||||
|
|
@ -640,6 +677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-radius: 3px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.icon.attachment::after {
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 100px;
|
||||
|
|
@ -649,6 +687,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 14px;
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.icon.attachment::before {
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
|
|
@ -670,6 +709,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.icon.blurb::after,
|
||||
.icon.blurb::before {
|
||||
content: "";
|
||||
|
|
@ -681,11 +721,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
background: currentColor;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.icon.blurb::before {
|
||||
width: 10px;
|
||||
left: 2px;
|
||||
box-shadow: 4px -4px 0;
|
||||
}
|
||||
|
||||
.icon.blurb::after {
|
||||
width: 3px;
|
||||
right: 2px;
|
||||
|
|
@ -698,6 +740,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon.calendar {
|
||||
position: relative;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
|
|
@ -731,6 +774,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 12px;
|
||||
perspective: 24px;
|
||||
}
|
||||
|
||||
.icon.camera::after,
|
||||
.icon.camera::before {
|
||||
content: "";
|
||||
|
|
@ -738,6 +782,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon.camera::before {
|
||||
border: 2px solid;
|
||||
border-left-color: transparent;
|
||||
|
|
@ -747,6 +792,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
right: -7px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.icon.camera::after {
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
|
|
@ -766,6 +812,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 14px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.icon.chat::after,
|
||||
.icon.chat::before {
|
||||
content: "";
|
||||
|
|
@ -776,11 +823,13 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 2px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.icon.chat::before {
|
||||
width: 10px;
|
||||
opacity: 0.5;
|
||||
box-shadow: 0 4px 0;
|
||||
}
|
||||
|
||||
.icon.chat::after {
|
||||
width: 14px;
|
||||
bottom: 0;
|
||||
|
|
@ -809,6 +858,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.icon.close::after,
|
||||
.icon.close::before {
|
||||
content: "";
|
||||
|
|
@ -823,6 +873,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 8px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.icon.close::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
|
@ -838,6 +889,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.icon.controller::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -869,6 +921,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-bottom-right-radius: 2px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.icon.download::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -882,6 +935,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
left: 2px;
|
||||
bottom: 4px;
|
||||
}
|
||||
|
||||
.icon.download::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -907,6 +961,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-radius: 3px;
|
||||
box-shadow: 0 -1px 0;
|
||||
}
|
||||
|
||||
.icon.essay::after,
|
||||
.icon.essay::before {
|
||||
content: "";
|
||||
|
|
@ -916,6 +971,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 6px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.icon.essay::before {
|
||||
background: currentColor;
|
||||
left: 2px;
|
||||
|
|
@ -925,6 +981,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-radius: 3px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.icon.essay::after {
|
||||
height: 10px;
|
||||
border: 2px solid;
|
||||
|
|
@ -941,6 +998,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.icon.exchange {
|
||||
position: relative;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
|
|
@ -948,16 +1006,19 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
-3px 3px 0 -1px,
|
||||
3px -3px 0 -1px;
|
||||
}
|
||||
|
||||
.icon.exchange::after,
|
||||
.icon.exchange::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.icon.exchange::before {
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.icon.exchange::after {
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
|
|
@ -973,6 +1034,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 14px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.icon.forum::after,
|
||||
.icon.forum::before {
|
||||
content: "";
|
||||
|
|
@ -981,6 +1043,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
position: absolute;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.icon.forum::before {
|
||||
border-left: 4px solid;
|
||||
left: 1px;
|
||||
|
|
@ -989,6 +1052,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.icon.forum::after {
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
|
|
@ -1007,6 +1071,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 16px;
|
||||
box-shadow: 6px -6px 0 -4px;
|
||||
}
|
||||
|
||||
.icon.forward-copy::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1019,6 +1084,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.icon.forward-copy::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1053,6 +1119,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-bottom-left-radius: 0;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.icon.home::after,
|
||||
.icon.home::before {
|
||||
content: "";
|
||||
|
|
@ -1060,6 +1127,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon.home::before {
|
||||
border-top: 2px solid;
|
||||
border-left: 2px solid;
|
||||
|
|
@ -1071,6 +1139,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 14px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.icon.home::after {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
|
|
@ -1087,6 +1156,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
.icon.live {
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
}
|
||||
|
||||
.icon.live,
|
||||
.icon.live::after,
|
||||
.icon.live::before {
|
||||
|
|
@ -1099,6 +1169,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.icon.live::after,
|
||||
.icon.live::before {
|
||||
content: "";
|
||||
|
|
@ -1108,6 +1179,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 2px;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.icon.live::after {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
|
@ -1138,6 +1210,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.icon.more::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1158,6 +1231,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
.icon.more-borderless {
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
}
|
||||
|
||||
.icon.more-borderless,
|
||||
.icon.more-borderless::after,
|
||||
.icon.more-borderless::before {
|
||||
|
|
@ -1169,15 +1243,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
background: currentColor;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.icon.more-borderless::after,
|
||||
.icon.more-borderless::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.icon.more-borderless::after {
|
||||
left: -6px;
|
||||
}
|
||||
|
||||
.icon.more-borderless::before {
|
||||
right: -6px;
|
||||
}
|
||||
|
|
@ -1193,6 +1270,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.icon.more-circle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1218,6 +1296,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 22px;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
}
|
||||
|
||||
.icon.phone::after,
|
||||
.icon.phone::before {
|
||||
content: "";
|
||||
|
|
@ -1225,6 +1304,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon.phone::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
|
@ -1239,6 +1319,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
linear-gradient(to left, currentColor 10px, transparent 0) no-repeat right 11px/6px 4px,
|
||||
linear-gradient(to left, currentColor 10px, transparent 0) no-repeat -1px 0/4px 6px;
|
||||
}
|
||||
|
||||
.icon.phone::before {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
|
@ -1259,17 +1340,20 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon.plus::after,
|
||||
.icon.plus::before {
|
||||
border-radius: 10px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.icon.plus {
|
||||
position: relative;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.icon.plus::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1278,6 +1362,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 0;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.icon.plus::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1297,6 +1382,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 16px;
|
||||
box-shadow: -6px -6px 0 -4px;
|
||||
}
|
||||
|
||||
.icon.reply::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1309,6 +1395,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.icon.reply::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1330,6 +1417,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.icon.resources {
|
||||
position: relative;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
|
|
@ -1337,6 +1425,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.icon.resources::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1363,6 +1452,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.send::after,
|
||||
.icon.send::before {
|
||||
content: "";
|
||||
|
|
@ -1375,6 +1465,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.icon.send::after {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
@ -1388,6 +1479,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
.icon.talk {
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
}
|
||||
|
||||
.icon.talk,
|
||||
.icon.talk::after {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -1398,6 +1490,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-radius: 100px;
|
||||
border: 2px dotted currentColor;
|
||||
}
|
||||
|
||||
.icon.talk::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
@ -1428,6 +1521,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border-bottom-right-radius: 1px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.icon.trash::after,
|
||||
.icon.trash::before {
|
||||
content: "";
|
||||
|
|
@ -1435,6 +1529,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon.trash::after {
|
||||
background: currentColor;
|
||||
border-radius: 3px;
|
||||
|
|
@ -1443,6 +1538,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: -4px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.icon.trash::before {
|
||||
width: 10px;
|
||||
height: 4px;
|
||||
|
|
@ -1463,6 +1559,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.icon.user {
|
||||
overflow: hidden;
|
||||
transform: scale(var(--icon-scale, 1));
|
||||
|
|
@ -1470,6 +1567,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 22px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon.user::after,
|
||||
.icon.user::before {
|
||||
content: "";
|
||||
|
|
@ -1479,6 +1577,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.icon.user::after {
|
||||
border-radius: 200px;
|
||||
top: 11px;
|
||||
|
|
@ -1498,6 +1597,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.icon.work::after,
|
||||
.icon.work::before {
|
||||
content: "";
|
||||
|
|
@ -1505,6 +1605,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon.work::before {
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
|
|
@ -1515,6 +1616,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
left: 2px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.icon.work::after {
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
|
|
@ -1532,6 +1634,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.icon.right::after,
|
||||
.icon.right::before {
|
||||
content: "";
|
||||
|
|
@ -1546,6 +1649,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 7px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.icon.right::after {
|
||||
right: 11px;
|
||||
}
|
||||
|
|
@ -1558,6 +1662,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.icon.left::after,
|
||||
.icon.left::before {
|
||||
content: "";
|
||||
|
|
@ -1572,6 +1677,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 7px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.icon.left::after {
|
||||
left: 11px;
|
||||
}
|
||||
|
|
@ -1587,6 +1693,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.skip-back::after,
|
||||
.icon.skip-back::before {
|
||||
content: "";
|
||||
|
|
@ -1596,12 +1703,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 8px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.icon.skip-back::before {
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
right: 11px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.icon.skip-back::after {
|
||||
width: 0;
|
||||
border-top: 4px solid transparent;
|
||||
|
|
@ -1620,6 +1729,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.icon.rewind::after,
|
||||
.icon.rewind::before {
|
||||
content: "";
|
||||
|
|
@ -1634,6 +1744,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 6px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.icon.rewind::after {
|
||||
left: 9px;
|
||||
}
|
||||
|
|
@ -1648,6 +1759,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.play::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1672,6 +1784,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.pause::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -1695,6 +1808,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.icon.fastforward::after,
|
||||
.icon.fastforward::before {
|
||||
content: "";
|
||||
|
|
@ -1709,6 +1823,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
top: 6px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.icon.fastforward::after {
|
||||
right: 9px;
|
||||
}
|
||||
|
|
@ -1723,6 +1838,7 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon.skip-forward::after,
|
||||
.icon.skip-forward::before {
|
||||
content: "";
|
||||
|
|
@ -1732,12 +1848,14 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
|
|||
height: 8px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.icon.skip-forward::before {
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
left: 11px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.icon.skip-forward::after {
|
||||
width: 0;
|
||||
border-top: 4px solid transparent;
|
||||
|
|
|
|||
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>
|
||||
|
||||
<script src="./js/_utils.js" type="text/javascript"></script>
|
||||
<script src="./js/api.js" type="text/javascript"></script>
|
||||
<!-- inlining these to force them to be scoped for everything else -->
|
||||
<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/datetimeutils.js" type="text/javascript"></script>
|
||||
<script src="./js/debounce.js" type="text/javascript"></script>
|
||||
|
|
@ -49,191 +53,4 @@
|
|||
<!-- #include file="./tabs/tabs.html" -->
|
||||
</main>
|
||||
</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>
|
||||
|
|
|
|||
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;
|
||||
border: 1px solid var(--border-normal);
|
||||
padding: 0.5rem;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#eventactionspopup[data-shown] {
|
||||
visibility: visible;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#eventactionspopup .icon.close {
|
||||
|
|
@ -61,9 +70,7 @@ function open_event_actions_popup(event) {
|
|||
event_actions_popup.style.left = position.x + "px";
|
||||
event_actions_popup.style.top = position.y + "px";
|
||||
|
||||
event_actions_popup.style.visibility = "visible";
|
||||
event_actions_popup.style.opacity = "1";
|
||||
event_actions_popup.style.display = "block";
|
||||
event_actions_popup.dataset.shown = true;
|
||||
}
|
||||
|
||||
function clear_event_actions_popup() {
|
||||
|
|
@ -71,9 +78,7 @@ function clear_event_actions_popup() {
|
|||
return;
|
||||
}
|
||||
|
||||
event_actions_popup.style.visibility = "hidden";
|
||||
event_actions_popup.style.opacity = "0";
|
||||
event_actions_popup.style.display = "none";
|
||||
delete event_actions_popup.dataset.shown;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ const reactions_popup_styling = `
|
|||
border: 1px solid var(--border-normal);
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#reactionspopup[data-shown] {
|
||||
visibility: visible;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#reactionspopup .icon.close {
|
||||
|
|
@ -103,10 +112,7 @@ function open_reactions_popup(event) {
|
|||
reactions_popup.style.left = position.x + "px";
|
||||
reactions_popup.style.top = position.y + "px";
|
||||
|
||||
reactions_popup.style.visibility = "visible";
|
||||
reactions_popup.style.opacity = "1";
|
||||
reactions_popup.style.display = "block";
|
||||
|
||||
reactions_popup.dataset.shown = true;
|
||||
reactions_popup_search_input.focus();
|
||||
}
|
||||
|
||||
|
|
@ -115,9 +121,7 @@ function clear_reactions_popup() {
|
|||
return;
|
||||
}
|
||||
|
||||
reactions_popup.style.visibility = "hidden";
|
||||
reactions_popup.style.opacity = "0";
|
||||
reactions_popup.style.display = "none";
|
||||
delete reactions_popup.dataset.shown;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
|
@ -160,7 +164,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<input
|
||||
type="hidden"
|
||||
name="creator_id"
|
||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
||||
generator="() => { return APP.user?.id; }"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
|
@ -194,7 +198,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
document.body.appendChild(reactions_popup);
|
||||
|
||||
reactions_popup_form = document.getElementById("reactions-selection-form");
|
||||
document.addEventListener("topic_changed", ({ detail: { topic_id } }) => {
|
||||
APP.on("topic_changed", ({ topic_id }) => {
|
||||
const reaction_topic_id = topic_id ?? document.body.dataset.topic;
|
||||
reactions_popup_form.action = reaction_topic_id
|
||||
? `/api/topics/${reaction_topic_id}/events`
|
||||
|
|
|
|||
|
|
@ -228,8 +228,12 @@ function smarten_feeds() {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( error.name === 'TypeError' && error.message === 'NetworkError when attempting to fetch resource.' ) {
|
||||
console.log( error.message );
|
||||
return;
|
||||
}
|
||||
|
||||
feed.dataset.error = JSON.stringify(error);
|
||||
console.trace(error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (feed.__started && feed.dataset.longpolling) {
|
||||
|
|
|
|||
|
|
@ -41,9 +41,7 @@ function smarten_forms() {
|
|||
form.uploaded = [];
|
||||
form.errors = [];
|
||||
|
||||
const user = document.body.dataset.user
|
||||
? JSON.parse(document.body.dataset.user)
|
||||
: undefined;
|
||||
const user = APP.user;
|
||||
if (!user) {
|
||||
throw new Error("You must be logged in to upload files here.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
document.addEventListener("topics_updated", ({ detail: { topics } }) => {
|
||||
APP.on("topics_updated", ({ topics }) => {
|
||||
const topic_list = document.getElementById("topic-list");
|
||||
topic_list.innerHTML = "";
|
||||
for (const topic of topics.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) {
|
||||
|
|
@ -16,19 +16,22 @@
|
|||
.forEach((element) => element.classList.remove("active"));
|
||||
|
||||
const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic;
|
||||
|
||||
if (!new_topic_id) {
|
||||
return;
|
||||
if (new_topic_id) {
|
||||
document
|
||||
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
||||
.forEach((element) => element.classList.add("active"));
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
||||
.forEach((element) => element.classList.add("active"));
|
||||
for ( const watch of APP.user_watches ) {
|
||||
// find the topic indicator for this watch
|
||||
// if there is new stuff - TODO implement a HEAD for getting latest event id?
|
||||
// add a class of 'new-content'
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("topics_updated", update_topic_indicators);
|
||||
document.addEventListener("topic_changed", update_topic_indicators);
|
||||
document.addEventListener("user_logged_in", update_topic_indicators);
|
||||
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() {
|
||||
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
|
||||
|
|
@ -64,7 +67,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const user = document.body.dataset.user && JSON.parse(document.body.dataset.user);
|
||||
const user = APP.user;
|
||||
if (!user) {
|
||||
alert("You must be logged in.");
|
||||
return;
|
||||
|
|
@ -103,22 +106,53 @@
|
|||
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
|
||||
</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>
|
||||
|
||||
<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 {
|
||||
z-index: 100;
|
||||
background: var(--bg);
|
||||
|
|
@ -126,15 +160,86 @@
|
|||
width: auto;
|
||||
left: 0;
|
||||
max-width: 32rem;
|
||||
padding: 0.5rem;
|
||||
padding-left: 6rem;
|
||||
transition: all ease-in-out 0.33s;
|
||||
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-icon {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
|
|
@ -285,324 +390,437 @@
|
|||
</style>
|
||||
|
||||
<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" />
|
||||
<label id="sidebar-toggle-icon" for="sidebar-toggle">
|
||||
<div class="icon right"></div>
|
||||
</label>
|
||||
|
||||
<script>
|
||||
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
||||
async function update_servers_list() {
|
||||
const template = document.getElementById( 'server-list-entry-template');
|
||||
|
||||
new MutationObserver((mutations, observer) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const user = document.body.dataset.user
|
||||
? JSON.parse(document.body.dataset.user)
|
||||
: null;
|
||||
try {
|
||||
const server = APP.server;
|
||||
const entry = eval("`" + template.innerHTML.trim() + "`");
|
||||
document.getElementById('this-server-container').innerHTML = entry;
|
||||
}
|
||||
catch( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
|
||||
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
||||
for (const user_bound_element of user_bound_elements) {
|
||||
const key =
|
||||
user_bound_element.dataset
|
||||
.bindToUserField; /* I hate that it converts the name */
|
||||
const key_elements = key.split(".");
|
||||
try {
|
||||
document.getElementById( 'suggested-servers-container').innerHTML = '';
|
||||
|
||||
let value = undefined;
|
||||
if (user) {
|
||||
let current = user;
|
||||
for (const key_element of key_elements) {
|
||||
current = current[key_element];
|
||||
if (!current) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = current;
|
||||
}
|
||||
|
||||
const target =
|
||||
typeof user_bound_element.dataset.userFieldTarget === "string" &&
|
||||
user_bound_element.dataset.userFieldTarget.length > 0
|
||||
? user_bound_element.dataset.userFieldTarget
|
||||
: "innerHTML";
|
||||
|
||||
const default_value =
|
||||
typeof user_bound_element.dataset.userFieldDefault === "string" &&
|
||||
user_bound_element.dataset.userFieldDefault.length > 0
|
||||
? user_bound_element.dataset.userFieldDefault
|
||||
: "";
|
||||
|
||||
user_bound_element[target] = value ?? default_value;
|
||||
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 );
|
||||
}
|
||||
|
||||
const primary_color_setting = user?.meta?.primary_color;
|
||||
if (primary_color_setting) {
|
||||
const root = document.querySelector(":root");
|
||||
root.style.setProperty("--base-color", primary_color_setting);
|
||||
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 );
|
||||
}
|
||||
});
|
||||
}).observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-user"],
|
||||
});
|
||||
}
|
||||
catch( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
}
|
||||
|
||||
APP.on( 'load', update_servers_list );
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.profile-container {
|
||||
max-width: 1024px;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.profile-container .avatar-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.profile-container .avatar-container input[type="file"] {
|
||||
<style>
|
||||
#server-list-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
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>
|
||||
<form class="profile-container">
|
||||
<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>
|
||||
const profile_form = document.currentScript.closest("form");
|
||||
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const inputs = profile_form.querySelectorAll("input");
|
||||
new MutationObserver((mutations, observer) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const user = APP.user;
|
||||
|
||||
async function update_from_input(input) {
|
||||
delete input.__debounce_timeout;
|
||||
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
||||
for (const user_bound_element of user_bound_elements) {
|
||||
const key =
|
||||
user_bound_element.dataset
|
||||
.bindToUserField; /* I hate that it converts the name */
|
||||
const key_elements = key.split(".");
|
||||
|
||||
if (!document.body.dataset.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = JSON.parse(document.body.dataset.user);
|
||||
|
||||
const updated_user = { ...user };
|
||||
|
||||
switch (input.name) {
|
||||
case "meta.avatar":
|
||||
const avatar = input.files[0];
|
||||
|
||||
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
|
||||
return alert(
|
||||
"You must select a valid image to upload as your avatar.",
|
||||
);
|
||||
let value = undefined;
|
||||
if (user) {
|
||||
let current = user;
|
||||
for (const key_element of key_elements) {
|
||||
current = current[key_element];
|
||||
if (!current) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: actually enforce this on the upload in serverus somehow
|
||||
if (avatar.size > 512_000) {
|
||||
return alert("512K is the largest allowed avatar size.");
|
||||
}
|
||||
|
||||
const avatar_upload_body = new FormData();
|
||||
avatar_upload_body.append(
|
||||
"file",
|
||||
avatar,
|
||||
encodeURIComponent(avatar.name),
|
||||
);
|
||||
|
||||
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
|
||||
|
||||
const avatar_upload_response = await api.fetch(avatar_path, {
|
||||
method: "PUT",
|
||||
body: avatar_upload_body,
|
||||
});
|
||||
|
||||
if (!avatar_upload_response.ok) {
|
||||
const error = await avatar_upload_response.json();
|
||||
return alert(error?.error?.message ?? "Unknown error.");
|
||||
}
|
||||
|
||||
updated_user.meta = updated_user.meta ?? {};
|
||||
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
|
||||
break;
|
||||
default:
|
||||
const elements = input.name.split(".");
|
||||
let current = updated_user;
|
||||
for (const element of elements.slice(0, elements.length - 1)) {
|
||||
current[element] = current[element] ?? {};
|
||||
current = current[element];
|
||||
}
|
||||
|
||||
current[elements.slice(elements.length - 1).shift()] =
|
||||
input.value.trim();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
json: updated_user,
|
||||
});
|
||||
|
||||
if (!saved_user_response.ok) {
|
||||
const error = await avatar_upload_response.json();
|
||||
return alert(error?.error?.message ?? "Unknown error.");
|
||||
}
|
||||
|
||||
const saved_user = await saved_user_response.json();
|
||||
|
||||
document.body.dataset.user = JSON.stringify(saved_user);
|
||||
document.body.dataset.perms = saved_user.permissions.join(":");
|
||||
}
|
||||
|
||||
for (const input of inputs) {
|
||||
function on_updated(event) {
|
||||
if (input.__debounce_timeout) {
|
||||
clearTimeout(input.__debounce_timeout);
|
||||
value = current;
|
||||
}
|
||||
|
||||
input.__debounce_timeout = setTimeout(() => {
|
||||
update_from_input(input);
|
||||
}, 250);
|
||||
const target =
|
||||
typeof user_bound_element.dataset.userFieldTarget === "string" &&
|
||||
user_bound_element.dataset.userFieldTarget.length > 0
|
||||
? user_bound_element.dataset.userFieldTarget
|
||||
: "innerHTML";
|
||||
|
||||
const default_value =
|
||||
typeof user_bound_element.dataset.userFieldDefault === "string" &&
|
||||
user_bound_element.dataset.userFieldDefault.length > 0
|
||||
? user_bound_element.dataset.userFieldDefault
|
||||
: "";
|
||||
|
||||
user_bound_element[target] = value ?? default_value;
|
||||
}
|
||||
|
||||
input.addEventListener("input", on_updated);
|
||||
input.addEventListener("change", on_updated);
|
||||
}
|
||||
const primary_color_setting = user?.meta?.primary_color;
|
||||
if (primary_color_setting) {
|
||||
const root = document.querySelector(":root");
|
||||
root.style.setProperty("--base-color", primary_color_setting);
|
||||
}
|
||||
});
|
||||
}).observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-user"],
|
||||
});
|
||||
</script>
|
||||
<div class="avatar-container xx-large">
|
||||
<img
|
||||
id="user-avatar"
|
||||
src="/images/default_avatar.gif"
|
||||
alt="User Avatar"
|
||||
data-bind-to-user-field="meta.avatar"
|
||||
data-user-field-target="src"
|
||||
data-user-field-default="/images/default_avatar.gif"
|
||||
/>
|
||||
<input type="file" accept="image/*" name="meta.avatar" />
|
||||
</div>
|
||||
|
||||
<details class="additional-profile">
|
||||
<summary>
|
||||
<div class="username-container">
|
||||
<span class="username" data-bind-to-user-field="username"></span>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="notifications-settings-container">
|
||||
<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
|
||||
Enable Notifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="color-settings-container">
|
||||
<input
|
||||
type="text"
|
||||
id="user-color-setting-primary"
|
||||
name="meta.primary_color"
|
||||
value=""
|
||||
data-bind-to-user-field="meta.primary_color"
|
||||
data-user-field-target="value"
|
||||
data-user-field-default=""
|
||||
/>
|
||||
<label class="placeholder" for="user-color-setting-primary">Primary Color</label>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
<button
|
||||
style="text-transform: uppercase; width: 100%; padding: 1.1rem 0"
|
||||
onclick="generate_invite(event)"
|
||||
>
|
||||
Invite Another Human
|
||||
</button>
|
||||
|
||||
<form
|
||||
data-smart="true"
|
||||
data-method="DELETE"
|
||||
action="/api/auth"
|
||||
style="position: absolute; left: 1rem; right: 1rem; bottom: 1rem"
|
||||
>
|
||||
<script>
|
||||
{
|
||||
const form = document.currentScript.closest("form");
|
||||
form.on_reply = (response) => {
|
||||
if (!response.deleted) {
|
||||
alert("error logging out? please reload.");
|
||||
return;
|
||||
}
|
||||
|
||||
delete document.body.dataset.user;
|
||||
delete document.body.dataset.perms;
|
||||
window.location = "/";
|
||||
|
||||
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
|
||||
};
|
||||
<style type="text/css">
|
||||
.profile-container {
|
||||
max-width: 1024px;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</script>
|
||||
<button class="primary">Log Out</button>
|
||||
</form>
|
||||
|
||||
<div class="topics-container">
|
||||
<div style="margin-bottom: 1rem">
|
||||
<span class="title">topics</span>
|
||||
</div>
|
||||
<ul id="topic-list" class="topic-list"></ul>
|
||||
.profile-container .avatar-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
<div id="topic-creation-container" data-requires-permission="topics.create">
|
||||
<button
|
||||
id="toggle-topic-creation-form-button"
|
||||
onclick="((event) => {
|
||||
event.preventDefault();
|
||||
const topic_create_form = document.getElementById( 'topic-create' );
|
||||
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
|
||||
})(event)"
|
||||
>
|
||||
<div class="icon plus"></div>
|
||||
</button>
|
||||
<form
|
||||
id="topic-create"
|
||||
data-smart="true"
|
||||
action="/api/topics"
|
||||
method="POST"
|
||||
style="
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s;
|
||||
"
|
||||
>
|
||||
<input
|
||||
id="new-topic-name-input"
|
||||
type="text"
|
||||
name="name"
|
||||
value=""
|
||||
placeholder="new topic"
|
||||
/>
|
||||
.profile-container .avatar-container input[type="file"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<form class="profile-container">
|
||||
<script>
|
||||
const profile_form = document.currentScript.closest("form");
|
||||
|
||||
<input type="submit" hidden />
|
||||
<script>
|
||||
{
|
||||
const form = document.currentScript.closest("form");
|
||||
const topic_create_form = document.getElementById("topic-create");
|
||||
const new_topic_name_input =
|
||||
document.getElementById("new-topic-name-input");
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const inputs = profile_form.querySelectorAll("input");
|
||||
|
||||
form.on_reply = (new_topic) => {
|
||||
const topic_list = document.getElementById("topic-list");
|
||||
topic_list.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
|
||||
);
|
||||
async function update_from_input(input) {
|
||||
delete input.__debounce_timeout;
|
||||
|
||||
new_topic_name_input.value = "";
|
||||
window.location.hash = `/topic/${new_topic.id}/chat`;
|
||||
topic_create_form.style["height"] = "0";
|
||||
};
|
||||
if (!document.body.dataset.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = APP.user;
|
||||
|
||||
const updated_user = { ...user };
|
||||
|
||||
switch (input.name) {
|
||||
case "meta.avatar":
|
||||
const avatar = input.files[0];
|
||||
|
||||
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
|
||||
return alert(
|
||||
"You must select a valid image to upload as your avatar.",
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: actually enforce this on the upload in serverus somehow
|
||||
if (avatar.size > 512_000) {
|
||||
return alert("512K is the largest allowed avatar size.");
|
||||
}
|
||||
|
||||
const avatar_upload_body = new FormData();
|
||||
avatar_upload_body.append(
|
||||
"file",
|
||||
avatar,
|
||||
encodeURIComponent(avatar.name),
|
||||
);
|
||||
|
||||
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
|
||||
|
||||
const avatar_upload_response = await api.fetch(avatar_path, {
|
||||
method: "PUT",
|
||||
body: avatar_upload_body,
|
||||
});
|
||||
|
||||
if (!avatar_upload_response.ok) {
|
||||
const error = await avatar_upload_response.json();
|
||||
return alert(error?.error?.message ?? "Unknown error.");
|
||||
}
|
||||
|
||||
updated_user.meta = updated_user.meta ?? {};
|
||||
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
|
||||
break;
|
||||
default:
|
||||
const elements = input.name.split(".");
|
||||
let current = updated_user;
|
||||
for (const element of elements.slice(0, elements.length - 1)) {
|
||||
current[element] = current[element] ?? {};
|
||||
current = current[element];
|
||||
}
|
||||
|
||||
current[elements.slice(elements.length - 1).shift()] =
|
||||
input.value.trim();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
json: updated_user,
|
||||
});
|
||||
|
||||
if (!saved_user_response.ok) {
|
||||
const error = await avatar_upload_response.json();
|
||||
return alert(error?.error?.message ?? "Unknown error.");
|
||||
}
|
||||
|
||||
const saved_user = await saved_user_response.json();
|
||||
APP.update_user( saved_user );
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
|
||||
for (const input of inputs) {
|
||||
function on_updated(event) {
|
||||
if (input.__debounce_timeout) {
|
||||
clearTimeout(input.__debounce_timeout);
|
||||
}
|
||||
|
||||
input.__debounce_timeout = setTimeout(() => {
|
||||
update_from_input(input);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
input.addEventListener("input", on_updated);
|
||||
input.addEventListener("change", on_updated);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="avatar-container xx-large">
|
||||
<img
|
||||
id="user-avatar"
|
||||
src="/images/default_avatar.gif"
|
||||
alt="User Avatar"
|
||||
data-bind-to-user-field="meta.avatar"
|
||||
data-user-field-target="src"
|
||||
data-user-field-default="/images/default_avatar.gif"
|
||||
/>
|
||||
<input type="file" accept="image/*" name="meta.avatar" />
|
||||
</div>
|
||||
|
||||
<details class="additional-profile">
|
||||
<summary>
|
||||
<div class="username-container">
|
||||
<span class="username" data-bind-to-user-field="username"></span>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="notifications-settings-container">
|
||||
<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
|
||||
Enable Notifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="color-settings-container">
|
||||
<input
|
||||
type="text"
|
||||
id="user-color-setting-primary"
|
||||
name="meta.primary_color"
|
||||
value=""
|
||||
data-bind-to-user-field="meta.primary_color"
|
||||
data-user-field-target="value"
|
||||
data-user-field-default=""
|
||||
/>
|
||||
<label class="placeholder" for="user-color-setting-primary">Primary Color</label>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
<button
|
||||
style="text-transform: uppercase; width: 100%; padding: 1.1rem 0"
|
||||
onclick="generate_invite(event)"
|
||||
>
|
||||
Invite Another Human
|
||||
</button>
|
||||
|
||||
<form
|
||||
data-smart="true"
|
||||
data-method="DELETE"
|
||||
action="/api/auth"
|
||||
style="position: absolute; left: 1rem; right: 1rem; bottom: 1rem"
|
||||
>
|
||||
<script>
|
||||
{
|
||||
const form = document.currentScript.closest("form");
|
||||
form.on_reply = (response) => {
|
||||
if (!response.deleted) {
|
||||
alert("error logging out? please reload.");
|
||||
return;
|
||||
}
|
||||
|
||||
APP.logout();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<button class="primary">Log Out</button>
|
||||
</form>
|
||||
|
||||
<div class="topics-container">
|
||||
<div style="margin-bottom: 1rem">
|
||||
<span class="title">topics</span>
|
||||
</div>
|
||||
<ul id="topic-list" class="topic-list"></ul>
|
||||
|
||||
<div id="topic-creation-container" data-requires-permission="topics.create">
|
||||
<button
|
||||
id="toggle-topic-creation-form-button"
|
||||
onclick="((event) => {
|
||||
event.preventDefault();
|
||||
const topic_create_form = document.getElementById( 'topic-create' );
|
||||
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
|
||||
})(event)"
|
||||
>
|
||||
<div class="icon plus"></div>
|
||||
</button>
|
||||
<form
|
||||
id="topic-create"
|
||||
data-smart="true"
|
||||
action="/api/topics"
|
||||
method="POST"
|
||||
style="
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s;
|
||||
"
|
||||
>
|
||||
<input
|
||||
id="new-topic-name-input"
|
||||
type="text"
|
||||
name="name"
|
||||
value=""
|
||||
placeholder="new topic"
|
||||
/>
|
||||
|
||||
<input type="submit" hidden />
|
||||
<script>
|
||||
{
|
||||
const form = document.currentScript.closest("form");
|
||||
const topic_create_form = document.getElementById("topic-create");
|
||||
const new_topic_name_input =
|
||||
document.getElementById("new-topic-name-input");
|
||||
|
||||
form.on_reply = (new_topic) => {
|
||||
const topic_list = document.getElementById("topic-list");
|
||||
topic_list.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
|
||||
);
|
||||
|
||||
new_topic_name_input.value = "";
|
||||
window.location.hash = `/topic/${new_topic.id}/chat`;
|
||||
topic_create_form.style["height"] = "0";
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -68,12 +68,7 @@
|
|||
const form = document.currentScript.closest("form");
|
||||
form.on_reply = (response) => {
|
||||
const user = response.user;
|
||||
document.body.dataset.user = JSON.stringify(user);
|
||||
document.body.dataset.perms = user.permissions.join(":");
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("user_logged_in", { detail: { user } }),
|
||||
);
|
||||
APP.login( user );
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
|
@ -127,7 +122,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
APP.on( 'load', () => {
|
||||
const query = new URL(document.location.toString())
|
||||
.searchParams;
|
||||
const invite_code = query.get("invite_code");
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@
|
|||
name="top-level-tabs"
|
||||
id="blurb-tab-input"
|
||||
class="tab-switch"
|
||||
data-view="blurb"
|
||||
data-view="blurbs"
|
||||
/>
|
||||
<label for="blurb-tab-input" class="tab-label"
|
||||
><div class="icon blurb"></div>
|
||||
|
|
@ -159,8 +159,8 @@
|
|||
{
|
||||
const feed = document.currentScript.closest("[data-feed]");
|
||||
|
||||
document.addEventListener("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||
document.addEventListener("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||
|
||||
feed.__target_element = (item) => {
|
||||
return (
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
return {
|
||||
event: item,
|
||||
blurb: item,
|
||||
creator: await USERS.get(item.creator_id),
|
||||
creator: await APP.USERS.get(item.creator_id),
|
||||
blurb_datetime
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
<input
|
||||
type="hidden"
|
||||
name="creator_id"
|
||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
||||
generator="() => { return APP.user?.id; }"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -32,12 +32,8 @@
|
|||
{
|
||||
const feed = document.currentScript.closest("[data-feed]");
|
||||
|
||||
document.addEventListener("topic_changed", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
document.addEventListener("user_logged_in", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||
|
||||
const time_tick_tock_timeout = 60_000;
|
||||
|
||||
|
|
@ -71,7 +67,7 @@
|
|||
|
||||
return {
|
||||
event: item,
|
||||
creator: await USERS.get(item.creator_id),
|
||||
creator: await APP.USERS.get(item.creator_id),
|
||||
event_datetime,
|
||||
time_tick_tock_class,
|
||||
user_tick_tock_class,
|
||||
|
|
@ -165,12 +161,9 @@
|
|||
<script>
|
||||
{
|
||||
const form = document.currentScript.closest("form");
|
||||
document.addEventListener(
|
||||
"topic_changed",
|
||||
({ detail: { topic_id } }) => {
|
||||
form.action = topic_id ? `/api/topics/${topic_id}/events` : "";
|
||||
},
|
||||
);
|
||||
APP.on( "topic_changed", ({ topic_id }) => {
|
||||
form.action = topic_id ? `/api/topics/${topic_id}/events` : "";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -192,7 +185,7 @@
|
|||
<input
|
||||
type="hidden"
|
||||
name="creator_id"
|
||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
||||
generator="() => { return APP.user?.id; }"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
name="top-level-tabs"
|
||||
id="essay-tab-input"
|
||||
class="tab-switch"
|
||||
data-view="essay"
|
||||
data-view="essays"
|
||||
/>
|
||||
<label for="essay-tab-input" class="tab-label"
|
||||
><div class="icon essay"></div>
|
||||
|
|
@ -126,12 +126,8 @@
|
|||
{
|
||||
const feed = document.currentScript.closest("[data-feed]");
|
||||
|
||||
document.addEventListener("topic_changed", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
document.addEventListener("user_logged_in", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||
|
||||
feed.__target_element = (item) => {
|
||||
let target = feed;
|
||||
|
|
@ -156,7 +152,7 @@
|
|||
return {
|
||||
event: item,
|
||||
essay: item,
|
||||
creator: await USERS.get(item.creator_id),
|
||||
creator: await APP.USERS.get(item.creator_id),
|
||||
essay_datetime,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
<input
|
||||
type="hidden"
|
||||
name="creator_id"
|
||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
||||
generator="() => { return APP.user?.id; }"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -156,12 +156,8 @@
|
|||
{
|
||||
const feed = document.currentScript.closest("[data-feed]");
|
||||
|
||||
document.addEventListener("topic_changed", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
document.addEventListener("user_logged_in", () => {
|
||||
feed.__reset && feed.__reset();
|
||||
});
|
||||
APP.on("topic_changed", () => { feed.__reset && feed.__reset(); });
|
||||
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
|
||||
|
||||
feed.__target_element = (item) => {
|
||||
let target = feed;
|
||||
|
|
@ -189,7 +185,7 @@
|
|||
return {
|
||||
event: item,
|
||||
post: item,
|
||||
creator: await USERS.get(item.creator_id),
|
||||
creator: await APP.USERS.get(item.creator_id),
|
||||
post_datetime,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<input
|
||||
type="hidden"
|
||||
name="creator_id"
|
||||
generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }"
|
||||
generator="() => { return APP.user?.id; }"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
document.addEventListener("view_changed", ({ detail: { view } }) => {
|
||||
APP.on( "view_changed", ({ view }) => {
|
||||
const target_tab = document.querySelector(`.tab-switch[data-view="${view}"]`);
|
||||
|
||||
if (target_tab) {
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
APP.on( 'load', () => {
|
||||
const tab_switchers = document.querySelectorAll(".tab-switch");
|
||||
for (const tab_switch of tab_switchers) {
|
||||
tab_switch.addEventListener("input", (event) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue