mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
Safety: account-level (per-creator) web-of-trust overrides
Add "trusted creator" overrides that suppress the web-of-trust warning/blur for ALL of a creator's videos until reset — the account-level companion to the existing per-video "Show anyway". - cache.js: per-author override store (bitvid:moderationAuthorOverrides:v1), keyed by author hex, with get/set/has/clear/list + persist/load, mirroring the per-video store. - moderationDecorator: overrideActive now also honors an account-level override for the video's author, so the whole class of WoT warnings clears for every video by that creator. - app.js: load the author overrides on startup. - Services + contract: getAuthorModerationOverrides / setAuthorModerationOverride / clearAuthorModerationOverride; set/clear refresh the feed + fire the shared moderation-override event so the Safety tab list updates. - Safety tab: a "Trusted creators" section — add by npub, list each with a Reset, mirroring the Active-overrides section. - tests: storage set/get/has/clear/list, hex normalization, persist→load, invalid-input guard. Next: the "Always show this creator" button on the video warning overlay (second entry point) — a larger multi-component UI change, done separately. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
18e7197bdc
commit
a0131c718e
9 files changed
+538
-5
No files matched your search
@@ -47,6 +47,11 @@ export class ProfileModerationController {
|
||||
this.moderationStatusText = document.getElementById("profileModerationStatus") || null;
|
||||
this.moderationOverridesList = document.getElementById("profileModerationOverridesList") || null;
|
||||
this.moderationOverridesEmpty = document.getElementById("profileModerationOverridesEmpty") || null;
|
||||
this.authorOverrideInput = document.getElementById("profileAuthorOverrideInput") || null;
|
||||
this.authorOverrideAddBtn = document.getElementById("profileAuthorOverrideAddBtn") || null;
|
||||
this.authorOverrideStatus = document.getElementById("profileAuthorOverrideStatus") || null;
|
||||
this.authorOverridesList = document.getElementById("profileAuthorOverridesList") || null;
|
||||
this.authorOverridesEmpty = document.getElementById("profileAuthorOverridesEmpty") || null;
|
||||
this.moderationTrustedContactsCount = document.getElementById("profileModerationTrustedContactsCount") || null;
|
||||
this.moderationTrustedMuteCount = document.getElementById("profileModerationTrustedMuteCount") || null;
|
||||
this.moderationTrustedReportCount = document.getElementById("profileModerationTrustedReportCount") || null;
|
||||
@@ -102,9 +107,24 @@ export class ProfileModerationController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.authorOverrideAddBtn instanceof HTMLElement) {
|
||||
this.authorOverrideAddBtn.addEventListener("click", () => {
|
||||
void this.handleAddAuthorOverride();
|
||||
});
|
||||
}
|
||||
if (this.authorOverrideInput instanceof HTMLInputElement) {
|
||||
this.authorOverrideInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void this.handleAddAuthorOverride();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.boundModerationOverridesUpdate && typeof document !== "undefined") {
|
||||
this.boundModerationOverridesUpdate = () => {
|
||||
this.refreshModerationOverridesUi();
|
||||
this.refreshAuthorOverridesUi();
|
||||
};
|
||||
document.addEventListener(
|
||||
"video:moderation-override",
|
||||
@@ -569,6 +589,159 @@ export class ProfileModerationController {
|
||||
}
|
||||
}
|
||||
|
||||
setAuthorOverrideStatus(message) {
|
||||
if (this.authorOverrideStatus instanceof HTMLElement) {
|
||||
this.authorOverrideStatus.textContent = message || "";
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddAuthorOverride() {
|
||||
const input =
|
||||
this.authorOverrideInput instanceof HTMLInputElement
|
||||
? this.authorOverrideInput.value.trim()
|
||||
: "";
|
||||
if (!input) {
|
||||
this.setAuthorOverrideStatus("Enter a creator npub.");
|
||||
return false;
|
||||
}
|
||||
const hex = this.mainController.normalizeHexPubkey(input);
|
||||
if (!hex) {
|
||||
this.setAuthorOverrideStatus("That doesn't look like a valid npub.");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof this.mainController.services.setAuthorModerationOverride !==
|
||||
"function"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.mainController.services.setAuthorModerationOverride(hex);
|
||||
if (this.authorOverrideInput instanceof HTMLInputElement) {
|
||||
this.authorOverrideInput.value = "";
|
||||
}
|
||||
this.setAuthorOverrideStatus("");
|
||||
this.refreshAuthorOverridesUi();
|
||||
this.mainController.showSuccess("Warnings turned off for this creator.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.mainController.showError(
|
||||
"Couldn't add that creator. Please try again.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async handleAuthorOverrideReset(entry) {
|
||||
if (
|
||||
!entry ||
|
||||
!entry.authorPubkey ||
|
||||
typeof this.mainController.services.clearAuthorModerationOverride !==
|
||||
"function"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.mainController.services.clearAuthorModerationOverride(
|
||||
entry.authorPubkey,
|
||||
);
|
||||
this.refreshAuthorOverridesUi();
|
||||
this.mainController.showSuccess("Warnings restored for this creator.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.mainController.showError("Unable to reset this creator.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshAuthorOverridesUi() {
|
||||
if (
|
||||
!(this.authorOverridesList instanceof HTMLElement) ||
|
||||
!(this.authorOverridesEmpty instanceof HTMLElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const services = this.mainController.services;
|
||||
const raw =
|
||||
typeof services.getAuthorModerationOverrides === "function"
|
||||
? services.getAuthorModerationOverrides()
|
||||
: [];
|
||||
const entries = (Array.isArray(raw) ? raw : [])
|
||||
.map((entry) => ({
|
||||
authorPubkey:
|
||||
typeof entry?.authorPubkey === "string"
|
||||
? this.mainController.normalizeHexPubkey(entry.authorPubkey) ||
|
||||
entry.authorPubkey
|
||||
: "",
|
||||
updatedAt: Number.isFinite(entry?.updatedAt)
|
||||
? Math.floor(entry.updatedAt)
|
||||
: 0,
|
||||
}))
|
||||
.filter((entry) => entry.authorPubkey)
|
||||
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||
|
||||
this.authorOverridesList.textContent = "";
|
||||
|
||||
if (!entries.length) {
|
||||
this.authorOverridesEmpty.classList.remove("hidden");
|
||||
this.authorOverridesList.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
this.authorOverridesEmpty.classList.add("hidden");
|
||||
this.authorOverridesList.classList.remove("hidden");
|
||||
|
||||
const entriesNeedingFetch = new Set();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = "card profile-account-card";
|
||||
|
||||
const authorKey = entry.authorPubkey;
|
||||
if (
|
||||
authorKey &&
|
||||
typeof services.getProfileCacheEntry === "function" &&
|
||||
!services.getProfileCacheEntry(authorKey)
|
||||
) {
|
||||
entriesNeedingFetch.add(authorKey);
|
||||
}
|
||||
|
||||
const summaryData =
|
||||
this.mainController.dmController.resolveProfileSummaryForPubkey(
|
||||
authorKey,
|
||||
);
|
||||
const summary =
|
||||
this.mainController.dmController.createCompactProfileSummary(
|
||||
summaryData,
|
||||
);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "profile-account-card__actions";
|
||||
const resetButton = this.mainController.createRemoveButton({
|
||||
label: "Reset",
|
||||
onRemove: () => this.handleAuthorOverrideReset(entry),
|
||||
});
|
||||
|
||||
if (summary) {
|
||||
item.appendChild(summary);
|
||||
}
|
||||
if (resetButton) {
|
||||
actions.appendChild(resetButton);
|
||||
item.appendChild(actions);
|
||||
}
|
||||
|
||||
this.authorOverridesList.appendChild(item);
|
||||
});
|
||||
|
||||
if (
|
||||
entriesNeedingFetch.size &&
|
||||
typeof services.batchFetchProfiles === "function"
|
||||
) {
|
||||
services.batchFetchProfiles(entriesNeedingFetch);
|
||||
}
|
||||
}
|
||||
|
||||
refreshModerationSettingsUi() {
|
||||
const service = this.getModerationSettingsService();
|
||||
if (!service) {
|
||||
@@ -620,6 +793,7 @@ export class ProfileModerationController {
|
||||
this.updateTrustedHideControlsVisibility();
|
||||
this.updateModerationTrustStats();
|
||||
this.refreshModerationOverridesUi();
|
||||
this.refreshAuthorOverridesUi();
|
||||
|
||||
this.applyModerationSettingsControlState({ resetStatus: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user