mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
[decompose] extract ProfileHashtagController from js/ui/profileModalController.js
Extracts all hashtag preference related logic into a new controller `js/ui/profileModal/ProfileHashtagController.js` to reduce the size of `js/ui/profileModalController.js` and improve maintainability. - Moves `normalizeHashtagTag`, `formatHashtagTag`, `getResolvedHashtagPreferences`, `setHashtagStatus`, `refreshHashtagBackgroundStatus`, `populateHashtagPreferences`, `renderHashtagList`, `sanitizeHashtagList`, `createHashtagListItem`, `persistHashtagPreferences`, `handleAddHashtagPreference`, `handleRemoveHashtagPreference`, `handleHashtagPreferencesChange`, `describeHashtagPreferencesError`. - Updates `ProfileModalController` to delegate to `ProfileHashtagController`. - Reduces `js/ui/profileModalController.js` by ~600 lines. - Updates file size baseline in `scripts/check-file-size.mjs`. Passes `npm run lint` and `npm run test:unit`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
7c1e2c0f6e
commit
5f1edebea7
5 files changed
+643
-670
No files matched your search
+7
-8
@@ -1,14 +1,13 @@
|
||||
# Context
|
||||
|
||||
**Goal:** Execute the `bitvid-const-refactor-agent` task (daily scheduler).
|
||||
**Scope:** Identify and refactor duplicated numeric constants in `js/` to use canonical named constants.
|
||||
**Focus:** Refactor `5000` (timeout) to `SHORT_TIMEOUT_MS` and `60000` (timeout) to `LONG_TIMEOUT_MS` in selected files.
|
||||
**Goal:** Execute the `bitvid-decompose-agent` task (daily scheduler).
|
||||
**Scope:** Decompose `js/ui/profileModalController.js` by extracting hashtag preference logic into `js/ui/profileModal/ProfileHashtagController.js`.
|
||||
**Focus:** Reduce file size by moving cohesive hashtag-related methods and state.
|
||||
|
||||
**Definition of Done:**
|
||||
- [ ] Identify candidate duplicates.
|
||||
- [ ] Refactor `js/ui/engagementController.js` (5000 -> SHORT_TIMEOUT_MS).
|
||||
- [ ] Refactor `js/ui/applicationBootstrap.js` (5000 -> SHORT_TIMEOUT_MS).
|
||||
- [ ] Refactor `js/webtorrent.js` (60000 -> LONG_TIMEOUT_MS).
|
||||
- [ ] Create `js/ui/profileModal/ProfileHashtagController.js`.
|
||||
- [ ] Move methods: `normalizeHashtagTag`, `formatHashtagTag`, `getResolvedHashtagPreferences`, `setHashtagStatus`, `refreshHashtagBackgroundStatus`, `populateHashtagPreferences`, `renderHashtagList`, `sanitizeHashtagList`, `createHashtagListItem`, `persistHashtagPreferences`, `handleAddHashtagPreference`, `handleRemoveHashtagPreference`, `handleHashtagPreferencesChange`, `describeHashtagPreferencesError`.
|
||||
- [ ] Integrate into `js/ui/profileModalController.js`.
|
||||
- [ ] Verify changes with `npm run lint` and `npm run test:unit`.
|
||||
- [ ] Update documentation (`DECISIONS.md`, `TEST_LOG.md`).
|
||||
- [ ] Update `scripts/check-file-size.mjs` baseline.
|
||||
- [ ] Update `docs/agents/AGENT_TASK_LOG.csv`.
|
||||
@@ -2,3 +2,5 @@ date,agent_name,prompt_file,status,branch,summary
|
||||
2026-02-12,audit-agent,bitvid-audit-agent.md,completed,unstable,"Ran audit scripts, created initial reports"
|
||||
2026-02-12,ci-health-agent,bitvid-ci-health-agent.md,completed,ai/ci-health-fix-watch-history-flake,"Fixed flaky test in watch-history.test.mjs using waitFor"
|
||||
2026-02-12,const-refactor-agent,bitvid-const-refactor-agent.md,completed,ai/constants-refactor/daily-run,"Refactored timeouts in UI controllers and webtorrent.js to use canonical constants"
|
||||
2026-02-13,decompose-agent,bitvid-decompose-agent.md,started,ai/decompose/daily-run,"Task claimed — execution beginning"
|
||||
2026-02-13,decompose-agent,bitvid-decompose-agent.md,completed,ai/decompose/daily-run,"Extracted ProfileHashtagController from profileModalController.js, reducing file size by ~600 lines"
|
||||
|
@@ -0,0 +1,598 @@
|
||||
import {
|
||||
normalizeHashtag,
|
||||
formatHashtag,
|
||||
} from "../../utils/hashtagNormalization.js";
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export class ProfileHashtagController {
|
||||
constructor(mainController) {
|
||||
this.mainController = mainController;
|
||||
this.hashtagStatusText = null;
|
||||
this.hashtagBackgroundLoading = false;
|
||||
this.hashtagInterestList = null;
|
||||
this.hashtagInterestEmpty = null;
|
||||
this.hashtagInterestInput = null;
|
||||
this.addHashtagInterestButton = null;
|
||||
this.profileHashtagInterestRefreshBtn = null;
|
||||
this.hashtagDisinterestList = null;
|
||||
this.hashtagDisinterestEmpty = null;
|
||||
this.hashtagDisinterestInput = null;
|
||||
this.addHashtagDisinterestButton = null;
|
||||
this.profileHashtagDisinterestRefreshBtn = null;
|
||||
|
||||
this.hashtagPreferencesService = (this.mainController.services ? this.mainController.services.hashtagPreferences : null);
|
||||
this.describeHashtagPreferencesErrorService =
|
||||
(this.mainController.services ? this.mainController.services.describeHashtagPreferencesError : null);
|
||||
this.getHashtagPreferencesSnapshotService =
|
||||
(this.mainController.services ? this.mainController.services.getHashtagPreferences : null);
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
this.hashtagPreferencesUnsubscribe = null;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (
|
||||
this.hashtagPreferencesService &&
|
||||
typeof this.hashtagPreferencesService.on === "function"
|
||||
) {
|
||||
this.hashtagPreferencesUnsubscribe = this.hashtagPreferencesService.on(
|
||||
"change",
|
||||
(detail) => {
|
||||
this.handleHashtagPreferencesChange({
|
||||
action:
|
||||
typeof detail?.action === "string" ? detail.action : "change",
|
||||
preferences: detail,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.hashtagStatusText =
|
||||
document.getElementById("profileHashtagStatus") || null;
|
||||
this.hashtagInterestList =
|
||||
document.getElementById("profileHashtagInterestList") || null;
|
||||
this.hashtagInterestEmpty =
|
||||
document.getElementById("profileHashtagInterestEmpty") || null;
|
||||
this.hashtagInterestInput =
|
||||
document.getElementById("profileHashtagInterestInput") || null;
|
||||
this.addHashtagInterestButton =
|
||||
document.getElementById("profileAddHashtagInterestBtn") || null;
|
||||
this.profileHashtagInterestRefreshBtn =
|
||||
document.getElementById("profileHashtagInterestRefreshBtn") || null;
|
||||
|
||||
this.hashtagDisinterestList =
|
||||
document.getElementById("profileHashtagDisinterestList") || null;
|
||||
this.hashtagDisinterestEmpty =
|
||||
document.getElementById("profileHashtagDisinterestEmpty") || null;
|
||||
this.hashtagDisinterestInput =
|
||||
document.getElementById("profileHashtagDisinterestInput") || null;
|
||||
this.addHashtagDisinterestButton =
|
||||
document.getElementById("profileAddHashtagDisinterestBtn") || null;
|
||||
this.profileHashtagDisinterestRefreshBtn =
|
||||
document.getElementById("profileHashtagDisinterestRefreshBtn") || null;
|
||||
}
|
||||
|
||||
normalizeHashtagTag(value) {
|
||||
return normalizeHashtag(value);
|
||||
}
|
||||
|
||||
formatHashtagTag(value) {
|
||||
return formatHashtag(value);
|
||||
}
|
||||
|
||||
sanitizeHashtagList(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
return list
|
||||
.map((entry) => {
|
||||
const tag = this.normalizeHashtagTag(entry);
|
||||
return tag ? tag : null;
|
||||
})
|
||||
.filter((tag) => typeof tag === "string");
|
||||
}
|
||||
|
||||
getResolvedHashtagPreferences(preferences = null) {
|
||||
const fallback = {
|
||||
interests: [],
|
||||
disinterests: [],
|
||||
lastLoadError: null,
|
||||
dataReady: false,
|
||||
uiReady: false,
|
||||
loadedFromCache: false,
|
||||
};
|
||||
|
||||
const getSnapshot = this.getHashtagPreferencesSnapshotService;
|
||||
|
||||
if (typeof getSnapshot !== "function") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Use passed preferences or get fresh snapshot
|
||||
const source =
|
||||
preferences && typeof preferences === "object"
|
||||
? preferences
|
||||
: getSnapshot();
|
||||
|
||||
if (!source || typeof source !== "object") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const interestsSource = Array.isArray(source.interests)
|
||||
? source.interests
|
||||
: [];
|
||||
const disinterestsSource = Array.isArray(source.disinterests)
|
||||
? source.disinterests
|
||||
: [];
|
||||
|
||||
return {
|
||||
interests: this.sanitizeHashtagList(interestsSource),
|
||||
disinterests: this.sanitizeHashtagList(disinterestsSource),
|
||||
lastLoadError: source.lastLoadError || null,
|
||||
dataReady: source.dataReady === true,
|
||||
uiReady: source.uiReady === true,
|
||||
loadedFromCache: source.loadedFromCache === true,
|
||||
};
|
||||
}
|
||||
|
||||
setHashtagStatus(message = "", tone = "muted") {
|
||||
if (!this.hashtagStatusText) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hashtagStatusText.textContent = message;
|
||||
this.hashtagStatusText.className = "text-xs mt-2 empty:hidden";
|
||||
|
||||
switch (tone) {
|
||||
case "info":
|
||||
this.hashtagStatusText.classList.add("text-accent");
|
||||
break;
|
||||
case "success":
|
||||
this.hashtagStatusText.classList.add("text-status-success");
|
||||
break;
|
||||
case "warning":
|
||||
this.hashtagStatusText.classList.add("text-status-warning");
|
||||
break;
|
||||
case "danger":
|
||||
this.hashtagStatusText.classList.add("text-status-danger");
|
||||
break;
|
||||
case "muted":
|
||||
default:
|
||||
this.hashtagStatusText.classList.add("text-muted");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refreshHashtagBackgroundStatus() {
|
||||
if (!this.hashtagBackgroundLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
|
||||
if (!statusText) {
|
||||
this.setHashtagStatus("Loading in background…", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusText === "Loading in background…") {
|
||||
// already showing spinner message
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
clearHashtagInputs() {
|
||||
if (this.hashtagInterestInput instanceof HTMLInputElement) {
|
||||
this.hashtagInterestInput.value = "";
|
||||
}
|
||||
if (this.hashtagDisinterestInput instanceof HTMLInputElement) {
|
||||
this.hashtagDisinterestInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
populateHashtagPreferences(preferences = null) {
|
||||
const snapshot = this.getResolvedHashtagPreferences(preferences);
|
||||
|
||||
if (!snapshot.uiReady) {
|
||||
this.setHashtagStatus("Loading hashtag preferences…", "info");
|
||||
this.renderHashtagList("interest", []);
|
||||
this.renderHashtagList("disinterest", []);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.dataReady) {
|
||||
const message = snapshot.lastLoadError
|
||||
? "Couldn’t sync hashtag preferences. Retry to load your lists."
|
||||
: "Hashtag preferences are unavailable right now. Retry to sync.";
|
||||
this.setHashtagStatus(message, "warning");
|
||||
this.renderHashtagList("interest", []);
|
||||
this.renderHashtagList("disinterest", []);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderHashtagList("interest", snapshot.interests);
|
||||
this.renderHashtagList("disinterest", snapshot.disinterests);
|
||||
|
||||
if (!snapshot.interests.length && !snapshot.disinterests.length) {
|
||||
if (snapshot.loadedFromCache) {
|
||||
this.setHashtagStatus(
|
||||
"No hashtag preferences yet (showing cached state).",
|
||||
"info",
|
||||
);
|
||||
} else {
|
||||
this.setHashtagStatus("", "muted");
|
||||
}
|
||||
}
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
}
|
||||
|
||||
renderHashtagList(type, tags) {
|
||||
const list =
|
||||
type === "interest" ? this.hashtagInterestList : this.hashtagDisinterestList;
|
||||
const empty =
|
||||
type === "interest" ? this.hashtagInterestEmpty : this.hashtagDisinterestEmpty;
|
||||
|
||||
if (!(list instanceof HTMLElement) || !(empty instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.textContent = "";
|
||||
|
||||
const normalized = this.sanitizeHashtagList(tags);
|
||||
if (!normalized.length) {
|
||||
empty.classList.remove("hidden");
|
||||
list.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
empty.classList.add("hidden");
|
||||
list.classList.remove("hidden");
|
||||
|
||||
normalized.forEach((tag) => {
|
||||
const item = this.createHashtagListItem(type, tag);
|
||||
if (item) {
|
||||
list.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createHashtagListItem(type, tag) {
|
||||
const normalized = this.normalizeHashtagTag(tag);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = document.createElement("li");
|
||||
item.className = "profile-hashtag-item";
|
||||
item.dataset.hashtagType = type;
|
||||
item.dataset.tag = normalized;
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "profile-hashtag-label";
|
||||
label.textContent = this.formatHashtagTag(normalized);
|
||||
item.appendChild(label);
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.type = "button";
|
||||
removeButton.className = "profile-hashtag-remove focus-ring";
|
||||
removeButton.dataset.hashtagType = type;
|
||||
removeButton.dataset.tag = normalized;
|
||||
removeButton.setAttribute(
|
||||
"aria-label",
|
||||
type === "interest"
|
||||
? `Remove ${this.formatHashtagTag(normalized)} from interests`
|
||||
: `Remove ${this.formatHashtagTag(normalized)} from disinterests`,
|
||||
);
|
||||
const icon = document.createElement("span");
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.textContent = "\u00D7";
|
||||
removeButton.appendChild(icon);
|
||||
removeButton.addEventListener("click", () => {
|
||||
void this.handleRemoveHashtagPreference(type, normalized);
|
||||
});
|
||||
|
||||
item.appendChild(removeButton);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async persistHashtagPreferences(options = {}) {
|
||||
const service = this.hashtagPreferencesService;
|
||||
const publish =
|
||||
service && typeof service.publish === "function" ? service.publish : null;
|
||||
|
||||
if (!publish) {
|
||||
const message = this.describeHashtagPreferencesError(null, {
|
||||
fallbackMessage: "Hashtag preferences are unavailable right now.",
|
||||
});
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
const error = new Error(
|
||||
message || "Hashtag preferences are unavailable right now.",
|
||||
);
|
||||
error.code = "service-unavailable";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.hashtagPreferencesPublishInFlight) {
|
||||
return this.hashtagPreferencesPublishPromise;
|
||||
}
|
||||
|
||||
const { successMessage, pubkey, progressMessage } =
|
||||
options && typeof options === "object" ? options : {};
|
||||
|
||||
const resolvedPubkeyCandidate =
|
||||
typeof pubkey === "string" && pubkey.trim()
|
||||
? pubkey
|
||||
: this.mainController.getActivePubkey();
|
||||
const normalizedPubkey = this.mainController.normalizeHexPubkey(resolvedPubkeyCandidate);
|
||||
|
||||
const payload = normalizedPubkey ? { pubkey: normalizedPubkey } : {};
|
||||
|
||||
const pendingMessage =
|
||||
typeof progressMessage === "string" && progressMessage.trim()
|
||||
? progressMessage.trim()
|
||||
: "Saving hashtag preferences…";
|
||||
const finalMessage =
|
||||
typeof successMessage === "string" && successMessage.trim()
|
||||
? successMessage.trim()
|
||||
: "Hashtag preferences saved.";
|
||||
|
||||
this.hashtagPreferencesPublishInFlight = true;
|
||||
this.setHashtagStatus(pendingMessage, "info");
|
||||
|
||||
const publishPromise = (async () => {
|
||||
try {
|
||||
const result = await publish.call(service, payload);
|
||||
this.setHashtagStatus(finalMessage, "success");
|
||||
return result;
|
||||
} catch (error) {
|
||||
const failure =
|
||||
error instanceof Error ? error : new Error(String(error || ""));
|
||||
if (!failure.code) {
|
||||
failure.code = "hashtag-preferences-publish-failed";
|
||||
}
|
||||
const message = this.describeHashtagPreferencesError(failure, {
|
||||
fallbackMessage:
|
||||
"Failed to update hashtag preferences. Please try again.",
|
||||
});
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
throw failure;
|
||||
} finally {
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
this.hashtagPreferencesPublishPromise = publishPromise;
|
||||
return publishPromise;
|
||||
}
|
||||
|
||||
async handleAddHashtagPreference(type) {
|
||||
const isInterest = type === "interest";
|
||||
const input = isInterest
|
||||
? this.hashtagInterestInput
|
||||
: this.hashtagDisinterestInput;
|
||||
|
||||
const rawValue =
|
||||
input instanceof HTMLInputElement ? input.value || "" : "";
|
||||
const normalized = this.normalizeHashtagTag(rawValue);
|
||||
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return { success: false, reason: "missing-input" };
|
||||
}
|
||||
|
||||
if (!normalized) {
|
||||
const message = "Enter a hashtag to add.";
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "empty" };
|
||||
}
|
||||
|
||||
const service = this.hashtagPreferencesService;
|
||||
const addMethod = isInterest
|
||||
? service?.addInterest
|
||||
: service?.addDisinterest;
|
||||
|
||||
if (typeof addMethod !== "function") {
|
||||
const message = "Hashtag preferences are unavailable right now.";
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "service-unavailable" };
|
||||
}
|
||||
|
||||
const snapshot = this.getResolvedHashtagPreferences();
|
||||
const alreadyInTarget = isInterest
|
||||
? snapshot.interests.includes(normalized)
|
||||
: snapshot.disinterests.includes(normalized);
|
||||
const inOpposite = isInterest
|
||||
? snapshot.disinterests.includes(normalized)
|
||||
: snapshot.interests.includes(normalized);
|
||||
|
||||
let result = false;
|
||||
try {
|
||||
result = addMethod.call(service, normalized);
|
||||
} catch (error) {
|
||||
const message = this.describeHashtagPreferencesError(error, {
|
||||
fallbackMessage: "Failed to update hashtag preferences. Please try again.",
|
||||
});
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
return { success: false, reason: error?.code || "service-error", error };
|
||||
} finally {
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const actionMessage = inOpposite
|
||||
? `${this.formatHashtagTag(normalized)} moved to ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`
|
||||
: `${this.formatHashtagTag(normalized)} added to ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.populateHashtagPreferences();
|
||||
try {
|
||||
await this.persistHashtagPreferences({
|
||||
successMessage: actionMessage,
|
||||
});
|
||||
this.mainController.showSuccess(actionMessage);
|
||||
return { success: true, reason: inOpposite ? "moved" : "added" };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error?.code || "publish-failed",
|
||||
error,
|
||||
};
|
||||
} finally {
|
||||
this.populateHashtagPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyInTarget) {
|
||||
const message = `${this.formatHashtagTag(normalized)} is already in your ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.mainController.showStatus(message);
|
||||
this.setHashtagStatus(message, "info");
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: "duplicate" };
|
||||
}
|
||||
|
||||
const fallbackMessage = this.describeHashtagPreferencesError(null, {
|
||||
fallbackMessage: `Failed to add ${this.formatHashtagTag(normalized)}.`,
|
||||
});
|
||||
if (fallbackMessage) {
|
||||
this.mainController.showError(fallbackMessage);
|
||||
this.setHashtagStatus(fallbackMessage, "warning");
|
||||
}
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: "no-change" };
|
||||
}
|
||||
|
||||
async handleRemoveHashtagPreference(type, candidate) {
|
||||
const normalized = this.normalizeHashtagTag(candidate);
|
||||
if (!normalized) {
|
||||
return { success: false, reason: "invalid" };
|
||||
}
|
||||
|
||||
const service = this.hashtagPreferencesService;
|
||||
const removeMethod =
|
||||
type === "interest"
|
||||
? service?.removeInterest
|
||||
: service?.removeDisinterest;
|
||||
|
||||
if (typeof removeMethod !== "function") {
|
||||
const message = "Hashtag preferences are unavailable right now.";
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "service-unavailable" };
|
||||
}
|
||||
|
||||
let removed = false;
|
||||
try {
|
||||
removed = removeMethod.call(service, normalized);
|
||||
} catch (error) {
|
||||
const message = this.describeHashtagPreferencesError(error, {
|
||||
fallbackMessage: `Failed to remove ${this.formatHashtagTag(normalized)}.`,
|
||||
});
|
||||
if (message) {
|
||||
this.mainController.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: error?.code || "service-error", error };
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
const message = `${this.formatHashtagTag(normalized)} removed from ${
|
||||
type === "interest" ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.populateHashtagPreferences();
|
||||
try {
|
||||
await this.persistHashtagPreferences({ successMessage: message });
|
||||
this.mainController.showSuccess(message);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error?.code || "publish-failed",
|
||||
error,
|
||||
};
|
||||
} finally {
|
||||
this.populateHashtagPreferences();
|
||||
}
|
||||
} else {
|
||||
const message = `${this.formatHashtagTag(normalized)} is already removed.`;
|
||||
this.mainController.showStatus(message);
|
||||
this.setHashtagStatus(message, "info");
|
||||
}
|
||||
|
||||
this.populateHashtagPreferences();
|
||||
return { success: removed, reason: removed ? "removed" : "already-removed" };
|
||||
}
|
||||
|
||||
handleHashtagPreferencesChange(detail = {}) {
|
||||
const preferences =
|
||||
detail && typeof detail.preferences === "object"
|
||||
? detail.preferences
|
||||
: detail;
|
||||
const action = typeof detail?.action === "string" ? detail.action : "";
|
||||
|
||||
if (action === "background-loading") {
|
||||
this.hashtagBackgroundLoading = true;
|
||||
this.setHashtagStatus("Loading in background…", "info");
|
||||
} else if (
|
||||
(action === "sync" || action === "background-loaded" || action === "reset") &&
|
||||
this.hashtagBackgroundLoading
|
||||
) {
|
||||
const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
|
||||
if (statusText === "Loading in background…") {
|
||||
this.setHashtagStatus("", "muted");
|
||||
}
|
||||
this.hashtagBackgroundLoading = false;
|
||||
}
|
||||
|
||||
this.populateHashtagPreferences(preferences);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
}
|
||||
|
||||
describeHashtagPreferencesError(error, options = {}) {
|
||||
const describe = this.describeHashtagPreferencesErrorService;
|
||||
|
||||
if (typeof describe !== "function") {
|
||||
const fallback =
|
||||
options && typeof options.fallbackMessage === "string"
|
||||
? options.fallbackMessage
|
||||
: "An error occurred with hashtag preferences.";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return describe(error, options);
|
||||
} catch (err) {
|
||||
devLogger.warn(
|
||||
"[ProfileModalController] describeHashtagPreferencesError service threw:",
|
||||
err,
|
||||
);
|
||||
return (
|
||||
options && typeof options.fallbackMessage === "string"
|
||||
? options.fallbackMessage
|
||||
: "An error occurred."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-661
@@ -24,12 +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 {
|
||||
normalizeHashtag,
|
||||
formatHashtag,
|
||||
} from "../utils/hashtagNormalization.js";
|
||||
import { formatTimeAgo } from "../utils/formatters.js";
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { buildPublicUrl, buildR2Key } from "../r2.js";
|
||||
import { buildProfileMetadataEvent } from "../nostrEventSchemas.js";
|
||||
import {
|
||||
@@ -53,6 +47,7 @@ import { ProfileWalletController } from "./profileModal/ProfileWalletController.
|
||||
import { ProfileStorageController } from "./profileModal/ProfileStorageController.js";
|
||||
import { ProfileDirectMessageController } from "./profileModal/ProfileDirectMessageController.js";
|
||||
import { ProfileRelayController } from "./profileModal/ProfileRelayController.js";
|
||||
import { ProfileHashtagController } from "./profileModal/ProfileHashtagController.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
@@ -103,6 +98,8 @@ export class ProfileModalController {
|
||||
|
||||
this.dmController = new ProfileDirectMessageController(this);
|
||||
this.relayController = new ProfileRelayController(this);
|
||||
this.hashtagController = new ProfileHashtagController(this);
|
||||
this.hashtagController.initialize();
|
||||
|
||||
const resolvedMaxWalletDefaultZap =
|
||||
typeof providedConstants.MAX_WALLET_DEFAULT_ZAP === "number" &&
|
||||
@@ -264,29 +261,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
this.hashtagPreferencesService = this.services.hashtagPreferences;
|
||||
this.describeHashtagPreferencesErrorService =
|
||||
this.services.describeHashtagPreferencesError;
|
||||
this.getHashtagPreferencesSnapshotService =
|
||||
this.services.getHashtagPreferences;
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
|
||||
if (
|
||||
this.hashtagPreferencesService &&
|
||||
typeof this.hashtagPreferencesService.on === "function"
|
||||
) {
|
||||
this.hashtagPreferencesUnsubscribe = this.hashtagPreferencesService.on(
|
||||
"change",
|
||||
(detail) => {
|
||||
this.handleHashtagPreferencesChange({
|
||||
action:
|
||||
typeof detail?.action === "string" ? detail.action : "change",
|
||||
preferences: detail,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.dmController.initializeDirectMessagesService();
|
||||
|
||||
@@ -446,18 +420,6 @@ export class ProfileModalController {
|
||||
this.walletDisconnectButton = null;
|
||||
this.walletStatusText = null;
|
||||
this.profileWalletStatusText = null;
|
||||
this.hashtagStatusText = null;
|
||||
this.hashtagBackgroundLoading = false;
|
||||
this.hashtagInterestList = null;
|
||||
this.hashtagInterestEmpty = null;
|
||||
this.hashtagInterestInput = null;
|
||||
this.addHashtagInterestButton = null;
|
||||
this.profileHashtagInterestRefreshBtn = null;
|
||||
this.hashtagDisinterestList = null;
|
||||
this.hashtagDisinterestEmpty = null;
|
||||
this.hashtagDisinterestInput = null;
|
||||
this.addHashtagDisinterestButton = null;
|
||||
this.profileHashtagDisinterestRefreshBtn = null;
|
||||
this.subscriptionsStatusText = null;
|
||||
this.subscriptionsBackgroundLoading = false;
|
||||
this.permissionPromptCta = null;
|
||||
@@ -645,7 +607,7 @@ export class ProfileModalController {
|
||||
this.authLoadingStateListener,
|
||||
);
|
||||
}
|
||||
this.populateHashtagPreferences();
|
||||
this.hashtagController.populateHashtagPreferences();
|
||||
this.refreshModerationSettingsUi();
|
||||
const preserveMenu = this.isMobileLayoutActive();
|
||||
this.selectPane(this.getActivePane(), { keepMenuView: preserveMenu });
|
||||
@@ -854,28 +816,7 @@ export class ProfileModalController {
|
||||
this.dmController.populateDmRelayPreferences();
|
||||
this.dmController.syncDmPrivacySettingsUi();
|
||||
|
||||
this.hashtagStatusText =
|
||||
document.getElementById("profileHashtagStatus") || null;
|
||||
this.hashtagInterestList =
|
||||
document.getElementById("profileHashtagInterestList") || null;
|
||||
this.hashtagInterestEmpty =
|
||||
document.getElementById("profileHashtagInterestEmpty") || null;
|
||||
this.hashtagInterestInput =
|
||||
document.getElementById("profileHashtagInterestInput") || null;
|
||||
this.addHashtagInterestButton =
|
||||
document.getElementById("profileAddHashtagInterestBtn") || null;
|
||||
this.profileHashtagInterestRefreshBtn =
|
||||
document.getElementById("profileHashtagInterestRefreshBtn") || null;
|
||||
this.hashtagDisinterestList =
|
||||
document.getElementById("profileHashtagDisinterestList") || null;
|
||||
this.hashtagDisinterestEmpty =
|
||||
document.getElementById("profileHashtagDisinterestEmpty") || null;
|
||||
this.hashtagDisinterestInput =
|
||||
document.getElementById("profileHashtagDisinterestInput") || null;
|
||||
this.addHashtagDisinterestButton =
|
||||
document.getElementById("profileAddHashtagDisinterestBtn") || null;
|
||||
this.profileHashtagDisinterestRefreshBtn =
|
||||
document.getElementById("profileHashtagDisinterestRefreshBtn") || null;
|
||||
this.hashtagController.cacheDomReferences();
|
||||
this.subscriptionsStatusText =
|
||||
document.getElementById("subscriptionsStatus") || null;
|
||||
this.applyPermissionPromptCtaState();
|
||||
@@ -992,11 +933,11 @@ export class ProfileModalController {
|
||||
|
||||
ensureAriaLabel(this.relayController.profileRelayRefreshBtn, "Refresh relay list");
|
||||
ensureAriaLabel(
|
||||
this.profileHashtagInterestRefreshBtn,
|
||||
this.hashtagController.profileHashtagInterestRefreshBtn,
|
||||
"Refresh interest hashtags",
|
||||
);
|
||||
ensureAriaLabel(
|
||||
this.profileHashtagDisinterestRefreshBtn,
|
||||
this.hashtagController.profileHashtagDisinterestRefreshBtn,
|
||||
"Refresh disinterest hashtags",
|
||||
);
|
||||
ensureAriaLabel(this.profileSubscriptionsRefreshBtn, "Refresh subscriptions");
|
||||
@@ -1906,9 +1847,9 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addHashtagInterestButton instanceof HTMLElement) {
|
||||
this.addHashtagInterestButton.addEventListener("click", () => {
|
||||
void this.handleAddHashtagPreference("interest");
|
||||
if (this.hashtagController.addHashtagInterestButton instanceof HTMLElement) {
|
||||
this.hashtagController.addHashtagInterestButton.addEventListener("click", () => {
|
||||
void this.hashtagController.handleAddHashtagPreference("interest");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1917,7 +1858,7 @@ export class ProfileModalController {
|
||||
if (!activeHex) {
|
||||
return;
|
||||
}
|
||||
const service = this.hashtagPreferencesService;
|
||||
const service = this.services.hashtagPreferences;
|
||||
if (!service || typeof service.load !== "function") {
|
||||
return;
|
||||
}
|
||||
@@ -1931,40 +1872,40 @@ export class ProfileModalController {
|
||||
});
|
||||
};
|
||||
|
||||
if (this.profileHashtagInterestRefreshBtn instanceof HTMLElement) {
|
||||
this.profileHashtagInterestRefreshBtn.addEventListener(
|
||||
if (this.hashtagController.profileHashtagInterestRefreshBtn instanceof HTMLElement) {
|
||||
this.hashtagController.profileHashtagInterestRefreshBtn.addEventListener(
|
||||
"click",
|
||||
handleHashtagRefresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.profileHashtagDisinterestRefreshBtn instanceof HTMLElement) {
|
||||
this.profileHashtagDisinterestRefreshBtn.addEventListener(
|
||||
if (this.hashtagController.profileHashtagDisinterestRefreshBtn instanceof HTMLElement) {
|
||||
this.hashtagController.profileHashtagDisinterestRefreshBtn.addEventListener(
|
||||
"click",
|
||||
handleHashtagRefresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.hashtagInterestInput instanceof HTMLElement) {
|
||||
this.hashtagInterestInput.addEventListener("keydown", (event) => {
|
||||
if (this.hashtagController.hashtagInterestInput instanceof HTMLElement) {
|
||||
this.hashtagController.hashtagInterestInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void this.handleAddHashtagPreference("interest");
|
||||
void this.hashtagController.handleAddHashtagPreference("interest");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addHashtagDisinterestButton instanceof HTMLElement) {
|
||||
this.addHashtagDisinterestButton.addEventListener("click", () => {
|
||||
void this.handleAddHashtagPreference("disinterest");
|
||||
if (this.hashtagController.addHashtagDisinterestButton instanceof HTMLElement) {
|
||||
this.hashtagController.addHashtagDisinterestButton.addEventListener("click", () => {
|
||||
void this.hashtagController.handleAddHashtagPreference("disinterest");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hashtagDisinterestInput instanceof HTMLElement) {
|
||||
this.hashtagDisinterestInput.addEventListener("keydown", (event) => {
|
||||
if (this.hashtagController.hashtagDisinterestInput instanceof HTMLElement) {
|
||||
this.hashtagController.hashtagDisinterestInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void this.handleAddHashtagPreference("disinterest");
|
||||
void this.hashtagController.handleAddHashtagPreference("disinterest");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2312,40 +2253,6 @@ export class ProfileModalController {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
describeHashtagPreferencesError(error, options = {}) {
|
||||
const describe = this.describeHashtagPreferencesErrorService;
|
||||
const { fallbackMessage, operation } =
|
||||
options && typeof options === "object" ? options : {};
|
||||
const normalizedOperation =
|
||||
typeof operation === "string" && operation.trim()
|
||||
? operation.trim().toLowerCase()
|
||||
: "update";
|
||||
|
||||
const fallback =
|
||||
typeof fallbackMessage === "string" && fallbackMessage.trim()
|
||||
? fallbackMessage.trim()
|
||||
: normalizedOperation === "load"
|
||||
? "Failed to load hashtag preferences. Please try again."
|
||||
: normalizedOperation === "reset"
|
||||
? "Failed to reset hashtag preferences."
|
||||
: "Failed to update hashtag preferences. Please try again.";
|
||||
|
||||
if (typeof describe === "function") {
|
||||
try {
|
||||
const message = describe(error, fallback);
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
return message.trim();
|
||||
}
|
||||
} catch (describeError) {
|
||||
devLogger.warn(
|
||||
"[ProfileModalController] describeHashtagPreferencesError service threw:",
|
||||
describeError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
setAddAccountLoading(isLoading) {
|
||||
if (!(this.addAccountButton instanceof HTMLElement)) {
|
||||
@@ -3643,13 +3550,13 @@ export class ProfileModalController {
|
||||
} else if (target === "storage") {
|
||||
this.storageController.populateStoragePane();
|
||||
} else if (target === "hashtags") {
|
||||
this.populateHashtagPreferences();
|
||||
if (activeHex && this.hashtagPreferencesService) {
|
||||
this.hashtagPreferencesService
|
||||
this.hashtagController.populateHashtagPreferences();
|
||||
if (activeHex && this.services.hashtagPreferences) {
|
||||
this.services.hashtagPreferences
|
||||
.load(activeHex, { allowPermissionPrompt: true })
|
||||
.catch(noop);
|
||||
}
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
this.hashtagController.refreshHashtagBackgroundStatus();
|
||||
} else if (target === "subscriptions") {
|
||||
if (activeHex && this.subscriptionsService) {
|
||||
this.subscriptionsService
|
||||
@@ -3777,91 +3684,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
normalizeHashtagTag(value) {
|
||||
return normalizeHashtag(value);
|
||||
}
|
||||
|
||||
formatHashtagTag(value) {
|
||||
return formatHashtag(value);
|
||||
}
|
||||
|
||||
sanitizeHashtagList(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
|
||||
list.forEach((entry) => {
|
||||
if (typeof entry !== "string") {
|
||||
return;
|
||||
}
|
||||
const tag = this.normalizeHashtagTag(entry);
|
||||
if (!tag || seen.has(tag)) {
|
||||
return;
|
||||
}
|
||||
seen.add(tag);
|
||||
normalized.push(tag);
|
||||
});
|
||||
|
||||
normalized.sort((a, b) => a.localeCompare(b));
|
||||
return normalized;
|
||||
}
|
||||
|
||||
getResolvedHashtagPreferences(preferences = null) {
|
||||
const candidate =
|
||||
preferences && typeof preferences === "object" ? preferences : null;
|
||||
|
||||
let snapshot = null;
|
||||
if (candidate) {
|
||||
snapshot = candidate;
|
||||
} else if (typeof this.getHashtagPreferencesSnapshotService === "function") {
|
||||
try {
|
||||
const resolved = this.getHashtagPreferencesSnapshotService();
|
||||
if (resolved && typeof resolved === "object") {
|
||||
snapshot = resolved;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[ProfileModalController] Failed to read hashtag preferences snapshot:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const service = this.hashtagPreferencesService || {};
|
||||
const loadState =
|
||||
typeof service.getLoadState === "function" ? service.getLoadState() : null;
|
||||
|
||||
const interestsSource = Array.isArray(snapshot?.interests)
|
||||
? snapshot.interests
|
||||
: typeof service.getInterests === "function"
|
||||
? service.getInterests()
|
||||
: [];
|
||||
const disinterestsSource = Array.isArray(snapshot?.disinterests)
|
||||
? snapshot.disinterests
|
||||
: typeof service.getDisinterests === "function"
|
||||
? service.getDisinterests()
|
||||
: [];
|
||||
|
||||
return {
|
||||
interests: this.sanitizeHashtagList(interestsSource),
|
||||
disinterests: this.sanitizeHashtagList(disinterestsSource),
|
||||
uiReady:
|
||||
loadState?.uiReady === true ||
|
||||
service.uiReady === true ||
|
||||
(!loadState && service.uiReady !== false),
|
||||
dataReady:
|
||||
loadState?.dataReady === true ||
|
||||
service.dataReady === true ||
|
||||
(!loadState && service.dataReady !== false),
|
||||
lastLoadError: loadState?.lastLoadError || service.lastLoadError || null,
|
||||
loadedFromCache:
|
||||
loadState?.loadedFromCache === true || service.loadedFromCache === true,
|
||||
};
|
||||
}
|
||||
|
||||
setPermissionPromptCtaState(nextState = {}) {
|
||||
if (!nextState || typeof nextState !== "object") {
|
||||
return;
|
||||
@@ -3938,7 +3760,7 @@ export class ProfileModalController {
|
||||
"Subscriptions loaded from cache; sync is retrying in background.",
|
||||
"warning",
|
||||
);
|
||||
this.setHashtagStatus(
|
||||
this.hashtagController.setHashtagStatus(
|
||||
"Hashtag preferences loaded from cache; sync is retrying in background.",
|
||||
"warning",
|
||||
);
|
||||
@@ -3950,7 +3772,7 @@ export class ProfileModalController {
|
||||
"Some profile lists failed to sync. Use Retry sync.",
|
||||
"warning",
|
||||
);
|
||||
this.setHashtagStatus(
|
||||
this.hashtagController.setHashtagStatus(
|
||||
"Some profile lists failed to sync. Use Retry sync.",
|
||||
"warning",
|
||||
);
|
||||
@@ -3983,49 +3805,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
setHashtagStatus(message = "", tone = "muted") {
|
||||
if (!(this.hashtagStatusText instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const classList = this.hashtagStatusText.classList;
|
||||
classList.remove(
|
||||
"text-status-success",
|
||||
"text-status-warning",
|
||||
"text-status-danger",
|
||||
"text-status-info",
|
||||
"text-muted",
|
||||
);
|
||||
|
||||
const normalized =
|
||||
typeof message === "string" && message.trim() ? message.trim() : "";
|
||||
|
||||
if (!normalized) {
|
||||
this.hashtagStatusText.textContent = "";
|
||||
this.hashtagStatusText.classList.add("text-muted", "hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
this.hashtagStatusText.textContent = normalized;
|
||||
this.hashtagStatusText.classList.remove("hidden");
|
||||
|
||||
switch (tone) {
|
||||
case "success":
|
||||
classList.add("text-status-success");
|
||||
break;
|
||||
case "warning":
|
||||
case "error":
|
||||
classList.add("text-status-warning");
|
||||
break;
|
||||
case "info":
|
||||
classList.add("text-status-info");
|
||||
break;
|
||||
default:
|
||||
classList.add("text-muted");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setSubscriptionsStatus(message = "", tone = "muted") {
|
||||
if (!(this.subscriptionsStatusText instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -4069,26 +3848,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
refreshHashtagBackgroundStatus() {
|
||||
const isBackground = this.hashtagPreferencesService?.backgroundLoading === true;
|
||||
const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
|
||||
|
||||
if (isBackground && !this.hashtagBackgroundLoading) {
|
||||
this.hashtagBackgroundLoading = true;
|
||||
if (!statusText) {
|
||||
this.setHashtagStatus("Loading in background…", "info");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBackground && this.hashtagBackgroundLoading) {
|
||||
if (statusText === "Loading in background…") {
|
||||
this.setHashtagStatus("", "muted");
|
||||
}
|
||||
this.hashtagBackgroundLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshSubscriptionsBackgroundStatus() {
|
||||
const isBackground = this.subscriptionsService?.backgroundLoading === true;
|
||||
const statusText = this.subscriptionsStatusText?.textContent?.trim?.() || "";
|
||||
@@ -4109,391 +3868,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
clearHashtagInputs() {
|
||||
if (this.hashtagInterestInput instanceof HTMLInputElement) {
|
||||
this.hashtagInterestInput.value = "";
|
||||
}
|
||||
if (this.hashtagDisinterestInput instanceof HTMLInputElement) {
|
||||
this.hashtagDisinterestInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
populateHashtagPreferences(preferences = null) {
|
||||
const snapshot = this.getResolvedHashtagPreferences(preferences);
|
||||
|
||||
if (!snapshot.uiReady) {
|
||||
this.setHashtagStatus("Loading hashtag preferences…", "info");
|
||||
this.renderHashtagList("interest", []);
|
||||
this.renderHashtagList("disinterest", []);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.dataReady) {
|
||||
const message = snapshot.lastLoadError
|
||||
? "Couldn’t sync hashtag preferences. Retry to load your lists."
|
||||
: "Hashtag preferences are unavailable right now. Retry to sync.";
|
||||
this.setHashtagStatus(message, "warning");
|
||||
this.renderHashtagList("interest", []);
|
||||
this.renderHashtagList("disinterest", []);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderHashtagList("interest", snapshot.interests);
|
||||
this.renderHashtagList("disinterest", snapshot.disinterests);
|
||||
|
||||
if (!snapshot.interests.length && !snapshot.disinterests.length) {
|
||||
if (snapshot.loadedFromCache) {
|
||||
this.setHashtagStatus(
|
||||
"No hashtag preferences yet (showing cached state).",
|
||||
"info",
|
||||
);
|
||||
} else {
|
||||
this.setHashtagStatus("", "muted");
|
||||
}
|
||||
}
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
}
|
||||
|
||||
renderHashtagList(type, tags) {
|
||||
const list =
|
||||
type === "interest" ? this.hashtagInterestList : this.hashtagDisinterestList;
|
||||
const empty =
|
||||
type === "interest" ? this.hashtagInterestEmpty : this.hashtagDisinterestEmpty;
|
||||
|
||||
if (!(list instanceof HTMLElement) || !(empty instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.textContent = "";
|
||||
|
||||
const normalized = this.sanitizeHashtagList(tags);
|
||||
if (!normalized.length) {
|
||||
empty.classList.remove("hidden");
|
||||
list.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
empty.classList.add("hidden");
|
||||
list.classList.remove("hidden");
|
||||
|
||||
normalized.forEach((tag) => {
|
||||
const item = this.createHashtagListItem(type, tag);
|
||||
if (item) {
|
||||
list.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createHashtagListItem(type, tag) {
|
||||
const normalized = this.normalizeHashtagTag(tag);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = document.createElement("li");
|
||||
item.className = "profile-hashtag-item";
|
||||
item.dataset.hashtagType = type;
|
||||
item.dataset.tag = normalized;
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = this.formatHashtagTag(normalized);
|
||||
item.appendChild(label);
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.type = "button";
|
||||
removeButton.className = "profile-hashtag-remove focus-ring";
|
||||
removeButton.dataset.hashtagType = type;
|
||||
removeButton.dataset.tag = normalized;
|
||||
removeButton.setAttribute(
|
||||
"aria-label",
|
||||
type === "interest"
|
||||
? `Remove ${this.formatHashtagTag(normalized)} from interests`
|
||||
: `Remove ${this.formatHashtagTag(normalized)} from disinterests`,
|
||||
);
|
||||
const icon = document.createElement("span");
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.textContent = "\u00D7";
|
||||
removeButton.appendChild(icon);
|
||||
removeButton.addEventListener("click", () => {
|
||||
void this.handleRemoveHashtagPreference(type, normalized);
|
||||
});
|
||||
|
||||
item.appendChild(removeButton);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async persistHashtagPreferences(options = {}) {
|
||||
const service = this.hashtagPreferencesService;
|
||||
const publish =
|
||||
service && typeof service.publish === "function" ? service.publish : null;
|
||||
|
||||
if (!publish) {
|
||||
const message = this.describeHashtagPreferencesError(null, {
|
||||
fallbackMessage: "Hashtag preferences are unavailable right now.",
|
||||
});
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
const error = new Error(
|
||||
message || "Hashtag preferences are unavailable right now.",
|
||||
);
|
||||
error.code = "service-unavailable";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.hashtagPreferencesPublishInFlight) {
|
||||
return this.hashtagPreferencesPublishPromise;
|
||||
}
|
||||
|
||||
const { successMessage, pubkey, progressMessage } =
|
||||
options && typeof options === "object" ? options : {};
|
||||
|
||||
const resolvedPubkeyCandidate =
|
||||
typeof pubkey === "string" && pubkey.trim()
|
||||
? pubkey
|
||||
: this.getActivePubkey();
|
||||
const normalizedPubkey = this.normalizeHexPubkey(resolvedPubkeyCandidate);
|
||||
|
||||
const payload = normalizedPubkey ? { pubkey: normalizedPubkey } : {};
|
||||
|
||||
const pendingMessage =
|
||||
typeof progressMessage === "string" && progressMessage.trim()
|
||||
? progressMessage.trim()
|
||||
: "Saving hashtag preferences…";
|
||||
const finalMessage =
|
||||
typeof successMessage === "string" && successMessage.trim()
|
||||
? successMessage.trim()
|
||||
: "Hashtag preferences saved.";
|
||||
|
||||
this.hashtagPreferencesPublishInFlight = true;
|
||||
this.setHashtagStatus(pendingMessage, "info");
|
||||
|
||||
const publishPromise = (async () => {
|
||||
try {
|
||||
const result = await publish.call(service, payload);
|
||||
this.setHashtagStatus(finalMessage, "success");
|
||||
return result;
|
||||
} catch (error) {
|
||||
const failure =
|
||||
error instanceof Error ? error : new Error(String(error || ""));
|
||||
if (!failure.code) {
|
||||
failure.code = "hashtag-preferences-publish-failed";
|
||||
}
|
||||
const message = this.describeHashtagPreferencesError(failure, {
|
||||
fallbackMessage:
|
||||
"Failed to update hashtag preferences. Please try again.",
|
||||
});
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
throw failure;
|
||||
} finally {
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
this.hashtagPreferencesPublishPromise = publishPromise;
|
||||
return publishPromise;
|
||||
}
|
||||
|
||||
async handleAddHashtagPreference(type) {
|
||||
const isInterest = type === "interest";
|
||||
const input = isInterest
|
||||
? this.hashtagInterestInput
|
||||
: this.hashtagDisinterestInput;
|
||||
|
||||
const rawValue =
|
||||
input instanceof HTMLInputElement ? input.value || "" : "";
|
||||
const normalized = this.normalizeHashtagTag(rawValue);
|
||||
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return { success: false, reason: "missing-input" };
|
||||
}
|
||||
|
||||
if (!normalized) {
|
||||
const message = "Enter a hashtag to add.";
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "empty" };
|
||||
}
|
||||
|
||||
const service = this.hashtagPreferencesService;
|
||||
const addMethod = isInterest
|
||||
? service?.addInterest
|
||||
: service?.addDisinterest;
|
||||
|
||||
if (typeof addMethod !== "function") {
|
||||
const message = "Hashtag preferences are unavailable right now.";
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "service-unavailable" };
|
||||
}
|
||||
|
||||
const snapshot = this.getResolvedHashtagPreferences();
|
||||
const alreadyInTarget = isInterest
|
||||
? snapshot.interests.includes(normalized)
|
||||
: snapshot.disinterests.includes(normalized);
|
||||
const inOpposite = isInterest
|
||||
? snapshot.disinterests.includes(normalized)
|
||||
: snapshot.interests.includes(normalized);
|
||||
|
||||
let result = false;
|
||||
try {
|
||||
result = addMethod.call(service, normalized);
|
||||
} catch (error) {
|
||||
const message = this.describeHashtagPreferencesError(error, {
|
||||
fallbackMessage: "Failed to update hashtag preferences. Please try again.",
|
||||
});
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
return { success: false, reason: error?.code || "service-error", error };
|
||||
} finally {
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const actionMessage = inOpposite
|
||||
? `${this.formatHashtagTag(normalized)} moved to ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`
|
||||
: `${this.formatHashtagTag(normalized)} added to ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.populateHashtagPreferences();
|
||||
try {
|
||||
await this.persistHashtagPreferences({
|
||||
successMessage: actionMessage,
|
||||
});
|
||||
this.showSuccess(actionMessage);
|
||||
return { success: true, reason: inOpposite ? "moved" : "added" };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error?.code || "publish-failed",
|
||||
error,
|
||||
};
|
||||
} finally {
|
||||
this.populateHashtagPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyInTarget) {
|
||||
const message = `${this.formatHashtagTag(normalized)} is already in your ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.showStatus(message);
|
||||
this.setHashtagStatus(message, "info");
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: "duplicate" };
|
||||
}
|
||||
|
||||
const fallbackMessage = this.describeHashtagPreferencesError(null, {
|
||||
fallbackMessage: `Failed to add ${this.formatHashtagTag(normalized)}.`,
|
||||
});
|
||||
if (fallbackMessage) {
|
||||
this.showError(fallbackMessage);
|
||||
this.setHashtagStatus(fallbackMessage, "warning");
|
||||
}
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: "no-change" };
|
||||
}
|
||||
|
||||
async handleRemoveHashtagPreference(type, candidate) {
|
||||
const normalized = this.normalizeHashtagTag(candidate);
|
||||
if (!normalized) {
|
||||
return { success: false, reason: "invalid" };
|
||||
}
|
||||
|
||||
const service = this.hashtagPreferencesService;
|
||||
const removeMethod =
|
||||
type === "interest"
|
||||
? service?.removeInterest
|
||||
: service?.removeDisinterest;
|
||||
|
||||
if (typeof removeMethod !== "function") {
|
||||
const message = "Hashtag preferences are unavailable right now.";
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
return { success: false, reason: "service-unavailable" };
|
||||
}
|
||||
|
||||
let removed = false;
|
||||
try {
|
||||
removed = removeMethod.call(service, normalized);
|
||||
} catch (error) {
|
||||
const message = this.describeHashtagPreferencesError(error, {
|
||||
fallbackMessage: `Failed to remove ${this.formatHashtagTag(normalized)}.`,
|
||||
});
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
this.setHashtagStatus(message, "warning");
|
||||
}
|
||||
this.populateHashtagPreferences();
|
||||
return { success: false, reason: error?.code || "service-error", error };
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
const message = `${this.formatHashtagTag(normalized)} removed from ${
|
||||
type === "interest" ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.populateHashtagPreferences();
|
||||
try {
|
||||
await this.persistHashtagPreferences({ successMessage: message });
|
||||
this.showSuccess(message);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: error?.code || "publish-failed",
|
||||
error,
|
||||
};
|
||||
} finally {
|
||||
this.populateHashtagPreferences();
|
||||
}
|
||||
} else {
|
||||
const message = `${this.formatHashtagTag(normalized)} is already removed.`;
|
||||
this.showStatus(message);
|
||||
this.setHashtagStatus(message, "info");
|
||||
}
|
||||
|
||||
this.populateHashtagPreferences();
|
||||
return { success: removed, reason: removed ? "removed" : "already-removed" };
|
||||
}
|
||||
|
||||
handleHashtagPreferencesChange(detail = {}) {
|
||||
const preferences =
|
||||
detail && typeof detail.preferences === "object"
|
||||
? detail.preferences
|
||||
: detail;
|
||||
const action = typeof detail?.action === "string" ? detail.action : "";
|
||||
|
||||
if (action === "background-loading") {
|
||||
this.hashtagBackgroundLoading = true;
|
||||
this.setHashtagStatus("Loading in background…", "info");
|
||||
} else if (
|
||||
(action === "sync" || action === "background-loaded" || action === "reset") &&
|
||||
this.hashtagBackgroundLoading
|
||||
) {
|
||||
const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
|
||||
if (statusText === "Loading in background…") {
|
||||
this.setHashtagStatus("", "muted");
|
||||
}
|
||||
this.hashtagBackgroundLoading = false;
|
||||
}
|
||||
|
||||
this.populateHashtagPreferences(preferences);
|
||||
this.refreshHashtagBackgroundStatus();
|
||||
}
|
||||
|
||||
handleSubscriptionsChange(detail = {}) {
|
||||
const action = typeof detail?.action === "string" ? detail.action : "";
|
||||
|
||||
@@ -8424,7 +7798,7 @@ export class ProfileModalController {
|
||||
void this.populateFriendsList();
|
||||
this.relayController.populateProfileRelays();
|
||||
this.walletController.refreshWalletPaneState();
|
||||
this.populateHashtagPreferences();
|
||||
this.hashtagController.populateHashtagPreferences();
|
||||
void this.storageController.populateStoragePane();
|
||||
this.handleActiveDmIdentityChanged(activePubkey);
|
||||
void this.refreshDmRelayPreferences({ force: true });
|
||||
@@ -8438,7 +7812,7 @@ export class ProfileModalController {
|
||||
void this.populateFriendsList();
|
||||
this.relayController.populateProfileRelays();
|
||||
this.walletController.refreshWalletPaneState();
|
||||
this.populateHashtagPreferences();
|
||||
this.hashtagController.populateHashtagPreferences();
|
||||
void this.storageController.populateStoragePane();
|
||||
void this.refreshDmRelayPreferences({ force: true });
|
||||
})
|
||||
@@ -8504,10 +7878,10 @@ export class ProfileModalController {
|
||||
this.clearFriendsList();
|
||||
this.relayController.populateProfileRelays();
|
||||
this.walletController.refreshWalletPaneState();
|
||||
this.populateHashtagPreferences();
|
||||
this.hashtagController.populateHashtagPreferences();
|
||||
void this.storageController.populateStoragePane();
|
||||
this.clearHashtagInputs();
|
||||
this.setHashtagStatus("", "muted");
|
||||
this.hashtagController.clearHashtagInputs();
|
||||
this.hashtagController.setHashtagStatus("", "muted");
|
||||
this.handleActiveDmIdentityChanged(null);
|
||||
this.setMessagesUnreadIndicator(false);
|
||||
this.dmController.populateDmRelayPreferences();
|
||||
|
||||
@@ -26,7 +26,7 @@ const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much f
|
||||
// These files are known to be oversized and are decomposition targets.
|
||||
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
|
||||
const GRANDFATHERED = {
|
||||
"js/ui/profileModalController.js": 8753,
|
||||
"js/ui/profileModalController.js": 8130,
|
||||
"js/ui/components/VideoModal.js": 6284,
|
||||
"js/app.js": 5602,
|
||||
"js/channelProfile.js": 5529,
|
||||
|
||||
Reference in new issue
Block a user