Compare commits

..

No commits in common. "2d121048757106aca833f6e8a8775cb54612d68e" and "19afb7f9faf4f91aea26f5c527edea513751dc95" have entirely different histories.

46 changed files with 1139 additions and 30964 deletions

View file

@ -1,10 +1,6 @@
# autonomous.contact # autonomous.contact
A hub for communities as a single service with no required external dependencies. Bringing the BBS back.
## Screenshots
![chat](/screenshots/screenshot-chat.png)
## TODO ## TODO
@ -17,13 +13,13 @@ feature discussions.
- [X] check for logged in user session - [X] check for logged in user session
- [X] log in - [X] log in
- [X] refactor login/sessions/totp - [X] refactor login/sessions/totp
- [/] media uploads - [ ] media uploads
- [X] local upload support (keep it simple for small instances) - [X] local upload support (keep it simple for small instances)
- [ ] S3 support (then self-host with your friends: https://garagehq.deuxfleurs.fr/) - [ ] S3 support (then self-host with your friends: https://garagehq.deuxfleurs.fr/)
- [ ] test mounting an s3 volume under /files and having no s3 support in the codebase - [ ] test mounting an s3 volume under /files and having no s3 support in the codebase
- [X] user profile page - [X] user profile page
- [X] logout button - [X] logout button
- [/] profile editing - [ ] profile editing
- [X] avatar uploads - [X] avatar uploads
- [X] chat channels - [X] chat channels
- [X] chat messages - [X] chat messages
@ -39,17 +35,17 @@ feature discussions.
- [X] '...' button to show actions - [X] '...' button to show actions
- [ ] delete message - [ ] delete message
- [ ] reply to message - [ ] reply to message
- [X] copy message link - [ ] copy message link
- [ ] @-prefixing of users for notifications/highlighting - [ ] @-prefixing of users for notifications/highlighting
- [ ] chat input box should auto-complete users for you - [ ] chat input box should auto-complete users for you
- [ ] messages to you should be highlighted somehow - [ ] messages to you should be highlighted somehow
- [/] chat message reactions - [ ] chat message reactions
- [X] sidebar on mobile needs to show/hide via a button - [X] sidebar on mobile needs to show/hide via a button
- [/] clean up after initial implementation - [ ] clean up after initial implementation
- [X] split the monolithic talk.html up - [X] split the monolithic talk.html up
- [X] rename talk => chat - [X] rename talk => chat
- [ ] way too much spaghetti has accumulated in the frontend - take a cleanup pass - [ ] way too much spaghetti has accumulated in the frontend - take a cleanup pass
- [/] chat message processing - [ ] chat message processing
- [X] auto-link urls - [X] auto-link urls
- [X] use this regex: `(?:(?<protocol>[a-zA-Z]+):)?(?:\/\/)?(?:(?<auth>(?<username>\S.+)\:(?<password>.+))\@)?(?<host>(?:(?<hostname>[-a-zA-Z0-9\.]+)\.)?(?<domain>(?:[-a-zA-Z0-9]+?\.(?<tld>[-a-zA-Z0-9]{2,64}))|localhost)(?:\:(?<port>[0-9]{1,6}))?)\b(?<path>[-a-zA-Z0-9@:%_{}\[\]<>\(\)\+.~&\/="]*?(?<extension>\.[^\.?/#"\n<>]+)?)(?:\?(?<query>[a-zA-Z0-9!$%&<>()*+,-\.\/\:\;\=\?\@_~"]+))?(?:#(?<hash>[a-zA-Z0-9!$&'()*+,-\.\/\:\;\=\?\@_~"]*?))?(?:$|\s)` - [X] use this regex: `(?:(?<protocol>[a-zA-Z]+):)?(?:\/\/)?(?:(?<auth>(?<username>\S.+)\:(?<password>.+))\@)?(?<host>(?:(?<hostname>[-a-zA-Z0-9\.]+)\.)?(?<domain>(?:[-a-zA-Z0-9]+?\.(?<tld>[-a-zA-Z0-9]{2,64}))|localhost)(?:\:(?<port>[0-9]{1,6}))?)\b(?<path>[-a-zA-Z0-9@:%_{}\[\]<>\(\)\+.~&\/="]*?(?<extension>\.[^\.?/#"\n<>]+)?)(?:\?(?<query>[a-zA-Z0-9!$%&<>()*+,-\.\/\:\;\=\?\@_~"]+))?(?:#(?<hash>[a-zA-Z0-9!$&'()*+,-\.\/\:\;\=\?\@_~"]*?))?(?:$|\s)`
- [ ] :emoji: replacements - [ ] :emoji: replacements
@ -57,11 +53,11 @@ feature discussions.
- [ ] add a button to allow for loading a preview - [ ] add a button to allow for loading a preview
- we want to do this loading client-side, and so should only do it if they ask us to - we want to do this loading client-side, and so should only do it if they ask us to
- [ ] parse head meta, eg: https://github.com/AndrejGajdos/link-preview-generator/blob/master/index.js - [ ] parse head meta, eg: https://github.com/AndrejGajdos/link-preview-generator/blob/master/index.js
- [/] embedded video for - [ ] embedded video for
- [X] youtube - [X] youtube
- [X] vimeo - [X] vimeo
- [ ] tiktok - [ ] tiktok
- [/] embedded audio or audio cards for: - [ ] embedded audio or audio cards for:
- [X] tidal - [X] tidal
- [X] spotify - [X] spotify
- [ ] youtube (any way to differentiate for yt music?) - [ ] youtube (any way to differentiate for yt music?)
@ -72,12 +68,12 @@ feature discussions.
- [ ] copy original link (hopefully just a button with some onclick we can slap next to the iframe and style?) - [ ] copy original link (hopefully just a button with some onclick we can slap next to the iframe and style?)
- [ ] toggle embed (toggle between showing the embed and the original link) - [ ] toggle embed (toggle between showing the embed and the original link)
- [X] punycode urls before url extraction? (see: https://stackoverflow.com/a/26618995) - [X] punycode urls before url extraction? (see: https://stackoverflow.com/a/26618995)
- [/] gif support - [ ] gif support
- [X] gif embeds - [X] gif embeds
- [X] mp4 embeds - [X] mp4 embeds
- [ ] start/stop gif control - [ ] start/stop gif control
- [ ] hide control - [ ] hide control
- [/] inline image support - [ ] inline image support
- [X] inline images - [X] inline images
- [ ] fullscreen images on click to show largest version - [ ] fullscreen images on click to show largest version
- [ ] hide control - [ ] hide control
@ -87,8 +83,8 @@ feature discussions.
- [ ] emoji picker - [ ] emoji picker
- [ ] custom emoji support - [ ] custom emoji support
- [ ] upload custom gif emoji - [ ] upload custom gif emoji
- [/] Notifications - [ ] Notifications
- [X] if web notifications are not enabled, show a button to turn them on - [ ] if web notifications are not enabled, show a button to turn them on
- [ ] if web notifications are enabled, emit on events - [ ] if web notifications are enabled, emit on events
- [ ] ability to mute - [ ] ability to mute
- [ ] users - [ ] users

View file

@ -11,15 +11,11 @@
"test": "DENO_ENV=test FSDB_ROOT=$PWD/tests/data/$(date --iso-8601=seconds) SERVERUS_ROOT=$PWD/public SERVERUS_PUT_PATHS_ALLOWED=./files SERVERUS_DELETE_PATHS_ALLOWED=./files deno test --allow-env --allow-read --allow-write --allow-net --allow-import --trace-leaks --fail-fast tests/" "test": "DENO_ENV=test FSDB_ROOT=$PWD/tests/data/$(date --iso-8601=seconds) SERVERUS_ROOT=$PWD/public SERVERUS_PUT_PATHS_ALLOWED=./files SERVERUS_DELETE_PATHS_ALLOWED=./files deno test --allow-env --allow-read --allow-write --allow-net --allow-import --trace-leaks --fail-fast tests/"
}, },
"test": { "test": {
"exclude": [ "exclude": ["tests/data/"]
"tests/data/"
]
}, },
"compilerOptions": {}, "compilerOptions": {},
"fmt": { "fmt": {
"include": [ "include": ["**/*.ts"],
"**/*.ts"
],
"options": { "options": {
"useTabs": true, "useTabs": true,
"lineWidth": 180, "lineWidth": 180,
@ -29,28 +25,22 @@
} }
}, },
"lint": { "lint": {
"include": [ "include": ["**/*.ts"],
"**/*.ts"
],
"rules": { "rules": {
"tags": [ "tags": ["recommended"],
"recommended" "exclude": ["no-explicit-any"]
],
"exclude": [
"no-explicit-any"
]
} }
}, },
"imports": { "imports": {
"@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.2.4", "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.2.4",
"@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0",
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.16.0", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0",
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1", "@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
"@std/assert": "jsr:@std/assert@^1.0.17", "@std/assert": "jsr:@std/assert@^1.0.15",
"@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/fs": "jsr:@std/fs@^1.0.22", "@std/fs": "jsr:@std/fs@^1.0.19",
"@std/http": "jsr:@std/http@^1.0.23", "@std/http": "jsr:@std/http@^1.0.21",
"@std/media-types": "jsr:@std/media-types@^1.1.0", "@std/media-types": "jsr:@std/media-types@^1.1.0",
"@std/path": "jsr:@std/path@^1.1.4" "@std/path": "jsr:@std/path@^1.1.2"
} }
} }

84
deno.lock generated
View file

@ -3,31 +3,31 @@
"specifiers": { "specifiers": {
"jsr:@andyburke/fsdb@^1.2.4": "1.2.4", "jsr:@andyburke/fsdb@^1.2.4": "1.2.4",
"jsr:@andyburke/lurid@0.2": "0.2.0", "jsr:@andyburke/lurid@0.2": "0.2.0",
"jsr:@andyburke/serverus@0.16": "0.16.0", "jsr:@andyburke/serverus@0.13": "0.13.0",
"jsr:@da/bcrypt@*": "1.0.1", "jsr:@da/bcrypt@*": "1.0.1",
"jsr:@da/bcrypt@^1.0.1": "1.0.1", "jsr:@da/bcrypt@^1.0.1": "1.0.1",
"jsr:@std/assert@^1.0.17": "1.0.17", "jsr:@std/assert@^1.0.15": "1.0.15",
"jsr:@std/cli@^1.0.19": "1.0.25", "jsr:@std/cli@^1.0.19": "1.0.23",
"jsr:@std/cli@^1.0.20": "1.0.25", "jsr:@std/cli@^1.0.20": "1.0.23",
"jsr:@std/cli@^1.0.21": "1.0.25", "jsr:@std/cli@^1.0.21": "1.0.23",
"jsr:@std/cli@^1.0.25": "1.0.25", "jsr:@std/cli@^1.0.23": "1.0.23",
"jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/fmt@^1.0.6": "1.0.8", "jsr:@std/fmt@^1.0.6": "1.0.8",
"jsr:@std/fmt@^1.0.8": "1.0.8", "jsr:@std/fmt@^1.0.8": "1.0.8",
"jsr:@std/fs@^1.0.18": "1.0.22", "jsr:@std/fs@^1.0.18": "1.0.19",
"jsr:@std/fs@^1.0.19": "1.0.22", "jsr:@std/fs@^1.0.19": "1.0.19",
"jsr:@std/fs@^1.0.21": "1.0.22",
"jsr:@std/fs@^1.0.22": "1.0.22",
"jsr:@std/html@^1.0.5": "1.0.5", "jsr:@std/html@^1.0.5": "1.0.5",
"jsr:@std/http@^1.0.20": "1.0.23", "jsr:@std/http@^1.0.20": "1.0.21",
"jsr:@std/http@^1.0.23": "1.0.23", "jsr:@std/http@^1.0.21": "1.0.21",
"jsr:@std/internal@^1.0.10": "1.0.12",
"jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/internal@^1.0.9": "1.0.12",
"jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.6": "1.0.6", "jsr:@std/net@^1.0.6": "1.0.6",
"jsr:@std/path@^1.1.0": "1.1.4", "jsr:@std/path@^1.1.0": "1.1.2",
"jsr:@std/path@^1.1.1": "1.1.4", "jsr:@std/path@^1.1.1": "1.1.2",
"jsr:@std/path@^1.1.4": "1.1.4", "jsr:@std/path@^1.1.2": "1.1.2",
"jsr:@std/streams@^1.0.16": "1.0.16", "jsr:@std/streams@^1.0.13": "1.0.13",
"npm:@types/node@*": "22.15.15" "npm:@types/node@*": "22.15.15"
}, },
"jsr": { "jsr": {
@ -45,8 +45,8 @@
"jsr:@std/cli@^1.0.19" "jsr:@std/cli@^1.0.19"
] ]
}, },
"@andyburke/serverus@0.16.0": { "@andyburke/serverus@0.13.0": {
"integrity": "625fc3f08ddc377beb86b282d603ca6154cf38e136d916ec19a87ae4c4ed86d5", "integrity": "73f451e1b68cd9be3938333b06290bfeab275361453559f40dfeab19dc4ad6d7",
"dependencies": [ "dependencies": [
"jsr:@std/cli@^1.0.21", "jsr:@std/cli@^1.0.21",
"jsr:@std/fmt@^1.0.6", "jsr:@std/fmt@^1.0.6",
@ -59,14 +59,14 @@
"@da/bcrypt@1.0.1": { "@da/bcrypt@1.0.1": {
"integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3" "integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3"
}, },
"@std/assert@1.0.17": { "@std/assert@1.0.15": {
"integrity": "df5ebfffe77c03b3fa1401e11c762cc8f603d51021c56c4d15a8c7ab45e90dbe", "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
"dependencies": [ "dependencies": [
"jsr:@std/internal" "jsr:@std/internal@^1.0.12"
] ]
}, },
"@std/cli@1.0.25": { "@std/cli@1.0.23": {
"integrity": "1f85051b370c97a7a9dfc6ba626e7ed57a91bea8c081597276d1e78d929d8c91" "integrity": "bf95b7a9425ba2af1ae5a6359daf58c508f2decf711a76ed2993cd352498ccca"
}, },
"@std/encoding@1.0.10": { "@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
@ -74,27 +74,27 @@
"@std/fmt@1.0.8": { "@std/fmt@1.0.8": {
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
}, },
"@std/fs@1.0.22": { "@std/fs@1.0.19": {
"integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308", "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
"dependencies": [ "dependencies": [
"jsr:@std/internal", "jsr:@std/internal@^1.0.9",
"jsr:@std/path@^1.1.4" "jsr:@std/path@^1.1.1"
] ]
}, },
"@std/html@1.0.5": { "@std/html@1.0.5": {
"integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e"
}, },
"@std/http@1.0.23": { "@std/http@1.0.21": {
"integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee", "integrity": "abb5c747651ee6e3ea6139858fd9b1810d2c97f53a5e6722f3b6d27a6d263edc",
"dependencies": [ "dependencies": [
"jsr:@std/cli@^1.0.25", "jsr:@std/cli@^1.0.23",
"jsr:@std/encoding", "jsr:@std/encoding",
"jsr:@std/fmt@^1.0.8", "jsr:@std/fmt@^1.0.8",
"jsr:@std/fs@^1.0.21", "jsr:@std/fs@^1.0.19",
"jsr:@std/html", "jsr:@std/html",
"jsr:@std/media-types", "jsr:@std/media-types",
"jsr:@std/net", "jsr:@std/net",
"jsr:@std/path@^1.1.4", "jsr:@std/path@^1.1.2",
"jsr:@std/streams" "jsr:@std/streams"
] ]
}, },
@ -107,14 +107,14 @@
"@std/net@1.0.6": { "@std/net@1.0.6": {
"integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c"
}, },
"@std/path@1.1.4": { "@std/path@1.1.2": {
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
"dependencies": [ "dependencies": [
"jsr:@std/internal" "jsr:@std/internal@^1.0.10"
] ]
}, },
"@std/streams@1.0.16": { "@std/streams@1.0.13": {
"integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4" "integrity": "772d208cd0d3e5dac7c1d9e6cdb25842846d136eea4a41a62e44ed4ab0c8dd9e"
} }
}, },
"npm": { "npm": {
@ -135,14 +135,14 @@
"dependencies": [ "dependencies": [
"jsr:@andyburke/fsdb@^1.2.4", "jsr:@andyburke/fsdb@^1.2.4",
"jsr:@andyburke/lurid@0.2", "jsr:@andyburke/lurid@0.2",
"jsr:@andyburke/serverus@0.16", "jsr:@andyburke/serverus@0.13",
"jsr:@da/bcrypt@^1.0.1", "jsr:@da/bcrypt@^1.0.1",
"jsr:@std/assert@^1.0.17", "jsr:@std/assert@^1.0.15",
"jsr:@std/encoding@^1.0.10", "jsr:@std/encoding@^1.0.10",
"jsr:@std/fs@^1.0.22", "jsr:@std/fs@^1.0.19",
"jsr:@std/http@^1.0.23", "jsr:@std/http@^1.0.21",
"jsr:@std/media-types@^1.1.0", "jsr:@std/media-types@^1.1.0",
"jsr:@std/path@^1.1.4" "jsr:@std/path@^1.1.2"
] ]
} }
} }

View file

View file

@ -6,11 +6,10 @@ import { SESSION, SESSIONS } from '../../../models/session.ts';
import { TOTP_ENTRIES } from '../../../models/totp_entry.ts'; import { TOTP_ENTRIES } from '../../../models/totp_entry.ts';
import { encodeBase64 } from '@std/encoding/base64'; import { encodeBase64 } from '@std/encoding/base64';
import parse_body from '../../../utils/bodyparser.ts'; import parse_body from '../../../utils/bodyparser.ts';
import { AUTHED_BEFORE_COOKIE_ID, get_session, get_user, PRECHECK_TABLE, require_user, SESSION_ID_TOKEN, SESSION_SECRET_TOKEN } from '../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user, SESSION_ID_TOKEN, SESSION_SECRET_TOKEN } from '../../../utils/prechecks.ts';
import * as bcrypt from '@da/bcrypt'; import * as bcrypt from '@da/bcrypt';
import { verifyTotp } from '../../../utils/totp.ts'; import { verifyTotp } from '../../../utils/totp.ts';
const AUTHED_BEFORE_EXPIRATION: number = 399 * (24 * (60 * (60 * 1_000))); // 399 days
const DEFAULT_SESSION_TIME: number = 365 * (24 * (60 * (60 * 1_000))); // 365 days const DEFAULT_SESSION_TIME: number = 365 * (24 * (60 * (60 * 1_000))); // 365 days
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
@ -207,7 +206,6 @@ export async function create_new_session(session_settings: SESSION_INFO): Promis
const headers = new Headers(); const headers = new Headers();
const expires_in_utc = new Date(session.timestamps.expires).toUTCString(); const expires_in_utc = new Date(session.timestamps.expires).toUTCString();
headers.append('Set-Cookie', `${AUTHED_BEFORE_COOKIE_ID}=1; Path=/; Secure; Expires=${new Date(new Date(now).valueOf() + AUTHED_BEFORE_EXPIRATION).toUTCString()}`);
headers.append('Set-Cookie', `${SESSION_ID_TOKEN}=${session.id}; Path=/; Secure; Expires=${expires_in_utc}`); headers.append('Set-Cookie', `${SESSION_ID_TOKEN}=${session.id}; Path=/; Secure; Expires=${expires_in_utc}`);
headers.append(`x-${SESSION_ID_TOKEN}`, session.id); headers.append(`x-${SESSION_ID_TOKEN}`, session.id);

View file

@ -1,5 +1,5 @@
import lurid from '@andyburke/lurid'; import lurid from '@andyburke/lurid';
import { get_session, get_user, PRECHECK_TABLE, require_user, user_has_create_permission_for_event } from '../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user, user_has_write_permission_for_event } from '../../../utils/prechecks.ts';
import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts';
import { EVENT, EVENTS, VALIDATE_EVENT } from '../../../models/event.ts'; import { EVENT, EVENTS, VALIDATE_EVENT } from '../../../models/event.ts';
import parse_body from '../../../utils/bodyparser.ts'; import parse_body from '../../../utils/bodyparser.ts';
@ -39,14 +39,7 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
const { const {
event_type, event_type,
event_id event_id
} = /^.*\/events\/.*\/(?<event_type>.*?)\:(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {}; } = /^.*\/events\/(?<event_type>.*?)\/.*\/(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {};
console.dir({
entry,
event_type,
event_id,
query: meta.query
});
if (meta.query.after_id && event_id <= meta.query.after_id) { if (meta.query.after_id && event_id <= meta.query.after_id) {
return false; return false;
@ -217,7 +210,7 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
}); });
} }
if (!user_has_create_permission_for_event(meta.user, event)) { if (!user_has_write_permission_for_event(meta.user, event)) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }

View file

@ -21,9 +21,7 @@
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--bg: hsl(from var(--base-color) h 20% 93%); --bg: hsl(from var(--base-color) h 20% 95%);
--bg-darker: hsl(from var(--base-color) h 20% 88%);
--bg-lighter: hsl(from var(--base-color) h 20% 98%);
--text: hsl(from var(--base-color) h 5% 5%); --text: hsl(from var(--base-color) h 5% 5%);
--accent: hsl(from var(--base-color) h calc(s + 10) calc(l + 20)); --accent: hsl(from var(--base-color) h calc(s + 10) calc(l + 20));
@ -171,15 +169,6 @@ body {
/* fixed height? */ /* fixed height? */
} }
#background-container {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: -1;
}
main { main {
position: relative; position: relative;
width: 100%; width: 100%;
@ -1693,32 +1682,6 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
left: 11px; left: 11px;
} }
.icon.map-pin {
box-sizing: border-box;
position: relative;
display: block;
transform: rotate(45deg) scale(var(--icon-scale, 1));
width: 18px;
height: 18px;
border-radius: 100% 100% 0 100%;
border: 2px solid;
margin-top: -4px;
}
.icon.map-pin::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
border: 2px solid;
top: 3px;
left: 3px;
border-radius: 40px;
}
/* AUDIO PLAYER ICONS */ /* AUDIO PLAYER ICONS */
.icon.skip-back { .icon.skip-back {
box-sizing: border-box; box-sizing: border-box;

View file

@ -1,6 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<h1>Hello World - foo</h1>
</body>
</html>

View file

@ -3,57 +3,54 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><!-- #include "./files/settings/title.txt" or "./title.txt" --></title> <title>autonomous.contact</title>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" ></link> <link rel="apple-touch-icon" sizes="180x180" href="./icons/apple-touch-icon.png" ></link>
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" ></link> <link rel="icon" type="image/png" sizes="32x32" href="./icons/favicon-32x32.png" ></link>
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" ></link> <link rel="icon" type="image/png" sizes="16x16" href="./icons/favicon-16x16.png" ></link>
<link rel="stylesheet" href="/base.css"></link> <link rel="stylesheet" href="./base.css"></link>
<link rel="stylesheet" href="/files/custom.css"></link>
<!-- inlining these to force them to be scoped for everything else --> <!-- inlining these to force them to be scoped for everything else -->
<script type="text/javascript"><!-- #include "./js/_utils.js" --></script> <script type="text/javascript"><!-- #include file="./js/_utils.js" --></script>
<script type="text/javascript"><!-- #include "./js/api.js" --></script> <script type="text/javascript"><!-- #include file="./js/api.js" --></script>
<script type="text/javascript"><!-- #include "./js/app.js" --></script> <script type="text/javascript"><!-- #include file="./js/app.js" --></script>
<!-- everything else --> <!-- everything else -->
<script src="/js/audioplayer.js" type="text/javascript"></script> <script src="./js/audioplayer.js" type="text/javascript"></script>
<script src="/js/datetimeutils.js" type="text/javascript"></script> <script src="./js/datetimeutils.js" type="text/javascript"></script>
<script src="/js/debounce.js" type="text/javascript"></script> <script src="./js/debounce.js" type="text/javascript"></script>
<script src="/js/external/md_to_html.js" type="text/javascript"></script> <script src="./js/external/md_to_html.js" type="text/javascript"></script>
<script src="/js/embeds/audio.js" type="text/javascript"></script> <script src="./js/embeds/audio.js" type="text/javascript"></script>
<script src="/js/embeds/gif.js" type="text/javascript"></script> <script src="./js/embeds/gif.js" type="text/javascript"></script>
<script src="/js/embeds/image.js" type="text/javascript"></script> <script src="./js/embeds/image.js" type="text/javascript"></script>
<script src="/js/embeds/link.js" type="text/javascript"></script> <script src="./js/embeds/link.js" type="text/javascript"></script>
<script src="/js/embeds/mkv.js" type="text/javascript"></script> <script src="./js/embeds/mkv.js" type="text/javascript"></script>
<script src="/js/embeds/mov.js" type="text/javascript"></script> <script src="./js/embeds/mov.js" type="text/javascript"></script>
<script src="/js/embeds/mp4.js" type="text/javascript"></script> <script src="./js/embeds/mp4.js" type="text/javascript"></script>
<script src="/js/embeds/spotify.js" type="text/javascript"></script> <script src="./js/embeds/spotify.js" type="text/javascript"></script>
<script src="/js/embeds/tidal.js" type="text/javascript"></script> <script src="./js/embeds/tidal.js" type="text/javascript"></script>
<script src="/js/embeds/vimeo.js" type="text/javascript"></script> <script src="./js/embeds/vimeo.js" type="text/javascript"></script>
<script src="/js/embeds/youtube.js" type="text/javascript"></script> <script src="./js/embeds/youtube.js" type="text/javascript"></script>
<script src="/js/emojis/en.js" type="text/javascript"></script> <script src="./js/emojis/en.js" type="text/javascript"></script>
<script src="/js/eventactions.js" type="text/javascript"></script> <script src="./js/eventactions.js" type="text/javascript"></script>
<script src="/js/htmlify.js" type="text/javascript"></script> <script src="./js/htmlify.js" type="text/javascript"></script>
<script src="/js/locationchange.js" type="text/javascript"></script> <script src="./js/locationchange.js" type="text/javascript"></script>
<script src="/js/notifications.js" type="text/javascript"></script> <script src="./js/notifications.js" type="text/javascript"></script>
<script src="/js/reactions.js" type="text/javascript"></script> <script src="./js/reactions.js" type="text/javascript"></script>
<script src="/js/smartfeeds.js" type="text/javascript"></script> <script src="./js/smartfeeds.js" type="text/javascript"></script>
<script src="/js/smartforms.js" type="text/javascript"></script> <script src="./js/smartforms.js" type="text/javascript"></script>
<script src="/js/textareaenhancements.js" type="text/javascript"></script> <script src="./js/textareaenhancements.js" type="text/javascript"></script>
<script src="/js/totp.js" type="text/javascript"></script> <script src="./js/totp.js" type="text/javascript"></script>
</head> </head>
<body> <body>
<div id="background-container"></div> <!-- #include file="./signup_login_wall.html" -->
<!-- #include "./signup_login_wall.html" -->
<main> <main>
<!-- #include "./sidebar/sidebar.html" --> <!-- #include file="./sidebar/sidebar.html" -->
<!-- #include "./tabs/tabs.html" --> <!-- #include file="./tabs/tabs.html" -->
</main> </main>
</body> </body>
</html> </html>

View file

@ -5,13 +5,10 @@ const api = {
...__options, ...__options,
}; };
// FIXME: this will break with different server settings
// TODO: we need the cookie names here to match any configured on the server
const session_id = (document.cookie.match( const session_id = (document.cookie.match(
/^(?:.*;)?\s*session_id\s*=\s*([^;]+)(?:.*)?$/, /^(?:.*;)?\s*session_id\s*=\s*([^;]+)(?:.*)?$/,
) || [, null])[1]; ) || [, null])[1];
// FIXME: this will break with different server settings
// TODO: this wasn't really intended to be persisted in a cookie // TODO: this wasn't really intended to be persisted in a cookie
const session_secret = (document.cookie.match( const session_secret = (document.cookie.match(
/^(?:.*;)?\s*session_secret\s*=\s*([^;]+)(?:.*)?$/, /^(?:.*;)?\s*session_secret\s*=\s*([^;]+)(?:.*)?$/,

View file

@ -11,11 +11,6 @@ const APP = {
on: function( event_name, callback ) { on: function( event_name, callback ) {
this._event_callbacks[ event_name ] = this._event_callbacks[ event_name ] ?? new Set(); this._event_callbacks[ event_name ] = this._event_callbacks[ event_name ] ?? new Set();
this._event_callbacks[event_name ].add( callback ); this._event_callbacks[event_name ].add( callback );
if ( event_name === 'load' && this._loaded ) {
setTimeout( callback, 0 );
}
return true; return true;
}, },
@ -73,13 +68,13 @@ const APP = {
this._emit( 'view_changed', { this._emit( 'view_changed', {
previous, previous,
view, view
channel_id
}); });
} }
if (!document.body.dataset.channel || document.body.dataset.channel !== channel_id) { if (!document.body.dataset.channel || document.body.dataset.channel !== channel_id) {
const previous = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined; const previous = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined;
if ( channel_id ) { if ( channel_id ) {
document.body.dataset.channel = channel_id; document.body.dataset.channel = channel_id;
} }
@ -87,15 +82,25 @@ const APP = {
delete document.body.dataset.channel; delete document.body.dataset.channel;
} }
this._emit( 'channel_changed', { const target_channel_id = channel_id ?? this.CHANNELS.CHANNEL_LIST[0]?.id;
previous,
channel_id // TODO: allow a different default than chat
}); const hash_target = `/${ view ? view : 'chat' }` + ( target_channel_id ? `/channel/${ target_channel_id }` : '' );
if ( window.location.hash?.slice( 1 ) !== hash_target ) {
if ( previous !== target_channel_id ) {
this._emit( 'channel_changed', {
previous,
channel_id: target_channel_id
});
}
window.location.hash = hash_target;
}
} }
}, },
load: async function() { load: async function() {
this._loaded = false;
this.server = { this.server = {
name: document.title, name: document.title,
url: window.location.origin ?? window.location.href, url: window.location.origin ?? window.location.href,
@ -132,7 +137,6 @@ const APP = {
this.check_if_logged_in(); this.check_if_logged_in();
this.extract_url_hash_info(); this.extract_url_hash_info();
this._emit( 'load', this ); this._emit( 'load', this );
this._loaded = true;
}, },
update_user: async function( user ) { update_user: async function( user ) {
@ -232,11 +236,22 @@ const APP = {
const channels_response = await api.fetch("/api/channels"); const channels_response = await api.fetch("/api/channels");
if (channels_response.ok) { if (channels_response.ok) {
const new_channels = await channels_response.json(); const new_channels = await channels_response.json();
APP.CHANNELS.CHANNEL_LIST = [...new_channels]; const has_differences =
APP.CHANNELS.CHANNEL_LIST.length !== new_channels.length ||
new_channels.some((channel, index) => {
return (
APP.CHANNELS.CHANNEL_LIST[index]?.id !== channel.id ||
APP.CHANNELS.CHANNEL_LIST[index]?.name !== channel.name
);
});
APP._emit( 'channels_updated', { if (has_differences) {
channels: APP.CHANNELS.CHANNEL_LIST APP.CHANNELS.CHANNEL_LIST = [...new_channels];
});
APP._emit( 'channels_updated', {
channels: APP.CHANNELS.CHANNEL_LIST
});
}
APP.CHANNELS._last_channel_update = now; APP.CHANNELS._last_channel_update = now;
} }
@ -256,8 +271,3 @@ const APP = {
}; };
document.addEventListener("DOMContentLoaded", APP.load.bind( APP )); document.addEventListener("DOMContentLoaded", APP.load.bind( APP ));
APP.on( 'view_changed', ( { view } ) => {
if ( !view ) {
window.location.hash = '/chat';
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,661 +0,0 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,28 +0,0 @@
/*
jquery-qrcode v0.14.0 - https://larsjung.de/jquery-qrcode/ */
'use strict';let G=null;class H{}H.render=function(w,B){G(w,B)};self.QrCreator=H;
(function(w){function B(t,c,a,e){var b={},h=w(a,c);h.u(t);h.J();e=e||0;var r=h.h(),d=h.h()+2*e;b.text=t;b.level=c;b.version=a;b.O=d;b.a=function(b,a){b-=e;a-=e;return 0>b||b>=r||0>a||a>=r?!1:h.a(b,a)};return b}function C(t,c,a,e,b,h,r,d,g,x){function u(b,a,f,c,d,r,g){b?(t.lineTo(a+r,f+g),t.arcTo(a,f,c,d,h)):t.lineTo(a,f)}r?t.moveTo(c+h,a):t.moveTo(c,a);u(d,e,a,e,b,-h,0);u(g,e,b,c,b,0,-h);u(x,c,b,c,a,h,0);u(r,c,a,e,a,0,h)}function z(t,c,a,e,b,h,r,d,g,x){function u(b,a,c,d){t.moveTo(b+c,a);t.lineTo(b,
a);t.lineTo(b,a+d);t.arcTo(b,a,b+c,a,h)}r&&u(c,a,h,h);d&&u(e,a,-h,h);g&&u(e,b,-h,-h);x&&u(c,b,h,-h)}function A(t,c){var a=c.fill;if("string"===typeof a)t.fillStyle=a;else{var e=a.type,b=a.colorStops;a=a.position.map((b)=>Math.round(b*c.size));if("linear-gradient"===e)var h=t.createLinearGradient.apply(t,a);else if("radial-gradient"===e)h=t.createRadialGradient.apply(t,a);else throw Error("Unsupported fill");b.forEach(([b,a])=>{h.addColorStop(b,a)});t.fillStyle=h}}function y(t,c){a:{var a=c.text,e=
c.v,b=c.N,h=c.K,r=c.P;b=Math.max(1,b||1);for(h=Math.min(40,h||40);b<=h;b+=1)try{var d=B(a,e,b,r);break a}catch(J){}d=void 0}if(!d)return null;a=t.getContext("2d");c.background&&(a.fillStyle=c.background,a.fillRect(c.left,c.top,c.size,c.size));e=d.O;h=c.size/e;a.beginPath();for(r=0;r<e;r+=1)for(b=0;b<e;b+=1){var g=a,x=c.left+b*h,u=c.top+r*h,p=r,q=b,f=d.a,k=x+h,m=u+h,D=p-1,E=p+1,n=q-1,l=q+1,y=Math.floor(Math.min(.5,Math.max(0,c.R))*h),v=f(p,q),I=f(D,n),w=f(D,q);D=f(D,l);var F=f(p,l);l=f(E,l);q=f(E,
q);E=f(E,n);p=f(p,n);x=Math.round(x);u=Math.round(u);k=Math.round(k);m=Math.round(m);v?C(g,x,u,k,m,y,!w&&!p,!w&&!F,!q&&!F,!q&&!p):z(g,x,u,k,m,y,w&&p&&I,w&&F&&D,q&&F&&l,q&&p&&E)}A(a,c);a.fill();return t}var v={minVersion:1,maxVersion:40,ecLevel:"L",left:0,top:0,size:200,fill:"#000",background:null,text:"no text",radius:.5,quiet:0};G=function(t,c){var a={};Object.assign(a,v,t);a.N=a.minVersion;a.K=a.maxVersion;a.v=a.ecLevel;a.left=a.left;a.top=a.top;a.size=a.size;a.fill=a.fill;a.background=a.background;
a.text=a.text;a.R=a.radius;a.P=a.quiet;if(c instanceof HTMLCanvasElement){if(c.width!==a.size||c.height!==a.size)c.width=a.size,c.height=a.size;c.getContext("2d").clearRect(0,0,c.width,c.height);y(c,a)}else t=document.createElement("canvas"),t.width=a.size,t.height=a.size,a=y(t,a),c.appendChild(a)}})(function(){function w(c){var a=C.s(c);return{S:function(){return 4},b:function(){return a.length},write:function(c){for(var b=0;b<a.length;b+=1)c.put(a[b],8)}}}function B(){var c=[],a=0,e={B:function(){return c},
c:function(b){return 1==(c[Math.floor(b/8)]>>>7-b%8&1)},put:function(b,h){for(var a=0;a<h;a+=1)e.m(1==(b>>>h-a-1&1))},f:function(){return a},m:function(b){var h=Math.floor(a/8);c.length<=h&&c.push(0);b&&(c[h]|=128>>>a%8);a+=1}};return e}function C(c,a){function e(b,h){for(var a=-1;7>=a;a+=1)if(!(-1>=b+a||d<=b+a))for(var c=-1;7>=c;c+=1)-1>=h+c||d<=h+c||(r[b+a][h+c]=0<=a&&6>=a&&(0==c||6==c)||0<=c&&6>=c&&(0==a||6==a)||2<=a&&4>=a&&2<=c&&4>=c?!0:!1)}function b(b,a){for(var f=d=4*c+17,k=Array(f),m=0;m<
f;m+=1){k[m]=Array(f);for(var p=0;p<f;p+=1)k[m][p]=null}r=k;e(0,0);e(d-7,0);e(0,d-7);f=y.G(c);for(k=0;k<f.length;k+=1)for(m=0;m<f.length;m+=1){p=f[k];var q=f[m];if(null==r[p][q])for(var n=-2;2>=n;n+=1)for(var l=-2;2>=l;l+=1)r[p+n][q+l]=-2==n||2==n||-2==l||2==l||0==n&&0==l}for(f=8;f<d-8;f+=1)null==r[f][6]&&(r[f][6]=0==f%2);for(f=8;f<d-8;f+=1)null==r[6][f]&&(r[6][f]=0==f%2);f=y.w(h<<3|a);for(k=0;15>k;k+=1)m=!b&&1==(f>>k&1),r[6>k?k:8>k?k+1:d-15+k][8]=m,r[8][8>k?d-k-1:9>k?15-k:14-k]=m;r[d-8][8]=!b;if(7<=
c){f=y.A(c);for(k=0;18>k;k+=1)m=!b&&1==(f>>k&1),r[Math.floor(k/3)][k%3+d-8-3]=m;for(k=0;18>k;k+=1)m=!b&&1==(f>>k&1),r[k%3+d-8-3][Math.floor(k/3)]=m}if(null==g){b=t.I(c,h);f=B();for(k=0;k<x.length;k+=1)m=x[k],f.put(4,4),f.put(m.b(),y.f(4,c)),m.write(f);for(k=m=0;k<b.length;k+=1)m+=b[k].j;if(f.f()>8*m)throw Error("code length overflow. ("+f.f()+">"+8*m+")");for(f.f()+4<=8*m&&f.put(0,4);0!=f.f()%8;)f.m(!1);for(;!(f.f()>=8*m);){f.put(236,8);if(f.f()>=8*m)break;f.put(17,8)}var u=0;m=k=0;p=Array(b.length);
q=Array(b.length);for(n=0;n<b.length;n+=1){var v=b[n].j,w=b[n].o-v;k=Math.max(k,v);m=Math.max(m,w);p[n]=Array(v);for(l=0;l<p[n].length;l+=1)p[n][l]=255&f.B()[l+u];u+=v;l=y.C(w);v=z(p[n],l.b()-1).l(l);q[n]=Array(l.b()-1);for(l=0;l<q[n].length;l+=1)w=l+v.b()-q[n].length,q[n][l]=0<=w?v.c(w):0}for(l=f=0;l<b.length;l+=1)f+=b[l].o;f=Array(f);for(l=u=0;l<k;l+=1)for(n=0;n<b.length;n+=1)l<p[n].length&&(f[u]=p[n][l],u+=1);for(l=0;l<m;l+=1)for(n=0;n<b.length;n+=1)l<q[n].length&&(f[u]=q[n][l],u+=1);g=f}b=g;f=
-1;k=d-1;m=7;p=0;a=y.F(a);for(q=d-1;0<q;q-=2)for(6==q&&--q;;){for(n=0;2>n;n+=1)null==r[k][q-n]&&(l=!1,p<b.length&&(l=1==(b[p]>>>m&1)),a(k,q-n)&&(l=!l),r[k][q-n]=l,--m,-1==m&&(p+=1,m=7));k+=f;if(0>k||d<=k){k-=f;f=-f;break}}}var h=A[a],r=null,d=0,g=null,x=[],u={u:function(b){b=w(b);x.push(b);g=null},a:function(b,a){if(0>b||d<=b||0>a||d<=a)throw Error(b+","+a);return r[b][a]},h:function(){return d},J:function(){for(var a=0,h=0,c=0;8>c;c+=1){b(!0,c);var d=y.D(u);if(0==c||a>d)a=d,h=c}b(!1,h)}};return u}
function z(c,a){if("undefined"==typeof c.length)throw Error(c.length+"/"+a);var e=function(){for(var b=0;b<c.length&&0==c[b];)b+=1;for(var r=Array(c.length-b+a),d=0;d<c.length-b;d+=1)r[d]=c[d+b];return r}(),b={c:function(b){return e[b]},b:function(){return e.length},multiply:function(a){for(var h=Array(b.b()+a.b()-1),c=0;c<b.b();c+=1)for(var g=0;g<a.b();g+=1)h[c+g]^=v.i(v.g(b.c(c))+v.g(a.c(g)));return z(h,0)},l:function(a){if(0>b.b()-a.b())return b;for(var c=v.g(b.c(0))-v.g(a.c(0)),h=Array(b.b()),
g=0;g<b.b();g+=1)h[g]=b.c(g);for(g=0;g<a.b();g+=1)h[g]^=v.i(v.g(a.c(g))+c);return z(h,0).l(a)}};return b}C.s=function(c){for(var a=[],e=0;e<c.length;e++){var b=c.charCodeAt(e);128>b?a.push(b):2048>b?a.push(192|b>>6,128|b&63):55296>b||57344<=b?a.push(224|b>>12,128|b>>6&63,128|b&63):(e++,b=65536+((b&1023)<<10|c.charCodeAt(e)&1023),a.push(240|b>>18,128|b>>12&63,128|b>>6&63,128|b&63))}return a};var A={L:1,M:0,Q:3,H:2},y=function(){function c(b){for(var a=0;0!=b;)a+=1,b>>>=1;return a}var a=[[],[6,18],
[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],
[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],e={w:function(b){for(var a=b<<10;0<=c(a)-c(1335);)a^=1335<<c(a)-c(1335);return(b<<10|a)^21522},A:function(b){for(var a=b<<12;0<=c(a)-c(7973);)a^=7973<<c(a)-c(7973);return b<<12|a},G:function(b){return a[b-1]},F:function(b){switch(b){case 0:return function(b,a){return 0==(b+a)%2};case 1:return function(b){return 0==b%2};case 2:return function(b,a){return 0==a%3};case 3:return function(b,a){return 0==
(b+a)%3};case 4:return function(b,a){return 0==(Math.floor(b/2)+Math.floor(a/3))%2};case 5:return function(b,a){return 0==b*a%2+b*a%3};case 6:return function(b,a){return 0==(b*a%2+b*a%3)%2};case 7:return function(b,a){return 0==(b*a%3+(b+a)%2)%2};default:throw Error("bad maskPattern:"+b);}},C:function(b){for(var a=z([1],0),c=0;c<b;c+=1)a=a.multiply(z([1,v.i(c)],0));return a},f:function(b,a){if(4!=b||1>a||40<a)throw Error("mode: "+b+"; type: "+a);return 10>a?8:16},D:function(b){for(var a=b.h(),c=0,
d=0;d<a;d+=1)for(var g=0;g<a;g+=1){for(var e=0,t=b.a(d,g),p=-1;1>=p;p+=1)if(!(0>d+p||a<=d+p))for(var q=-1;1>=q;q+=1)0>g+q||a<=g+q||(0!=p||0!=q)&&t==b.a(d+p,g+q)&&(e+=1);5<e&&(c+=3+e-5)}for(d=0;d<a-1;d+=1)for(g=0;g<a-1;g+=1)if(e=0,b.a(d,g)&&(e+=1),b.a(d+1,g)&&(e+=1),b.a(d,g+1)&&(e+=1),b.a(d+1,g+1)&&(e+=1),0==e||4==e)c+=3;for(d=0;d<a;d+=1)for(g=0;g<a-6;g+=1)b.a(d,g)&&!b.a(d,g+1)&&b.a(d,g+2)&&b.a(d,g+3)&&b.a(d,g+4)&&!b.a(d,g+5)&&b.a(d,g+6)&&(c+=40);for(g=0;g<a;g+=1)for(d=0;d<a-6;d+=1)b.a(d,g)&&!b.a(d+
1,g)&&b.a(d+2,g)&&b.a(d+3,g)&&b.a(d+4,g)&&!b.a(d+5,g)&&b.a(d+6,g)&&(c+=40);for(g=e=0;g<a;g+=1)for(d=0;d<a;d+=1)b.a(d,g)&&(e+=1);return c+=Math.abs(100*e/a/a-50)/5*10}};return e}(),v=function(){for(var c=Array(256),a=Array(256),e=0;8>e;e+=1)c[e]=1<<e;for(e=8;256>e;e+=1)c[e]=c[e-4]^c[e-5]^c[e-6]^c[e-8];for(e=0;255>e;e+=1)a[c[e]]=e;return{g:function(b){if(1>b)throw Error("glog("+b+")");return a[b]},i:function(b){for(;0>b;)b+=255;for(;256<=b;)b-=255;return c[b]}}}(),t=function(){function c(b,c){switch(c){case A.L:return a[4*
(b-1)];case A.M:return a[4*(b-1)+1];case A.Q:return a[4*(b-1)+2];case A.H:return a[4*(b-1)+3]}}var a=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,
2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,
45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,
151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],
[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],
[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],e={I:function(b,a){var e=c(b,a);if("undefined"==
typeof e)throw Error("bad rs block @ typeNumber:"+b+"/errorCorrectLevel:"+a);b=e.length/3;a=[];for(var d=0;d<b;d+=1)for(var g=e[3*d],h=e[3*d+1],t=e[3*d+2],p=0;p<g;p+=1){var q=t,f={};f.o=h;f.j=q;a.push(f)}return a}};return e}();return C}());
//# sourceMappingURL=/js/external/qr-creator.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,25 +1,14 @@
const NOTIFICATIONS = { const NOTIFICATIONS = {
_notifications: [],
send: function (message, options = {}) { send: function (message, options = {}) {
if (!Notification || Notification.permission !== "granted") { if (!Notification || Notification.permission !== "granted") {
return; return;
} }
const notification = new Notification(message, options); const notification = new Notification(message, options);
this._notifications.push( notification );
return notification; return notification;
}, },
close: function() { request_permissions: function () {
for ( const notification of this._notifications ) {
notification?.close();
}
this._notifications.length = 0;
},
request_permission: function () {
if (Notification && Notification.permission === "granted") { if (Notification && Notification.permission === "granted") {
return; return;
} }

View file

@ -1,4 +1,3 @@
<script src="/js/external/qr-creator.min.js"></script>
<script> <script>
function clear_invite_popup() { function clear_invite_popup() {
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove()); document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
@ -20,9 +19,8 @@
</form>`; </form>`;
document.body.appendChild(invite_div); document.body.appendChild(invite_div);
const document_width = document.body.getBoundingClientRect().width; invite_div.style.left = button.getBoundingClientRect().left + "px";
invite_div.style.left = document_width > 800 ? button.getBoundingClientRect().left + "px" : '1rem'; invite_div.style.top = button.getBoundingClientRect().top + "px";
invite_div.style.top = document_width > 800 ? button.getBoundingClientRect().top + "px" : '1rem';
} }
async function create_invite(click_event) { async function create_invite(click_event) {
@ -57,8 +55,6 @@
} }
const invite_code = await invite_code_response.json(); const invite_code = await invite_code_response.json();
const invite_url = `${window.location.protocol}//${window.location.host}/?invite_code=${ encodeURIComponent(invite_code.code) }`;
const qr_div_id = 'qr-div-' + Math.random().toString(36).substring(2, 8);
invite_popover.innerHTML = ` invite_popover.innerHTML = `
<div> <div>
@ -70,24 +66,11 @@
</div> </div>
<div class="share-option"> <div class="share-option">
<span class="name">Link</span> <span class="name">Link</span>
<input readonly type="text" name="code" value="${ invite_url }" /> <input readonly type="text" name="code" value="${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}" />
<button onclick="navigator.clipboard.writeText('${ invite_url }');" />Copy</button> <button onclick="navigator.clipboard.writeText('${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}');" />Copy</button>
</div>
<div class="share-option">
<span class="name">Scan</span>
<div id="${ qr_div_id }" class="qr-invite-container"></div>
</div> </div>
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button> <button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
</div>`; </div>`;
QrCreator.render({
text: invite_url,
radius: 0.5, // 0.0 to 0.5
ecLevel: 'H', // L, M, Q, H
fill: '#999999', // foreground color
background: null, // color or null for transparent
size: 256 // in pixels
}, document.querySelector(`#${ qr_div_id }`));
} }
</script> </script>
@ -104,11 +87,6 @@
border-right: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle);
} }
#sidebar .profile-container {
width: 100%;
max-width: 100%;
}
#sidebar #sidebar-context-menu { #sidebar #sidebar-context-menu {
display: none; display: none;
visibility: hidden; visibility: hidden;
@ -255,7 +233,7 @@
.invitepopover .share-option input { .invitepopover .share-option input {
padding: 0.75rem; padding: 0.75rem;
margin: 0 0.75rem 1rem 0; margin: 0 1rem 1rem 0;
background: none; background: none;
color: var(--text); color: var(--text);
border: 1px solid var(--border-highlight); border: 1px solid var(--border-highlight);
@ -274,12 +252,6 @@
margin-left: -0.75rem; margin-left: -0.75rem;
} }
.invitepopover .qr-invite-container {
width: 256px;
height: 256px;
margin: 1rem auto;
}
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.invitepopover { .invitepopover {
margin: 0; margin: 0;
@ -292,8 +264,9 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.invitepopover .share-option input { .invitepopover .share-option button {
max-width: 14rem; display: block;
margin: 0 auto 1rem;
} }
} }
</style> </style>
@ -620,7 +593,7 @@
</summary> </summary>
<div class="notifications-settings-container"> <div class="notifications-settings-container">
<button onclick="NOTIFICATIONS.request_permission()"> <button class="mockup" onclick="NOTIFICATIONS.request_permission()">
Enable Notifications Enable Notifications
</button> </button>
</div> </div>

View file

@ -5,7 +5,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: fixed; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
@ -14,53 +14,15 @@
background: var(--bg); background: var(--bg);
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transition: all 0.33s;
} }
#auth-container { #login-tab .tab-content {
width: 95%; min-height: 17rem;
max-height: calc(min(90vh,900px));
border-radius: 10px;
position: relative;
background: var(--bg-lighter);
max-width: 40em;
margin: 0 auto;
border-radius: calc( var(--border-radius) * 2.5);
padding: 2em 1em;
overflow-y: scroll;
transition: all 0.33s ease;
animation: zoomsettle 0.4s ease;
} }
#signup-tab, #signup-tab .tab-content {
#login-tab { min-height: 21rem;
padding: 0.5em;
height: auto;
}
@media screen and (max-width: 480px) {
#auth-container {
padding: 0.5em;
}
#auth-container h1 {
font-size: x-large;
}
#auth-container h3 {
font-size: medium;
}
}
@keyframes zoomsettle {
from {
opacity: 0;
transform: scale(1.2);
}
to {
opacity: 1;
transform: scale(1.0);
}
} }
body[data-user] #signup-login-wall { body[data-user] #signup-login-wall {
@ -69,116 +31,117 @@
display: none; display: none;
} }
#signup-login-wall .limiter {
width: 95%;
min-height: 24rem;
position: relative;
background: hsl(from var(--bg) h s calc(l/1.1));
max-width: 40em;
margin: 0 auto;
border-radius: var(--border-radius);
overflow: hidden;
}
#signup-login-wall form { #signup-login-wall form {
width: 100%; width: 100%;
padding: 0.5rem 1.5rem 0; padding: 1.5rem 1.5rem 0 1.5rem;
} }
</style> </style>
<div id="auth-container" class="limiter"> <!-- #include file="./signup_pitch.md" -->
<!-- #include "./files/settings/signup_pitch.html" or "./files/settings/signup_pitch.md" or "./signup_pitch.default.md" -->
<div class="limiter">
<div class="tabs"> <div class="tabs">
<input <div id="login-tab" class="tab">
type="radio" <input
name="signup-login-tabs" type="radio"
id="signup-tab-input" name="signup-login-tabs"
class="tabswitch" id="login-tab-input"
checked="checked" class="tab-switch"
/> checked="checked"
<label for="signup-tab-input" class="tablabel"> />
<div class="label">Sign Up</div> <label for="login-tab-input" class="tab-label">
</label> <div class="label">Log In</div>
<div id="signup-tab" class="panel"> </label>
<form data-smart="true" data-method="POST" id="signup-form" action="/api/users"> <div class="tab-content">
<script> <form data-smart="true" data-method="POST" id="login-form" action="/api/auth">
{
const form = document.currentScript.closest("form");
form.on_reply = (response) => {
const user = response.user;
APP.login( user );
};
}
</script>
<div>
<input id="signup-username" type="text" name="username" required />
<label class="placeholder" for="signup-username">username</label>
</div>
<div>
<input id="signup-password" type="password" name="password" required />
<label class="placeholder" for="signup-password">password</label>
</div>
<div>
<script> <script>
APP.on( 'load', () => { {
const query = new URL(document.location.toString()) const form = document.currentScript.closest("form");
.searchParams; form.on_reply = (response) => {
const invite_code = query.get("invite_code"); const user = response.user;
if (typeof invite_code === "string" && invite_code.length) { APP.login( user );
document.getElementById("signup-invite-code").value = };
decodeURIComponent(invite_code); }
document.getElementById("signup-tab-input").checked = true;
}
});
</script> </script>
<input <div>
id="signup-invite-code" <input id="login-username" type="text" name="username" required />
type="text" <label class="placeholder" for="login-username">username</label>
name="invite_code" </div>
required <div>
/> <input id="login-password" type="password" name="password" required />
<label class="placeholder" for="signup-invite-code">invite code</label> <label class="placeholder" for="login-password">password</label>
</div> </div>
<button id="signup-submit" type="submit" class="primary">Sign Up</button> <div>
</form> <button id="login-submit" type="submit" class="primary">Log In</button>
</div>
</form>
</div>
</div> </div>
<div id="signup-tab" class="tab">
<input
type="radio"
name="signup-login-tabs"
id="signup-tab-input"
class="tab-switch"
/>
<label for="signup-tab-input" class="tab-label">
<div class="label">Sign Up</div>
</label>
<div class="tab-content">
<form data-smart="true" data-method="POST" id="signup-form" action="/api/users">
<script>
{
const form = document.currentScript.closest("form");
form.on_reply = (response) => {
const user = response.user;
APP.login( user );
};
}
</script>
<div>
<input id="signup-username" type="text" name="username" required />
<label class="placeholder" for="signup-username">username</label>
</div>
<div>
<input id="signup-password" type="password" name="password" required />
<label class="placeholder" for="signup-password">password</label>
</div>
<div>
<script>
APP.on( 'load', () => {
const query = new URL(document.location.toString())
.searchParams;
const invite_code = query.get("invite_code");
if (typeof invite_code === "string" && invite_code.length) {
document.getElementById("signup-invite-code").value =
decodeURIComponent(invite_code);
<input document.getElementById("signup-tab-input").checked = true;
type="radio" }
name="signup-login-tabs" });
id="login-tab-input" </script>
class="tabswitch" <input
/> id="signup-invite-code"
<label for="login-tab-input" class="tablabel"> type="text"
<div class="label">Log In</div> name="invite_code"
</label> />
<div id="login-tab" class="panel"> <label class="placeholder" for="signup-invite-code">invite code</label>
<form data-smart="true" data-method="POST" id="login-form" action="/api/auth"> </div>
<script> <button id="signup-submit" type="submit" class="primary">Sign Up</button>
{ </form>
const form = document.currentScript.closest("form"); </div>
form.on_reply = (response) => {
const user = response.user;
APP.login( user );
};
}
</script>
<div>
<input id="login-username" type="text" name="username" required />
<label class="placeholder" for="login-username">username</label>
</div>
<div>
<input id="login-password" type="password" name="password" required />
<label class="placeholder" for="login-password">password</label>
</div>
<div>
<button id="login-submit" type="submit" class="primary">Log In</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
<script>
document.addEventListener( 'DOMContentLoaded', () => {
const authed_before = (document.cookie.match(
/^(?:.*;)?\s*authed_before\s*=\s*([^;]+)(?:.*)?$/,
) || [, null])[1];
if ( authed_before ) {
document.getElementById("login-tab-input").checked = true;
}
});
</script>
</div> </div>

View file

@ -1,6 +1,6 @@
# Welcome! # verifiedhuman.network
## You're here because someone else thought you needed an invite. ## You're here because someone else said you were a cool human.
### Use your invite code to gain access. ### Use your invite code to gain access.

View file

@ -127,120 +127,122 @@
} }
</style> </style>
<input <div id="blurbs" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="blurb-tab-input" name="top-level-tabs"
class="tabswitch" id="blurb-tab-input"
data-view="blurbs" class="tab-switch"
/> data-view="blurbs"
<label for="blurb-tab-input" class="tablabel" />
><div class="icon blurb"></div> <label for="blurb-tab-input" class="tab-label"
<div class="label">Blurbs</div> ><div class="icon blurb"></div>
</label> <div class="label">Blurbs</div>
<div class="panel"> </label>
<div id="blurbs-container" class="container"> <div class="tab-content">
<!-- #include "./README.md" --> <div id="blurbs-container" class="container">
<!-- #include file="./README.md" -->
<!-- #include "./new_blurb.html" --> <!-- #include file="./new_blurb.html" -->
<div <div
id="blurbs-list" id="blurbs-list"
data-feed data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.blurb' ) !== -1" data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.blurb' ) !== -1"
data-source="/api/events?type=blurb,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }" data-source="/api/events?type=blurb,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true" data-longpolling="true"
data-reverse="true" data-reverse="true"
data-insert="prepend" data-insert="prepend"
data-autoscroll="true" data-autoscroll="true"
> >
<script> <script>
{ {
const feed = document.currentScript.closest("[data-feed]"); const feed = document.currentScript.closest("[data-feed]");
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); }); APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
feed.__target_element = (item) => { feed.__target_element = (item) => {
return ( return (
document.querySelector( document.querySelector(
`.blurb-container[data-blurb_id='${item.parent_id}'] > .replies-container`, `.blurb-container[data-blurb_id='${item.parent_id}'] > .replies-container`,
) ?? feed ) ?? feed
); );
};
feed.__target_element = (item) => {
let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.blurb-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "blurb":
default:
target =
document.querySelector(
`.blurb-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
return target;
};
feed.__context = async (item) => {
const blurb_datetime = datetime_to_local(item.timestamps.created);
return {
event: item,
blurb: item,
creator: await APP.USERS.get(item.creator_id),
blurb_datetime
}; };
}; feed.__target_element = (item) => {
} let target = feed;
</script> switch (item.type) {
<template data-for_type="blurb"> case "reaction":
<div target = document.querySelector(
id="${ context.blurb.id }" `.blurb-container[data-event_id='${item.parent_id}'] > .reactions-container`,
class="blurb-container" );
data-event_id="${context.blurb.id}" break;
data-creator_id="${context.creator.id}" case "blurb":
data-blurb_id="${context.blurb.id}" default:
data-temp_id="${context.blurb.meta?.temp_id ?? ""}"> target =
<div class="media-preview-container"> document.querySelector(
${context.blurb.data?.media?.length ? context.blurb.data.media.map(function(url) { return url ? `<img src='${url}' loading="lazy"/>` : ''; }).join('\n') : ''} `.blurb-container[data-event_id='${item.parent_id}'] > .replies-container`,
</div> ) ?? feed;
break;
}
<div class="info-container"> return target;
<div class="avatar-container inline"> };
<img src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}" alt="user avatar" loading="lazy" />
feed.__context = async (item) => {
const blurb_datetime = datetime_to_local(item.timestamps.created);
return {
event: item,
blurb: item,
creator: await APP.USERS.get(item.creator_id),
blurb_datetime
};
};
}
</script>
<template data-for_type="blurb">
<div
id="${ context.blurb.id }"
class="blurb-container"
data-event_id="${context.blurb.id}"
data-creator_id="${context.creator.id}"
data-blurb_id="${context.blurb.id}"
data-temp_id="${context.blurb.meta?.temp_id ?? ""}">
<div class="media-preview-container">
${context.blurb.data?.media?.length ? context.blurb.data.media.map(function(url) { return url ? `<img src='${url}' loading="lazy"/>` : ''; }).join('\n') : ''}
</div> </div>
<div class="username-container">
<span class="username">${context.creator.username}</span> <div class="info-container">
</div> <div class="avatar-container inline">
<div class="datetime-container"> <img src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}" alt="user avatar" loading="lazy" />
<span class="long">${context.blurb_datetime.long}</span> </div>
<span class="short">${context.blurb_datetime.short}</span> <div class="username-container">
<span class="username">${context.creator.username}</span>
</div>
<div class="datetime-container">
<span class="long">${context.blurb_datetime.long}</span>
<span class="short">${context.blurb_datetime.short}</span>
</div>
</div> </div>
<div class="content-container">${htmlify(md_to_html(context.blurb.data.blurb))}</div>
<div class="reactions-container"></div>
<button class="icon more" commandfor="eventactionspopover"></button>
<!-- #include file="./new_blurb.html" -->
<div class="replies-container"></div>
</div> </div>
<div class="content-container">${htmlify(md_to_html(context.blurb.data.blurb))}</div> </template>
<div class="reactions-container"></div> <template data-for_type="reaction">
<button class="icon more" commandfor="eventactionspopover"></button> <div
<!-- #include "./new_blurb.html" --> id="${context.event.id}"
<div class="replies-container"></div> class="reaction-container"
</div> data-event_id="${context.event.id}"
</template> data-creator_id="${context.creator.id}"
<template data-for_type="reaction"> data-temp_id="${context.event.meta?.temp_id ?? ''}"
<div >
id="${context.event.id}" <span class="reaction">${ context.event.data.reaction }</span>
class="reaction-container" </div>
data-event_id="${context.event.id}" </template>
data-creator_id="${context.creator.id}" </div>
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,14 +1,16 @@
<input <div id="calendar" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="calendar-tab-input" name="top-level-tabs"
class="tabswitch" id="calendar-tab-input"
data-view="calendar" class="tab-switch"
/> data-view="calendar"
<label for="calendar-tab-input" class="tablabel mockup" />
><div class="icon calendar"></div> <label for="calendar-tab-input" class="tab-label mockup"
<div class="label">Calendar</div></label ><div class="icon calendar"></div>
> <div class="label">Calendar</div></label
<div class="panel"> >
<!-- #include "./README.md" --> <div class="tab-content">
<!-- #include file="./README.md" -->
</div>
</div> </div>

View file

@ -1,53 +1,13 @@
<script> <script>
function on_channels_updated({ channels }) { APP.on("channels_updated", ({ channels }) => {
const channel_list = document.getElementById("channel-list"); const channel_list = document.getElementById("channel-list");
if ( !channel_list ) {
setTimeout( () => {
on_channels_updated( { channels } );
}, 100 );
return;
}
channel_list.innerHTML = ""; channel_list.innerHTML = "";
for (const channel of channels.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) { for (const channel of channels.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) {
if ( !document.body.dataset.channel ) {
document.body.dataset.channel = APP.user?.meta?.chat?.last_channel ?? channel.id;
if ( APP.view === 'chat' ) {
window.location.hash = '/chat/channel/' + document.body.dataset.channel;
}
}
channel_list.insertAdjacentHTML( channel_list.insertAdjacentHTML(
"beforeend", "beforeend",
`<li id="channel-selector-${channel.id}" class="channel" data-channel-selector-for="${channel.id}"><a href="#/chat/channel/${channel.id}">${channel.name}</a></li>`, `<li id="channel-selector-${channel.id}" class="channel" data-channel-selector-for="${channel.id}"><a href="#/channel/${channel.id}/chat">${channel.name}</a></li>`,
); );
} }
}
APP.on("channels_updated", on_channels_updated );
APP.on( 'view_changed', async ( { previous, view, channel_id } ) => {
if ( view !== 'chat' ) {
return;
}
const previous_channel = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined;
if ( channel_id ) {
document.body.dataset.channel = channel_id;
}
else {
delete document.body.dataset.channel;
}
await APP.CHANNELS.update(); // don't force, but ensure we have channels
const target_channel_id = channel_id ?? APP.CHANNELS.CHANNEL_LIST[0]?.id;
const hash_target = `/chat` + ( target_channel_id ? `/channel/${ target_channel_id }` : '' );
if ( window.location.hash?.slice( 1 ) !== hash_target ) {
window.location.hash = hash_target;
}
}); });
function update_channel_indicators(event) { function update_channel_indicators(event) {
@ -117,25 +77,20 @@
} }
}); });
APP.on( 'view_changed', ( {previous, view} ) => { APP.on( 'view_changed', ( {view} ) => {
if ( !view === 'chat' ) {
return;
}
const sidebar_dynamic_container = document.getElementById( 'sidebar-dynamic-container'); const sidebar_dynamic_container = document.getElementById( 'sidebar-dynamic-container');
if ( !sidebar_dynamic_container ) { if ( !sidebar_dynamic_container ) {
console.error( 'could not get #sidebar-dynamic-container' ); console.error( 'could not get #sidebar-dynamic-container' );
return; return;
} }
if ( view !== 'chat' && previous === 'chat' ) {
sidebar_dynamic_container.innerHTML = '';
delete document.body.dataset.channel;
return;
}
else if ( view !== 'chat' ) {
return;
}
const template = document.getElementById( 'channel-list-template'); const template = document.getElementById( 'channel-list-template');
sidebar_dynamic_container.innerHTML = template.innerHTML.trim(); sidebar_dynamic_container.innerHTML = template.innerHTML.trim();
APP.CHANNELS.update(true); APP.CHANNELS.update();
}); });
</script> </script>
<style> <style>

View file

@ -1,10 +1,10 @@
#chat-container { #chat #chat-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#chat-container #chat-content { #chat #chat-content {
overflow-y: scroll; overflow-y: scroll;
scroll-behavior: smooth; scroll-behavior: smooth;
position: absolute; position: absolute;
@ -14,14 +14,13 @@
bottom: 5rem; bottom: 5rem;
padding: 1.5rem 1.5rem 0.75rem 1.5rem; padding: 1.5rem 1.5rem 0.75rem 1.5rem;
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
#chat-container #chat-content { #chat #chat-content {
padding: 0.75rem; padding: 0.75rem;
} }
} }
#chat-container #chat-entry-container { #chat #chat-entry-container {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -30,18 +29,18 @@
padding: 1rem; padding: 1rem;
} }
#chat-container #chat-entry-container form { #chat #chat-entry-container form {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
#chat-container #chat-entry-container form input[type="file"] { #chat #chat-entry-container form input[type="file"] {
opacity: 0; opacity: 0;
display: none; display: none;
} }
#chat-container #chat-entry-container form button, #chat #chat-entry-container form button,
#chat-container #chat-entry-container form label { #chat #chat-entry-container form label {
position: relative; position: relative;
top: inherit; top: inherit;
font-size: inherit; font-size: inherit;
@ -55,12 +54,12 @@
border: 1px solid rgba(128, 128, 128, 0.2); border: 1px solid rgba(128, 128, 128, 0.2);
} }
#chat-container #chat-entry-container form textarea { #chat #chat-entry-container form textarea {
flex-grow: 1; flex-grow: 1;
resize: none; resize: none;
} }
#chat-container .message-container { #chat .message-container {
position: relative; position: relative;
transition: all 0.33s; transition: all 0.33s;
background: var(--bg-lighter); background: var(--bg-lighter);
@ -69,22 +68,22 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
#chat-container .message-container .html-from-markdown { #chat .message-container .html-from-markdown {
padding: 0; padding: 0;
} }
#chat-container .user-tock.time-tock:has(+ .user-tock.time-tick), .user-tock.time-tock:has(+ .user-tock.time-tick),
#chat-container .user-tock.time-tick:has(+ .user-tock.time-tock), .user-tock.time-tick:has(+ .user-tock.time-tock),
#chat-container .user-tick.time-tock:has(+ .user-tick.time-tick), .user-tick.time-tock:has(+ .user-tick.time-tick),
#chat-container .user-tick.time-tick:has(+ .user-tick.time-tock), .user-tick.time-tick:has(+ .user-tick.time-tock),
#chat-container .user-tock.time-tock:has(+ .user-tick.time-tick), .user-tock.time-tock:has(+ .user-tick.time-tick),
#chat-container .user-tock.time-tick:has(+ .user-tick.time-tock), .user-tock.time-tick:has(+ .user-tick.time-tock),
#chat-container .user-tick.time-tock:has(+ .user-tock.time-tick), .user-tick.time-tock:has(+ .user-tock.time-tick),
#chat-container .user-tick.time-tick:has(+ .user-tock.time-tock), .user-tick.time-tick:has(+ .user-tock.time-tock),
#chat-container .user-tock.time-tock:last-of-type, .user-tock.time-tock:last-of-type,
#chat-container .user-tock.time-tick:last-of-type, .user-tock.time-tick:last-of-type,
#chat-container .user-tick.time-tock:last-of-type, .user-tick.time-tock:last-of-type,
#chat-container .user-tick.time-tick:last-of-type { .user-tick.time-tick:last-of-type {
/* border: 1px dotted red; */ /* border: 1px dotted red; */
margin-bottom: 1rem !important; margin-bottom: 1rem !important;
padding-bottom: 0.25rem !important; padding-bottom: 0.25rem !important;
@ -92,42 +91,54 @@
margin-top: 0.5rem !important; margin-top: 0.5rem !important;
} }
#chat-container .user-tock.time-tock:has(+ .user-tock.time-tock), .user-tock.time-tock:has(+ .user-tock.time-tock),
#chat-container .user-tock.time-tick:has(+ .user-tock.time-tick), .user-tock.time-tick:has(+ .user-tock.time-tick),
#chat-container .user-tick.time-tock:has(+ .user-tick.time-tock), .user-tick.time-tock:has(+ .user-tick.time-tock),
#chat-container .user-tick.time-tick:has(+ .user-tick.time-tick) { .user-tick.time-tick:has(+ .user-tick.time-tick) {
/* border: 1px dotted blue; */ /* border: 1px dotted blue; */
margin-bottom: -0.75rem !important; margin-bottom: -0.75rem !important;
padding-top: 0 !important; padding-top: 0 !important;
} }
#chat-container .user-tock.time-tock+.user-tock.time-tick:has(+ .user-tock.time-tick), .user-tock.time-tock + .user-tock.time-tick:has(+ .user-tock.time-tick),
#chat-container .user-tock.time-tick+.user-tock.time-tock:has(+ .user-tock.time-tock), .user-tock.time-tick + .user-tock.time-tock:has(+ .user-tock.time-tock),
#chat-container .user-tick.time-tock+.user-tick.time-tick:has(+ .user-tick.time-tick), .user-tick.time-tock + .user-tick.time-tick:has(+ .user-tick.time-tick),
#chat-container .user-tick.time-tick+.user-tick.time-tock:has(+ .user-tick.time-tock), .user-tick.time-tick + .user-tick.time-tock:has(+ .user-tick.time-tock),
#chat-container .user-tock.time-tock+.user-tick.time-tick:has(+ .user-tick.time-tick), .user-tock.time-tock + .user-tick.time-tick:has(+ .user-tick.time-tick),
#chat-container .user-tock.time-tick+.user-tick.time-tock:has(+ .user-tick.time-tock), .user-tock.time-tick + .user-tick.time-tock:has(+ .user-tick.time-tock),
#chat-container .user-tick.time-tock+.user-tock.time-tick:has(+ .user-tock.time-tick), .user-tick.time-tock + .user-tock.time-tick:has(+ .user-tock.time-tick),
#chat-container .user-tick.time-tick+.user-tock.time-tock:has(+ .user-tock.time-tock) { .user-tick.time-tick + .user-tock.time-tock:has(+ .user-tock.time-tock) {
/* border: 1px dotted green; */ /* border: 1px dotted green; */
margin-bottom: -0.75rem !important; margin-bottom: -0.75rem !important;
} }
#chat-container .message-container.user-tick.time-tick+.message-container.user-tick.time-tick .info-container, #chat
#chat-container .message-container.user-tick.time-tock+.message-container.user-tick.time-tock .info-container, .message-container.user-tick.time-tick
#chat-container .message-container.user-tock.time-tick+.message-container.user-tock.time-tick .info-container, + .message-container.user-tick.time-tick
#chat-container .message-container.user-tock.time-tock+.message-container.user-tock.time-tock .info-container { .info-container,
#chat
.message-container.user-tick.time-tock
+ .message-container.user-tick.time-tock
.info-container,
#chat
.message-container.user-tock.time-tick
+ .message-container.user-tock.time-tick
.info-container,
#chat
.message-container.user-tock.time-tock
+ .message-container.user-tock.time-tock
.info-container {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
height: 0; height: 0;
margin: 0; margin: 0;
} }
#chat-container .message-container.sending { #chat .message-container.sending {
opacity: 0.75; opacity: 0.75;
} }
#chat-container .message-container button[commandfor] { #chat .message-container button[commandfor] {
position: absolute; position: absolute;
top: 0.1rem; top: 0.1rem;
right: 0.1rem; right: 0.1rem;
@ -136,13 +147,13 @@
z-index: 10; z-index: 10;
} }
#chat-container .message-container .message-actions-container:has(input[type="checkbox"]:checked) { #chat .message-container .message-actions-container:has(input[type="checkbox"]:checked) {
background: rgb(from var(--bg) r g b / 0.9); background: rgb(from var(--bg) r g b / 0.9);
border-color: var(--border-subtle); border-color: var(--border-subtle);
z-index: 11; z-index: 11;
} }
#chat-container .message-container .message-actions-container .message-action { #chat .message-container .message-actions-container .message-action {
border: none; border: none;
opacity: 0; opacity: 0;
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
@ -158,75 +169,79 @@
background: none; background: none;
} }
#chat-container .message-container .message-actions-container label { #chat .message-container .message-actions-container label {
cursor: pointer; cursor: pointer;
width: 2rem; width: 2rem;
text-align: right; text-align: right;
padding-top: 0.5rem; padding-top: 0.5rem;
} }
#chat-container .message-container .message-actions-container input[type="checkbox"] { #chat .message-container .message-actions-container input[type="checkbox"] {
opacity: 0; opacity: 0;
display: none; display: none;
} }
#chat-container .message-container .message-actions-container input[type="checkbox"]:checked~.message-action { #chat
.message-container
.message-actions-container
input[type="checkbox"]:checked
~ .message-action {
opacity: 1; opacity: 1;
width: 3.25rem; width: 3.25rem;
margin-right: 1.25rem; margin-right: 1.25rem;
} }
#chat-container .message-container .message-actions-container .message-action .action-name { #chat .message-container .message-actions-container .message-action .action-name {
font-size: x-small; font-size: x-small;
} }
#chat-container .message-container .info-container { #chat .message-container .info-container {
display: flex; display: flex;
margin-bottom: -1.75rem; margin-bottom: -1.75rem;
height: 3.75rem; height: 3.75rem;
} }
#chat-container .message-container .info-container .username-container { #chat .message-container .info-container .username-container {
margin: 0 4px; margin: 0 4px;
font-weight: bold; font-weight: bold;
padding-top: 0.5rem; padding-top: 0.5rem;
} }
#chat-container .message-container .info-container .datetime-container { #chat .message-container .info-container .datetime-container {
margin: 0 4px; margin: 0 4px;
padding-top: 0.4rem; padding-top: 0.4rem;
} }
#chat-container .message-container .info-container .datetime-container .long { #chat .message-container .info-container .datetime-container .long {
font-size: x-small; font-size: x-small;
text-transform: uppercase; text-transform: uppercase;
} }
#chat-container .message-container .info-container .datetime-container .short { #chat .message-container .info-container .datetime-container .short {
font-size: xx-small; font-size: xx-small;
visibility: hidden; visibility: hidden;
display: none; display: none;
} }
#chat-container .message-container .message-content-container, #chat .message-container .message-content-container,
#chat-container .message-container .message-media-container, #chat .message-container .message-media-container,
#chat-container .message-container .reactions-container { #chat .message-container .reactions-container {
padding-left: 8rem; padding-left: 8rem;
overflow-x: auto; overflow-x: auto;
} }
#chat-container .message-container .reactions-container:has(> .reaction-container) { #chat .message-container .reactions-container:has(> .reaction-container) {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
#chat-container .embed-container { #chat .embed-container {
position: relative; position: relative;
width: 100%; width: 100%;
max-width: 640px; max-width: 640px;
} }
#chat-container .embed-container .embed-actions-container { #chat .embed-container .embed-actions-container {
position: absolute; position: absolute;
z-index: 100; z-index: 100;
top: 0.25rem; top: 0.25rem;
@ -238,31 +253,31 @@
opacity: 0; opacity: 0;
} }
#chat-container .embed-container audio { #chat .embed-container audio {
width: 100%; width: 100%;
} }
#chat-container .embed-container.rounded { #chat .embed-container.rounded {
border-radius: 6px; border-radius: 6px;
} }
#chat-container .embed-container.short { #chat .embed-container.short {
height: 120px; height: 120px;
overflow: hidden; overflow: hidden;
overflow-y: auto; overflow-y: auto;
} }
#chat-container .embed-container.tidal { #chat .embed-container.tidal {
border-radius: 12px; border-radius: 12px;
} }
#chat-container .embed-container.vertical { #chat .embed-container.vertical {
max-width: 320px; max-width: 320px;
overflow: hidden; overflow: hidden;
aspect-ratio: 9 / 16 !important; aspect-ratio: 9 / 16 !important;
} }
#chat-container .embed-container.letterbox { #chat .embed-container.letterbox {
/* height: 0; */ /* height: 0; */
overflow: hidden; overflow: hidden;
overflow-y: auto; overflow-y: auto;
@ -270,14 +285,14 @@
aspect-ratio: 16 / 9 !important; aspect-ratio: 16 / 9 !important;
} }
#chat-container .embed-container.square { #chat .embed-container.square {
overflow: hidden; overflow: hidden;
overflow-y: auto; overflow-y: auto;
aspect-ratio: 1 / 1 !important; aspect-ratio: 1 / 1 !important;
} }
#chat-container .embed-container iframe, #chat .embed-container iframe,
#chat-container .embed-container video { #chat .embed-container video {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -290,7 +305,7 @@
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
#chat-container #chat-container { #chat #chat-container {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -298,28 +313,28 @@
bottom: 0; bottom: 0;
} }
#chat-container #chat-container #chat-entry-container, #chat #chat-container #chat-entry-container,
#chat-container #chat-container #chat-entry-container form { #chat #chat-container #chat-entry-container form {
padding: 0.25rem; padding: 0.25rem;
} }
#chat-container #chat-container #chat-entry-container form button, #chat #chat-container #chat-entry-container form button,
#chat-container #chat-container #chat-entry-container form label { #chat #chat-container #chat-entry-container form label {
margin: 0 0.5rem; margin: 0 0.5rem;
} }
#chat-container .message-container .message-content-container, #chat .message-container .message-content-container,
#chat-container .message-container .message-media-container, #chat .message-container .message-media-container,
#chat-container .message-container .reactions-container { #chat .message-container .reactions-container {
padding-left: 4rem; padding-left: 4rem;
} }
#chat-container .message-container .info-container .datetime-container .long { #chat .message-container .info-container .datetime-container .long {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
#chat-container .message-container .info-container .datetime-container .short { #chat .message-container .info-container .datetime-container .short {
display: inline-block; display: inline-block;
visibility: visible; visibility: visible;
} }

View file

@ -1,233 +1,235 @@
<input <div id="chat" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="chat-tab-input" name="top-level-tabs"
class="tabswitch" id="chat-tab-input"
data-view="chat" class="tab-switch"
/> data-view="chat"
<label for="chat-tab-input" class="tablabel" />
><div class="icon chat"></div> <label for="chat-tab-input" class="tab-label"
<div class="label">Chat</div> ><div class="icon chat"></div>
</label> <div class="label">Chat</div>
<div class="panel"> </label>
<style> <div class="tab-content">
<!-- #include "./chat.css" --> <style>
</style> <!-- #include file="./chat.css" -->
<script src="/js/external/mimetypes.js" type="text/javascript"></script> </style>
<script src="/js/external/punycode.js" type="text/javascript"></script> <script src="/js/external/mimetypes.js" type="text/javascript"></script>
<script src="/js/external/punycode.js" type="text/javascript"></script>
<div id="chat-container"> <div id="chat-container">
<div <div
id="chat-content" id="chat-content"
data-feed data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.chat' ) !== -1 && document.body.dataset.channel" data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.chat' ) !== -1 && document.body.dataset.channel"
data-source="/api/channels/${ document.body.dataset.channel }/events?type=chat,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }" data-source="/api/channels/${ document.body.dataset.channel }/events?type=chat,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true" data-longpolling="true"
data-reverse="true" data-reverse="true"
data-insert="append" data-insert="append"
data-autoscroll="true" data-autoscroll="true"
>
<script>
{
const feed = document.currentScript.closest("[data-feed]");
APP.on("channel_changed", () => { feed.__reset && feed.__reset(); });
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
const time_tick_tock_timeout = 60_000;
let last_event_datetime_value = 0;
let time_tick_tock_class = "time-tock";
let last_creator_id = "";
let user_tick_tock_class = "user-tock";
feed.__context = async (item) => {
const event_datetime = datetime_to_local(item.timestamps.created);
if (
event_datetime.value - last_event_datetime_value >
time_tick_tock_timeout
) {
time_tick_tock_class =
time_tick_tock_class === "time-tick"
? "time-tock"
: "time-tick";
}
last_event_datetime_value = event_datetime.value;
if (last_creator_id !== item.creator_id) {
user_tick_tock_class =
user_tick_tock_class === "user-tick"
? "user-tock"
: "user-tick";
last_creator_id = item.creator_id;
}
return {
event: item,
creator: await APP.USERS.get(item.creator_id),
event_datetime,
time_tick_tock_class,
user_tick_tock_class,
};
};
feed.__target_element = (item) => {
let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "chat":
default:
target =
document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
return target;
};
}
</script>
<template data-for_type="chat">
<div
id="${context.event.id}"
class="message-container ${context.user_tick_tock_class} ${context.time_tick_tock_class}"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<button class="icon more" commandfor="eventactionspopover"></button>
<div class="info-container">
<div class="avatar-container inline">
<img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}"
alt="user avatar"
loading="lazy"
/>
</div>
<div class="username-container">
<span class="username"
>${context.creator.username ?? 'unknown'}</span
>
</div>
<div class="datetime-container">
<span class="long">${context.event_datetime.long}</span>
<span class="short">${context.event_datetime.short}</span>
</div>
</div>
<div class="message-content-container">
${htmlify(md_to_html(context.event.data.content))}
</div>
<div class="message-media-container">
${htmlify(context.event.data.media?.join("\n") ?? "")}
</div>
<div class="reactions-container"></div>
<div class="replies-container"></div>
</div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div>
<div id="chat-entry-container">
<form
id="chat-entry"
data-smart="true"
action="/api/events"
data-requires-permission="events.write.chat"
method="POST"
class="post-creation-form collapsible"
style="
margin-top: 1rem
width: 100%;
transition: all 0.5s;
"
on_reply="async (event) => { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.remove('sending'); }"
on_parsed="async (event) => { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.add('sending'); }"
> >
<input type="hidden" name="type" value="chat" /> <script>
{
const feed = document.currentScript.closest("[data-feed]");
<input APP.on("channel_changed", () => { feed.__reset && feed.__reset(); });
type="hidden" APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
name="id"
generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="meta.temp_id"
generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
reset-on-submit
/>
<input const time_tick_tock_timeout = 60_000;
type="hidden"
name="creator_id"
generator="() => { return APP.user?.id; }"
/>
<input let last_event_datetime_value = 0;
type="hidden" let time_tick_tock_class = "time-tock";
name="channel"
generator="() => { return document.body.dataset.channel; }"
/>
<input let last_creator_id = "";
type="hidden" let user_tick_tock_class = "user-tock";
name="timestamps.created"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="timestamps.updated"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input type="hidden" name="parent_id" reset-on-submit /> feed.__context = async (item) => {
const event_datetime = datetime_to_local(item.timestamps.created);
if (
event_datetime.value - last_event_datetime_value >
time_tick_tock_timeout
) {
time_tick_tock_class =
time_tick_tock_class === "time-tick"
? "time-tock"
: "time-tick";
}
last_event_datetime_value = event_datetime.value;
if (last_creator_id !== item.creator_id) {
user_tick_tock_class =
user_tick_tock_class === "user-tick"
? "user-tock"
: "user-tick";
last_creator_id = item.creator_id;
}
return {
event: item,
creator: await APP.USERS.get(item.creator_id),
event_datetime,
time_tick_tock_class,
user_tick_tock_class,
};
};
feed.__target_element = (item) => {
let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "chat":
default:
target =
document.querySelector(
`.message-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
return target;
};
}
</script>
<template data-for_type="chat">
<div
id="${context.event.id}"
class="message-container ${context.user_tick_tock_class} ${context.time_tick_tock_class}"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<button class="icon more" commandfor="eventactionspopover"></button>
<div class="info-container">
<div class="avatar-container inline">
<img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}"
alt="user avatar"
loading="lazy"
/>
</div>
<div class="username-container">
<span class="username"
>${context.creator.username ?? 'unknown'}</span
>
</div>
<div class="datetime-container">
<span class="long">${context.event_datetime.long}</span>
<span class="short">${context.event_datetime.short}</span>
</div>
</div>
<div class="message-content-container">
${htmlify(md_to_html(context.event.data.content))}
</div>
<div class="message-media-container">
${htmlify(context.event.data.media?.join("\n") ?? "")}
</div>
<div class="reactions-container"></div>
<div class="replies-container"></div>
</div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div>
<div id="chat-entry-container">
<form
id="chat-entry"
data-smart="true"
action="/api/events"
data-requires-permission="events.write.chat"
method="POST"
class="post-creation-form collapsible"
style="
margin-top: 1rem
width: 100%;
transition: all 0.5s;
"
on_reply="async (event) => { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.remove('sending'); }"
on_parsed="async (event) => { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.add('sending'); }"
>
<input type="hidden" name="type" value="chat" />
<label>
<input <input
aria-label="Upload and share file" type="hidden"
type="file" name="id"
multiple generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
data-smartforms-save-to-home="true" reset-on-submit
name="data.media" />
<input
type="hidden"
name="meta.temp_id"
generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()"
reset-on-submit reset-on-submit
/> />
<div class="icon attachment"></div>
</label>
<textarea <input
id="chat-input" type="hidden"
class="chat-input" name="creator_id"
rows="1" generator="() => { return APP.user?.id; }"
name="data.content" />
reset-on-submit
focus-on-submit
enter-key-submits
></textarea>
<button id="chat-send" class="primary" aria-label="Send a message"> <input
<i class="icon send"></i> type="hidden"
</button> name="channel"
</form> generator="() => { return document.body.dataset.channel; }"
/>
<input
type="hidden"
name="timestamps.created"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input
type="hidden"
name="timestamps.updated"
generator="(_input, form) => form.__submitted_at.toISOString()"
reset-on-submit
/>
<input type="hidden" name="parent_id" reset-on-submit />
<label>
<input
aria-label="Upload and share file"
type="file"
multiple
data-smartforms-save-to-home="true"
name="data.media"
reset-on-submit
/>
<div class="icon attachment"></div>
</label>
<textarea
id="chat-input"
class="chat-input"
rows="1"
name="data.content"
reset-on-submit
focus-on-submit
enter-key-submits
></textarea>
<button id="chat-send" class="primary" aria-label="Send a message">
<i class="icon send"></i>
</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- #include "./channel_sidebar.html" --> <!-- #include file="./channel_sidebar.html" -->

View file

@ -14,7 +14,6 @@
"preview" "preview"
"title" "title"
"info" "info"
"reactions"
"content" "content"
"newessay" "newessay"
"replies"; "replies";
@ -72,10 +71,6 @@
font-size: xx-large; font-size: xx-large;
} }
.essay-container .reactions-container {
grid-area: reactions;
}
.essay-container .content-container { .essay-container .content-container {
grid-area: content; grid-area: content;
padding-left: 0.25rem; padding-left: 0.25rem;
@ -83,10 +78,6 @@
font-size: large; font-size: large;
} }
.essay-container .content-container .html-from-markdown {
padding: 1em;
}
.essay-container button[commandfor="eventactionspopover"] { .essay-container button[commandfor="eventactionspopover"] {
position: absolute; position: absolute;
bottom: 0.25rem; bottom: 0.25rem;
@ -104,119 +95,121 @@
} }
</style> </style>
<input <div id="essays" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="essay-tab-input" name="top-level-tabs"
class="tabswitch" id="essay-tab-input"
data-view="essays" class="tab-switch"
/> data-view="essays"
<label for="essay-tab-input" class="tablabel" />
><div class="icon essay"></div> <label for="essay-tab-input" class="tab-label"
<div class="label">Essays</div> ><div class="icon essay"></div>
</label> <div class="label">Essays</div>
<div class="panel"> </label>
<div id="essays-container" class="container"> <div class="tab-content">
<!-- #include "./README.md" --> <div id="essays-container" class="container">
<!-- #include "./new_essay.html" --> <!-- #include file="./README.md" -->
<!-- #include file="./new_essay.html" -->
<div <div
id="essays-list" id="essays-list"
data-feed data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.essay' ) !== -1" data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.essay' ) !== -1"
data-source="/api/events?type=essay,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }" data-source="/api/events?type=essay,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true" data-longpolling="true"
data-reverse="true" data-reverse="true"
data-insert="prepend" data-insert="prepend"
data-autoscroll="true" data-autoscroll="true"
> >
<script> <script>
{ {
const feed = document.currentScript.closest("[data-feed]"); const feed = document.currentScript.closest("[data-feed]");
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); }); APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
feed.__target_element = (item) => { feed.__target_element = (item) => {
let target = feed; let target = feed;
switch (item.type) { switch (item.type) {
case "reaction": case "reaction":
target = document.querySelector( target = document.querySelector(
`.essay-container[data-event_id='${item.parent_id}'] > .reactions-container`, `.essay-container[data-event_id='${item.parent_id}'] > .reactions-container`,
); );
break; break;
case "essay": case "essay":
default: default:
target = feed; target = feed;
break; break;
} }
return target; return target;
};
feed.__context = async (item) => {
const essay_datetime = datetime_to_local(item.timestamps.created);
return {
event: item,
essay: item,
creator: await APP.USERS.get(item.creator_id),
essay_datetime,
}; };
};
}
</script>
<template data-for_type="essay">
<div
id="${context.essay.id}"
class="essay-container"
data-event_id="${context.essay.id}"
data-creator_id="${context.creator.id}"
data-essay_id="${context.essay.id}"
data-temp_id="${context.essay.meta?.temp_id ?? ''}"
>
<div class="media-preview-container">
${context.essay.data?.media?.length ?
context.essay.data.media.map(function(url) { return `<img
src="${url}"
loading="lazy"
/>` }).join('\n') : ''}
</div>
<div class="info-container"> feed.__context = async (item) => {
<div class="avatar-container inline medium"> const essay_datetime = datetime_to_local(item.timestamps.created);
<img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}" return {
alt="user avatar" event: item,
essay: item,
creator: await APP.USERS.get(item.creator_id),
essay_datetime,
};
};
}
</script>
<template data-for_type="essay">
<div
id="${context.essay.id}"
class="essay-container"
data-event_id="${context.essay.id}"
data-creator_id="${context.creator.id}"
data-essay_id="${context.essay.id}"
data-temp_id="${context.essay.meta?.temp_id ?? ''}"
>
<div class="media-preview-container">
${context.essay.data?.media?.length ?
context.essay.data.media.map(function(url) { return `<img
src="${url}"
loading="lazy" loading="lazy"
/> />` }).join('\n') : ''}
</div> </div>
<div class="username-container">
<span class="username">${context.creator.username}</span> <div class="info-container">
<div class="avatar-container inline medium">
<img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}"
alt="user avatar"
loading="lazy"
/>
</div>
<div class="username-container">
<span class="username">${context.creator.username}</span>
</div>
<div class="datetime-container">
<span class="long">${context.essay_datetime.long}</span>
<span class="short">${context.essay_datetime.short}</span>
</div>
</div> </div>
<div class="datetime-container"> <div class="title-container">${context.essay.data.title}</div>
<span class="long">${context.essay_datetime.long}</span> <div class="content-container">
<span class="short">${context.essay_datetime.short}</span> ${htmlify(md_to_html(context.essay.data.essay))}
</div> </div>
<div class="reactions-container"></div>
<button class="icon more" commandfor="eventactionspopover"></button>
</div> </div>
<div class="reactions-container"></div> </template>
<div class="title-container">${context.essay.data.title}</div> <template data-for_type="reaction">
<div class="content-container"> <div
${htmlify(md_to_html(context.essay.data.essay))} id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div> </div>
<button class="icon more" commandfor="eventactionspopover"></button> </template>
</div> </div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,14 +1,16 @@
<input <div id="exchange" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="exchange-tab-input" name="top-level-tabs"
class="tabswitch" id="exchange-tab-input"
data-view="exchange" class="tab-switch"
/> data-view="exchange"
<label for="exchange-tab-input" class="tablabel" />
><div class="icon exchange"></div> <label for="exchange-tab-input" class="tab-label"
<div class="label">Exchange</div></label ><div class="icon exchange"></div>
> <div class="label">Exchange</div></label
<div class="panel"> >
<!-- #include "./README.md" --> <div class="tab-content">
<!-- #include file="./README.md" -->
</div>
</div> </div>

View file

@ -11,7 +11,6 @@
grid-template-areas: grid-template-areas:
"expander preview info" "expander preview info"
"expander preview subject" "expander preview subject"
". . reactions"
". . content" ". . content"
". . newpost" ". . newpost"
". . replies"; ". . replies";
@ -112,10 +111,6 @@
margin-top: 2rem; margin-top: 2rem;
} }
.post-container .reactions-container {
grid-area: reactions;
}
.post-container button[commandfor="eventactionspopover"] { .post-container button[commandfor="eventactionspopover"] {
position: absolute; position: absolute;
bottom: 0.25rem; bottom: 0.25rem;
@ -130,159 +125,132 @@
.post-container .replies-container { .post-container .replies-container {
grid-area: replies; grid-area: replies;
} }
@media screen and (max-width: 480px) {
.post-container {
grid-template-rows: 120px auto auto auto 1fr;
grid-template-columns: auto 1fr;
grid-template-areas:
". preview"
"expander info"
"expander subject"
". reactions"
". content"
". newpost"
". replies";
max-height: 16rem;
}
.post-container .media-preview-container {
overflow: hidden;
display: grid;
align-content: center;
width: 100%;
height: 7rem;
}
.post-container .media-preview-container img {
object-fit: cover;
}
}
</style> </style>
<input <div id="forum" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="forum-tab-input" name="top-level-tabs"
class="tabswitch" id="forum-tab-input"
data-view="forum" class="tab-switch"
/> data-view="forum"
<label for="forum-tab-input" class="tablabel" />
><div class="icon forum"></div> <label for="forum-tab-input" class="tab-label"
<div class="label">Forum</div></label ><div class="icon forum"></div>
> <div class="label">Forum</div></label
<div class="panel forum-container">
<!-- #include "./README.md" -->
<div
id="posts-list"
data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.post' ) !== -1"
data-source="/api/events?type=post,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true"
data-reverse="true"
data-insert="prepend"
data-autoscroll="true"
> >
<script> <div class="tab-content forum-container">
{ <!-- #include file="./README.md" -->
const feed = document.currentScript.closest("[data-feed]");
APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); }); <div
id="posts-list"
data-feed
data-precheck="!!document.body.dataset.user && document.body.dataset.user.indexOf( 'events.read.post' ) !== -1"
data-source="/api/events?type=post,reaction&limit=100&sort=newest&wait=true&after_id=${ feed.__newest_id ?? 'able-able-able-able-able-able-able-able-able-able' }"
data-longpolling="true"
data-reverse="true"
data-insert="prepend"
data-autoscroll="true"
>
<script>
{
const feed = document.currentScript.closest("[data-feed]");
feed.__target_element = (item) => { APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); });
let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.post-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "chat":
default:
target =
document.querySelector(
`.post-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
return target; feed.__target_element = (item) => {
}; let target = feed;
switch (item.type) {
case "reaction":
target = document.querySelector(
`.post-container[data-event_id='${item.parent_id}'] > .reactions-container`,
);
break;
case "chat":
default:
target =
document.querySelector(
`.post-container[data-event_id='${item.parent_id}'] > .replies-container`,
) ?? feed;
break;
}
feed.__context = async (item) => { return target;
const post_datetime = datetime_to_local(item.timestamps.created);
return {
event: item,
post: item,
creator: await APP.USERS.get(item.creator_id),
post_datetime,
}; };
};
}
</script>
<template data-for_type="post">
<div
id="${context.post.id}"
class="post-container"
data-event_id="${context.post.id}"
data-creator_id="${context.creator.id}"
data-post_id="${context.post.id}"
>
<div class="expand-toggle-container">
<label>
<input type="checkbox" name="expanded" /><i class="icon plus"></i
><i class="icon minus"></i>
</label>
</div>
<div class="media-preview-container"> feed.__context = async (item) => {
<img const post_datetime = datetime_to_local(item.timestamps.created);
src="/images/placeholders/${String((context.post_datetime.ms % 9) + 1).padStart(2, '0')}.svg"
loading="lazy"
/>
</div>
<div class="info-container"> return {
<div class="avatar-container inline"> event: item,
post: item,
creator: await APP.USERS.get(item.creator_id),
post_datetime,
};
};
}
</script>
<template data-for_type="post">
<div
id="${context.post.id}"
class="post-container"
data-event_id="${context.post.id}"
data-creator_id="${context.creator.id}"
data-post_id="${context.post.id}"
>
<div class="expand-toggle-container">
<label>
<input type="checkbox" name="expanded" /><i class="icon plus"></i
><i class="icon minus"></i>
</label>
</div>
<div class="media-preview-container">
<img <img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}" src="/images/placeholders/${String((context.post_datetime.ms % 9) + 1).padStart(2, '0')}.svg"
alt="user avatar"
loading="lazy" loading="lazy"
/> />
</div> </div>
<div class="username-container">
<span class="username">${context.creator.username}</span>
</div>
<div class="datetime-container">
<span class="long">${context.post_datetime.long}</span>
<span class="short">${context.post_datetime.short}</span>
</div>
</div>
<div class="subject-container">${context.post.data.subject}</div>
<div class="content-container">
${htmlify(md_to_html(context.post.data.content))}
</div>
<div class="reactions-container"></div>
<button class="icon more" commandfor="eventactionspopover"></button>
<!-- #include "./new_post.html" -->
<div class="replies-container"></div>
</div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div>
<!-- #include "./new_post.html" --> <div class="info-container">
<div class="avatar-container inline">
<img
src="${context.creator.meta?.avatar ?? '/images/default_avatar.gif'}"
alt="user avatar"
loading="lazy"
/>
</div>
<div class="username-container">
<span class="username">${context.creator.username}</span>
</div>
<div class="datetime-container">
<span class="long">${context.post_datetime.long}</span>
<span class="short">${context.post_datetime.short}</span>
</div>
</div>
<div class="subject-container">${context.post.data.subject}</div>
<div class="content-container">
${htmlify(md_to_html(context.post.data.content))}
</div>
<div class="reactions-container"></div>
<button class="icon more" commandfor="eventactionspopover"></button>
<!-- #include file="./new_post.html" -->
<div class="replies-container"></div>
</div>
</template>
<template data-for_type="reaction">
<div
id="${context.event.id}"
class="reaction-container"
data-event_id="${context.event.id}"
data-creator_id="${context.creator.id}"
data-temp_id="${context.event.meta?.temp_id ?? ''}"
>
<span class="reaction">${ context.event.data.reaction }</span>
</div>
</template>
</div>
<!-- #include file="./new_post.html" -->
</div>
</div> </div>

View file

@ -1,15 +1,17 @@
<input <div id="home" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="home-tab-input" name="top-level-tabs"
checked="checked" id="home-tab-input"
class="tabswitch" checked="checked"
data-view="home" class="tab-switch"
/> data-view="home"
<label for="home-tab-input" class="tablabel"> />
<div class="icon home"></div> <label for="home-tab-input" class="tab-label">
<div class="label">Home</div> <div class="icon home"></div>
</label> <div class="label">Home</div>
<div class="panel"> </label>
<!-- #include "./README.md" --> <div class="tab-content">
<!-- #include file="./README.md" -->
</div>
</div> </div>

View file

@ -1,71 +1,73 @@
<script></script> <script></script>
<style></style> <style></style>
<input <div id="live-tab" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="live-tab-input" name="top-level-tabs"
class="tabswitch" id="live-tab-input"
data-view="profile" class="tab-switch"
/> data-view="profile"
<label for="live-tab-input" class="tablabel mockup" />
><div class="icon live"></div> <label for="live-tab-input" class="tab-label mockup"
<div class="label">Live</div></label ><div class="icon live"></div>
> <div class="label">Live</div></label
<div class="panel">
<div
class="live-container"
style="
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-content: center;
justify-content: center;
flex-flow: wrap;
"
> >
<div class="tab-content">
<div <div
class="mockup" class="live-container"
style=" style="
width: 95%; position: relative;
max-width: 1024px; width: 100%;
border: 1px solid rgba(128, 128, 128, 0.5); height: 100%;
height: 66vh;
overflow: hidden; overflow: hidden;
"
></div>
<div
class="live-actions-container"
style="
width: 80%;
margin: 0 auto;
display: flex; display: flex;
align-items: center; align-content: center;
justify-content: space-evenly; justify-content: center;
height: 4rem; flex-flow: wrap;
" "
> >
<input <div
id="toggle-live-audio" class="mockup"
type="checkbox" style="
style="display: none; visibility: hidden" width: 95%;
/> max-width: 1024px;
<label for="toggle-live-audio" class="mockup"> border: 1px solid rgba(128, 128, 128, 0.5);
<i class="icon phone"></i> height: 66vh;
<span>Audio</span> overflow: hidden;
</label> "
></div>
<input <div
id="toggle-live-video" class="live-actions-container"
type="checkbox" style="
style="display: none; visibility: hidden" width: 80%;
/> margin: 0 auto;
<label for="toggle-live-video" class="mockup"> display: flex;
<i class="icon camera"></i> align-items: center;
<span>Video</span> justify-content: space-evenly;
</label> height: 4rem;
"
>
<input
id="toggle-live-audio"
type="checkbox"
style="display: none; visibility: hidden"
/>
<label for="toggle-live-audio" class="mockup">
<i class="icon phone"></i>
<span>Audio</span>
</label>
<input
id="toggle-live-video"
type="checkbox"
style="display: none; visibility: hidden"
/>
<label for="toggle-live-video" class="mockup">
<i class="icon camera"></i>
<span>Video</span>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,54 +0,0 @@
<link rel="stylesheet" href="/js/external/leaflet/leaflet.css"/>
<script src="/js/external/leaflet/leaflet.js"></script>
<script type="module">
var map = L.map('map').setView([33.88,-118.13], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
APP.on( "view_changed", ({ view }) => {
if ( view === 'map' ) {
map.invalidateSize();
}
});
</script>
<style>
#map-tab .panel {
overflow: hidden;
}
#map {
position: absolute;
top: 4em;
left: 4em;
right: 4em;
bottom: 4em;
z-index: 0;
}
@media screen and (max-width: 640px) {
#map {
top: 1em;
left: 1em;
right: 1em;
bottom: 1em;
}
}
</style>
<input
type="radio"
name="top-level-tabs"
id="map-tab-tab-input"
class="tabswitch"
data-view="map"
/>
<label for="map-tab-tab-input" class="tablabel"
><div class="icon map-pin"></div>
<div class="label">Map</div>
</label>
<div class="panel">
<div id="map"></div>
</div>

View file

@ -1,12 +1,14 @@
<input <div id="resources" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="resources-tab-input" name="top-level-tabs"
class="tabswitch" id="resources-tab-input"
data-view="resources" class="tab-switch"
/> data-view="resources"
<label for="resources-tab-input" class="tablabel mockup" />
><div class="icon resources"></div> <label for="resources-tab-input" class="tab-label mockup"
<div class="label">Resources</div></label ><div class="icon resources"></div>
> <div class="label">Resources</div></label
<div class="panel"><!-- #include "./README.md" --></div> >
<div class="tab-content"><!-- #include file="./README.md" --></div>
</div>

View file

@ -1,6 +1,6 @@
<script> <script>
APP.on( "view_changed", ({ view }) => { APP.on( "view_changed", ({ view }) => {
const target_tab = document.querySelector(`.tabswitch[data-view="${view}"]`); const target_tab = document.querySelector(`.tab-switch[data-view="${view}"]`);
if (target_tab) { if (target_tab) {
target_tab.click(); target_tab.click();
@ -8,67 +8,130 @@
}); });
APP.on( 'load', () => { APP.on( 'load', () => {
console.log( 'loaded' ); const tab_switchers = document.querySelectorAll(".tab-switch");
const tab_switchers = document.querySelectorAll(".tabswitch");
for (const tab_switch of tab_switchers) { for (const tab_switch of tab_switchers) {
tab_switch.addEventListener("input", (event) => { tab_switch.addEventListener("input", (event) => {
const tab_selector = event.target; const tab_selector = event.target;
const view = tab_selector.dataset.view; const view = tab_selector.dataset.view;
if (view) { if (view) {
window.location.hash = `/${view}`; window.location.hash = `/${view}${ document.body.dataset.channel ? `/channel/${ document.body.dataset.channel }` : '' }`;
} }
}); });
} }
}); });
</script> </script>
<style> <style>
:root { .tabs-container {
--tab-label-height: 4em; position: relative;
} height: 100%;
}
.tabs { .tabs {
display: flex; position: relative;
flex-wrap: wrap; height: 100%;
justify-content: space-evenly; width: 100%;
} display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
min-height: inherit;
background: inherit;
}
.tabs .tabswitch { .tabs::before,
position: absolute; .tabs::after {
opacity: 0; content: "";
} display: table;
}
.tabs .tablabel { .tabs::after {
cursor: pointer; clear: both;
border: none; }
text-align: center;
padding: 1em;
height: var(--tab-label-height);
line-height: 2em;
}
.tabs .tabswitch:checked + .tablabel { .tab {
border-bottom: 1px solid var(--accent); background: inherit;
} }
.tabs .panel { .tab-switch {
display: none; display: none;
position: relative; }
padding: 1em;
width: 100%;
height: calc( 100vh - var(--tab-label-height) );
order: 99;
}
.tabs .tabswitch:checked + .tablabel + .panel { .tab-label {
display: block; position: relative;
} width: 6rem;
height: 5rem;
cursor: pointer;
transition: all 0.25s;
display: flex;
align-items: center;
justify-content: end;
flex-direction: column;
}
.tab-label .label {
margin: 0.75rem 0;
}
.tab-content {
position: absolute;
z-index: 1;
top: 5rem;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transition: all 0.35s;
border-top: 1px solid var(--border-subtle);
margin-top: 1px;
overflow-y: scroll;
visibility: hidden;
display: none;
background: inherit;
}
.tab-switch,
.tab-switch + .tab-label {
transition: none;
}
.tab-switch:checked + .tab-label {
margin-top: 1px;
border-bottom: 1px solid var(--accent);
z-index: 1;
}
.tab-switch:checked + label + .tab-content {
z-index: 2;
opacity: 1;
visibility: visible;
display: block;
}
@media screen and (max-width: 800px) {
.tab-label {
width: 3rem;
}
.tab-label .label {
font-size: small;
}
}
@media screen and (max-width: 400px) {
.tab-label {
width: 2.5rem;
}
.tab-label .label {
font-size: 8px;
}
}
</style> </style>
<div class="tabs"> <div class="tabs">
<!-- #include "./chat/chat.html" --> <!-- #include file="./chat/chat.html" -->
<!-- #include "./blurbs/blurbs.html" --> <!-- #include file="./blurbs/blurbs.html" -->
<!-- #include "./forum/forum.html" --> <!-- #include file="./forum/forum.html" -->
<!-- #include "./essays/essays.html" --> <!-- #include file="./essays/essays.html" -->
<!-- #include "./map/map.html" -->
</div> </div>

View file

@ -1,14 +1,14 @@
<input <div id="work" class="tab">
type="radio" <input
name="top-level-tabs" type="radio"
id="work-tab-input" name="top-level-tabs"
class="tabswitch" id="work-tab-input"
data-view="work" class="tab-switch"
/> data-view="work"
<label for="work-tab-input" class="tablabel" />
><div class="icon work"></div> <label for="work-tab-input" class="tab-label"
<div class="label">Work</div> ><div class="icon work"></div>
</label> <div class="label">Work</div>
<div class="panel"> </label>
<!-- #include "./README.md" --> <div class="tab-content"><!-- #include file="./README.md" --></div>
</div> </div>

View file

@ -1 +0,0 @@
autonomous.contact

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View file

@ -8,7 +8,6 @@ import { EVENT } from '../models/event.ts';
export type PRECHECK = (req: Request, meta: Record<string, any>) => Promise<Response | undefined> | Response | undefined; export type PRECHECK = (req: Request, meta: Record<string, any>) => Promise<Response | undefined> | Response | undefined;
export type PRECHECK_TABLE = Record<string, PRECHECK[]>; export type PRECHECK_TABLE = Record<string, PRECHECK[]>;
export const AUTHED_BEFORE_COOKIE_ID: string = Deno.env.get('AUTHED_BEFORE_COOKIE_ID') ?? 'authed_before';
export const SESSION_ID_TOKEN: string = Deno.env.get('SESSION_ID_TOKEN') ?? 'session_id'; export const SESSION_ID_TOKEN: string = Deno.env.get('SESSION_ID_TOKEN') ?? 'session_id';
export const SESSION_SECRET_TOKEN: string = Deno.env.get('SESSION_SECRET_TOKEN') ?? 'session_secret'; export const SESSION_SECRET_TOKEN: string = Deno.env.get('SESSION_SECRET_TOKEN') ?? 'session_secret';
export const TOTP_TOKEN: string = Deno.env.get('TOTP_TOKEN') ?? 'totp'; export const TOTP_TOKEN: string = Deno.env.get('TOTP_TOKEN') ?? 'totp';
@ -44,9 +43,6 @@ export function require_user(
} }
} }
export function user_has_create_permission_for_event(user: USER, event: EVENT) { export function user_has_write_permission_for_event(user: USER, event: EVENT) {
return user.permissions.includes('events.create.' + event.type) || (Deno.env.get('DENO_ENV') === 'test' && event.type === 'test'); return user.permissions.includes('events.create.' + event.type) || (Deno.env.get('DENO_ENV') === 'test' && event.type === 'test');
} }
export function user_has_write_permission_for_event(user: USER, event: EVENT) {
return user.permissions.includes('events.write.' + event.type) || (Deno.env.get('DENO_ENV') === 'test' && event.type === 'test');
}