mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2635 from PR0M3TH3AN/agents/daily/decompose-agent-2026-02-19-18143835048867505986
[decompose] extract BlockList and EditProfile from ProfileModalController
This commit is contained in:
8 files changed
+1047
-742
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# Decomposition Context
|
||||
|
||||
- **Task:** Decompose `js/ui/profileModalController.js` (6258 lines).
|
||||
- **Reason:** Largest grandfathered file, highly coupled UI logic.
|
||||
- **Goal:** Extract "Block List" and "Edit Profile" logic into separate controllers to reduce line count by >200 lines and improve maintainability.
|
||||
- **Target Branch:** `unstable` (via `agents/daily/...` branch).
|
||||
- **Plan:**
|
||||
1. Extract `ProfileBlockListController` to manage blocked users list, add/remove blocks.
|
||||
2. Extract `ProfileEditController` to manage profile editing form, uploads, and saving.
|
||||
3. Update `ProfileModalController` to delegate to these new controllers.
|
||||
|
||||
## Selected Blocks
|
||||
|
||||
1. **Block List Management**:
|
||||
- `populateBlockedList`
|
||||
- `handleAddBlockedCreator`
|
||||
- `handleRemoveBlockedCreator`
|
||||
- `ensureBlockListStatusElement`
|
||||
- `setBlockListLoadingState`
|
||||
- `mutateBlocklist`
|
||||
- Event listeners and DOM cache for block list elements.
|
||||
|
||||
2. **Edit Profile Management**:
|
||||
- `handleEditProfile`
|
||||
- `populateEditPane`
|
||||
- `checkStorageForUploads`
|
||||
- `handleUpload`
|
||||
- `handleSaveProfile`
|
||||
- Event listeners and DOM cache for edit profile elements.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Decomposition Decisions
|
||||
|
||||
- **File:** `js/ui/profileModalController.js`
|
||||
- **Extracted Controllers:**
|
||||
- `js/ui/profileModal/ProfileBlockListController.js` (Block list logic)
|
||||
- `js/ui/profileModal/ProfileEditController.js` (Edit profile logic)
|
||||
- **Rationale:**
|
||||
- Block list management is a distinct feature set (add, remove, list, refresh).
|
||||
- Edit profile is a self-contained form handling workflow (populate, upload, save).
|
||||
- Reducing the main controller size improves readability and separation of concerns.
|
||||
- **Backwards Compatibility:**
|
||||
- New controllers alias DOM references back to the main controller (`this.mainController.blockList = ...`) to maintain compatibility with tests and potentially other external code that expects these properties on the main controller.
|
||||
- Delegate methods added to `ProfileModalController` to forward calls to the new controllers, preserving the public API.
|
||||
- **Baseline Update:**
|
||||
- `js/ui/profileModalController.js` reduced from 6258 to 5541 lines (-717 lines).
|
||||
@@ -0,0 +1,11 @@
|
||||
# Decompose Agent Completion Log
|
||||
|
||||
- **Agent:** decompose-agent
|
||||
- **Date:** 2026-02-19
|
||||
- **Status:** Completed
|
||||
- **Task:** Decompose `js/ui/profileModalController.js`.
|
||||
- **Result:**
|
||||
- Extracted `ProfileBlockListController.js` and `ProfileEditController.js`.
|
||||
- Reduced `js/ui/profileModalController.js` by 717 lines (6258 -> 5541).
|
||||
- Verified with lint and unit tests.
|
||||
- Updated file size baseline.
|
||||
@@ -0,0 +1,613 @@
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
|
||||
const FALLBACK_PROFILE_AVATAR = "assets/svg/default-profile.svg";
|
||||
|
||||
export class ProfileBlockListController {
|
||||
constructor(mainController) {
|
||||
this.mainController = mainController;
|
||||
|
||||
this.blockList = null;
|
||||
this.blockListEmpty = null;
|
||||
this.blockListStatus = null;
|
||||
this.blockListLoadingState = "idle";
|
||||
this.blockInput = null;
|
||||
this.addBlockedButton = null;
|
||||
this.profileBlockedRefreshBtn = null;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.blockList = document.getElementById("blockedList") || null;
|
||||
this.blockListEmpty = document.getElementById("blockedEmpty") || null;
|
||||
this.blockInput = document.getElementById("blockedInput") || null;
|
||||
this.addBlockedButton = document.getElementById("addBlockedBtn") || null;
|
||||
this.profileBlockedRefreshBtn = document.getElementById("blockedRefreshBtn") || null;
|
||||
|
||||
// Attempt to locate status element or create it if missing
|
||||
this.blockListStatus =
|
||||
(this.mainController.panes.blocked?.querySelector &&
|
||||
this.mainController.panes.blocked.querySelector("[data-role=\"blocked-list-status\"]")) ||
|
||||
null;
|
||||
|
||||
// Backwards compatibility alias
|
||||
this.mainController.blockList = this.blockList;
|
||||
this.mainController.blockListEmpty = this.blockListEmpty;
|
||||
this.mainController.blockInput = this.blockInput;
|
||||
this.mainController.addBlockedButton = this.addBlockedButton;
|
||||
|
||||
this.mainController.profileBlockedList = this.blockList;
|
||||
this.mainController.profileBlockedEmpty = this.blockListEmpty;
|
||||
this.mainController.profileBlockedInput = this.blockInput;
|
||||
this.mainController.profileAddBlockedBtn = this.addBlockedButton;
|
||||
this.mainController.profileBlockedRefreshBtn = this.profileBlockedRefreshBtn;
|
||||
this.mainController.blockListStatus = this.blockListStatus;
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.addBlockedButton instanceof HTMLElement) {
|
||||
this.addBlockedButton.addEventListener("click", () => {
|
||||
void this.handleAddBlockedCreator();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileBlockedRefreshBtn instanceof HTMLElement) {
|
||||
this.profileBlockedRefreshBtn.addEventListener("click", () => {
|
||||
const activeHex = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!activeHex) {
|
||||
return;
|
||||
}
|
||||
const blocksService = this.mainController.services.userBlocks;
|
||||
if (!blocksService || typeof blocksService.loadBlocks !== "function") {
|
||||
return;
|
||||
}
|
||||
void blocksService
|
||||
.loadBlocks(activeHex)
|
||||
.then(() => {
|
||||
this.populateBlockedList();
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn("[profileModal] Failed to refresh blocked list:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.blockInput instanceof HTMLElement) {
|
||||
this.blockInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void this.handleAddBlockedCreator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ensureBlockListStatusElement() {
|
||||
if (this.blockListStatus instanceof HTMLElement) {
|
||||
return this.blockListStatus;
|
||||
}
|
||||
|
||||
const anchor =
|
||||
this.blockList instanceof HTMLElement
|
||||
? this.blockList
|
||||
: this.blockListEmpty instanceof HTMLElement
|
||||
? this.blockListEmpty
|
||||
: null;
|
||||
|
||||
if (!anchor || !(anchor.parentElement instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = anchor.parentElement.querySelector(
|
||||
'[data-role="blocked-list-status"]',
|
||||
);
|
||||
if (existing instanceof HTMLElement) {
|
||||
this.blockListStatus = existing;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const status = document.createElement("div");
|
||||
status.dataset.role = "blocked-list-status";
|
||||
status.className = "mt-4 flex items-center gap-3 text-sm text-muted hidden";
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
|
||||
if (this.blockList instanceof HTMLElement) {
|
||||
anchor.parentElement.insertBefore(status, this.blockList);
|
||||
} else {
|
||||
anchor.parentElement.appendChild(status);
|
||||
}
|
||||
|
||||
this.blockListStatus = status;
|
||||
return status;
|
||||
}
|
||||
|
||||
setBlockListLoadingState(state = "idle", options = {}) {
|
||||
const statusEl = this.ensureBlockListStatusElement();
|
||||
if (!statusEl) {
|
||||
this.blockListLoadingState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof options.message === "string" && options.message.trim()
|
||||
? options.message.trim()
|
||||
: "";
|
||||
|
||||
statusEl.textContent = "";
|
||||
statusEl.classList.remove("text-status-warning");
|
||||
statusEl.classList.add("text-muted");
|
||||
statusEl.classList.add("hidden");
|
||||
|
||||
this.blockListLoadingState = state;
|
||||
|
||||
if (state === "loading") {
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "status-spinner status-spinner--inline";
|
||||
spinner.setAttribute("aria-hidden", "true");
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = message || "Loading blocked creators…";
|
||||
|
||||
statusEl.appendChild(spinner);
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
statusEl.classList.remove("text-muted");
|
||||
statusEl.classList.add("text-status-warning");
|
||||
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent =
|
||||
message || "Blocked creators may be out of date. Try again later.";
|
||||
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
populateBlockedList(blocked = null) {
|
||||
if (!this.blockList || !this.blockListEmpty) {
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceEntries =
|
||||
Array.isArray(blocked) && blocked.length
|
||||
? blocked
|
||||
: this.mainController.services.userBlocks.getBlockedPubkeys();
|
||||
|
||||
const normalizedEntries = [];
|
||||
const pushEntry = (hex, label) => {
|
||||
if (!hex || !label) {
|
||||
return;
|
||||
}
|
||||
normalizedEntries.push({ hex, label });
|
||||
};
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
if (typeof entry === "string") {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.mainController.safeDecodeNpub(trimmed);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.mainController.safeEncodeNpub(decoded) || trimmed;
|
||||
pushEntry(decoded, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
const hex = trimmed.toLowerCase();
|
||||
const label = this.mainController.safeEncodeNpub(hex) || hex;
|
||||
pushEntry(hex, label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry && typeof entry === "object") {
|
||||
const candidateNpub =
|
||||
typeof entry.npub === "string" ? entry.npub.trim() : "";
|
||||
const candidateHex =
|
||||
typeof entry.pubkey === "string" ? entry.pubkey.trim() : "";
|
||||
|
||||
if (candidateHex && /^[0-9a-f]{64}$/i.test(candidateHex)) {
|
||||
const normalizedHex = candidateHex.toLowerCase();
|
||||
const label =
|
||||
candidateNpub && candidateNpub.startsWith("npub1")
|
||||
? candidateNpub
|
||||
: this.mainController.safeEncodeNpub(normalizedHex) || normalizedHex;
|
||||
pushEntry(normalizedHex, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidateNpub && candidateNpub.startsWith("npub1")) {
|
||||
const decoded = this.mainController.safeDecodeNpub(candidateNpub);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.mainController.safeEncodeNpub(decoded) || candidateNpub;
|
||||
pushEntry(decoded, label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deduped = [];
|
||||
const seenHex = new Set();
|
||||
normalizedEntries.forEach((entry) => {
|
||||
if (!seenHex.has(entry.hex)) {
|
||||
seenHex.add(entry.hex);
|
||||
deduped.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
this.blockList.textContent = "";
|
||||
|
||||
if (!deduped.length) {
|
||||
this.blockListEmpty.classList.remove("hidden");
|
||||
this.blockList.classList.add("hidden");
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
this.blockList.classList.remove("hidden");
|
||||
|
||||
const formatNpub =
|
||||
typeof this.mainController.formatShortNpub === "function"
|
||||
? (value) => this.mainController.formatShortNpub(value)
|
||||
: (value) => (typeof value === "string" ? value : "");
|
||||
const entriesNeedingFetch = new Set();
|
||||
|
||||
deduped.forEach(({ hex, label }) => {
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"card flex items-center justify-between gap-4 p-4";
|
||||
|
||||
let cachedProfile = null;
|
||||
if (hex) {
|
||||
const cacheEntry = this.mainController.services.getProfileCacheEntry(hex);
|
||||
cachedProfile = cacheEntry?.profile || null;
|
||||
if (!cacheEntry) {
|
||||
entriesNeedingFetch.add(hex);
|
||||
}
|
||||
}
|
||||
|
||||
const encodedNpub =
|
||||
hex && typeof this.mainController.safeEncodeNpub === "function"
|
||||
? this.mainController.safeEncodeNpub(hex)
|
||||
: label;
|
||||
const displayNpub = formatNpub(encodedNpub) || encodedNpub || label;
|
||||
const displayName =
|
||||
cachedProfile?.name?.trim() || displayNpub || "Blocked profile";
|
||||
const avatarSrc =
|
||||
cachedProfile?.picture || FALLBACK_PROFILE_AVATAR;
|
||||
|
||||
const summary = this.mainController.dmController.createCompactProfileSummary({
|
||||
displayName,
|
||||
displayNpub,
|
||||
avatarSrc,
|
||||
});
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "flex flex-wrap items-center justify-end gap-2";
|
||||
|
||||
const viewButton = this.mainController.createViewChannelButton({
|
||||
targetNpub: encodedNpub,
|
||||
displayNpub,
|
||||
});
|
||||
if (viewButton) {
|
||||
actions.appendChild(viewButton);
|
||||
}
|
||||
|
||||
const copyButton = this.mainController.createCopyNpubButton({
|
||||
targetNpub: encodedNpub,
|
||||
displayNpub,
|
||||
});
|
||||
if (copyButton) {
|
||||
actions.appendChild(copyButton);
|
||||
}
|
||||
|
||||
const removeButton = this.mainController.createRemoveButton({
|
||||
label: "Remove",
|
||||
onRemove: () => this.handleRemoveBlockedCreator(hex),
|
||||
});
|
||||
if (removeButton) {
|
||||
removeButton.dataset.blockedHex = hex;
|
||||
actions.appendChild(removeButton);
|
||||
}
|
||||
|
||||
item.appendChild(summary);
|
||||
if (actions.childElementCount > 0) {
|
||||
item.appendChild(actions);
|
||||
}
|
||||
|
||||
this.blockList.appendChild(item);
|
||||
});
|
||||
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
|
||||
if (
|
||||
entriesNeedingFetch.size &&
|
||||
typeof this.mainController.services.batchFetchProfiles === "function"
|
||||
) {
|
||||
this.mainController.services.batchFetchProfiles(entriesNeedingFetch);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddBlockedCreator() {
|
||||
const input = this.blockInput || null;
|
||||
const rawValue = typeof input?.value === "string" ? input.value : "";
|
||||
const trimmed = rawValue.trim();
|
||||
|
||||
const context = {
|
||||
input,
|
||||
rawValue,
|
||||
value: trimmed,
|
||||
success: false,
|
||||
reason: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
if (!input) {
|
||||
context.reason = "missing-input";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (!trimmed) {
|
||||
this.mainController.showError("Enter an npub to block.");
|
||||
context.reason = "empty";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
const activePubkey = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!activePubkey) {
|
||||
this.mainController.showError("Please login to manage your block list.");
|
||||
context.reason = "no-active-pubkey";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
const actorHex = activePubkey;
|
||||
let targetHex = "";
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.mainController.safeDecodeNpub(trimmed) || "";
|
||||
if (!targetHex) {
|
||||
this.mainController.showError("Invalid npub. Please double-check and try again.");
|
||||
context.reason = "invalid-npub";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
} else {
|
||||
this.mainController.showError("Enter a valid npub or hex pubkey.");
|
||||
context.reason = "invalid-value";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (targetHex === actorHex) {
|
||||
this.mainController.showError("You cannot block yourself.");
|
||||
context.reason = "self";
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
context.targetHex = targetHex;
|
||||
|
||||
try {
|
||||
const mutationResult = await this.mutateBlocklist({
|
||||
action: "add",
|
||||
actorHex,
|
||||
targetHex,
|
||||
controller: this.mainController,
|
||||
});
|
||||
|
||||
context.result = mutationResult;
|
||||
|
||||
if (mutationResult?.ok) {
|
||||
this.mainController.showSuccess(
|
||||
"Creator blocked. You won't see their videos anymore.",
|
||||
);
|
||||
context.success = true;
|
||||
context.reason = mutationResult.reason || "blocked";
|
||||
} else if (mutationResult?.reason === "already-blocked") {
|
||||
this.mainController.showSuccess("You already blocked this creator.");
|
||||
context.reason = "already-blocked";
|
||||
} else {
|
||||
const message =
|
||||
mutationResult?.error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: mutationResult?.error?.code ===
|
||||
"extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: mutationResult?.error?.message ||
|
||||
"Failed to update your mute/block list. Please try again.";
|
||||
context.error = mutationResult?.error || null;
|
||||
context.reason = mutationResult?.reason || "service-error";
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.blockInput) {
|
||||
this.blockInput.value = "";
|
||||
}
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to add creator to personal block list:", error);
|
||||
context.error = error;
|
||||
context.reason = error?.code || "service-error";
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: error?.code === "extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: "Failed to update your mute/block list. Please try again.";
|
||||
this.mainController.showError(message);
|
||||
}
|
||||
|
||||
this.mainController.callbacks.onAddBlocked(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
async handleRemoveBlockedCreator(candidate) {
|
||||
const activePubkey = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!activePubkey) {
|
||||
this.mainController.showError("Please login to manage your block list.");
|
||||
return;
|
||||
}
|
||||
|
||||
let targetHex = "";
|
||||
if (typeof candidate === "string") {
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.mainController.safeDecodeNpub(trimmed) || "";
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetHex) {
|
||||
userLogger.warn("No valid pubkey to remove from block list:", candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mutationResult = await this.mutateBlocklist({
|
||||
action: "remove",
|
||||
actorHex: activePubkey,
|
||||
targetHex,
|
||||
controller: this.mainController,
|
||||
});
|
||||
|
||||
if (mutationResult?.ok) {
|
||||
this.mainController.showSuccess("Creator removed from your mute/block list.");
|
||||
} else if (mutationResult?.reason === "not-blocked") {
|
||||
this.mainController.showSuccess("Creator already removed from your mute/block list.");
|
||||
} else if (mutationResult?.error) {
|
||||
const message =
|
||||
mutationResult.error.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: mutationResult.error.code ===
|
||||
"extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: mutationResult.error.message ||
|
||||
"Failed to update your mute/block list. Please try again.";
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.error(
|
||||
"Failed to remove creator from personal block list:",
|
||||
error,
|
||||
);
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: error?.code === "extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: "Failed to update your mute/block list. Please try again.";
|
||||
this.mainController.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
async mutateBlocklist({ action, actorHex, targetHex } = {}) {
|
||||
const callback = this.mainController.callbacks.onBlocklistMutation;
|
||||
const noop = () => {};
|
||||
if (callback && callback !== noop) {
|
||||
const result = await callback({
|
||||
controller: this.mainController,
|
||||
action,
|
||||
actorHex,
|
||||
targetHex,
|
||||
});
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const context = { ok: false, reason: null, error: null };
|
||||
if (!actorHex || !targetHex) {
|
||||
context.reason = "invalid-target";
|
||||
return context;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.mainController.services.userBlocks.ensureLoaded(actorHex);
|
||||
const isBlocked = this.mainController.services.userBlocks.isBlocked(targetHex);
|
||||
|
||||
if (action === "add") {
|
||||
if (isBlocked) {
|
||||
context.reason = "already-blocked";
|
||||
return context;
|
||||
}
|
||||
await this.mainController.services.userBlocks.addBlock(targetHex, actorHex);
|
||||
context.ok = true;
|
||||
context.reason = "blocked";
|
||||
} else if (action === "remove") {
|
||||
if (!isBlocked) {
|
||||
context.reason = "not-blocked";
|
||||
return context;
|
||||
}
|
||||
await this.mainController.services.userBlocks.removeBlock(targetHex, actorHex);
|
||||
context.ok = true;
|
||||
context.reason = "unblocked";
|
||||
} else {
|
||||
context.reason = "invalid-action";
|
||||
return context;
|
||||
}
|
||||
|
||||
if (context.ok) {
|
||||
try {
|
||||
await this.mainController.services.onVideosShouldRefresh({
|
||||
reason: `blocklist-${action}`,
|
||||
actorHex,
|
||||
targetHex,
|
||||
});
|
||||
} catch (refreshError) {
|
||||
userLogger.warn(
|
||||
"[ProfileModalController] Failed to refresh videos after blocklist mutation:",
|
||||
refreshError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
} catch (error) {
|
||||
context.error = error;
|
||||
context.reason = error?.code || "service-error";
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { buildProfileMetadataEvent } from "../../nostrEventSchemas.js";
|
||||
import { buildPublicUrl, buildR2Key } from "../../r2.js";
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
|
||||
export class ProfileEditController {
|
||||
constructor(mainController) {
|
||||
this.mainController = mainController;
|
||||
|
||||
this.profileEditBtn = null;
|
||||
this.profileEditBackBtn = null;
|
||||
this.editNameInput = null;
|
||||
this.editDisplayNameInput = null;
|
||||
this.editAboutInput = null;
|
||||
this.editWebsiteInput = null;
|
||||
this.editNip05Input = null;
|
||||
this.editLud16Input = null;
|
||||
this.editPictureInput = null;
|
||||
this.editBannerInput = null;
|
||||
|
||||
this.editPictureFile = null;
|
||||
this.editPictureUploadBtn = null;
|
||||
this.editPictureStorageHint = null;
|
||||
this.editPictureConfigureLink = null;
|
||||
|
||||
this.editBannerFile = null;
|
||||
this.editBannerUploadBtn = null;
|
||||
this.editBannerStorageHint = null;
|
||||
this.editBannerConfigureLink = null;
|
||||
|
||||
this.editSaveBtn = null;
|
||||
this.editCancelBtn = null;
|
||||
this.editStatusText = null;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.profileEditBtn = document.getElementById("profileEditBtn") || null;
|
||||
this.profileEditBackBtn = document.getElementById("profileEditBackBtn") || null;
|
||||
|
||||
this.editNameInput = document.getElementById("editNameInput") || null;
|
||||
this.editDisplayNameInput = document.getElementById("editDisplayNameInput") || null;
|
||||
this.editAboutInput = document.getElementById("editAboutInput") || null;
|
||||
this.editWebsiteInput = document.getElementById("editWebsiteInput") || null;
|
||||
this.editNip05Input = document.getElementById("editNip05Input") || null;
|
||||
this.editLud16Input = document.getElementById("editLud16Input") || null;
|
||||
this.editPictureInput = document.getElementById("editPictureInput") || null;
|
||||
this.editBannerInput = document.getElementById("editBannerInput") || null;
|
||||
|
||||
this.editPictureFile = document.getElementById("editPictureFile") || null;
|
||||
this.editPictureUploadBtn = document.getElementById("editPictureUploadBtn") || null;
|
||||
this.editPictureStorageHint = document.getElementById("editPictureStorageHint") || null;
|
||||
this.editPictureConfigureLink = document.getElementById("editPictureConfigureLink") || null;
|
||||
|
||||
this.editBannerFile = document.getElementById("editBannerFile") || null;
|
||||
this.editBannerUploadBtn = document.getElementById("editBannerUploadBtn") || null;
|
||||
this.editBannerStorageHint = document.getElementById("editBannerStorageHint") || null;
|
||||
this.editBannerConfigureLink = document.getElementById("editBannerConfigureLink") || null;
|
||||
|
||||
this.editSaveBtn = document.getElementById("editSaveBtn") || null;
|
||||
this.editCancelBtn = document.getElementById("editCancelBtn") || null;
|
||||
this.editStatusText = document.getElementById("editStatusText") || null;
|
||||
|
||||
// Backwards compatibility aliases
|
||||
this.mainController.profileEditBtn = this.profileEditBtn;
|
||||
this.mainController.profileEditBackBtn = this.profileEditBackBtn;
|
||||
this.mainController.editNameInput = this.editNameInput;
|
||||
this.mainController.editDisplayNameInput = this.editDisplayNameInput;
|
||||
this.mainController.editAboutInput = this.editAboutInput;
|
||||
this.mainController.editWebsiteInput = this.editWebsiteInput;
|
||||
this.mainController.editNip05Input = this.editNip05Input;
|
||||
this.mainController.editLud16Input = this.editLud16Input;
|
||||
this.mainController.editPictureInput = this.editPictureInput;
|
||||
this.mainController.editBannerInput = this.editBannerInput;
|
||||
this.mainController.editPictureFile = this.editPictureFile;
|
||||
this.mainController.editPictureUploadBtn = this.editPictureUploadBtn;
|
||||
this.mainController.editPictureStorageHint = this.editPictureStorageHint;
|
||||
this.mainController.editPictureConfigureLink = this.editPictureConfigureLink;
|
||||
this.mainController.editBannerFile = this.editBannerFile;
|
||||
this.mainController.editBannerUploadBtn = this.editBannerUploadBtn;
|
||||
this.mainController.editBannerStorageHint = this.editBannerStorageHint;
|
||||
this.mainController.editBannerConfigureLink = this.editBannerConfigureLink;
|
||||
this.mainController.editSaveBtn = this.editSaveBtn;
|
||||
this.mainController.editCancelBtn = this.editCancelBtn;
|
||||
this.mainController.editStatusText = this.editStatusText;
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.profileEditBtn instanceof HTMLElement) {
|
||||
this.profileEditBtn.addEventListener("click", () => {
|
||||
this.handleEditProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileEditBackBtn instanceof HTMLElement) {
|
||||
this.profileEditBackBtn.addEventListener("click", () => {
|
||||
this.mainController.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editCancelBtn instanceof HTMLElement) {
|
||||
this.editCancelBtn.addEventListener("click", () => {
|
||||
this.mainController.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editSaveBtn instanceof HTMLElement) {
|
||||
this.editSaveBtn.addEventListener("click", () => {
|
||||
void this.handleSaveProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureUploadBtn instanceof HTMLElement) {
|
||||
this.editPictureUploadBtn.addEventListener("click", () => {
|
||||
if (this.editPictureFile) this.editPictureFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureFile instanceof HTMLElement) {
|
||||
this.editPictureFile.addEventListener("change", () => {
|
||||
void this.handleUpload("picture");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerUploadBtn instanceof HTMLElement) {
|
||||
this.editBannerUploadBtn.addEventListener("click", () => {
|
||||
if (this.editBannerFile) this.editBannerFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerFile instanceof HTMLElement) {
|
||||
this.editBannerFile.addEventListener("change", () => {
|
||||
void this.handleUpload("banner");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureConfigureLink instanceof HTMLElement) {
|
||||
this.editPictureConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.mainController.selectPane("storage");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerConfigureLink instanceof HTMLElement) {
|
||||
this.editBannerConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.mainController.selectPane("storage");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleEditProfile() {
|
||||
this.mainController.selectPane("edit");
|
||||
void this.populateEditPane();
|
||||
}
|
||||
|
||||
async populateEditPane() {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheEntry = this.mainController.services.getProfileCacheEntry(pubkey);
|
||||
const profile = cacheEntry?.profile || {};
|
||||
|
||||
if (this.editNameInput) this.editNameInput.value = profile.name || "";
|
||||
if (this.editDisplayNameInput)
|
||||
this.editDisplayNameInput.value = profile.display_name || "";
|
||||
if (this.editAboutInput) this.editAboutInput.value = profile.about || "";
|
||||
if (this.editWebsiteInput)
|
||||
this.editWebsiteInput.value = profile.website || "";
|
||||
if (this.editNip05Input) this.editNip05Input.value = profile.nip05 || "";
|
||||
if (this.editLud16Input) this.editLud16Input.value = profile.lud16 || "";
|
||||
if (this.editPictureInput)
|
||||
this.editPictureInput.value = profile.picture || "";
|
||||
if (this.editBannerInput) this.editBannerInput.value = profile.banner || "";
|
||||
|
||||
void this.checkStorageForUploads(pubkey);
|
||||
}
|
||||
|
||||
async checkStorageForUploads(pubkey) {
|
||||
const r2Service = this.mainController.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
let hasStorage = false;
|
||||
try {
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
hasStorage = !!credentials;
|
||||
} catch (e) {
|
||||
hasStorage = false;
|
||||
}
|
||||
|
||||
const updateUI = (uploadBtn, hint, has) => {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = !has;
|
||||
if (!has) uploadBtn.setAttribute("aria-disabled", "true");
|
||||
else uploadBtn.removeAttribute("aria-disabled");
|
||||
}
|
||||
if (hint) {
|
||||
if (has) hint.classList.add("hidden");
|
||||
else hint.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
updateUI(
|
||||
this.editPictureUploadBtn,
|
||||
this.editPictureStorageHint,
|
||||
hasStorage,
|
||||
);
|
||||
updateUI(this.editBannerUploadBtn, this.editBannerStorageHint, hasStorage);
|
||||
}
|
||||
|
||||
async handleUpload(type) {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const r2Service = this.mainController.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
const fileInput =
|
||||
type === "picture" ? this.editPictureFile : this.editBannerFile;
|
||||
const urlInput =
|
||||
type === "picture" ? this.editPictureInput : this.editBannerInput;
|
||||
const uploadBtn =
|
||||
type === "picture" ? this.editPictureUploadBtn : this.editBannerUploadBtn;
|
||||
|
||||
if (!fileInput || !fileInput.files.length) return;
|
||||
const file = fileInput.files[0];
|
||||
|
||||
try {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = "Uploading...";
|
||||
}
|
||||
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
if (!credentials) {
|
||||
this.mainController.showError("Storage configuration missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const key = buildR2Key(pubkey, file);
|
||||
await r2Service.uploadFile({
|
||||
file,
|
||||
...credentials,
|
||||
bucket: credentials.bucket,
|
||||
key,
|
||||
});
|
||||
|
||||
const url = buildPublicUrl(credentials.baseDomain, key);
|
||||
if (urlInput) urlInput.value = url;
|
||||
|
||||
fileInput.value = "";
|
||||
} catch (error) {
|
||||
this.mainController.showError("Upload failed: " + (error.message || "Unknown error"));
|
||||
devLogger.error("Upload error:", error);
|
||||
} finally {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleSaveProfile() {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const profile = {
|
||||
name: this.editNameInput?.value?.trim() || "",
|
||||
display_name: this.editDisplayNameInput?.value?.trim() || "",
|
||||
about: this.editAboutInput?.value?.trim() || "",
|
||||
website: this.editWebsiteInput?.value?.trim() || "",
|
||||
nip05: this.editNip05Input?.value?.trim() || "",
|
||||
lud16: this.editLud16Input?.value?.trim() || "",
|
||||
picture: this.editPictureInput?.value?.trim() || "",
|
||||
banner: this.editBannerInput?.value?.trim() || "",
|
||||
};
|
||||
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = true;
|
||||
this.editSaveBtn.textContent = "Saving...";
|
||||
}
|
||||
|
||||
try {
|
||||
const event = buildProfileMetadataEvent({
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
metadata: profile,
|
||||
});
|
||||
|
||||
const result =
|
||||
await this.mainController.services.nostrClient.signAndPublishEvent(event);
|
||||
|
||||
if (result && result.signedEvent) {
|
||||
if (this.mainController.services.nostrClient.handleEvent) {
|
||||
this.mainController.services.nostrClient.handleEvent(result.signedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this.mainController.showSuccess("Profile updated!");
|
||||
this.mainController.selectPane("account");
|
||||
this.mainController.renderSavedProfiles();
|
||||
} catch (error) {
|
||||
this.mainController.showError("Failed to save profile: " + error.message);
|
||||
} finally {
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = false;
|
||||
this.editSaveBtn.textContent = "Save Profile";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-742
@@ -24,8 +24,6 @@ import { getBreakpointLg } from "../designSystem/metrics.js";
|
||||
import { getProviderMetadata } from "../services/authProviders/index.js";
|
||||
import { AppShell } from "./dm/index.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { buildPublicUrl, buildR2Key } from "../r2.js";
|
||||
import { buildProfileMetadataEvent } from "../nostrEventSchemas.js";
|
||||
import {
|
||||
describeAttachment,
|
||||
extractAttachmentsFromMessage,
|
||||
@@ -50,6 +48,8 @@ import { ProfileRelayController } from "./profileModal/ProfileRelayController.js
|
||||
import { ProfileHashtagController } from "./profileModal/ProfileHashtagController.js";
|
||||
import { ProfileAdminController } from "./profileModal/ProfileAdminController.js";
|
||||
import { ProfileModerationController } from "./profileModal/ProfileModerationController.js";
|
||||
import { ProfileBlockListController } from "./profileModal/ProfileBlockListController.js";
|
||||
import { ProfileEditController } from "./profileModal/ProfileEditController.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
@@ -211,6 +211,8 @@ export class ProfileModalController {
|
||||
this.hashtagController = new ProfileHashtagController(this);
|
||||
this.adminController = new ProfileAdminController(this);
|
||||
this.moderationController = new ProfileModerationController(this);
|
||||
this.blockListController = new ProfileBlockListController(this);
|
||||
this.editController = new ProfileEditController(this);
|
||||
this.hashtagController.initialize();
|
||||
|
||||
this.dmController.enableNip17RelayWarning = resolvedEnableNip17RelayWarning;
|
||||
@@ -726,12 +728,7 @@ export class ProfileModalController {
|
||||
this.profileFriendsRefreshBtn =
|
||||
document.getElementById("friendsRefreshBtn") || null;
|
||||
|
||||
this.blockList = document.getElementById("blockedList") || null;
|
||||
this.blockListEmpty = document.getElementById("blockedEmpty") || null;
|
||||
this.blockInput = document.getElementById("blockedInput") || null;
|
||||
this.addBlockedButton = document.getElementById("addBlockedBtn") || null;
|
||||
this.profileBlockedRefreshBtn =
|
||||
document.getElementById("blockedRefreshBtn") || null;
|
||||
this.blockListController.cacheDomReferences();
|
||||
|
||||
this.dmController.profileMessagesPane =
|
||||
document.getElementById("profilePaneMessages") || null;
|
||||
@@ -831,13 +828,6 @@ export class ProfileModalController {
|
||||
this.profileFriendsEmpty = this.friendListEmpty;
|
||||
this.profileFriendsInput = this.friendInput;
|
||||
this.profileAddFriendBtn = this.addFriendButton;
|
||||
this.profileBlockedList = this.blockList;
|
||||
this.profileBlockedEmpty = this.blockListEmpty;
|
||||
this.profileBlockedInput = this.blockInput;
|
||||
this.profileAddBlockedBtn = this.addBlockedButton;
|
||||
this.blockListStatus =
|
||||
this.panes.blocked?.querySelector("[data-role=\"blocked-list-status\"]") ||
|
||||
null;
|
||||
this.moderationController.cacheDomReferences();
|
||||
this.adminController.cacheDomReferences();
|
||||
|
||||
@@ -887,31 +877,10 @@ export class ProfileModalController {
|
||||
);
|
||||
ensureAriaLabel(this.profileSubscriptionsRefreshBtn, "Refresh subscriptions");
|
||||
ensureAriaLabel(this.profileFriendsRefreshBtn, "Refresh friends");
|
||||
|
||||
this.editController.cacheDomReferences();
|
||||
ensureAriaLabel(this.profileBlockedRefreshBtn, "Refresh muted & blocked users");
|
||||
|
||||
this.editNameInput = document.getElementById("editNameInput") || null;
|
||||
this.editDisplayNameInput = document.getElementById("editDisplayNameInput") || null;
|
||||
this.editAboutInput = document.getElementById("editAboutInput") || null;
|
||||
this.editWebsiteInput = document.getElementById("editWebsiteInput") || null;
|
||||
this.editNip05Input = document.getElementById("editNip05Input") || null;
|
||||
this.editLud16Input = document.getElementById("editLud16Input") || null;
|
||||
this.editPictureInput = document.getElementById("editPictureInput") || null;
|
||||
this.editBannerInput = document.getElementById("editBannerInput") || null;
|
||||
|
||||
this.editPictureFile = document.getElementById("editPictureFile") || null;
|
||||
this.editPictureUploadBtn = document.getElementById("editPictureUploadBtn") || null;
|
||||
this.editPictureStorageHint = document.getElementById("editPictureStorageHint") || null;
|
||||
this.editPictureConfigureLink = document.getElementById("editPictureConfigureLink") || null;
|
||||
|
||||
this.editBannerFile = document.getElementById("editBannerFile") || null;
|
||||
this.editBannerUploadBtn = document.getElementById("editBannerUploadBtn") || null;
|
||||
this.editBannerStorageHint = document.getElementById("editBannerStorageHint") || null;
|
||||
this.editBannerConfigureLink = document.getElementById("editBannerConfigureLink") || null;
|
||||
|
||||
this.editSaveBtn = document.getElementById("editSaveBtn") || null;
|
||||
this.editCancelBtn = document.getElementById("editCancelBtn") || null;
|
||||
this.editStatusText = document.getElementById("editStatusText") || null;
|
||||
|
||||
if (this.createWatchHistoryRenderer) {
|
||||
this.ensureProfileHistoryRenderer();
|
||||
}
|
||||
@@ -1692,41 +1661,7 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addBlockedButton instanceof HTMLElement) {
|
||||
this.addBlockedButton.addEventListener("click", () => {
|
||||
void this.handleAddBlockedCreator();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileBlockedRefreshBtn instanceof HTMLElement) {
|
||||
this.profileBlockedRefreshBtn.addEventListener("click", () => {
|
||||
const activeHex = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!activeHex) {
|
||||
return;
|
||||
}
|
||||
const blocksService = this.services.userBlocks;
|
||||
if (!blocksService || typeof blocksService.loadBlocks !== "function") {
|
||||
return;
|
||||
}
|
||||
void blocksService
|
||||
.loadBlocks(activeHex)
|
||||
.then(() => {
|
||||
this.populateBlockedList();
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn("[profileModal] Failed to refresh blocked list:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.blockInput instanceof HTMLElement) {
|
||||
this.blockInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void this.handleAddBlockedCreator();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.blockListController.registerEventListeners();
|
||||
|
||||
if (this.profileSubscriptionsRefreshBtn instanceof HTMLElement) {
|
||||
this.profileSubscriptionsRefreshBtn.addEventListener("click", () => {
|
||||
@@ -1954,67 +1889,7 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
|
||||
if (this.profileEditBtn instanceof HTMLElement) {
|
||||
this.profileEditBtn.addEventListener("click", () => {
|
||||
this.handleEditProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileEditBackBtn instanceof HTMLElement) {
|
||||
this.profileEditBackBtn.addEventListener("click", () => {
|
||||
this.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editCancelBtn instanceof HTMLElement) {
|
||||
this.editCancelBtn.addEventListener("click", () => {
|
||||
this.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editSaveBtn instanceof HTMLElement) {
|
||||
this.editSaveBtn.addEventListener("click", () => {
|
||||
void this.handleSaveProfile();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureUploadBtn instanceof HTMLElement) {
|
||||
this.editPictureUploadBtn.addEventListener("click", () => {
|
||||
if (this.editPictureFile) this.editPictureFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureFile instanceof HTMLElement) {
|
||||
this.editPictureFile.addEventListener("change", () => {
|
||||
void this.handleUpload("picture");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerUploadBtn instanceof HTMLElement) {
|
||||
this.editBannerUploadBtn.addEventListener("click", () => {
|
||||
if (this.editBannerFile) this.editBannerFile.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerFile instanceof HTMLElement) {
|
||||
this.editBannerFile.addEventListener("change", () => {
|
||||
void this.handleUpload("banner");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editPictureConfigureLink instanceof HTMLElement) {
|
||||
this.editPictureConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.selectPane("storage");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editBannerConfigureLink instanceof HTMLElement) {
|
||||
this.editBannerConfigureLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.selectPane("storage");
|
||||
});
|
||||
}
|
||||
this.editController.registerEventListeners();
|
||||
|
||||
this.dmController.updateMessagesReloadState();
|
||||
}
|
||||
@@ -3390,98 +3265,8 @@ export class ProfileModalController {
|
||||
|
||||
|
||||
|
||||
ensureBlockListStatusElement() {
|
||||
if (this.blockListStatus instanceof HTMLElement) {
|
||||
return this.blockListStatus;
|
||||
}
|
||||
|
||||
const anchor =
|
||||
this.blockList instanceof HTMLElement
|
||||
? this.blockList
|
||||
: this.blockListEmpty instanceof HTMLElement
|
||||
? this.blockListEmpty
|
||||
: null;
|
||||
|
||||
if (!anchor || !(anchor.parentElement instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = anchor.parentElement.querySelector(
|
||||
'[data-role="blocked-list-status"]',
|
||||
);
|
||||
if (existing instanceof HTMLElement) {
|
||||
this.blockListStatus = existing;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const status = document.createElement("div");
|
||||
status.dataset.role = "blocked-list-status";
|
||||
status.className = "mt-4 flex items-center gap-3 text-sm text-muted hidden";
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
|
||||
if (this.blockList instanceof HTMLElement) {
|
||||
anchor.parentElement.insertBefore(status, this.blockList);
|
||||
} else {
|
||||
anchor.parentElement.appendChild(status);
|
||||
}
|
||||
|
||||
this.blockListStatus = status;
|
||||
return status;
|
||||
}
|
||||
|
||||
setBlockListLoadingState(state = "idle", options = {}) {
|
||||
const statusEl = this.ensureBlockListStatusElement();
|
||||
if (!statusEl) {
|
||||
this.blockListLoadingState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof options.message === "string" && options.message.trim()
|
||||
? options.message.trim()
|
||||
: "";
|
||||
|
||||
statusEl.textContent = "";
|
||||
statusEl.classList.remove("text-status-warning");
|
||||
statusEl.classList.add("text-muted");
|
||||
statusEl.classList.add("hidden");
|
||||
|
||||
this.blockListLoadingState = state;
|
||||
|
||||
if (state === "loading") {
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "status-spinner status-spinner--inline";
|
||||
spinner.setAttribute("aria-hidden", "true");
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = message || "Loading blocked creators…";
|
||||
|
||||
statusEl.appendChild(spinner);
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
statusEl.classList.remove("text-muted");
|
||||
statusEl.classList.add("text-status-warning");
|
||||
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent =
|
||||
message || "Blocked creators may be out of date. Try again later.";
|
||||
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
}
|
||||
setBlockListLoadingState(...args) {
|
||||
return this.blockListController.setBlockListLoadingState(...args);
|
||||
}
|
||||
|
||||
setPermissionPromptCtaState(nextState = {}) {
|
||||
@@ -3689,184 +3474,8 @@ export class ProfileModalController {
|
||||
this.refreshSubscriptionsBackgroundStatus();
|
||||
}
|
||||
|
||||
populateBlockedList(blocked = null) {
|
||||
if (!this.blockList || !this.blockListEmpty) {
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceEntries =
|
||||
Array.isArray(blocked) && blocked.length
|
||||
? blocked
|
||||
: this.services.userBlocks.getBlockedPubkeys();
|
||||
|
||||
const normalizedEntries = [];
|
||||
const pushEntry = (hex, label) => {
|
||||
if (!hex || !label) {
|
||||
return;
|
||||
}
|
||||
normalizedEntries.push({ hex, label });
|
||||
};
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
if (typeof entry === "string") {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.safeEncodeNpub(decoded) || trimmed;
|
||||
pushEntry(decoded, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
const hex = trimmed.toLowerCase();
|
||||
const label = this.safeEncodeNpub(hex) || hex;
|
||||
pushEntry(hex, label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry && typeof entry === "object") {
|
||||
const candidateNpub =
|
||||
typeof entry.npub === "string" ? entry.npub.trim() : "";
|
||||
const candidateHex =
|
||||
typeof entry.pubkey === "string" ? entry.pubkey.trim() : "";
|
||||
|
||||
if (candidateHex && /^[0-9a-f]{64}$/i.test(candidateHex)) {
|
||||
const normalizedHex = candidateHex.toLowerCase();
|
||||
const label =
|
||||
candidateNpub && candidateNpub.startsWith("npub1")
|
||||
? candidateNpub
|
||||
: this.safeEncodeNpub(normalizedHex) || normalizedHex;
|
||||
pushEntry(normalizedHex, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidateNpub && candidateNpub.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(candidateNpub);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.safeEncodeNpub(decoded) || candidateNpub;
|
||||
pushEntry(decoded, label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deduped = [];
|
||||
const seenHex = new Set();
|
||||
normalizedEntries.forEach((entry) => {
|
||||
if (!seenHex.has(entry.hex)) {
|
||||
seenHex.add(entry.hex);
|
||||
deduped.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
this.blockList.textContent = "";
|
||||
|
||||
if (!deduped.length) {
|
||||
this.blockListEmpty.classList.remove("hidden");
|
||||
this.blockList.classList.add("hidden");
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
this.blockList.classList.remove("hidden");
|
||||
|
||||
const formatNpub =
|
||||
typeof this.formatShortNpub === "function"
|
||||
? (value) => this.formatShortNpub(value)
|
||||
: (value) => (typeof value === "string" ? value : "");
|
||||
const entriesNeedingFetch = new Set();
|
||||
|
||||
deduped.forEach(({ hex, label }) => {
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"card flex items-center justify-between gap-4 p-4";
|
||||
|
||||
let cachedProfile = null;
|
||||
if (hex) {
|
||||
const cacheEntry = this.services.getProfileCacheEntry(hex);
|
||||
cachedProfile = cacheEntry?.profile || null;
|
||||
if (!cacheEntry) {
|
||||
entriesNeedingFetch.add(hex);
|
||||
}
|
||||
}
|
||||
|
||||
const encodedNpub =
|
||||
hex && typeof this.safeEncodeNpub === "function"
|
||||
? this.safeEncodeNpub(hex)
|
||||
: label;
|
||||
const displayNpub = formatNpub(encodedNpub) || encodedNpub || label;
|
||||
const displayName =
|
||||
cachedProfile?.name?.trim() || displayNpub || "Blocked profile";
|
||||
const avatarSrc =
|
||||
cachedProfile?.picture || FALLBACK_PROFILE_AVATAR;
|
||||
|
||||
const summary = this.dmController.createCompactProfileSummary({
|
||||
displayName,
|
||||
displayNpub,
|
||||
avatarSrc,
|
||||
});
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "flex flex-wrap items-center justify-end gap-2";
|
||||
|
||||
const viewButton = this.createViewChannelButton({
|
||||
targetNpub: encodedNpub,
|
||||
displayNpub,
|
||||
});
|
||||
if (viewButton) {
|
||||
actions.appendChild(viewButton);
|
||||
}
|
||||
|
||||
const copyButton = this.createCopyNpubButton({
|
||||
targetNpub: encodedNpub,
|
||||
displayNpub,
|
||||
});
|
||||
if (copyButton) {
|
||||
actions.appendChild(copyButton);
|
||||
}
|
||||
|
||||
const removeButton = this.createRemoveButton({
|
||||
label: "Remove",
|
||||
onRemove: () => this.handleRemoveBlockedCreator(hex),
|
||||
});
|
||||
if (removeButton) {
|
||||
removeButton.dataset.blockedHex = hex;
|
||||
actions.appendChild(removeButton);
|
||||
}
|
||||
|
||||
item.appendChild(summary);
|
||||
if (actions.childElementCount > 0) {
|
||||
item.appendChild(actions);
|
||||
}
|
||||
|
||||
this.blockList.appendChild(item);
|
||||
});
|
||||
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
|
||||
if (
|
||||
entriesNeedingFetch.size &&
|
||||
typeof this.services.batchFetchProfiles === "function"
|
||||
) {
|
||||
this.services.batchFetchProfiles(entriesNeedingFetch);
|
||||
}
|
||||
populateBlockedList(...args) {
|
||||
return this.blockListController.populateBlockedList(...args);
|
||||
}
|
||||
|
||||
async populateSubscriptionsList(subscriptions = null) {
|
||||
@@ -4556,354 +4165,28 @@ export class ProfileModalController {
|
||||
return { success: true, reason: successReason };
|
||||
}
|
||||
|
||||
async handleAddBlockedCreator() {
|
||||
const input = this.blockInput || null;
|
||||
const rawValue = typeof input?.value === "string" ? input.value : "";
|
||||
const trimmed = rawValue.trim();
|
||||
|
||||
const context = {
|
||||
input,
|
||||
rawValue,
|
||||
value: trimmed,
|
||||
success: false,
|
||||
reason: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
if (!input) {
|
||||
context.reason = "missing-input";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (!trimmed) {
|
||||
this.showError("Enter an npub to block.");
|
||||
context.reason = "empty";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
|
||||
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!activePubkey) {
|
||||
this.showError("Please login to manage your block list.");
|
||||
context.reason = "no-active-pubkey";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
|
||||
const actorHex = activePubkey;
|
||||
let targetHex = "";
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.safeDecodeNpub(trimmed) || "";
|
||||
if (!targetHex) {
|
||||
this.showError("Invalid npub. Please double-check and try again.");
|
||||
context.reason = "invalid-npub";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
} else {
|
||||
this.showError("Enter a valid npub or hex pubkey.");
|
||||
context.reason = "invalid-value";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (targetHex === actorHex) {
|
||||
this.showError("You cannot block yourself.");
|
||||
context.reason = "self";
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
}
|
||||
|
||||
context.targetHex = targetHex;
|
||||
|
||||
try {
|
||||
const mutationResult = await this.mutateBlocklist({
|
||||
action: "add",
|
||||
actorHex,
|
||||
targetHex,
|
||||
controller: this,
|
||||
});
|
||||
|
||||
context.result = mutationResult;
|
||||
|
||||
if (mutationResult?.ok) {
|
||||
this.showSuccess(
|
||||
"Creator blocked. You won't see their videos anymore.",
|
||||
);
|
||||
context.success = true;
|
||||
context.reason = mutationResult.reason || "blocked";
|
||||
} else if (mutationResult?.reason === "already-blocked") {
|
||||
this.showSuccess("You already blocked this creator.");
|
||||
context.reason = "already-blocked";
|
||||
} else {
|
||||
const message =
|
||||
mutationResult?.error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: mutationResult?.error?.code ===
|
||||
"extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: mutationResult?.error?.message ||
|
||||
"Failed to update your mute/block list. Please try again.";
|
||||
context.error = mutationResult?.error || null;
|
||||
context.reason = mutationResult?.reason || "service-error";
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.blockInput) {
|
||||
this.blockInput.value = "";
|
||||
}
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to add creator to personal block list:", error);
|
||||
context.error = error;
|
||||
context.reason = error?.code || "service-error";
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: error?.code === "extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: "Failed to update your mute/block list. Please try again.";
|
||||
this.showError(message);
|
||||
}
|
||||
|
||||
this.callbacks.onAddBlocked(context, this);
|
||||
return context;
|
||||
handleAddBlockedCreator(...args) {
|
||||
return this.blockListController.handleAddBlockedCreator(...args);
|
||||
}
|
||||
|
||||
async handleRemoveBlockedCreator(candidate) {
|
||||
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!activePubkey) {
|
||||
this.showError("Please login to manage your block list.");
|
||||
return;
|
||||
}
|
||||
|
||||
let targetHex = "";
|
||||
if (typeof candidate === "string") {
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.safeDecodeNpub(trimmed) || "";
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetHex) {
|
||||
userLogger.warn("No valid pubkey to remove from block list:", candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mutationResult = await this.mutateBlocklist({
|
||||
action: "remove",
|
||||
actorHex: activePubkey,
|
||||
targetHex,
|
||||
controller: this,
|
||||
});
|
||||
|
||||
if (mutationResult?.ok) {
|
||||
this.showSuccess("Creator removed from your mute/block list.");
|
||||
} else if (mutationResult?.reason === "not-blocked") {
|
||||
this.showSuccess("Creator already removed from your mute/block list.");
|
||||
} else if (mutationResult?.error) {
|
||||
const message =
|
||||
mutationResult.error.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: mutationResult.error.code ===
|
||||
"extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: mutationResult.error.message ||
|
||||
"Failed to update your mute/block list. Please try again.";
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.error(
|
||||
"Failed to remove creator from personal block list:",
|
||||
error,
|
||||
);
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: error?.code === "extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your mute/block list."
|
||||
: "Failed to update your mute/block list. Please try again.";
|
||||
this.showError(message);
|
||||
}
|
||||
handleRemoveBlockedCreator(...args) {
|
||||
return this.blockListController.handleRemoveBlockedCreator(...args);
|
||||
}
|
||||
|
||||
handleEditProfile() {
|
||||
this.selectPane("edit");
|
||||
void this.populateEditPane();
|
||||
handleEditProfile(...args) {
|
||||
return this.editController.handleEditProfile(...args);
|
||||
}
|
||||
|
||||
async populateEditPane() {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheEntry = this.services.getProfileCacheEntry(pubkey);
|
||||
const profile = cacheEntry?.profile || {};
|
||||
|
||||
if (this.editNameInput) this.editNameInput.value = profile.name || "";
|
||||
if (this.editDisplayNameInput)
|
||||
this.editDisplayNameInput.value = profile.display_name || "";
|
||||
if (this.editAboutInput) this.editAboutInput.value = profile.about || "";
|
||||
if (this.editWebsiteInput)
|
||||
this.editWebsiteInput.value = profile.website || "";
|
||||
if (this.editNip05Input) this.editNip05Input.value = profile.nip05 || "";
|
||||
if (this.editLud16Input) this.editLud16Input.value = profile.lud16 || "";
|
||||
if (this.editPictureInput)
|
||||
this.editPictureInput.value = profile.picture || "";
|
||||
if (this.editBannerInput) this.editBannerInput.value = profile.banner || "";
|
||||
|
||||
void this.checkStorageForUploads(pubkey);
|
||||
populateEditPane(...args) {
|
||||
return this.editController.populateEditPane(...args);
|
||||
}
|
||||
|
||||
async checkStorageForUploads(pubkey) {
|
||||
const r2Service = this.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
let hasStorage = false;
|
||||
try {
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
hasStorage = !!credentials;
|
||||
} catch (e) {
|
||||
hasStorage = false;
|
||||
}
|
||||
|
||||
const updateUI = (uploadBtn, hint, has) => {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = !has;
|
||||
if (!has) uploadBtn.setAttribute("aria-disabled", "true");
|
||||
else uploadBtn.removeAttribute("aria-disabled");
|
||||
}
|
||||
if (hint) {
|
||||
if (has) hint.classList.add("hidden");
|
||||
else hint.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
updateUI(
|
||||
this.editPictureUploadBtn,
|
||||
this.editPictureStorageHint,
|
||||
hasStorage,
|
||||
);
|
||||
updateUI(this.editBannerUploadBtn, this.editBannerStorageHint, hasStorage);
|
||||
async handleUpload(...args) {
|
||||
return this.editController.handleUpload(...args);
|
||||
}
|
||||
|
||||
async handleUpload(type) {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const r2Service = this.services.r2Service;
|
||||
if (!r2Service) return;
|
||||
|
||||
const fileInput =
|
||||
type === "picture" ? this.editPictureFile : this.editBannerFile;
|
||||
const urlInput =
|
||||
type === "picture" ? this.editPictureInput : this.editBannerInput;
|
||||
const uploadBtn =
|
||||
type === "picture" ? this.editPictureUploadBtn : this.editBannerUploadBtn;
|
||||
|
||||
if (!fileInput || !fileInput.files.length) return;
|
||||
const file = fileInput.files[0];
|
||||
|
||||
try {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = "Uploading...";
|
||||
}
|
||||
|
||||
const credentials = await r2Service.resolveConnection(pubkey);
|
||||
if (!credentials) {
|
||||
this.showError("Storage configuration missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const key = buildR2Key(pubkey, file);
|
||||
await r2Service.uploadFile({
|
||||
file,
|
||||
...credentials,
|
||||
bucket: credentials.bucket,
|
||||
key,
|
||||
});
|
||||
|
||||
const url = buildPublicUrl(credentials.baseDomain, key);
|
||||
if (urlInput) urlInput.value = url;
|
||||
|
||||
fileInput.value = "";
|
||||
} catch (error) {
|
||||
this.showError("Upload failed: " + (error.message || "Unknown error"));
|
||||
devLogger.error("Upload error:", error);
|
||||
} finally {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleSaveProfile() {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!pubkey) return;
|
||||
|
||||
const profile = {
|
||||
name: this.editNameInput?.value?.trim() || "",
|
||||
display_name: this.editDisplayNameInput?.value?.trim() || "",
|
||||
about: this.editAboutInput?.value?.trim() || "",
|
||||
website: this.editWebsiteInput?.value?.trim() || "",
|
||||
nip05: this.editNip05Input?.value?.trim() || "",
|
||||
lud16: this.editLud16Input?.value?.trim() || "",
|
||||
picture: this.editPictureInput?.value?.trim() || "",
|
||||
banner: this.editBannerInput?.value?.trim() || "",
|
||||
};
|
||||
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = true;
|
||||
this.editSaveBtn.textContent = "Saving...";
|
||||
}
|
||||
|
||||
try {
|
||||
const event = buildProfileMetadataEvent({
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
metadata: profile,
|
||||
});
|
||||
|
||||
const result =
|
||||
await this.services.nostrClient.signAndPublishEvent(event);
|
||||
|
||||
if (result && result.signedEvent) {
|
||||
if (this.services.nostrClient.handleEvent) {
|
||||
this.services.nostrClient.handleEvent(result.signedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this.showSuccess("Profile updated!");
|
||||
this.selectPane("account");
|
||||
this.renderSavedProfiles();
|
||||
} catch (error) {
|
||||
this.showError("Failed to save profile: " + error.message);
|
||||
} finally {
|
||||
if (this.editSaveBtn) {
|
||||
this.editSaveBtn.disabled = false;
|
||||
this.editSaveBtn.textContent = "Save Profile";
|
||||
}
|
||||
}
|
||||
async handleSaveProfile(...args) {
|
||||
return this.editController.handleSaveProfile(...args);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Test Log
|
||||
|
||||
## Commands Run
|
||||
|
||||
1. `npm run lint`
|
||||
- Result: Passed (ignoring assets check).
|
||||
2. `node scripts/run-targeted-tests.mjs tests/profile-modal-controller.test.mjs`
|
||||
- Result: Passed (18/18 tests).
|
||||
- Initial failure due to missing alias for `this.blockList` on main controller. Fixed by adding aliases in `ProfileBlockListController.js`.
|
||||
3. `node scripts/check-file-size.mjs --report`
|
||||
- Result: `js/ui/profileModalController.js` is now 5541 lines.
|
||||
|
||||
## Manual Verification Notes
|
||||
|
||||
- Code inspection confirmed extracted logic matches original behavior.
|
||||
- Delegate methods ensure API compatibility.
|
||||
- Constructor instantiation order preserved (new controllers added before `hashtagController` initialization).
|
||||
@@ -0,0 +1,26 @@
|
||||
# Decomposition Todo
|
||||
|
||||
- [ ] Create `js/ui/profileModal/ProfileBlockListController.js`
|
||||
- [ ] Import dependencies (`devLogger`, `userLogger`, `FALLBACK_PROFILE_AVATAR`).
|
||||
- [ ] Implement `cacheDomReferences`.
|
||||
- [ ] Implement `registerEventListeners`.
|
||||
- [ ] Move `populateBlockedList` logic.
|
||||
- [ ] Move `mutateBlocklist` logic.
|
||||
- [ ] Move helper methods (`ensureBlockListStatusElement`, `setBlockListLoadingState`).
|
||||
- [ ] Create `js/ui/profileModal/ProfileEditController.js`
|
||||
- [ ] Import dependencies (`buildProfileMetadataEvent`, `buildPublicUrl`, `buildR2Key`).
|
||||
- [ ] Implement `cacheDomReferences`.
|
||||
- [ ] Implement `registerEventListeners`.
|
||||
- [ ] Move `populateEditPane` logic.
|
||||
- [ ] Move `handleSaveProfile` logic.
|
||||
- [ ] Move `handleUpload` logic.
|
||||
- [ ] Update `js/ui/profileModalController.js`
|
||||
- [ ] Import new controllers.
|
||||
- [ ] Instantiate in `constructor`.
|
||||
- [ ] Call `cacheDomReferences` and `registerEventListeners`.
|
||||
- [ ] Delegate method calls (wrappers or direct usage).
|
||||
- [ ] Remove extracted code.
|
||||
- [ ] Verification
|
||||
- [ ] `npm run lint`
|
||||
- [ ] `node scripts/run-targeted-tests.mjs tests/profile-modal-controller.test.mjs`
|
||||
- [ ] Verify line count reduction.
|
||||
Reference in new issue
Block a user