commit 01be0ea041b9f7b8077f4fa196bdcdc93be4d8a0
parent b29dce0e46364d740b5fecd5811bc33a7736ab08
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date: Mon, 16 Feb 2026 23:15:40 -0700
UI polish
first round of UI polish
Diffstat:
6 files changed, 464 insertions(+), 0 deletions(-)
diff --git a/CLEAN_INSTALL/public/app.js b/CLEAN_INSTALL/public/app.js
@@ -19,6 +19,7 @@ const mobilePagerEl = document.getElementById("mobilePager");
const mobileModBtn = document.getElementById("mobileModBtn");
const enableNotifsBtn = document.getElementById("enableNotifs");
const notifStatus = document.getElementById("notifStatus");
+const toggleReactionsEl = document.getElementById("toggleReactions");
const authHint = document.getElementById("authHint");
const userLabel = document.getElementById("userLabel");
@@ -57,6 +58,13 @@ const modModalPrimary = document.getElementById("modModalPrimary");
const modModalCancel = document.getElementById("modModalCancel");
const modModalClose = document.getElementById("modModalClose");
const modModalStatus = document.getElementById("modModalStatus");
+const mediaModal = document.getElementById("mediaModal");
+const mediaModalTitle = document.getElementById("mediaModalTitle");
+const mediaModalImg = document.getElementById("mediaModalImg");
+const mediaModalOpenLink = document.getElementById("mediaModalOpenLink");
+const mediaModalCopyLink = document.getElementById("mediaModalCopyLink");
+const mediaModalClose = document.getElementById("mediaModalClose");
+const mediaModalStatus = document.getElementById("mediaModalStatus");
const newPostForm = document.getElementById("newPostForm");
const pollinatePanel = document.getElementById("pollinatePanel");
@@ -206,6 +214,7 @@ const CLIENT_AUDIO_UPLOAD_MAX_BYTES = 150 * 1024 * 1024;
let allowedPostReactions = ["👍", "❤️", "😡", "😭", "🥺", "😂", "⭐"];
let allowedChatReactions = ["👍", "❤️", "😡", "😭", "🥺", "😂"];
let userPrefs = { starredPostIds: [], hiddenPostIds: [], ignoredUsers: [], blockedUsers: [] };
+let showReactions = localStorage.getItem("bzl_showReactions") !== "0";
let activeHiveView = "all";
let collections = [];
let customRoles = [];
@@ -365,6 +374,27 @@ function setModModalOpen(open) {
}
}
+function setMediaModalOpen(open) {
+ if (!mediaModal) return;
+ mediaModal.classList.toggle("hidden", !open);
+ if (!open) {
+ if (mediaModalImg) mediaModalImg.src = "";
+ if (mediaModalOpenLink) mediaModalOpenLink.href = "#";
+ if (mediaModalStatus) mediaModalStatus.textContent = "";
+ if (mediaModalTitle) mediaModalTitle.textContent = "Media";
+ }
+}
+
+function openMediaModal(url) {
+ const src = String(url || "").trim();
+ if (!src) return;
+ if (!mediaModalImg) return;
+ mediaModalImg.src = src;
+ if (mediaModalOpenLink) mediaModalOpenLink.href = src;
+ if (mediaModalStatus) mediaModalStatus.textContent = "";
+ setMediaModalOpen(true);
+}
+
function gateTokenLabel(token) {
const t = String(token || "").trim().toLowerCase();
if (!t) return { label: "", color: "" };
@@ -1959,6 +1989,7 @@ function markReactPulse(kind, id, emoji) {
}
function renderReactionButtons({ kind, id, reactions, postId }) {
+ if (!showReactions) return "";
const r = reactions && typeof reactions === "object" ? reactions : {};
const emojis = kind === "post" ? allowedPostReactions : allowedChatReactions;
return `<div class="reactionsRow">
@@ -2725,6 +2756,7 @@ function renderModPanel() {
}
function renderChatPanel(forceScroll = false) {
+ const mediaState = captureMediaState(chatMessagesEl);
if (activeDmThreadId) {
const thread = dmThreadsById.get(activeDmThreadId) || null;
if (!thread) {
@@ -2756,6 +2788,7 @@ function renderChatPanel(forceScroll = false) {
? `<button type="button" class="ghost smallBtn" data-dmrequest="${escapeHtml(thread.other)}">Request again</button>`
: `<div class="muted">Waiting for @${escapeHtml(thread.other)}…</div>`;
chatMessagesEl.innerHTML = `<div class="small muted">${promptHtml}</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -2781,6 +2814,7 @@ function renderChatPanel(forceScroll = false) {
decorateMentionNodesInElement(contentEl);
decorateYouTubeEmbedsInElement(contentEl);
}
+ restoreMediaState(chatMessagesEl, mediaState);
if (forceScroll || atBottomBefore) chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
return;
}
@@ -2794,6 +2828,7 @@ function renderChatPanel(forceScroll = false) {
if (walkieBarEl) walkieBarEl.classList.add("hidden");
if (chatForm) chatForm.classList.remove("hidden");
chatMessagesEl.innerHTML = `<div class="small muted">No chat selected.</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -2820,6 +2855,7 @@ function renderChatPanel(forceScroll = false) {
if (chatSendBtn) chatSendBtn.disabled = !(loggedInUser && canChatWrite && !isWalkie);
if (post.deleted) {
chatMessagesEl.innerHTML = `<div class="small muted">Post was deleted.</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -2898,9 +2934,50 @@ function renderChatPanel(forceScroll = false) {
decorateMentionNodesInElement(contentEl);
decorateYouTubeEmbedsInElement(contentEl);
}
+ restoreMediaState(chatMessagesEl, mediaState);
if (forceScroll || atBottomBefore) chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
}
+function captureMediaState(containerEl) {
+ if (!containerEl) return [];
+ const list = [];
+ for (const el of containerEl.querySelectorAll("audio, video")) {
+ try {
+ const src = el.currentSrc || el.getAttribute("src") || "";
+ if (!src) continue;
+ list.push({
+ src,
+ currentTime: Number(el.currentTime || 0),
+ paused: Boolean(el.paused),
+ volume: Number.isFinite(el.volume) ? el.volume : 1,
+ playbackRate: Number.isFinite(el.playbackRate) ? el.playbackRate : 1
+ });
+ } catch {
+ // ignore
+ }
+ }
+ return list;
+}
+
+function restoreMediaState(containerEl, mediaState) {
+ if (!containerEl || !Array.isArray(mediaState) || mediaState.length === 0) return;
+ const els = Array.from(containerEl.querySelectorAll("audio, video"));
+ for (const s of mediaState) {
+ const src = String(s?.src || "");
+ if (!src) continue;
+ const el = els.find((x) => (x.currentSrc || x.getAttribute("src") || "") === src);
+ if (!el) continue;
+ try {
+ if (Number.isFinite(s.volume)) el.volume = s.volume;
+ if (Number.isFinite(s.playbackRate)) el.playbackRate = s.playbackRate;
+ if (Number.isFinite(s.currentTime)) el.currentTime = s.currentTime;
+ if (!s.paused) el.play().catch(() => {});
+ } catch {
+ // ignore
+ }
+ }
+}
+
function appendChatHtmlAndDecorate(html, atBottomBefore) {
if (!chatMessagesEl) return null;
chatMessagesEl.insertAdjacentHTML("beforeend", html);
@@ -3535,6 +3612,48 @@ function insertAudioTag(target, srcUrl) {
document.execCommand("insertHTML", false, `<audio controls preload="none" src="${safe}"></audio>`);
}
+function installDropUpload(targetEl, { allowImages = true, allowAudio = true } = {}) {
+ if (!targetEl) return;
+ const setActive = (on) => {
+ try {
+ targetEl.classList.toggle("isDropActive", Boolean(on));
+ } catch {
+ // ignore
+ }
+ };
+ targetEl.addEventListener("dragover", (e) => {
+ if (!e.dataTransfer) return;
+ if (!e.dataTransfer.types || !Array.from(e.dataTransfer.types).includes("Files")) return;
+ e.preventDefault();
+ setActive(true);
+ });
+ targetEl.addEventListener("dragleave", () => setActive(false));
+ targetEl.addEventListener("drop", async (e) => {
+ setActive(false);
+ const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
+ if (!files.length) return;
+ e.preventDefault();
+ e.stopPropagation();
+
+ for (const file of files.slice(0, 4)) {
+ const type = String(file.type || "").toLowerCase();
+ const name = String(file.name || "").toLowerCase();
+ const isImg = type.startsWith("image/") || /\.(gif|png|jpe?g|webp)$/.test(name);
+ const isAud = type.startsWith("audio/") || /\.(mp3|wav|ogg|m4a|aac|webm)$/.test(name);
+ if (isImg && allowImages) {
+ const url = await uploadMediaFile(file, "image");
+ if (!url) continue;
+ targetEl.focus();
+ document.execCommand("insertImage", false, url);
+ } else if (isAud && allowAudio) {
+ const url = await uploadMediaFile(file, "audio");
+ if (!url) continue;
+ insertAudioTag(targetEl, url);
+ }
+ }
+ });
+}
+
document.querySelector(".editorShell .toolbar")?.addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
@@ -5305,6 +5424,55 @@ applyChatWidth(readStoredChatWidth(), false);
applyModWidth(readStoredModWidth(), false);
applyPeopleWidth(readStoredPeopleWidth(), false);
+if (toggleReactionsEl) {
+ toggleReactionsEl.checked = showReactions;
+ toggleReactionsEl.addEventListener("change", () => {
+ showReactions = Boolean(toggleReactionsEl.checked);
+ localStorage.setItem("bzl_showReactions", showReactions ? "1" : "0");
+ renderFeed();
+ renderChatPanel();
+ });
+}
+
+installDropUpload(editor, { allowImages: true, allowAudio: true });
+installDropUpload(chatEditor, { allowImages: true, allowAudio: true });
+installDropUpload(profileBioEditor, { allowImages: true, allowAudio: true });
+installDropUpload(editModalEditor, { allowImages: true, allowAudio: true });
+
+mediaModal?.addEventListener("click", (e) => {
+ if (e.target?.getAttribute?.("data-mediamodalclose")) setMediaModalOpen(false);
+});
+mediaModalClose?.addEventListener("click", () => setMediaModalOpen(false));
+mediaModalCopyLink?.addEventListener("click", async () => {
+ const url = String(mediaModalOpenLink?.href || "").trim();
+ if (!url || url === "#") return;
+ try {
+ await navigator.clipboard.writeText(url);
+ if (mediaModalStatus) mediaModalStatus.textContent = "Copied.";
+ } catch {
+ if (mediaModalStatus) mediaModalStatus.textContent = "Copy failed (clipboard blocked).";
+ }
+});
+document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape" && mediaModal && !mediaModal.classList.contains("hidden")) setMediaModalOpen(false);
+});
+document.body.addEventListener("click", (e) => {
+ const img = e.target?.closest?.("img");
+ if (!img) return;
+ if (img.id === "profilePreview") return;
+ if (img.closest("#mediaModal")) return;
+ const inAllowed =
+ img.closest(".chatMsg .content") ||
+ img.closest(".profileBio") ||
+ img.closest(".profileCard") ||
+ img.closest(".editor") ||
+ img.closest("#editModalEditor");
+ if (!inAllowed) return;
+ const src = img.getAttribute("src") || "";
+ if (!src) return;
+ openMediaModal(src);
+});
+
setSidebarHidden(getSidebarHidden());
toggleSidebarBtn?.addEventListener("click", () => setSidebarHidden(true));
showSidebarBtn?.addEventListener("click", () => setSidebarHidden(false));
diff --git a/CLEAN_INSTALL/public/index.html b/CLEAN_INSTALL/public/index.html
@@ -26,6 +26,14 @@
</div>
<section class="panel">
+ <div class="panelTitle">View</div>
+ <label class="checkRow">
+ <span>Show reactions bar</span>
+ <input id="toggleReactions" type="checkbox" />
+ </label>
+ </section>
+
+ <section class="panel">
<div class="panelTitle">Account</div>
<div class="small muted" id="authHint">Sign in to post, chat, and boost.</div>
@@ -462,6 +470,26 @@
</div>
</div>
+ <div id="mediaModal" class="modal hidden" role="dialog" aria-modal="true" aria-label="Media preview">
+ <div class="modalBackdrop" data-mediamodalclose="1"></div>
+ <div class="modalCard panel">
+ <div class="panelHeader">
+ <div class="panelTitle" id="mediaModalTitle">Media</div>
+ <div class="row">
+ <button id="mediaModalClose" class="ghost smallBtn" type="button">Close</button>
+ </div>
+ </div>
+ <div class="modalBody" id="mediaModalBody">
+ <img id="mediaModalImg" alt="Expanded media" />
+ <div class="row modalActions" style="justify-content:flex-start">
+ <a id="mediaModalOpenLink" class="ghost" href="#" target="_blank" rel="noreferrer">Open original</a>
+ <button id="mediaModalCopyLink" class="ghost" type="button">Copy link</button>
+ </div>
+ <div id="mediaModalStatus" class="small muted"></div>
+ </div>
+ </div>
+ </div>
+
<script src="/app.js?v=83"></script>
</body>
</html>
diff --git a/CLEAN_INSTALL/public/styles.css b/CLEAN_INSTALL/public/styles.css
@@ -1219,8 +1219,21 @@ button:disabled {
.profileBio img {
max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
border-radius: 12px;
display: block;
+ cursor: zoom-in;
+}
+
+.editor img {
+ max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
+ border-radius: 12px;
+ display: block;
+ margin-top: 8px;
+ cursor: zoom-in;
}
.ytEmbed {
@@ -1807,9 +1820,16 @@ button:disabled {
.chatMsg .content img {
max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
border-radius: 10px;
display: block;
margin-top: 6px;
+ cursor: zoom-in;
+}
+
+.chatReactions:empty {
+ display: none;
}
.chatMsg .content audio,
@@ -1819,6 +1839,22 @@ button:disabled {
margin-top: 8px;
}
+#mediaModalImg {
+ max-width: min(92vw, 1200px);
+ max-height: 78vh;
+ width: auto;
+ height: auto;
+ display: block;
+ margin: 0 auto;
+ border-radius: 14px;
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.editor.isDropActive {
+ outline: 2px dashed rgba(255, 62, 165, 0.7);
+ outline-offset: 6px;
+}
+
.chatMsg .meta {
font-size: 11px;
color: var(--muted);
diff --git a/public/app.js b/public/app.js
@@ -19,6 +19,7 @@ const mobilePagerEl = document.getElementById("mobilePager");
const mobileModBtn = document.getElementById("mobileModBtn");
const enableNotifsBtn = document.getElementById("enableNotifs");
const notifStatus = document.getElementById("notifStatus");
+const toggleReactionsEl = document.getElementById("toggleReactions");
const authHint = document.getElementById("authHint");
const userLabel = document.getElementById("userLabel");
@@ -48,6 +49,13 @@ const modModalPrimary = document.getElementById("modModalPrimary");
const modModalCancel = document.getElementById("modModalCancel");
const modModalClose = document.getElementById("modModalClose");
const modModalStatus = document.getElementById("modModalStatus");
+const mediaModal = document.getElementById("mediaModal");
+const mediaModalTitle = document.getElementById("mediaModalTitle");
+const mediaModalImg = document.getElementById("mediaModalImg");
+const mediaModalOpenLink = document.getElementById("mediaModalOpenLink");
+const mediaModalCopyLink = document.getElementById("mediaModalCopyLink");
+const mediaModalClose = document.getElementById("mediaModalClose");
+const mediaModalStatus = document.getElementById("mediaModalStatus");
const newPostForm = document.getElementById("newPostForm");
const pollinatePanel = document.getElementById("pollinatePanel");
@@ -201,6 +209,7 @@ const CLIENT_AUDIO_UPLOAD_MAX_BYTES = 150 * 1024 * 1024;
let allowedPostReactions = ["👍", "❤️", "😡", "😭", "🥺", "😂", "⭐"];
let allowedChatReactions = ["👍", "❤️", "😡", "😭", "🥺", "😂"];
let userPrefs = { starredPostIds: [], hiddenPostIds: [], ignoredUsers: [], blockedUsers: [] };
+let showReactions = localStorage.getItem("bzl_showReactions") !== "0";
let activeHiveView = "all";
let collections = [];
let customRoles = [];
@@ -570,6 +579,27 @@ function setModModalOpen(open) {
}
}
+function setMediaModalOpen(open) {
+ if (!mediaModal) return;
+ mediaModal.classList.toggle("hidden", !open);
+ if (!open) {
+ if (mediaModalImg) mediaModalImg.src = "";
+ if (mediaModalOpenLink) mediaModalOpenLink.href = "#";
+ if (mediaModalStatus) mediaModalStatus.textContent = "";
+ if (mediaModalTitle) mediaModalTitle.textContent = "Media";
+ }
+}
+
+function openMediaModal(url) {
+ const src = String(url || "").trim();
+ if (!src) return;
+ if (!mediaModalImg) return;
+ mediaModalImg.src = src;
+ if (mediaModalOpenLink) mediaModalOpenLink.href = src;
+ if (mediaModalStatus) mediaModalStatus.textContent = "";
+ setMediaModalOpen(true);
+}
+
function gateTokenLabel(token) {
const t = String(token || "").trim().toLowerCase();
if (!t) return { label: "", color: "" };
@@ -2193,6 +2223,7 @@ function markReactPulse(kind, id, emoji) {
}
function renderReactionButtons({ kind, id, reactions, postId }) {
+ if (!showReactions) return "";
const r = reactions && typeof reactions === "object" ? reactions : {};
const emojis = kind === "post" ? allowedPostReactions : allowedChatReactions;
return `<div class="reactionsRow">
@@ -3202,6 +3233,7 @@ function renderModPanel() {
}
function renderChatPanel(forceScroll = false) {
+ const mediaState = captureMediaState(chatMessagesEl);
if (activeDmThreadId) {
const thread = dmThreadsById.get(activeDmThreadId) || null;
if (!thread) {
@@ -3233,6 +3265,7 @@ function renderChatPanel(forceScroll = false) {
? `<button type="button" class="ghost smallBtn" data-dmrequest="${escapeHtml(thread.other)}">Request again</button>`
: `<div class="muted">Waiting for @${escapeHtml(thread.other)}…</div>`;
chatMessagesEl.innerHTML = `<div class="small muted">${promptHtml}</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -3258,6 +3291,7 @@ function renderChatPanel(forceScroll = false) {
decorateMentionNodesInElement(contentEl);
decorateYouTubeEmbedsInElement(contentEl);
}
+ restoreMediaState(chatMessagesEl, mediaState);
if (forceScroll || atBottomBefore) chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
return;
}
@@ -3271,6 +3305,7 @@ function renderChatPanel(forceScroll = false) {
if (walkieBarEl) walkieBarEl.classList.add("hidden");
if (chatForm) chatForm.classList.remove("hidden");
chatMessagesEl.innerHTML = `<div class="small muted">No chat selected.</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -3297,6 +3332,7 @@ function renderChatPanel(forceScroll = false) {
if (chatSendBtn) chatSendBtn.disabled = !(loggedInUser && canChatWrite && !isWalkie);
if (post.deleted) {
chatMessagesEl.innerHTML = `<div class="small muted">Post was deleted.</div>`;
+ restoreMediaState(chatMessagesEl, mediaState);
setReplyToMessage(null);
return;
}
@@ -3375,9 +3411,50 @@ function renderChatPanel(forceScroll = false) {
decorateMentionNodesInElement(contentEl);
decorateYouTubeEmbedsInElement(contentEl);
}
+ restoreMediaState(chatMessagesEl, mediaState);
if (forceScroll || atBottomBefore) chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
}
+function captureMediaState(containerEl) {
+ if (!containerEl) return [];
+ const list = [];
+ for (const el of containerEl.querySelectorAll("audio, video")) {
+ try {
+ const src = el.currentSrc || el.getAttribute("src") || "";
+ if (!src) continue;
+ list.push({
+ src,
+ currentTime: Number(el.currentTime || 0),
+ paused: Boolean(el.paused),
+ volume: Number.isFinite(el.volume) ? el.volume : 1,
+ playbackRate: Number.isFinite(el.playbackRate) ? el.playbackRate : 1
+ });
+ } catch {
+ // ignore
+ }
+ }
+ return list;
+}
+
+function restoreMediaState(containerEl, mediaState) {
+ if (!containerEl || !Array.isArray(mediaState) || mediaState.length === 0) return;
+ const els = Array.from(containerEl.querySelectorAll("audio, video"));
+ for (const s of mediaState) {
+ const src = String(s?.src || "");
+ if (!src) continue;
+ const el = els.find((x) => (x.currentSrc || x.getAttribute("src") || "") === src);
+ if (!el) continue;
+ try {
+ if (Number.isFinite(s.volume)) el.volume = s.volume;
+ if (Number.isFinite(s.playbackRate)) el.playbackRate = s.playbackRate;
+ if (Number.isFinite(s.currentTime)) el.currentTime = s.currentTime;
+ if (!s.paused) el.play().catch(() => {});
+ } catch {
+ // ignore
+ }
+ }
+}
+
function appendChatHtmlAndDecorate(html, atBottomBefore) {
if (!chatMessagesEl) return null;
chatMessagesEl.insertAdjacentHTML("beforeend", html);
@@ -4012,6 +4089,48 @@ function insertAudioTag(target, srcUrl) {
document.execCommand("insertHTML", false, `<audio controls preload="none" src="${safe}"></audio>`);
}
+function installDropUpload(targetEl, { allowImages = true, allowAudio = true } = {}) {
+ if (!targetEl) return;
+ const setActive = (on) => {
+ try {
+ targetEl.classList.toggle("isDropActive", Boolean(on));
+ } catch {
+ // ignore
+ }
+ };
+ targetEl.addEventListener("dragover", (e) => {
+ if (!e.dataTransfer) return;
+ if (!e.dataTransfer.types || !Array.from(e.dataTransfer.types).includes("Files")) return;
+ e.preventDefault();
+ setActive(true);
+ });
+ targetEl.addEventListener("dragleave", () => setActive(false));
+ targetEl.addEventListener("drop", async (e) => {
+ setActive(false);
+ const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
+ if (!files.length) return;
+ e.preventDefault();
+ e.stopPropagation();
+
+ for (const file of files.slice(0, 4)) {
+ const type = String(file.type || "").toLowerCase();
+ const name = String(file.name || "").toLowerCase();
+ const isImg = type.startsWith("image/") || /\.(gif|png|jpe?g|webp)$/.test(name);
+ const isAud = type.startsWith("audio/") || /\.(mp3|wav|ogg|m4a|aac|webm)$/.test(name);
+ if (isImg && allowImages) {
+ const url = await uploadMediaFile(file, "image");
+ if (!url) continue;
+ targetEl.focus();
+ document.execCommand("insertImage", false, url);
+ } else if (isAud && allowAudio) {
+ const url = await uploadMediaFile(file, "audio");
+ if (!url) continue;
+ insertAudioTag(targetEl, url);
+ }
+ }
+ });
+}
+
document.querySelector(".editorShell .toolbar")?.addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
@@ -6020,6 +6139,55 @@ applyChatWidth(readStoredChatWidth(), false);
applyModWidth(readStoredModWidth(), false);
applyPeopleWidth(readStoredPeopleWidth(), false);
+if (toggleReactionsEl) {
+ toggleReactionsEl.checked = showReactions;
+ toggleReactionsEl.addEventListener("change", () => {
+ showReactions = Boolean(toggleReactionsEl.checked);
+ localStorage.setItem("bzl_showReactions", showReactions ? "1" : "0");
+ renderFeed();
+ renderChatPanel();
+ });
+}
+
+installDropUpload(editor, { allowImages: true, allowAudio: true });
+installDropUpload(chatEditor, { allowImages: true, allowAudio: true });
+installDropUpload(profileBioEditor, { allowImages: true, allowAudio: true });
+installDropUpload(editModalEditor, { allowImages: true, allowAudio: true });
+
+mediaModal?.addEventListener("click", (e) => {
+ if (e.target?.getAttribute?.("data-mediamodalclose")) setMediaModalOpen(false);
+});
+mediaModalClose?.addEventListener("click", () => setMediaModalOpen(false));
+mediaModalCopyLink?.addEventListener("click", async () => {
+ const url = String(mediaModalOpenLink?.href || "").trim();
+ if (!url || url === "#") return;
+ try {
+ await navigator.clipboard.writeText(url);
+ if (mediaModalStatus) mediaModalStatus.textContent = "Copied.";
+ } catch {
+ if (mediaModalStatus) mediaModalStatus.textContent = "Copy failed (clipboard blocked).";
+ }
+});
+document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape" && mediaModal && !mediaModal.classList.contains("hidden")) setMediaModalOpen(false);
+});
+document.body.addEventListener("click", (e) => {
+ const img = e.target?.closest?.("img");
+ if (!img) return;
+ if (img.id === "profilePreview") return;
+ if (img.closest("#mediaModal")) return;
+ const inAllowed =
+ img.closest(".chatMsg .content") ||
+ img.closest(".profileBio") ||
+ img.closest(".profileCard") ||
+ img.closest(".editor") ||
+ img.closest("#editModalEditor");
+ if (!inAllowed) return;
+ const src = img.getAttribute("src") || "";
+ if (!src) return;
+ openMediaModal(src);
+});
+
setSidebarHidden(getSidebarHidden());
toggleSidebarBtn?.addEventListener("click", () => setSidebarHidden(true));
showSidebarBtn?.addEventListener("click", () => setSidebarHidden(false));
diff --git a/public/index.html b/public/index.html
@@ -26,6 +26,14 @@
</div>
<section class="panel">
+ <div class="panelTitle">View</div>
+ <label class="checkRow">
+ <span>Show reactions bar</span>
+ <input id="toggleReactions" type="checkbox" />
+ </label>
+ </section>
+
+ <section class="panel">
<div class="panelTitle">Account</div>
<div class="small muted" id="authHint">Sign in to post, chat, and boost.</div>
@@ -438,6 +446,26 @@
</div>
</div>
+ <div id="mediaModal" class="modal hidden" role="dialog" aria-modal="true" aria-label="Media preview">
+ <div class="modalBackdrop" data-mediamodalclose="1"></div>
+ <div class="modalCard panel">
+ <div class="panelHeader">
+ <div class="panelTitle" id="mediaModalTitle">Media</div>
+ <div class="row">
+ <button id="mediaModalClose" class="ghost smallBtn" type="button">Close</button>
+ </div>
+ </div>
+ <div class="modalBody" id="mediaModalBody">
+ <img id="mediaModalImg" alt="Expanded media" />
+ <div class="row modalActions" style="justify-content:flex-start">
+ <a id="mediaModalOpenLink" class="ghost" href="#" target="_blank" rel="noreferrer">Open original</a>
+ <button id="mediaModalCopyLink" class="ghost" type="button">Copy link</button>
+ </div>
+ <div id="mediaModalStatus" class="small muted"></div>
+ </div>
+ </div>
+ </div>
+
<script src="/app.js?v=91"></script>
</body>
</html>
diff --git a/public/styles.css b/public/styles.css
@@ -1223,8 +1223,21 @@ button:disabled {
.profileBio img {
max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
+ border-radius: 12px;
+ display: block;
+ cursor: zoom-in;
+}
+
+.editor img {
+ max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
border-radius: 12px;
display: block;
+ margin-top: 8px;
+ cursor: zoom-in;
}
.ytEmbed {
@@ -1811,9 +1824,16 @@ button:disabled {
.chatMsg .content img {
max-width: 100%;
+ max-height: min(420px, 55vh);
+ object-fit: contain;
border-radius: 10px;
display: block;
margin-top: 6px;
+ cursor: zoom-in;
+}
+
+.chatReactions:empty {
+ display: none;
}
.chatMsg .content audio,
@@ -1823,6 +1843,22 @@ button:disabled {
margin-top: 8px;
}
+#mediaModalImg {
+ max-width: min(92vw, 1200px);
+ max-height: 78vh;
+ width: auto;
+ height: auto;
+ display: block;
+ margin: 0 auto;
+ border-radius: 14px;
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.editor.isDropActive {
+ outline: 2px dashed rgba(255, 62, 165, 0.7);
+ outline-offset: 6px;
+}
+
.chatMsg .meta {
font-size: 11px;
color: var(--muted);