51 Commits

Author SHA1 Message Date
eee7c899ab Merge pull request 'smoothies' (#37) from master into 98
Reviewed-on: #37
2026-07-28 20:30:21 +00:00
c30b3535ae smoothies 2026-07-28 22:30:04 +02:00
6e8ae2f7cb some 98 css additions 2026-07-28 22:26:56 +02:00
90ef78bbf3 Merge pull request 'truncate .' (#36) from master into 98
Reviewed-on: #36
2026-07-28 20:06:46 +00:00
eed8b993e1 truncate . 2026-07-28 22:06:31 +02:00
2eae7f93e5 Merge pull request 'master' (#35) from master into 98
Reviewed-on: #35
2026-07-28 20:02:13 +00:00
9c239769e6 quote truncation 2026-07-28 22:00:58 +02:00
6e07194b27 possible fix for weird ass username display bug 2026-07-28 16:42:18 +02:00
2758f2a72e Merge pull request 'master' (#34) from master into 98
Reviewed-on: #34
2026-07-28 14:26:51 +00:00
866a8003fe fix comment quoting 2026-07-28 16:26:26 +02:00
5ab1a9614a d 2026-07-28 07:47:49 +02:00
e45df6a769 d 2026-07-28 07:46:06 +02:00
99eb9735a1 show pill 2026-07-28 06:38:20 +02:00
b651621cee foobarbaz 2026-07-28 06:30:50 +02:00
d3ce2e38f0 update readme 2026-07-28 03:34:57 +02:00
096786516e 503 2026-07-28 03:11:16 +02:00
ec895a285b onion rules 2026-07-28 03:08:46 +02:00
20aaf183d8 less is more 2026-07-28 03:01:30 +02:00
c0c32ddfe4 Merge pull request 'adding tor switch and update readme' (#33) from master into 98
Reviewed-on: #33
2026-07-27 10:37:04 +00:00
25ab4ba949 adding tor switch and update readme 2026-07-27 12:32:08 +02:00
a6e2689909 Merge pull request 'master' (#32) from master into 98
Reviewed-on: #32
2026-07-27 10:28:47 +00:00
6a52445987 custom location for CF Gate and removing box-shadow 2026-07-27 12:28:06 +02:00
63c39ebe90 deactivate captcha for onion 2026-07-27 10:26:05 +02:00
d1dd088bc6 Merge pull request 'positioning is kinda weird for notic-count' (#31) from master into 98
Reviewed-on: #31
2026-07-26 22:44:49 +00:00
0a21b6c8f8 positioning is kinda weird for notic-count 2026-07-27 00:44:00 +02:00
1bb3dcff21 Merge pull request 'master' (#30) from master into 98
Reviewed-on: #30
2026-07-26 22:23:32 +00:00
a7aabac956 update example gate 2026-07-27 00:22:13 +02:00
9f5510bc04 add custom gate template for private society 2026-07-27 00:12:51 +02:00
fc6749cd11 Merge pull request 'respect config settings' (#29) from master into 98
Reviewed-on: #29
2026-07-26 21:25:01 +00:00
54e2e8871b respect config settings 2026-07-26 23:24:28 +02:00
4d685dff06 Merge pull request 'fix invalid csrf token' (#28) from master into 98
Reviewed-on: #28
2026-07-26 21:20:50 +00:00
c99f3e11d8 fix invalid csrf token 2026-07-26 23:20:20 +02:00
b0053a8de8 Merge pull request 'correctly display url in gate' (#27) from master into 98
Reviewed-on: #27
2026-07-26 21:11:14 +00:00
d14feaf3eb correctly display url in gate 2026-07-26 23:10:41 +02:00
280020c19f Merge pull request 'Revert "adding hardcoded meta for onion"' (#26) from master into 98
Reviewed-on: #26
2026-07-26 21:07:57 +00:00
5e35fd48d4 Revert "adding hardcoded meta for onion"
This reverts commit a07c21f22b.
2026-07-26 23:07:27 +02:00
0d3819c113 Merge pull request 'adding hardcoded meta for onion' (#25) from master into 98
Reviewed-on: #25
2026-07-26 21:05:20 +00:00
a07c21f22b adding hardcoded meta for onion 2026-07-26 23:04:45 +02:00
34d2b62909 Merge pull request 'f' (#24) from master into 98
Reviewed-on: #24
2026-07-26 20:47:21 +00:00
65fb96f675 f 2026-07-26 22:38:10 +02:00
18c898c910 Merge pull request 'break long text in info modal' (#23) from master into 98
Reviewed-on: #23
2026-07-26 20:28:36 +00:00
20a6ffcb04 break long text in info modal 2026-07-26 22:27:57 +02:00
cbfbcb04bf Merge pull request 'master' (#22) from master into 98
Reviewed-on: #22
2026-07-26 20:23:11 +00:00
8518e87c8d fix pagination 2026-07-26 22:09:44 +02:00
a3148848f2 option to advertise onion url 2026-07-26 21:43:39 +02:00
abedf7a789 trying HS 2026-07-26 21:37:07 +02:00
d2ce6f412d Merge pull request 'fix scroller tagging invalid csrf token' (#21) from master into 98
Reviewed-on: #21
2026-07-26 19:22:10 +00:00
ec4db7a22b Merge pull request 'mobile default tab selection' (#20) from master into 98
Reviewed-on: #20
2026-07-26 18:45:46 +00:00
c0d70baf83 Merge pull request 'master' (#19) from master into 98
Reviewed-on: #19
2026-07-25 20:50:24 +00:00
122f57eacc update build script 2026-07-25 22:16:15 +02:00
07de9d4488 add f0ck98 theme 2026-07-25 22:14:54 +02:00
26 changed files with 1043 additions and 144 deletions

View File

@@ -1,9 +1,9 @@
POSTGRES_USER=f0ckm POSTGRES_USER=f0ckm
POSTGRES_DB=f0ckm POSTGRES_DB=f0ckm
POSTGRES_PASSWORD=f0ckm POSTGRES_PASSWORD=f0ckm
# --- Nginx & Let's Encrypt Configuration (Optional) --- # --- Nginx & Tor Profiles (Optional) ---
# Set to 'f0ckm-nginx' to enable the Nginx & Let's Encrypt proxy stack # Set to 'f0ckm-nginx' for Nginx, 'tor' for Tor hidden service, or 'f0ckm-nginx,tor' for both
# COMPOSE_PROFILES=f0ckm-nginx # COMPOSE_PROFILES=tor
# #
# VIRTUAL_HOST=yourdomain.com # VIRTUAL_HOST=yourdomain.com
# LETSENCRYPT_HOST=yourdomain.com # LETSENCRYPT_HOST=yourdomain.com

View File

@@ -78,3 +78,23 @@ now visit http://localhost:1337 in your browser, you can develop without needing
## NGINX ## NGINX
uncomment in .env # COMPOSE_PROFILES=f0ckm-nginx to enable nginx proxy with automatic lets encrypt. uncomment in .env # COMPOSE_PROFILES=f0ckm-nginx to enable nginx proxy with automatic lets encrypt.
## Tor Hidden Service (.onion)
Tor is bundled in `docker-compose.yml` and preconfigured as long as COMPOSE_PROFILES=f0ckm-tor is set with a hidden service pointing to the application (`f0ckm:1337`).
When running `docker compose up -d`, Tor automatically generates a `.onion` address for your node. You can find your onion address by running:
```bash
cat ./f0ckm-data/tor/f0ckm_hs/hostname
```
### Automatic Onion-Location Header
To advertise your `.onion` address to Tor Browser users visiting your clearnet site, add your `.onion` address to `config.json`:
```json
"main": {
"onion": "http://yourgeneratedaddress.onion"
}
```

View File

@@ -16,7 +16,7 @@ fi
# Parse arguments # Parse arguments
TARGET="master" TARGET="master"
CACHE_FLAG="" CACHE_FLAG=""
BRANCH_TARGET="" CUSTOM_BRANCH=""
for arg in "$@"; do for arg in "$@"; do
case $arg in case $arg in
@@ -27,8 +27,8 @@ for arg in "$@"; do
CACHE_FLAG="--no-cache" CACHE_FLAG="--no-cache"
;; ;;
*) *)
if [ -z "$BRANCH_TARGET" ]; then if [ -z "$CUSTOM_BRANCH" ]; then
BRANCH_TARGET=$arg CUSTOM_BRANCH=$arg
fi fi
;; ;;
esac esac
@@ -43,7 +43,10 @@ fi
# Set Image name and branch defaults # Set Image name and branch defaults
IMAGE_NAME="f0ckm" IMAGE_NAME="f0ckm"
if [ "$TARGET" == "master" ]; then if [ -n "$CUSTOM_BRANCH" ]; then
BRANCH_TARGET="$CUSTOM_BRANCH"
echo "--- CUSTOM BRANCH DEPLOYMENT (${BRANCH_TARGET}) ---"
elif [ "$TARGET" == "master" ]; then
echo "--- PRODUCTION DEPLOYMENT (master) ---" echo "--- PRODUCTION DEPLOYMENT (master) ---"
BRANCH_TARGET="master" BRANCH_TARGET="master"
elif [ "$TARGET" == "stg" ]; then elif [ "$TARGET" == "stg" ]; then

9
config/tor/torrc Normal file
View File

@@ -0,0 +1,9 @@
# Tor configuration for f0ckm
# Enable SOCKS proxy for internal container & external connections
SocksPort 0.0.0.0:9050
# Tor Hidden Service (v3 Onion Service) configuration
# Tor will automatically generate private keys and hostnames in /var/lib/tor/f0ckm_hs/
HiddenServiceDir /var/lib/tor/f0ckm_hs/
HiddenServicePort 80 f0ckm:1337

View File

@@ -5,6 +5,7 @@
"domain": "example.com", "domain": "example.com",
"regex": "example\\.com" "regex": "example\\.com"
}, },
"onion": "http://your-onion-address.onion",
"socks": "socks5://127.0.0.1:9050", "socks": "socks5://127.0.0.1:9050",
"mail": "admin@example.com", "mail": "admin@example.com",
"maxfilesize": 104857600, "maxfilesize": 104857600,
@@ -61,6 +62,7 @@
"show_koepfe": false, "show_koepfe": false,
"hide_sidebar_default": true, "hide_sidebar_default": true,
"koepfe": [], "koepfe": [],
"enable_tor_hs": true,
"enable_global_chat": true, "enable_global_chat": true,
"enable_danmaku": true, "enable_danmaku": true,
"private_messages": true, "private_messages": true,
@@ -132,6 +134,8 @@
"open_registration_require_mail_andor_token": false, "open_registration_require_mail_andor_token": false,
"private_society": false, "private_society": false,
"private_society_gate": "cloudflare", "private_society_gate": "cloudflare",
"private_society_gate_location": "Frankfurt",
"private_society_gate_template": "_gate_template",
"public_nsfw": false, "public_nsfw": false,
"paths": { "paths": {
"images": "/b", "images": "/b",

View File

@@ -49,9 +49,14 @@ services:
tor: tor:
image: dockurr/tor image: dockurr/tor
container_name: tor container_name: tor
profiles:
- tor
ports: ports:
- "9050:9050" - "9050:9050"
- "8118:8118" - "8118:8118"
volumes:
- ./config/tor:/etc/tor:ro
- ./f0ckm-data/tor:/var/lib/tor:Z
networks: networks:
- f0ckm-net - f0ckm-net
restart: always restart: always

357
public/s/css/98.css Normal file
View File

@@ -0,0 +1,357 @@
git pu/* 98.css framework - Scoped exclusively for f0ck98 theme */
@font-face {
font-family: "Pixelated MS Sans Serif";
font-style: normal;
font-weight: 400;
src: url("/s/fonts/ms_sans_serif.woff2") format("woff2");
}
@font-face {
font-family: "Pixelated MS Sans Serif";
font-style: normal;
font-weight: 700;
src: url("/s/fonts/ms_sans_serif_bold.woff2") format("woff2");
}
html[theme="f0ck98"] {
--w98-silver: #c0c0c0;
--w98-teal: #008080;
--w98-navy: #000080;
--w98-navy-gradient: linear-gradient(90deg, navy, #1084d0);
--w98-dark-shadow: #0a0a0a;
--w98-light-highlight: #ffffff;
--w98-mid-gray: grey;
--w98-light-gray: #dfdfdf;
color: #000;
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
font-size: 11px;
background-color: var(--w98-silver) !important;
background: var(--w98-silver) !important;
}
html[theme="f0ck98"] body {
color: #000;
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
font-size: 11px;
}
html[theme="f0ck98"] .title-bar,
html[theme="f0ck98"] .window,
html[theme="f0ck98"] button,
html[theme="f0ck98"] input,
html[theme="f0ck98"] label,
html[theme="f0ck98"] legend,
html[theme="f0ck98"] li[role=tab],
html[theme="f0ck98"] option,
html[theme="f0ck98"] select,
html[theme="f0ck98"] table,
html[theme="f0ck98"] textarea,
html[theme="f0ck98"] ul.tree-view {
-webkit-font-smoothing: none;
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
font-size: 11px;
}
html[theme="f0ck98"] u {
border-bottom: 0.5px solid #222;
text-decoration: none;
}
/* Windows 98 Buttons */
html[theme="f0ck98"] button,
html[theme="f0ck98"] input[type=reset],
html[theme="f0ck98"] input[type=submit],
html[theme="f0ck98"] .btn {
background: silver;
border: none;
border-radius: 0;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
box-sizing: border-box;
color: #000;
min-height: 23px;
}
html[theme="f0ck98"] button.default,
html[theme="f0ck98"] input[type=reset].default,
html[theme="f0ck98"] input[type=submit].default,
html[theme="f0ck98"] .btn.default {
box-shadow: inset -2px -2px #0a0a0a, inset 1px 1px #0a0a0a, inset 2px 2px #fff, inset -3px -3px grey, inset 3px 3px #dfdfdf;
}
html[theme="f0ck98"] .vertical-bar {
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
height: 20px;
width: 4px;
}
html[theme="f0ck98"] button:not(:disabled):active,
html[theme="f0ck98"] input[type=reset]:not(:disabled):active,
html[theme="f0ck98"] input[type=submit]:not(:disabled):active,
html[theme="f0ck98"] .btn:not(:disabled):active {
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
}
html[theme="f0ck98"] button.default:not(:disabled):active,
html[theme="f0ck98"] input[type=reset].default:not(:disabled):active,
html[theme="f0ck98"] input[type=submit].default:not(:disabled):active {
box-shadow: inset 2px 2px #0a0a0a, inset -1px -1px #0a0a0a, inset -2px -2px #fff, inset 3px 3px grey, inset -3px -3px #dfdfdf;
}
html[theme="f0ck98"] button:focus,
html[theme="f0ck98"] input[type=reset]:focus,
html[theme="f0ck98"] input[type=submit]:focus,
html[theme="f0ck98"] .btn:focus {
outline: 1px dotted #000;
outline-offset: -4px;
}
html[theme="f0ck98"] :disabled,
html[theme="f0ck98"] :disabled+label,
html[theme="f0ck98"] input[readonly],
html[theme="f0ck98"] input[readonly]+label {
color: grey;
}
html[theme="f0ck98"] :disabled+label,
html[theme="f0ck98"] button:disabled,
html[theme="f0ck98"] input[type=reset]:disabled,
html[theme="f0ck98"] input[type=submit]:disabled,
html[theme="f0ck98"] .btn:disabled {
text-shadow: 1px 1px 0 #fff;
}
/* Window Shell & Title Bars */
html[theme="f0ck98"] .window {
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
padding: 3px;
}
html[theme="f0ck98"] .title-bar {
align-items: center;
background: linear-gradient(90deg, navy, #1084d0);
display: flex;
justify-content: space-between;
padding: 3px 2px 3px 3px;
}
html[theme="f0ck98"] .title-bar.inactive {
background: linear-gradient(90deg, grey, #b5b5b5);
}
html[theme="f0ck98"] .title-bar-text {
color: #fff;
font-weight: 700;
letter-spacing: 0;
margin-right: 24px;
}
html[theme="f0ck98"] .title-bar-controls {
display: flex;
}
html[theme="f0ck98"] .title-bar-controls button {
display: block;
min-height: 14px;
min-width: 16px;
padding: 0;
}
html[theme="f0ck98"] .title-bar-controls button:active {
padding: 0;
}
html[theme="f0ck98"] .title-bar-controls button:focus {
outline: none;
}
html[theme="f0ck98"] .title-bar-controls button[aria-label=Minimize],
html[theme="f0ck98"] .title-bar-controls button[aria-label].minimize {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='6' height='2' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23000' d='M0 0h6v2H0z'/%3E%3C/svg%3E");
background-position: bottom 3px left 4px;
background-repeat: no-repeat;
}
html[theme="f0ck98"] .title-bar-controls button[aria-label=Maximize],
html[theme="f0ck98"] .title-bar-controls button[aria-label].maximize {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='9' height='9' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 0H0v9h9V0zM8 2H1v6h7V2z' fill='%23000'/%3E%3C/svg%3E");
background-position: top 2px left 3px;
background-repeat: no-repeat;
}
html[theme="f0ck98"] .title-bar-controls button[aria-label=Close],
html[theme="f0ck98"] .title-bar-controls button[aria-label].close {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='8' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h2v1h1v1h2V1h1V0h2v1H7v1H6v1H5v1h1v1h1v1h1v1H6V6H5V5H3v1H2v1H0V6h1V5h1V4h1V3H2V2H1V1H0V0z' fill='%23000'/%3E%3C/svg%3E");
background-position: top 3px left 4px;
background-repeat: no-repeat;
margin-left: 2px;
}
/* Status Bar */
html[theme="f0ck98"] .status-bar {
gap: 1px;
display: flex;
margin: 0 1px;
}
html[theme="f0ck98"] .status-bar-field {
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
flex-grow: 1;
margin: 0;
padding: 2px 3px;
}
html[theme="f0ck98"] .window-body {
margin: 8px;
}
html[theme="f0ck98"] fieldset {
border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='5' height='5' fill='gray' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h5v5H0V2h2v1h1V2H0' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h4v4H0V1h1v2h2V1H0'/%3E%3C/svg%3E") 2;
margin: 0;
padding: 10px;
padding-block-start: 8px;
}
html[theme="f0ck98"] legend {
background: silver;
}
html[theme="f0ck98"] .field-row {
align-items: center;
display: flex;
}
html[theme="f0ck98"] [class^=field-row]+[class^=field-row] {
margin-top: 6px;
}
html[theme="f0ck98"] .field-row>*+* {
margin-left: 6px;
}
html[theme="f0ck98"] label {
align-items: center;
display: inline-flex;
user-select: none;
}
/* Form Inputs & Textarea */
html[theme="f0ck98"] input[type=email],
html[theme="f0ck98"] input[type=number],
html[theme="f0ck98"] input[type=password],
html[theme="f0ck98"] input[type=search],
html[theme="f0ck98"] input[type=tel],
html[theme="f0ck98"] input[type=text],
html[theme="f0ck98"] input[type=url],
html[theme="f0ck98"] select,
html[theme="f0ck98"] textarea {
background-color: #fff;
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
box-sizing: border-box;
padding: 3px 4px;
border: none;
border-radius: 0;
color: #000;
}
html[theme="f0ck98"] input[type=email]:disabled,
html[theme="f0ck98"] input[type=number]:disabled,
html[theme="f0ck98"] input[type=password]:disabled,
html[theme="f0ck98"] input[type=search]:disabled,
html[theme="f0ck98"] input[type=tel]:disabled,
html[theme="f0ck98"] input[type=text]:disabled,
html[theme="f0ck98"] input[type=url]:disabled,
html[theme="f0ck98"] textarea:disabled {
background-color: silver;
}
html[theme="f0ck98"] select {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1V6z' fill='%23000'/%3E%3C/svg%3E");
background-position: top 2px right 2px;
background-repeat: no-repeat;
border-radius: 0;
padding-right: 32px;
}
html[theme="f0ck98"] input[type=email]:focus,
html[theme="f0ck98"] input[type=number]:focus,
html[theme="f0ck98"] input[type=password]:focus,
html[theme="f0ck98"] input[type=search]:focus,
html[theme="f0ck98"] input[type=tel]:focus,
html[theme="f0ck98"] input[type=text]:focus,
html[theme="f0ck98"] input[type=url]:focus,
html[theme="f0ck98"] select:focus,
html[theme="f0ck98"] textarea:focus {
outline: none;
}
html[theme="f0ck98"] a {
color: #000080;
}
html[theme="f0ck98"] a:focus {
outline: 1px dotted #000080;
}
html[theme="f0ck98"] .sunken-panel {
background-color: #fff;
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
box-sizing: border-box;
overflow: auto;
}
/* Scoped Scrollbars for f0ck98 */
html[theme="f0ck98"] ::-webkit-scrollbar {
width: 16px;
height: 17px;
}
html[theme="f0ck98"] ::-webkit-scrollbar-corner {
background: #dfdfdf;
}
html[theme="f0ck98"] ::-webkit-scrollbar-track {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='2' height='2' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M1 0H0v1h1v1h1V1H1V0z' fill='silver'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 0H1v1H0v1h1V1h1V0z' fill='%23fff'/%3E%3C/svg%3E");
}
html[theme="f0ck98"] ::-webkit-scrollbar-thumb {
background-color: #dfdfdf;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
}
html[theme="f0ck98"] ::-webkit-scrollbar-button:vertical:start {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 6H7v1H6v1H5v1H4v1h7V9h-1V8H9V7H8V6z' fill='%23000'/%3E%3C/svg%3E");
height: 17px;
display: block;
}
html[theme="f0ck98"] ::-webkit-scrollbar-button:vertical:end {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1V6z' fill='%23000'/%3E%3C/svg%3E");
height: 17px;
display: block;
}
html[theme="f0ck98"] .sidebar-activity .comment-content.has-overflow:not(.expanded) .comment-content-inner::after {
background: linear-gradient(to bottom, transparent, rgb(192, 192, 192));
}
html[theme="f0ck98"] .item-sidebar-right, .index-sidebar-right, .global-sidebar-right {
box-shadow: none!important;
}
html[theme="f0ck98"] .sidebar-activity .read-more-btn, .sidebar-activity .see-less-btn {
padding: 5px;
}
html[theme="f0ck98"] .modal-content p {
color: var(--black);
}
html[theme="f0ck98"] .drop-zone {
border: 2px dashed rgba(0, 0, 0, 0.2);
}

View File

@@ -1237,7 +1237,7 @@ html[theme='95'] .item-main-content .metadata {
.notif-count { .notif-count {
position: absolute; position: absolute;
top: 0px; top: 0;
right: -5px; right: -5px;
background: var(--badge-nsfw); background: var(--badge-nsfw);
color: white; color: white;
@@ -1650,6 +1650,10 @@ body.layout-modern .global-sidebar-right {
opacity: 1; opacity: 1;
} }
body.sidebar-right-hidden #sidebar-drag-zone {
opacity: 0.7;
}
/* Always show the handle on touch devices (no hover state available) */ /* Always show the handle on touch devices (no hover state available) */
@media (pointer: coarse) { @media (pointer: coarse) {
#sidebar-drag-zone { #sidebar-drag-zone {
@@ -1984,7 +1988,7 @@ body.layout-modern .global-sidebar-right {
right: 0; right: 0;
width: 100%; width: 100%;
height: 20px; height: 20px;
background: linear-gradient(to bottom, transparent, rgba(28, 27, 27, 0.8)); background: linear-gradient(to bottom, transparent, var(--bg));
pointer-events: none; pointer-events: none;
} }
@@ -3874,7 +3878,7 @@ html[theme="f0ck95d"] ._204863 {
line-height: 1.7; line-height: 1.7;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 4px; padding: 0px 5px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -3893,8 +3897,6 @@ html[theme="f0ck95d"] ._204863 .location::before {
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
image-rendering: pixelated;
image-rendering: crisp-edges;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -4026,11 +4028,338 @@ html[theme="f0ck95d"] .admin-search button {
color: black; color: black;
} }
html[theme="f0ck95d"] .embed-responsive.embed-responsive-16by9 { html[theme="f0ck98"] .embed-responsive.embed-responsive-16by9 {
background: black; background: black;
} }
/* ==========================================
f0ck98 Theme - Windows 98 Authentic Retro
========================================== */
html[theme="f0ck98"] {
--accent: #000080;
--bg: silver;
--black: #000000;
--white: #ffffff;
--gray: silver;
--nav-bg: silver;
--nav-brand-border: outset 2px silver;
--nav-brand-bg: silver;
--navigation-links-bg: silver;
--navigation-links-background-linear-gradient: none;
--navigation-links-border-color: #808080;
--navigation-links-box-shadow: none;
--nav-link-background-linear-gradient: none;
--nav-link-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
--nav-link-hover-bg: #000080;
--nav-border-color: #808080;
--dropdown-bg: silver;
--dropdown-item-hover: #000080;
--nav-brand-font: 'Pixelated MS Sans Serif', Arial, sans-serif;
--font: 'Pixelated MS Sans Serif', Arial, sans-serif;
--pagination-background: silver;
--pagination-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
--pagination-anchor-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
--pagination-background-hover: #000080;
--pagination-border-color: #808080;
--metadata-bg: silver;
--badge-bg: silver;
--posts-meta-bg: rgba(192, 192, 192, 0.95);
--badge-sfw: #008080;
--badge-nsfw: #a72828;
--badge-nsfl: #660000;
--badge-tag: #808080;
--scrollbar-color: silver;
--scroller-bg: silver;
--footbar-color: #000000;
--loading-indicator-color: #000000;
--img-border-width: 0;
--img-border-color: #808080;
--bg-gradient: none;
--motd-bg: silver;
--comment-bg: #ffffff;
background-color: silver !important;
background: silver !important;
}
html[theme="f0ck98"] body {
color: #000000;
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
}
html[theme="f0ck98"] a {
color: #000080;
}
html[theme="f0ck98"] a:hover {
color: #1084d0;
}
/* Header & Titlebar */
html[theme="f0ck98"] ._204863 {
background: linear-gradient(90deg, #000080, #1084d0);
color: #ffffff;
width: 100%;
border: 2px outset silver;
line-height: 1.7;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px;
box-sizing: border-box;
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
font-weight: bold;
}
html[theme="f0ck98"] ._204863 .location {
display: inline-flex;
align-items: center;
gap: 6px;
color: #ffffff;
}
html[theme="f0ck98"] ._204863 .location::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
background-image: var(--favicon-url, url('/s/img/favicon.gif'));
background-size: contain;
background-repeat: no-repeat;
background-position: center;
image-rendering: pixelated;
image-rendering: crisp-edges;
flex-shrink: 0;
}
html[theme="f0ck98"] ._204863::after {
content: " _ ❐ ✕ ";
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
font-size: 10px;
font-weight: bold;
background: silver;
color: #000000;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
padding: 0 4px;
height: 14px;
line-height: 12px;
display: inline-flex;
align-items: center;
margin-left: auto;
letter-spacing: 1px;
cursor: default;
user-select: none;
}
html[theme="f0ck98"] #tags .badge>a:first-child {
text-shadow: none;
}
html[theme="f0ck98"] .item_title {
background: silver;
color: #000000;
border-left: 2px outset silver;
border-right: 2px outset silver;
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
font-size: 11px;
font-weight: normal;
padding: 3px 8px;
-webkit-font-smoothing: none;
}
/* Navbar */
html[theme="f0ck98"] .nav,
html[theme="f0ck98"] .navbar {
background: silver !important;
border-bottom: 2px outset #fff;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
}
html[theme="f0ck98"] .nav-link {
color: #000000 !important;
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
border-radius: 0;
}
html[theme="f0ck98"] .nav-link:hover,
html[theme="f0ck98"] .nav-link.active {
background: #000080 !important;
color: #ffffff !important;
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
}
/* Sidebars & Window Containers */
html[theme="f0ck98"] .global-sidebar-left,
html[theme="f0ck98"] .global-sidebar-right,
html[theme="f0ck98"] .about,
html[theme="f0ck98"] .modal-content,
html[theme="f0ck98"] .card {
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
color: #000000;
padding: 6px;
border-radius: 0;
}
/* Item Grid Tiles */
html[theme="f0ck98"] div.posts > a:not(.notif-item) {
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
background-color: silver;
}
html[theme="f0ck98"] div.posts > a:not(.notif-item):hover,
html[theme="f0ck98"] div.posts > a:not(.notif-item):focus-visible {
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
}
html[theme="f0ck98"] div.posts > a:not(.notif-item)::after {
color: #000000;
background: silver;
border-top: 1px solid grey;
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
text-shadow: none;
}
/* Comments Section */
html[theme="f0ck98"] .comment-item,
html[theme="f0ck98"] .comments-container {
background: #ffffff;
color: #000000;
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
}
html[theme="f0ck98"] .comment-author {
color: #000080 !important;
font-weight: bold;
}
html[theme="f0ck98"] .comment-content {
color: #000000;
}
html[theme="f0ck98"] .comment-children {
border-left: 1px dotted grey;
}
html[theme="f0ck98"] .comment-actions button,
html[theme="f0ck98"] .report-comment-btn,
html[theme="f0ck98"] .reply-btn,
html[theme="f0ck98"] .quote-btn {
color: #000000 !important;
opacity: 1 !important;
}
html[theme="f0ck98"] .comment-actions button svg,
html[theme="f0ck98"] .report-comment-btn svg,
html[theme="f0ck98"] .reply-btn i,
html[theme="f0ck98"] .quote-btn i,
html[theme="f0ck98"] .report-comment-btn i {
color: #000000 !important;
fill: #000000 !important;
}
html[theme="f0ck98"] .report-comment-btn:hover,
html[theme="f0ck98"] .report-comment-btn:hover svg,
html[theme="f0ck98"] .report-comment-btn:hover i {
color: #a72828 !important;
fill: #a72828 !important;
}
html[theme="f0ck98"] .reply-btn:hover,
html[theme="f0ck98"] .quote-btn:hover,
html[theme="f0ck98"] .reply-btn:hover i,
html[theme="f0ck98"] .quote-btn:hover i {
color: #000080 !important;
}
/* Navigation Controls (.steuerung) */
html[theme="f0ck98"] .steuerung a,
html[theme="f0ck98"] .steuerung button,
html[theme="f0ck98"] .steuerung.steuerung-icon a,
html[theme="f0ck98"] .steuerung.steuerung-icon button {
background: silver !important;
color: #000000 !important;
border: none !important;
border-radius: 0 !important;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf !important;
box-sizing: border-box !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-height: 23px !important;
min-width: 28px !important;
width: auto !important;
padding: 2px 8px !important;
text-decoration: none !important;
cursor: pointer !important;
}
html[theme="f0ck98"] .steuerung a:active,
html[theme="f0ck98"] .steuerung button:active,
html[theme="f0ck98"] .steuerung.steuerung-icon a:active,
html[theme="f0ck98"] .steuerung.steuerung-icon button:active {
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey !important;
}
html[theme="f0ck98"] .steuerung a:hover,
html[theme="f0ck98"] .steuerung button:hover,
html[theme="f0ck98"] .steuerung.steuerung-icon a:hover,
html[theme="f0ck98"] .steuerung.steuerung-icon button:hover {
color: #000080 !important;
}
html[theme="f0ck98"] .steuerung.steuerung-icon .fa-solid {
color: #000000;
}
html[theme="f0ck98"] .steuerung.steuerung-icon a:hover .fa-solid,
html[theme="f0ck98"] .steuerung.steuerung-icon button:hover .fa-solid {
color: #000080;
}
/* Badges & Tags */
html[theme="f0ck98"] .badge,
html[theme="f0ck98"] .badge-light,
html[theme="f0ck98"] .badge-dark,
html[theme="f0ck98"] .badge-success,
html[theme="f0ck98"] .badge-danger {
background: silver;
color: #000000;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
border-radius: 0;
text-shadow: none;
}
/* Dropdown Menu */
html[theme="f0ck98"] .dropdown-menu {
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
border: none;
border-radius: 0;
}
html[theme="f0ck98"] .dropdown-item {
color: #000000;
}
html[theme="f0ck98"] .dropdown-item:hover {
background: #000080;
color: #ffffff;
}
/* Footer / Status Bar */
html[theme="f0ck98"] footer,
html[theme="f0ck98"] .footbar {
background: silver;
color: #000000;
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
padding: 3px 6px;
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
}
/* fullscreen */ /* fullscreen */
html[res="fullscreen"] ._204863 { html[res="fullscreen"] ._204863 {
border-top: none; border-top: none;
@@ -4623,16 +4952,17 @@ span.placeholder {
transition: right 0.3s ease-in-out; transition: right 0.3s ease-in-out;
} }
/* Sidebar hidden — reclaim the full width */
body.sidebar-right-hidden .pagination-container-fluid {
right: 0;
}
/* Modern layout: wider sidebar needs wider offset */ /* Modern layout: wider sidebar needs wider offset */
body.layout-modern .pagination-container-fluid { body.layout-modern .pagination-container-fluid {
right: 400px; right: 400px;
} }
/* Sidebar hidden — reclaim the full width */
body.sidebar-right-hidden .pagination-container-fluid,
body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
right: 0;
}
.bottom-pagination.fixed-pagination { .bottom-pagination.fixed-pagination {
margin: 10px auto; margin: 10px auto;
pointer-events: auto; pointer-events: auto;
@@ -4641,7 +4971,6 @@ body.layout-modern .pagination-container-fluid {
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
padding: 2px; padding: 2px;
max-width: fit-content; max-width: fit-content;
} }
@@ -10176,7 +10505,7 @@ ol {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
padding: 0 15px; padding: 0 5px;
flex-shrink: 0; flex-shrink: 0;
margin-left: auto; margin-left: auto;
} }
@@ -16133,12 +16462,21 @@ body.scroller-active #gchat-reopen-bubble {
color: #ccc; color: #ccc;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.9em; font-size: 0.9em;
word-break: break-word;
overflow-wrap: anywhere;
} }
.info-table td a { .info-table td a {
color: var(--accent, #9f0); color: var(--accent, #9f0);
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
word-break: break-all;
overflow-wrap: anywhere;
}
#info-modal .modal-content .info-table td a {
word-break: break-all;
overflow-wrap: anywhere;
} }
.info-table td a:hover { .info-table td a:hover {

Binary file not shown.

Binary file not shown.

View File

@@ -1160,17 +1160,13 @@ class CommentSystem {
const author = openerEl.dataset.display || openerEl.dataset.username || 'System'; const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
const contentEl = body.querySelector('.comment-content'); const contentEl = body.querySelector('.comment-content');
if (contentEl) { if (contentEl) {
const LINE_MAX = 200;
let rawText = (contentEl.dataset.raw || '').trim(); let rawText = (contentEl.dataset.raw || '').trim();
if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) { if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) {
const txt = document.createElement('textarea'); const txt = document.createElement('textarea');
txt.innerHTML = rawText; txt.innerHTML = rawText;
rawText = txt.value; rawText = txt.value;
} }
// Preserve all lines but cap any single line exceeding LINE_MAX chars const lines = rawText.split('\n');
const lines = rawText.split('\n').map(line =>
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
);
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`; const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
if (isNew) { if (isNew) {
@@ -1625,18 +1621,26 @@ class CommentSystem {
syncLevel(roots, list, false); syncLevel(roots, list, false);
} }
// Maximum characters to fully render in the item view per comment // Maximum characters & lines to fully render in the item view per comment
static get ITEM_VIEW_MAX_CHARS() { return 2000; } static get ITEM_VIEW_MAX_CHARS() { return 500; }
static get ITEM_VIEW_MAX_LINES() { return 6; }
renderCommentContent(content, commentId = null, bypassTruncation = false) { renderCommentContent(content, commentId = null, bypassTruncation = false) {
if (!content) return ''; if (!content) return '';
// Truncate extremely long comments before any processing // Truncate long comments before any processing
let truncated = false; let truncated = false;
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) { if (!bypassTruncation) {
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS) + '\u2026'; const lines = content.split('\n');
if (lines.length > CommentSystem.ITEM_VIEW_MAX_LINES) {
content = lines.slice(0, CommentSystem.ITEM_VIEW_MAX_LINES).join('\n');
truncated = true; truncated = true;
} }
if (content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS);
truncated = true;
}
}
if (typeof marked === 'undefined') { if (typeof marked === 'undefined') {
console.warn('Marked.js not loaded, falling back to plain text'); console.warn('Marked.js not loaded, falling back to plain text');
@@ -1784,10 +1788,11 @@ class CommentSystem {
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) { if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
const quoteContent = line.substring(line.indexOf('>') + 1); const quoteContent = line.substring(line.indexOf('>') + 1);
const quoteEmojis = window.f0ckSession?.quote_emojis === true; const quoteEmojis = window.f0ckSession?.quote_emojis === true;
const escapedQuote = quoteContent.replace(/>/g, '>');
const renderedContent = quoteEmojis const renderedContent = quoteEmojis
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)) ? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
: quoteContent; : escapedQuote;
return `<span class="greentext">&gt;${renderedContent.replace(/>/g, '&gt;')}</span>`; return `<span class="greentext">&gt;${renderedContent}</span>`;
} }
// 2. Per-line limit to prevent marked.parse recursion on single giant lines // 2. Per-line limit to prevent marked.parse recursion on single giant lines
@@ -3603,9 +3608,9 @@ class CommentSystem {
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every // element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
// optimistic DOM insert to guarantee the button regardless of rendering path. // optimistic DOM insert to guarantee the button regardless of rendering path.
_ensureTruncationButton(commentEl, rawContent) { _ensureTruncationButton(commentEl, rawContent) {
console.log('[ensureBtn] called, rawContent.length:', rawContent?.length, 'threshold:', CommentSystem.ITEM_VIEW_MAX_CHARS); if (!rawContent) return;
if (!rawContent || rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS) { const lineCount = rawContent.split('\n').length;
console.log('[ensureBtn] below threshold, skipping'); if (rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS && lineCount <= CommentSystem.ITEM_VIEW_MAX_LINES) {
return; return;
} }
const contentEl = commentEl.querySelector('.comment-content'); const contentEl = commentEl.querySelector('.comment-content');

View File

@@ -7567,30 +7567,49 @@ class NotificationSystem {
// Helper: apply a display name change to all relevant DOM nodes // Helper: apply a display name change to all relevant DOM nodes
this.applyDisplayNameUpdate = (display_name, user) => { this.applyDisplayNameUpdate = (display_name, user) => {
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
// Capture old value before overwriting — needed for thumb matching below // Capture old value before overwriting — needed for thumb matching below
const oldDisplayName = window.f0ckSession?.display_name || null; const oldDisplayName = isSelf ? (window.f0ckSession?.display_name || null) : null;
if (window.f0ckSession) window.f0ckSession.display_name = display_name || null; if (isSelf && window.f0ckSession) {
window.f0ckSession.display_name = display_name || null;
}
const userName = user || window.f0ckSession?.user; const userName = user || window.f0ckSession?.user;
// Update ALL nav-display-name instances (handles the profile page having a duplicate ID) // Update navbar display name only if this update is for the logged-in user
if (isSelf) {
document.querySelectorAll('#nav-display-name').forEach(nameSpan => { document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
// preserve the badges (Admin, Mod)
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) { for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
let node = nameSpan.childNodes[i]; let node = nameSpan.childNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) { if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
// Match and preserve any non-breaking spaces at the start (from admin/mod badges)
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)')); const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
node.nodeValue = (match ? match[1] : '') + (display_name || userName || ''); node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
break; break;
} }
} }
}); });
}
// Update profile header display name if currently viewing this user's profile
const profileBox = document.getElementById('live-profile-preview-box');
if (profileBox && profileBox.dataset.bannerUser && userName && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
const profileNameSpan = document.getElementById('profile-display-name');
if (profileNameSpan) {
for (let i = profileNameSpan.childNodes.length - 1; i >= 0; i--) {
let node = profileNameSpan.childNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
break;
}
}
}
const bracket = document.getElementById('username-bracket'); const bracket = document.getElementById('username-bracket');
if (bracket) { if (bracket) {
bracket.style.display = display_name ? 'inline' : 'none'; bracket.style.display = display_name ? 'inline' : 'none';
} }
}
if (userName) { if (userName) {
const lowerHref = `/user/${userName.toLowerCase()}`; const lowerHref = `/user/${userName.toLowerCase()}`;
@@ -7616,6 +7635,7 @@ class NotificationSystem {
// Helper: apply a username color change to relevant DOM nodes // Helper: apply a username color change to relevant DOM nodes
this.applyUsernameColorUpdate = (username_color, user) => { this.applyUsernameColorUpdate = (username_color, user) => {
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
const userName = user || window.f0ckSession?.user; const userName = user || window.f0ckSession?.user;
if (!userName) return; if (!userName) return;
@@ -7662,22 +7682,21 @@ class NotificationSystem {
} }
}); });
// Special case for settings page preview block // Profile header display name color (if currently viewing this user's profile)
if (window.f0ckSession && userName === window.f0ckSession.user) { const profileBox = document.getElementById('live-profile-preview-box');
const previewBlock = document.getElementById('live-profile-preview-box'); if (profileBox && profileBox.dataset.bannerUser && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
if (previewBlock) { const profileNameSpan = document.getElementById('profile-display-name');
if (profileNameSpan) {
if (username_color) { if (username_color) {
previewBlock.style.setProperty('--author-accent', username_color); profileNameSpan.style.color = username_color;
previewBlock.style.setProperty('--author-border', username_color);
} else { } else {
previewBlock.style.setProperty('--author-accent', 'var(--accent)'); profileNameSpan.style.removeProperty('color');
previewBlock.style.setProperty('--author-border', 'var(--accent)');
} }
} }
} }
// Handle the nav-display-name span elements (profile header and top navbar) // Navbar display name color (only for current user)
if (window.f0ckSession && userName === window.f0ckSession.user) { if (isSelf) {
document.querySelectorAll('#nav-display-name').forEach(el => { document.querySelectorAll('#nav-display-name').forEach(el => {
if (username_color) { if (username_color) {
el.style.color = username_color; el.style.color = username_color;
@@ -7690,14 +7709,21 @@ class NotificationSystem {
// Helper: apply an avatar update to relevant DOM nodes // Helper: apply an avatar update to relevant DOM nodes
this.applyAvatarUpdate = (avatar_file, user) => { this.applyAvatarUpdate = (avatar_file, user) => {
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
const userName = user || window.f0ckSession?.user; const userName = user || window.f0ckSession?.user;
if (!userName) return; if (!userName) return;
const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png'; const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png';
// Update Nav Avatar & profile header Avatar (only for current user) // Update Nav Avatar only for current user
if (window.f0ckSession && userName === window.f0ckSession.user) { if (isSelf) {
document.querySelectorAll('.nav-avatar-img, .profile_head_avatar img').forEach(img => img.src = newSrc); document.querySelectorAll('.nav-avatar-img').forEach(img => img.src = newSrc);
}
// Update profile header avatar if currently viewing this user's profile
const profileBox = document.getElementById('live-profile-preview-box');
if (profileBox && profileBox.dataset.bannerUser && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
document.querySelectorAll('.profile_head_avatar img').forEach(img => img.src = newSrc);
} }
// Update globally (comments, sidebar, polls, rank, infoboxes, favs) // Update globally (comments, sidebar, polls, rank, infoboxes, favs)

View File

@@ -153,12 +153,6 @@
const renderCommentContent = (content, commentId = null, itemId = null) => { const renderCommentContent = (content, commentId = null, itemId = null) => {
if (!content) return ''; if (!content) return '';
// Truncate extremely long comments before any processing to keep the sidebar
// fast and the DOM lean, regardless of markdown / regex complexity.
if (content.length > SIDEBAR_CONTENT_TRUNCATE) {
content = content.substring(0, SIDEBAR_CONTENT_TRUNCATE) + '\u2026';
}
if (typeof marked === 'undefined') { if (typeof marked === 'undefined') {
return escapeHtml(content) return escapeHtml(content)
.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)); .replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n));
@@ -268,10 +262,11 @@
// Manual greentext handling — apply emoji if the user preference allows it // Manual greentext handling — apply emoji if the user preference allows it
const quoteContent = line.substring(line.indexOf('>') + 1); const quoteContent = line.substring(line.indexOf('>') + 1);
const quoteEmojis = window.f0ckSession?.quote_emojis === true; const quoteEmojis = window.f0ckSession?.quote_emojis === true;
const escapedQuote = quoteContent.replace(/>/g, '&gt;');
const rendered = quoteEmojis const rendered = quoteEmojis
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)) ? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
: quoteContent; : escapedQuote;
return `<span class="greentext">&gt;${rendered.replace(/>/g, '&gt;')}</span>`; return `<span class="greentext">&gt;${rendered}</span>`;
} }
// Per-line limit to prevent marked.parse recursion on single giant lines // Per-line limit to prevent marked.parse recursion on single giant lines

View File

@@ -15,7 +15,7 @@
} }
}; };
const themes = window.f0ckThemes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'iced', 'orange', 'p1nk', '4d']; const themes = window.f0ckThemes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'f0ck98', 'iced', 'orange', 'p1nk', '4d'];
const defaultTheme = window.f0ckDefaultTheme || (window.f0ckSession && window.f0ckSession.default_theme) || themes[0] || 'amoled'; const defaultTheme = window.f0ckDefaultTheme || (window.f0ckSession && window.f0ckSession.default_theme) || themes[0] || 'amoled';
const setTheme = (theme) => { const setTheme = (theme) => {

View File

@@ -306,7 +306,12 @@ if (!window.UserCommentSystem) {
const trimmed = line.trimStart(); const trimmed = line.trimStart();
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) { if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
const quoteContent = line.substring(line.indexOf('>') + 1); const quoteContent = line.substring(line.indexOf('>') + 1);
return `<span class="greentext">&gt;${quoteContent.replace(/>/g, '&gt;')}</span>`; const quoteEmojis = window.f0ckSession?.quote_emojis === true;
const escapedQuote = quoteContent.replace(/>/g, '&gt;');
const rendered = quoteEmojis
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
: escapedQuote;
return `<span class="greentext">&gt;${rendered}</span>`;
} }
// Per-line limit // Per-line limit

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
const cssDir = path.join(process.cwd(), 'public/s/css'); const cssDir = path.join(process.cwd(), 'public/s/css');
const files = ['f0ckm.css', 'v0ck.css']; const files = ['f0ckm.css', 'v0ck.css', '98.css'];
const outputFile = path.join(cssDir, 'bundle.css'); const outputFile = path.join(cssDir, 'bundle.css');
let combined = ''; let combined = '';

View File

@@ -460,4 +460,15 @@ export default new class {
console.error(`[ERROR REF ${errId}] ${context}:`, err); console.error(`[ERROR REF ${errId}] ${context}:`, err);
return `Internal Error. Reference: ${errId}`; return `Internal Error. Reference: ${errId}`;
} }
isOnionRequest(req) {
if (!req || !req.headers) return false;
const rawHost = req.headers['x-forwarded-host'] || req.headers['host'] || req.headers['x-forwarded-server'] || '';
if (!rawHost) return false;
const hostStr = Array.isArray(rawHost) ? rawHost[0] : String(rawHost);
const firstHost = hostStr.split(',')[0].trim();
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
return hostNoPort.endsWith('.onion');
}
}; };

View File

@@ -1428,8 +1428,8 @@ export default (router, tpl) => {
const userId = newUser[0].id; const userId = newUser[0].id;
await db` await db`
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping) INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping, use_alternative_infobox)
VALUES (${userId}, 3, 'amoled', 0, NULL, 'default.png', ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}) VALUES (${userId}, 3, 'amoled', 0, NULL, 'default.png', ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}, ${cfg.websrv.user_alternative_infobox !== false})
`; `;
await audit.log(req.session.id, 'admin_create_user', 'user', userId, { await audit.log(req.session.id, 'admin_create_user', 'user', userId, {

View File

@@ -48,7 +48,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg })); return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg }));
} }
return res.reply({ return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }) body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }, req)
}); });
} }
@@ -58,7 +58,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg })); return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
} }
return res.reply({ return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }) body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }, req)
}); });
}; };
@@ -67,7 +67,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg })); return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg }));
} }
return res.reply({ return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }) body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }, req)
}); });
}; };
@@ -88,8 +88,9 @@ export default (router, tpl) => {
return renderError("Passwords do not match."); return renderError("Passwords do not match.");
} }
// reCAPTCHA verification // reCAPTCHA verification (bypassed for .onion requests as Google reCAPTCHA cannot validate .onion domains)
if (cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) { const isOnion = lib.isOnionRequest(req);
if (!isOnion && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
const rcToken = req.post['g-recaptcha-response']; const rcToken = req.post['g-recaptcha-response'];
if (!rcToken) return renderError("Please complete the reCAPTCHA."); if (!rcToken) return renderError("Please complete the reCAPTCHA.");
try { try {
@@ -167,8 +168,8 @@ export default (router, tpl) => {
const avatarFile = 'default.png'; const avatarFile = 'default.png';
await db` await db`
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping) insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping, use_alternative_infobox)
values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}) values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}, ${cfg.websrv.user_alternative_infobox !== false})
`; `;
} catch (err) { } catch (err) {
console.error(`[REGISTER] DB Error during user creation:`, err); console.error(`[REGISTER] DB Error during user creation:`, err);

View File

@@ -49,7 +49,7 @@ const gateOptions = {
}, },
cloudflare_status: { cloudflare_status: {
status: 'ok', status: 'ok',
location: 'Frankfurt', location: cfg.websrv.private_society_gate_location || 'Frankfurt',
name: 'Cloudflare', name: 'Cloudflare',
status_text: 'Working', status_text: 'Working',
}, },
@@ -79,11 +79,13 @@ const nginx502Fallback = `<html>
</html>`; </html>`;
// Login + Register modal injected before </body> // Login + Register modal injected before </body>
// This string is built at startup so it can reference cfg.recaptcha values. // Dynamic function so reCAPTCHA can be bypassed for .onion requests
const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key); const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || ''; const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
const gateLoginInjection = ` function getGateLoginInjection(req) {
const rcEnabled = _rcEnabled && !lib.isOnionRequest(req);
return `
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div> <div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
<div id="gate-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10000;align-items:center;justify-content:center;"> <div id="gate-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10000;align-items:center;justify-content:center;">
@@ -123,7 +125,7 @@ const gateLoginInjection = `
<input type="text" name="token" placeholder="Invite token" autocomplete="off" <input type="text" name="token" placeholder="Invite token" autocomplete="off"
style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" /> style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" />
<input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" /> <input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" />
${_rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''} ${rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
<button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;" <button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;"
onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button> onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button>
<p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;"> <p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;">
@@ -264,8 +266,9 @@ const gateLoginInjection = `
if (_sb.length > 12) _sb = _sb.slice(-12); if (_sb.length > 12) _sb = _sb.slice(-12);
}); });
</script> </script>
${_rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''} ${rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
`; `;
}
// Text injected into the "What can I do?" section // Text injected into the "What can I do?" section
@@ -282,11 +285,40 @@ try {
} }
// Called on every gated request — produces a fresh page with unique Ray ID + timestamp // Called on every gated request — produces a fresh page with unique Ray ID + timestamp
// Accepts an optional request object to inject the visitor's real IP into the footer reveal. // Accepts an optional request object to inject the visitor's real IP into the footer reveal and dynamic host into page text/title.
function buildGatePage(req) { function buildGatePage(req) {
if (!_cfRender) return nginx502Fallback; if (!_cfRender) return nginx502Fallback;
let html = _cfRender({ ...gateOptions, what_can_i_do: gateSignInButton }); let reqHost = null;
if (req && req.headers) {
const rawHost = req.headers['x-forwarded-host'] || req.headers['host'];
if (rawHost) {
const firstHost = rawHost.split(',')[0].trim();
let extracted = '';
if (firstHost.startsWith('[')) {
const closeBracket = firstHost.indexOf(']');
extracted = closeBracket !== -1 ? firstHost.substring(1, closeBracket) : firstHost;
} else {
extracted = firstHost.split(':')[0].trim();
}
const isLocalHost = ['localhost', '127.0.0.1', '::1', 'f0ckm', '0.0.0.0', 'localhost.localdomain'].includes(extracted.toLowerCase());
if (extracted && !isLocalHost) {
reqHost = extracted;
}
}
}
if (!reqHost) reqHost = cfg.main.url.domain;
const currentGateOptions = {
...gateOptions,
host_status: {
...gateOptions.host_status,
location: reqHost,
},
what_can_i_do: gateSignInButton,
};
let html = _cfRender(currentGateOptions);
// Inject real visitor IP into the footer "Your IP: [Click to reveal]" span // Inject real visitor IP into the footer "Your IP: [Click to reveal]" span
if (req) { if (req) {
@@ -305,9 +337,9 @@ function buildGatePage(req) {
// Patch the <title> to include the domain — matches real Cloudflare behaviour // Patch the <title> to include the domain — matches real Cloudflare behaviour
html = html.replace( html = html.replace(
'<title>502: Bad Gateway</title>', '<title>502: Bad Gateway</title>',
`<title>${cfg.main.url.domain} | 502: Bad gateway</title>` `<title>${reqHost} | 502: Bad gateway</title>`
); );
html = html.replace('</body>', gateLoginInjection + '\n</body>'); html = html.replace('</body>', getGateLoginInjection(req) + '\n</body>');
return html; return html;
} }
@@ -316,7 +348,14 @@ const nginx502 = (cfg.websrv.private_society && cfg.websrv.private_society_gate
? null ? null
: nginx502Fallback; : nginx502Fallback;
// Custom gate template — resolved once at boot from config
// Set private_society_gate: "custom" and private_society_gate_template: "your-template-name" (no .html)
const _customGateTemplate = (cfg.websrv.private_society && cfg.websrv.private_society_gate === 'custom' && cfg.websrv.private_society_gate_template)
? String(cfg.websrv.private_society_gate_template).replace(/\.html$/i, '')
: null;
if (nginx502 === null) console.log('[BOOT] Private society gate: Cloudflare dynamic mode'); if (nginx502 === null) console.log('[BOOT] Private society gate: Cloudflare dynamic mode');
if (_customGateTemplate) console.log(`[BOOT] Private society gate: Custom template → views/${_customGateTemplate}.html (flummpress resolves extension)`);
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@@ -456,6 +495,22 @@ process.on('uncaughtException', err => {
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes // Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
// Block incoming requests to .onion if Tor Hidden Service is explicitly disabled in config
if (cfg.websrv?.enable_tor_hs === false && lib.isOnionRequest(req)) {
res.writeHead(503).end();
req.url.pathname = '/tor_hs_disabled_bypass';
return;
}
// Tor Onion-Location header: advertises .onion service counterpart to Tor Browser users when enabled
if (cfg.websrv?.enable_tor_hs !== false && cfg.main?.onion && !lib.isOnionRequest(req)) {
const p = req.url?.pathname || '/';
const s = req.url?.search || '';
const onionHost = cfg.main.onion.replace(/\/+$/, '');
res.setHeader('Onion-Location', `${onionHost}${p}${s}`);
}
if (isSecure) { if (isSecure) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
} }
@@ -604,7 +659,7 @@ process.on('uncaughtException', err => {
} }
return; return;
} }
const availableThemes = cfg.websrv.themes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'iced', 'orange', 'p1nk', '4d']; const availableThemes = cfg.websrv.themes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'f0ck98', 'iced', 'orange', 'p1nk', '4d'];
const defaultTheme = cfg.websrv.theme || 'amoled'; const defaultTheme = cfg.websrv.theme || 'amoled';
req.theme = (req.cookies?.theme && availableThemes.includes(req.cookies.theme)) ? req.cookies.theme : defaultTheme; req.theme = (req.cookies?.theme && availableThemes.includes(req.cookies.theme)) ? req.cookies.theme : defaultTheme;
req.fullscreen = req.cookies.fullscreen || 0; req.fullscreen = req.cookies.fullscreen || 0;
@@ -801,18 +856,21 @@ process.on('uncaughtException', err => {
req.url.pathname = '/private_society_bypass'; req.url.pathname = '/private_society_bypass';
return; return;
} }
// For page requests, return 502 Bad Gateway for all paths except homepage (which shows the gate) // For page requests, serve the gate for all paths (custom template or fallback)
if (req.url.pathname !== '/') { if (_customGateTemplate) {
res.writeHead(200, { 'Content-Type': 'text/html' }).end(tpl.render(_customGateTemplate, {}, req));
} else if (req.url.pathname !== '/') {
// Non-home paths: always show the static/cloudflare gate
res.writeHead(200, { 'Content-Type': 'text/html' }).end(nginx502 ?? buildGatePage(req)); res.writeHead(200, { 'Content-Type': 'text/html' }).end(nginx502 ?? buildGatePage(req));
req.url.pathname = '/private_society_bypass'; } else if (nginx502 === null) {
return; // Homepage in cloudflare dynamic mode: serve the CF gate directly
}
// Homepage: in cloudflare dynamic mode serve gate directly; otherwise fall through to template
if (nginx502 === null) {
res.writeHead(200, { 'Content-Type': 'text/html' }).end(buildGatePage(req)); res.writeHead(200, { 'Content-Type': 'text/html' }).end(buildGatePage(req));
req.url.pathname = '/private_society_bypass'; } else {
// Homepage in plain nginx mode: fall through to index.html template
return; return;
} }
req.url.pathname = '/private_society_bypass';
return;
} }
} }
}); });
@@ -1292,6 +1350,7 @@ process.on('uncaughtException', err => {
show_mime_picker: cfg.websrv.show_mime_picker !== false, show_mime_picker: cfg.websrv.show_mime_picker !== false,
private_society: cfg.websrv.private_society || false, private_society: cfg.websrv.private_society || false,
private_society_gate: cfg.websrv.private_society_gate || '', private_society_gate: cfg.websrv.private_society_gate || '',
private_society_gate_location: cfg.websrv.private_society_gate_location || 'Frankfurt',
show_content_warning: cfg.websrv.show_content_warning !== false, show_content_warning: cfg.websrv.show_content_warning !== false,
web_url_upload: !!cfg.websrv.web_url_upload, web_url_upload: !!cfg.websrv.web_url_upload,
enable_youtube_upload: cfg.websrv.enable_youtube_upload !== false, enable_youtube_upload: cfg.websrv.enable_youtube_upload !== false,
@@ -1403,26 +1462,39 @@ process.on('uncaughtException', err => {
} }
} }
// Resolve per-request recaptcha preference (disabled for .onion requests or when passed false)
const effectiveReq = req || (data && data.req);
const defaultRecaptcha = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
let perRequestRecaptcha = defaultRecaptcha;
if (effectiveReq && lib.isOnionRequest(effectiveReq)) {
perRequestRecaptcha = false;
} else if (data && typeof data.recaptcha_enabled === 'boolean') {
perRequestRecaptcha = data.recaptcha_enabled;
}
// Build data: globals first, then caller-supplied data, then per-request i18n last // Build data: globals first, then caller-supplied data, then per-request i18n last
// so t/lang always reflect the user's language, not the site default. // so t/lang always reflect the user's language, not the site default.
// ALSO mutate globals.t and globals.lang: flummpress spreads this.#globals LAST // ALSO mutate globals.t, globals.lang, globals.recaptcha_enabled: flummpress spreads this.#globals LAST
// inside render(), so globals must carry the per-request values too. // inside render(), so globals must carry the per-request values too.
globals.t = perRequestT; globals.t = perRequestT;
globals.lang = perRequestLang; globals.lang = perRequestLang;
globals.recaptcha_enabled = perRequestRecaptcha;
// Resolve per-request infobox preference // Resolve per-request infobox preference
// Guests always get false — the alternative infobox is a logged-in user preference only // Guests always get false — the alternative infobox is a logged-in user preference only
const useAltInfobox = (req && req.session && typeof req.session.use_alternative_infobox === 'boolean') const activeReq = req || effectiveReq;
? req.session.use_alternative_infobox const useAltInfobox = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_infobox === 'boolean')
: (req && !req.session ? activeReq.session.use_alternative_infobox
: (activeReq && !activeReq.session
? false ? false
: (data && typeof data.user_alternative_infobox === 'boolean' : (data && typeof data.user_alternative_infobox === 'boolean'
? data.user_alternative_infobox ? data.user_alternative_infobox
: (cfg.websrv.user_alternative_infobox !== false))); : (cfg.websrv.user_alternative_infobox !== false)));
const useAltSteuerung = (req && req.session && typeof req.session.use_alternative_steuerung === 'boolean') const useAltSteuerung = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_steuerung === 'boolean')
? req.session.use_alternative_steuerung ? activeReq.session.use_alternative_steuerung
: (req && !req.session : (activeReq && !activeReq.session
? false ? false
: (data && typeof data.user_alternative_steuerung === 'boolean' : (data && typeof data.user_alternative_steuerung === 'boolean'
? data.user_alternative_steuerung ? data.user_alternative_steuerung
@@ -1431,11 +1503,12 @@ process.on('uncaughtException', err => {
data = Object.assign({}, globals, data || {}, { data = Object.assign({}, globals, data || {}, {
t: perRequestT, t: perRequestT,
lang: perRequestLang, lang: perRequestLang,
recaptcha_enabled: perRequestRecaptcha,
user_alternative_infobox: useAltInfobox, user_alternative_infobox: useAltInfobox,
user_alternative_steuerung: useAltSteuerung, user_alternative_steuerung: useAltSteuerung,
user_banner_enabled: cfg.websrv.user_banner_enabled !== false, user_banner_enabled: cfg.websrv.user_banner_enabled !== false,
comment_display_mode: (req && req.session && typeof req.session.comment_display_mode === 'number') comment_display_mode: (activeReq && activeReq.session && typeof activeReq.session.comment_display_mode === 'number')
? req.session.comment_display_mode ? activeReq.session.comment_display_mode
: (data && typeof data.comment_display_mode === 'number' : (data && typeof data.comment_display_mode === 'number'
? data.comment_display_mode ? data.comment_display_mode
: (cfg.websrv.default_comment_display_mode || 0)) : (cfg.websrv.default_comment_display_mode || 0))
@@ -1447,17 +1520,19 @@ process.on('uncaughtException', err => {
data.custom_brand_image = brand[Math.floor(Math.random() * brand.length)]; data.custom_brand_image = brand[Math.floor(Math.random() * brand.length)];
} }
if (req) { if (activeReq) {
if (req.mode !== undefined) data.mode = req.mode; data.recaptcha_enabled = perRequestRecaptcha;
data.theme = req.theme || req.cookies?.theme || cfg.websrv.theme || 'f0ck'; if (activeReq.mode !== undefined) data.mode = activeReq.mode;
if (!data.url) data.url = req.url; data.theme = activeReq.theme || activeReq.cookies?.theme || cfg.websrv.theme || 'f0ck';
data.user_strict_bool = (req.session && req.session.strict_mode) ? true : false; if (!data.url) data.url = activeReq.url;
data.user_logged_in_bool = !!req.session; data.user_strict_bool = (activeReq.session && activeReq.session.strict_mode) ? true : false;
data.csrf_token = req.session?.csrf_token || ''; data.user_logged_in_bool = !!activeReq.session;
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1)); data.csrf_token = activeReq.session?.csrf_token || '';
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1)); data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
data.web_url_upload = data.web_url_upload !== undefined ? data.web_url_upload : !!cfg.websrv.web_url_upload; data.web_url_upload = data.web_url_upload !== undefined ? data.web_url_upload : !!cfg.websrv.web_url_upload;
} else { } else {
data.recaptcha_enabled = perRequestRecaptcha;
data.theme = data.theme || cfg.websrv.theme || 'f0ck'; data.theme = data.theme || cfg.websrv.theme || 'f0ck';
data.user_strict_bool = false; data.user_strict_bool = false;
data.user_logged_in_bool = false; data.user_logged_in_bool = false;

35
views/_gate_template.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html theme="@if(typeof theme !== 'undefined'){{ theme }}@else{{ default_theme || 'amoled' }}@endif">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ domain }}</title>
<link rel="icon" type="image/gif" href="/s/img/favicon.gif" />
<link rel="stylesheet" id="injected-gate-styles" href="/s/css/f0ckm.css?v={{ ts }}">
</head>
<body class="{{ default_layout === 'legacy' ? 'layout-legacy' : 'layout-modern' }}">
<div class="wrapper" style="text-align: center;">
<h2>Example Gate</h2>
<button type="button" onclick="openLoginGate();">Login</button>
<div id="gate-container" style="display:none;">
@include(snippets/navbar)
@include(snippets/footer)
</div>
</div>
<script>
function openLoginGate() {
document.getElementById('gate-container').style.display = 'block';
const tryOpen = () => {
const m = document.getElementById('login-modal');
if (m) { m.style.display = 'flex'; }
else { setTimeout(tryOpen, 50); }
};
tryOpen();
}
</script>
</body>
</html>

View File

@@ -319,7 +319,7 @@
ModAction.confirm('Verify User', 'Manually verify account for <strong>' + escHTML(userName) + '</strong>?', async () => { ModAction.confirm('Verify User', 'Manually verify account for <strong>' + escHTML(userName) + '</strong>?', async () => {
var res = await fetch('/api/v2/admin/users/activate', { var res = await fetch('/api/v2/admin/users/activate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id }) body: JSON.stringify({ user_id: id })
}); });
var data = await res.json(); var data = await res.json();
@@ -339,7 +339,7 @@
ModAction.confirm('Ban User', 'Reason for banning <strong>' + escHTML(userName) + '</strong>?', async (reason) => { ModAction.confirm('Ban User', 'Reason for banning <strong>' + escHTML(userName) + '</strong>?', async (reason) => {
var res = await fetch('/api/v2/admin/ban', { var res = await fetch('/api/v2/admin/ban', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, reason: reason, duration: 'permanent' }) body: JSON.stringify({ user_id: id, reason: reason, duration: 'permanent' })
}); });
var data = await res.json(); var data = await res.json();
@@ -359,7 +359,7 @@
ModAction.confirm('Unban User', 'Unban account for <strong>' + escHTML(userName) + '</strong>?', async () => { ModAction.confirm('Unban User', 'Unban account for <strong>' + escHTML(userName) + '</strong>?', async () => {
var res = await fetch('/api/v2/admin/unban', { var res = await fetch('/api/v2/admin/unban', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id }) body: JSON.stringify({ user_id: id })
}); });
var data = await res.json(); var data = await res.json();
@@ -379,7 +379,7 @@
ModAction.confirm('Delete Uploads', 'Are you SURE you want to delete ALL uploads by <strong>' + escHTML(userName) + '</strong>? This cannot be undone.', async () => { ModAction.confirm('Delete Uploads', 'Are you SURE you want to delete ALL uploads by <strong>' + escHTML(userName) + '</strong>? This cannot be undone.', async () => {
var res = await fetch('/api/v2/admin/users/bulk-delete-items', { var res = await fetch('/api/v2/admin/users/bulk-delete-items', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, username: btn.dataset.username }) body: JSON.stringify({ user_id: id, username: btn.dataset.username })
}); });
var data = await res.json(); var data = await res.json();
@@ -398,7 +398,7 @@
ModAction.confirm('Delete Comments', 'Are you SURE you want to delete ALL comments by <strong>' + escHTML(userName) + '</strong>? This will be permanent.', async () => { ModAction.confirm('Delete Comments', 'Are you SURE you want to delete ALL comments by <strong>' + escHTML(userName) + '</strong>? This will be permanent.', async () => {
var res = await fetch('/api/v2/admin/users/bulk-delete-comments', { var res = await fetch('/api/v2/admin/users/bulk-delete-comments', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, username: btn.dataset.username }) body: JSON.stringify({ user_id: id, username: btn.dataset.username })
}); });
var data = await res.json(); var data = await res.json();
@@ -422,7 +422,7 @@
ModAction.confirm('Set Display Name', hint, async (newName) => { ModAction.confirm('Set Display Name', hint, async (newName) => {
var res = await fetch('/api/v2/admin/users/set-display-name', { var res = await fetch('/api/v2/admin/users/set-display-name', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, display_name: newName || '' }) body: JSON.stringify({ user_id: id, display_name: newName || '' })
}); });
var data = await res.json(); var data = await res.json();
@@ -457,7 +457,7 @@
ModAction.confirm('Unlock Layout', 'Unlock comment layout for <strong>' + escHTML(userName) + '</strong>? They will be able to change it again.', async () => { ModAction.confirm('Unlock Layout', 'Unlock comment layout for <strong>' + escHTML(userName) + '</strong>? They will be able to change it again.', async () => {
var res = await fetch('/api/v2/admin/users/lock-layout', { var res = await fetch('/api/v2/admin/users/lock-layout', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, lock: false }) body: JSON.stringify({ user_id: id, lock: false })
}); });
var data = await res.json(); var data = await res.json();
@@ -481,7 +481,7 @@
var mode = document.getElementById('force-mode-select').value; var mode = document.getElementById('force-mode-select').value;
var res = await fetch('/api/v2/admin/users/lock-layout', { var res = await fetch('/api/v2/admin/users/lock-layout', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, lock: true, mode: mode }) body: JSON.stringify({ user_id: id, lock: true, mode: mode })
}); });
var data = await res.json(); var data = await res.json();
@@ -616,7 +616,7 @@
try { try {
var res = await fetch('/api/v2/admin/users/create', { var res = await fetch('/api/v2/admin/users/create', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ username, email: email || null, password, role }) body: JSON.stringify({ username, email: email || null, password, role })
}); });
var data = await res.json(); var data = await res.json();
@@ -662,7 +662,7 @@
var role = selectEl.value; var role = selectEl.value;
var res = await fetch('/api/v2/admin/users/set-role', { var res = await fetch('/api/v2/admin/users/set-role', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
body: JSON.stringify({ user_id: id, role }) body: JSON.stringify({ user_id: id, role })
}); });
var data = await res.json(); var data = await res.json();

View File

@@ -296,7 +296,7 @@
@if(item.src.short) @if(item.src.short)
<tr> <tr>
<th>{{ t('info_modal.source') || 'Source' }}</th> <th>{{ t('info_modal.source') || 'Source' }}</th>
<td><a href="{{ item.src.long }}" target="_blank">{{ item.src.short }}</a></td> <td><a href="{{ item.src.long }}" target="_blank" style="word-break: break-all;">{{ item.src.short }}</a></td>
</tr> </tr>
@endif @endif
</table> </table>

View File

@@ -238,7 +238,7 @@
@if(item.src.short) @if(item.src.short)
<tr> <tr>
<th>{{ t('info_modal.source') || 'Source' }}</th> <th>{{ t('info_modal.source') || 'Source' }}</th>
<td><a href="{{ item.src.long }}" target="_blank">{{ item.src.short }}</a></td> <td><a href="{{ item.src.long }}" target="_blank" style="word-break: break-all;">{{ item.src.short }}</a></td>
</tr> </tr>
@endif @endif
</table> </table>

View File

@@ -56,6 +56,7 @@
@if(!private_society || session) @if(!private_society || session)
@if(development || (private_society && !session)) @if(development || (private_society && !session))
<link rel="stylesheet" href="/s/css/f0ckm.css?v={{ ts }}"> <link rel="stylesheet" href="/s/css/f0ckm.css?v={{ ts }}">
<link rel="stylesheet" href="/s/css/98.css?v={{ ts }}">
@endif @endif
@if(development) @if(development)
<link rel="stylesheet" href="/s/css/v0ck.css?v={{ ts }}"> <link rel="stylesheet" href="/s/css/v0ck.css?v={{ ts }}">

View File

@@ -48,7 +48,7 @@
<div class="profile_head_username"> <div class="profile_head_username">
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container"> <div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
<span id="nav-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)&#9889;&nbsp;@elseif(user.is_moderator)&#128737;&nbsp;@endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif <span id="profile-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)&#9889;&nbsp;@elseif(user.is_moderator)&#128737;&nbsp;@endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif
@if(session && session.id === user.user_id) @if(session && session.id === user.user_id)
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color"> <button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
<i class="fa-solid fa-pen"></i> <i class="fa-solid fa-pen"></i>
@@ -509,18 +509,27 @@
nameContainer.style.display = 'flex'; nameContainer.style.display = 'flex';
nameEditContainer.style.display = 'none'; nameEditContainer.style.display = 'none';
const nameSpan = document.getElementById('nav-display-name'); const updateNameNode = (el, text) => {
if (nameSpan) { if (!el) return;
nameSpan.style.color = color; for (let i = el.childNodes.length - 1; i >= 0; i--) {
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) { let node = el.childNodes[i];
let node = nameSpan.childNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) { if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
// Match and preserve any non-breaking spaces at the start (from admin/mod badges) const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
const match = node.nodeValue.match(new RegExp('^(\\\\s*)(.*)')); node.nodeValue = (match ? match[1] : '') + text;
node.nodeValue = (match ? match[1] : '') + (displayName ? displayName : '{{ user.user }}');
break; break;
} }
} }
};
const targetName = displayName ? displayName : '{{ user.user }}';
const profileSpan = document.getElementById('profile-display-name');
if (profileSpan) {
profileSpan.style.color = color;
updateNameNode(profileSpan, targetName);
}
const navSpan = document.getElementById('nav-display-name');
if (navSpan) {
navSpan.style.color = color;
updateNameNode(navSpan, targetName);
} }
const bracket = document.getElementById('username-bracket'); const bracket = document.getElementById('username-bracket');
if (bracket) { if (bracket) {