mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor shared helpers into utility modules
This commit is contained in:
1 parent
245f6bd0d1
commit
2e6e7012a6
5 files changed
+179
-141
No files matched your search
@@ -52,22 +52,20 @@ import {
|
||||
formatViewCount,
|
||||
ingestLocalViewEvent,
|
||||
} from "./viewCounter.js";
|
||||
|
||||
function truncateMiddle(text, maxLength = 72) {
|
||||
if (!text || typeof text !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const ellipsis = "…";
|
||||
const charsToShow = maxLength - ellipsis.length;
|
||||
const front = Math.ceil(charsToShow / 2);
|
||||
const back = Math.floor(charsToShow / 2);
|
||||
return `${text.slice(0, front)}${ellipsis}${text.slice(text.length - back)}`;
|
||||
}
|
||||
import {
|
||||
formatAbsoluteTimestamp as formatAbsoluteTimestampUtil,
|
||||
formatTimeAgo as formatTimeAgoUtil,
|
||||
truncateMiddle,
|
||||
} from "./utils/formatters.js";
|
||||
import {
|
||||
escapeHTML as escapeHtml,
|
||||
removeTrackingScripts,
|
||||
} from "./utils/domUtils.js";
|
||||
import {
|
||||
readUrlHealthFromStorage,
|
||||
removeUrlHealthFromStorage,
|
||||
writeUrlHealthToStorage,
|
||||
} from "./utils/storage.js";
|
||||
|
||||
function pointerArrayToKey(pointer) {
|
||||
if (!Array.isArray(pointer) || pointer.length < 2) {
|
||||
@@ -109,7 +107,6 @@ const ADMIN_DM_IMAGE_URL =
|
||||
const BITVID_WEBSITE_URL = "https://bitvid.network/";
|
||||
const MAX_DISCUSSION_COUNT_VIDEOS = 24;
|
||||
const VIDEO_EVENT_KIND = 30078;
|
||||
const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
|
||||
const EMPTY_VIDEO_LIST_SIGNATURE = "__EMPTY__";
|
||||
/**
|
||||
* Local storage keys for cached profile metadata and saved authentication
|
||||
@@ -134,76 +131,9 @@ const URL_HEALTH_TTL_MS = 45 * 60 * 1000; // 45 minutes
|
||||
const URL_HEALTH_TIMEOUT_RETRY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const URL_PROBE_TIMEOUT_MS = 8 * 1000; // 8 seconds
|
||||
const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds
|
||||
const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:";
|
||||
const urlHealthCache = new Map();
|
||||
const urlHealthInFlight = new Map();
|
||||
|
||||
function removeTrackingScripts(root) {
|
||||
if (!root || typeof root.querySelectorAll !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
root.querySelectorAll("script[src]").forEach((script) => {
|
||||
const src = script.getAttribute("src") || "";
|
||||
if (TRACKING_SCRIPT_PATTERN.test(src)) {
|
||||
script.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getUrlHealthStorageKey(eventId) {
|
||||
return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`;
|
||||
}
|
||||
|
||||
function readUrlHealthFromStorage(eventId) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getUrlHealthStorageKey(eventId));
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
return parsed;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse stored URL health for ${eventId}:`, err);
|
||||
}
|
||||
|
||||
removeUrlHealthFromStorage(eventId);
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeUrlHealthToStorage(eventId, entry) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
getUrlHealthStorageKey(eventId),
|
||||
JSON.stringify(entry)
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to persist URL health for ${eventId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function removeUrlHealthFromStorage(eventId) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(getUrlHealthStorageKey(eventId));
|
||||
} catch (err) {
|
||||
console.warn(`Failed to remove URL health for ${eventId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function isUrlHealthEntryFresh(entry, url) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return false;
|
||||
@@ -11528,58 +11458,15 @@ class bitvidApp {
|
||||
* Format "time ago" for a given timestamp (in seconds).
|
||||
*/
|
||||
formatAbsoluteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return "Unknown date";
|
||||
}
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return "Unknown date";
|
||||
}
|
||||
|
||||
try {
|
||||
return date.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (err) {
|
||||
return date.toISOString();
|
||||
}
|
||||
return formatAbsoluteTimestampUtil(timestamp);
|
||||
}
|
||||
|
||||
formatTimeAgo(timestamp) {
|
||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
||||
const intervals = {
|
||||
year: 31536000,
|
||||
month: 2592000,
|
||||
week: 604800,
|
||||
day: 86400,
|
||||
hour: 3600,
|
||||
minute: 60,
|
||||
};
|
||||
for (const [unit, secInUnit] of Object.entries(intervals)) {
|
||||
const int = Math.floor(seconds / secInUnit);
|
||||
if (int >= 1) {
|
||||
return `${int} ${unit}${int > 1 ? "s" : ""} ago`;
|
||||
}
|
||||
}
|
||||
return "just now";
|
||||
return formatTimeAgoUtil(timestamp);
|
||||
}
|
||||
|
||||
escapeHTML(unsafe) {
|
||||
if (unsafe === null || typeof unsafe === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
return escapeHtml(unsafe);
|
||||
}
|
||||
|
||||
showError(msg) {
|
||||
|
||||
+1
-11
@@ -9,6 +9,7 @@ import { subscriptions } from "./subscriptions.js"; // <-- NEW import
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { accessControl } from "./accessControl.js";
|
||||
import { escapeHTML } from "./utils/domUtils.js";
|
||||
|
||||
let currentChannelHex = null;
|
||||
let currentChannelNpub = null;
|
||||
@@ -912,14 +913,3 @@ function dedupeToNewestByRoot(videos) {
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic escaping to avoid XSS.
|
||||
*/
|
||||
function escapeHTML(unsafe = "") {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// js/utils/domUtils.js
|
||||
|
||||
const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
|
||||
|
||||
/**
|
||||
* Removes script tags that match known tracking script patterns.
|
||||
*/
|
||||
export function removeTrackingScripts(root) {
|
||||
if (!root || typeof root.querySelectorAll !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
root.querySelectorAll("script[src]").forEach((script) => {
|
||||
const src = script.getAttribute("src") || "";
|
||||
if (TRACKING_SCRIPT_PATTERN.test(src)) {
|
||||
script.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML entities in a string to prevent XSS when injecting content.
|
||||
*/
|
||||
export function escapeHTML(unsafe) {
|
||||
if (unsafe === null || typeof unsafe === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// js/utils/formatters.js
|
||||
|
||||
/**
|
||||
* Shortens a string by replacing the middle with an ellipsis when it exceeds
|
||||
* the desired length. Ensures the returned string never exceeds `maxLength`.
|
||||
*/
|
||||
export function truncateMiddle(text, maxLength = 72) {
|
||||
if (!text || typeof text !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const ellipsis = "…";
|
||||
const charsToShow = maxLength - ellipsis.length;
|
||||
const front = Math.ceil(charsToShow / 2);
|
||||
const back = Math.floor(charsToShow / 2);
|
||||
return `${text.slice(0, front)}${ellipsis}${text.slice(text.length - back)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a timestamp (in seconds) to a localized string. Falls back to
|
||||
* ISO-8601 formatting if locale formatting fails.
|
||||
*/
|
||||
export function formatAbsoluteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return "Unknown date";
|
||||
}
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return "Unknown date";
|
||||
}
|
||||
|
||||
try {
|
||||
return date.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (err) {
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human readable "time ago" string for a timestamp (in seconds).
|
||||
*/
|
||||
export function formatTimeAgo(timestamp) {
|
||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
||||
const intervals = {
|
||||
year: 31536000,
|
||||
month: 2592000,
|
||||
week: 604800,
|
||||
day: 86400,
|
||||
hour: 3600,
|
||||
minute: 60,
|
||||
};
|
||||
for (const [unit, secInUnit] of Object.entries(intervals)) {
|
||||
const int = Math.floor(seconds / secInUnit);
|
||||
if (int >= 1) {
|
||||
return `${int} ${unit}${int > 1 ? "s" : ""} ago`;
|
||||
}
|
||||
}
|
||||
return "just now";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// js/utils/storage.js
|
||||
|
||||
const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:";
|
||||
|
||||
export function getUrlHealthStorageKey(eventId) {
|
||||
return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`;
|
||||
}
|
||||
|
||||
export function readUrlHealthFromStorage(eventId) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getUrlHealthStorageKey(eventId));
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
return parsed;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse stored URL health for ${eventId}:`, err);
|
||||
}
|
||||
|
||||
removeUrlHealthFromStorage(eventId);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writeUrlHealthToStorage(eventId, entry) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
getUrlHealthStorageKey(eventId),
|
||||
JSON.stringify(entry)
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to persist URL health for ${eventId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeUrlHealthFromStorage(eventId) {
|
||||
if (!eventId || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(getUrlHealthStorageKey(eventId));
|
||||
} catch (err) {
|
||||
console.warn(`Failed to remove URL health for ${eventId}:`, err);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user