mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 02:42:28 +00:00
Merge pull request #903 from PR0M3TH3AN/codex/update-profile-modal-for-async-hashtag-publishing
Ensure hashtag preference publishing runs from modal and menu
This commit is contained in:
4 files changed
+337
-7
No files matched your search
@@ -315,6 +315,8 @@ class Application {
|
||||
this.computeHashtagPreferencesSignature(
|
||||
this.hashtagPreferencesSnapshot,
|
||||
);
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
this.boundHashtagPreferencesChangeHandler = null;
|
||||
this.unsubscribeFromHashtagPreferencesChange = null;
|
||||
if (
|
||||
@@ -7919,7 +7921,7 @@ class Application {
|
||||
membership: this.getTagPreferenceMembership(entry.tag),
|
||||
designSystem: this.designSystemContext,
|
||||
onAction: (action, actionDetail = {}) => {
|
||||
this.handleTagPreferenceMenuAction(action, {
|
||||
void this.handleTagPreferenceMenuAction(action, {
|
||||
tag: entry.tag,
|
||||
trigger,
|
||||
video: entry.video || null,
|
||||
@@ -8047,7 +8049,61 @@ class Application {
|
||||
return closed;
|
||||
}
|
||||
|
||||
handleTagPreferenceMenuAction(action, detail = {}) {
|
||||
async persistHashtagPreferencesFromMenu() {
|
||||
const service = this.hashtagPreferences;
|
||||
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);
|
||||
}
|
||||
const error = new Error(
|
||||
message || "Hashtag preferences are unavailable right now.",
|
||||
);
|
||||
error.code = "service-unavailable";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.hashtagPreferencesPublishInFlight) {
|
||||
return this.hashtagPreferencesPublishPromise;
|
||||
}
|
||||
|
||||
const normalizedPubkey = this.normalizeHexPubkey(this.pubkey);
|
||||
const payload = normalizedPubkey ? { pubkey: normalizedPubkey } : {};
|
||||
|
||||
this.hashtagPreferencesPublishInFlight = true;
|
||||
|
||||
const publishPromise = (async () => {
|
||||
try {
|
||||
return await publish.call(service, payload);
|
||||
} 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, {
|
||||
operation: "update",
|
||||
});
|
||||
if (message) {
|
||||
this.showError(message);
|
||||
}
|
||||
throw failure;
|
||||
} finally {
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
this.hashtagPreferencesPublishPromise = publishPromise;
|
||||
return publishPromise;
|
||||
}
|
||||
|
||||
async handleTagPreferenceMenuAction(action, detail = {}) {
|
||||
const tag = typeof detail?.tag === "string" ? detail.tag : "";
|
||||
if (!tag) {
|
||||
return;
|
||||
@@ -8098,6 +8154,15 @@ class Application {
|
||||
this.updateCachedHashtagPreferences();
|
||||
this.refreshTagPreferenceUi();
|
||||
|
||||
try {
|
||||
await this.persistHashtagPreferencesFromMenu();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateCachedHashtagPreferences();
|
||||
this.refreshTagPreferenceUi();
|
||||
|
||||
if (typeof detail?.closePopover === "function") {
|
||||
detail.closePopover({ restoreFocus: false });
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ const SERVICE_CONTRACT = [
|
||||
removeInterest: () => false,
|
||||
addDisinterest: () => false,
|
||||
removeDisinterest: () => false,
|
||||
publish: async () => false,
|
||||
on: () => () => {},
|
||||
}),
|
||||
},
|
||||
@@ -856,6 +857,8 @@ export class ProfileModalController {
|
||||
this.services.describeHashtagPreferencesError;
|
||||
this.getHashtagPreferencesSnapshotService =
|
||||
this.services.getHashtagPreferences;
|
||||
this.hashtagPreferencesPublishInFlight = false;
|
||||
this.hashtagPreferencesPublishPromise = null;
|
||||
|
||||
if (
|
||||
this.hashtagPreferencesService &&
|
||||
@@ -3209,6 +3212,83 @@ export class ProfileModalController {
|
||||
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
|
||||
@@ -3276,10 +3356,22 @@ export class ProfileModalController {
|
||||
: `${this.formatHashtagTag(normalized)} added to ${
|
||||
isInterest ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.showSuccess(actionMessage);
|
||||
this.setHashtagStatus(actionMessage, "success");
|
||||
this.populateHashtagPreferences();
|
||||
return { success: true, reason: inOpposite ? "moved" : "added" };
|
||||
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) {
|
||||
@@ -3341,8 +3433,19 @@ export class ProfileModalController {
|
||||
const message = `${this.formatHashtagTag(normalized)} removed from ${
|
||||
type === "interest" ? "interests" : "disinterests"
|
||||
}.`;
|
||||
this.showSuccess(message);
|
||||
this.setHashtagStatus(message, "success");
|
||||
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);
|
||||
|
||||
@@ -259,6 +259,7 @@ function createController(options = {}) {
|
||||
}
|
||||
return hashtagStore.disinterests.delete(normalized);
|
||||
},
|
||||
publish: async () => ({ ok: true }),
|
||||
on: () => () => {},
|
||||
};
|
||||
|
||||
@@ -777,6 +778,53 @@ for (const _ of [0]) {
|
||||
});
|
||||
});
|
||||
|
||||
test('handleAddHashtagPreference publishes updates', async (t) => {
|
||||
const publishCalls = [];
|
||||
const controller = createController({
|
||||
services: {
|
||||
hashtagPreferences: {
|
||||
publish: async (payload) => {
|
||||
publishCalls.push(payload);
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await controller.load();
|
||||
applyDesignSystemAttributes(document);
|
||||
|
||||
controller.state.setActivePubkey(defaultActorHex);
|
||||
|
||||
let cleanupRan = false;
|
||||
const cleanup = () => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
cleanupRan = true;
|
||||
};
|
||||
|
||||
try {
|
||||
await controller.show('hashtags');
|
||||
await waitForAnimationFrame(window, 2);
|
||||
|
||||
controller.hashtagInterestInput.value = '#nostr';
|
||||
const result = await controller.handleAddHashtagPreference('interest');
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(publishCalls.length, 1);
|
||||
assert.deepEqual(publishCalls[0], { pubkey: defaultActorHex });
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
if (!cleanupRan) {
|
||||
cleanup();
|
||||
}
|
||||
resetRuntimeFlags();
|
||||
});
|
||||
});
|
||||
|
||||
test('Hashtag pane resets after logout when service clears tags', async (t) => {
|
||||
const controller = createController();
|
||||
await controller.load();
|
||||
|
||||
@@ -6,6 +6,7 @@ import createTagPreferenceMenu, {
|
||||
TAG_PREFERENCE_ACTIONS,
|
||||
applyTagPreferenceMenuState,
|
||||
} from "../../js/ui/components/tagPreferenceMenu.js";
|
||||
import Application from "../../js/app.js";
|
||||
|
||||
test("createTagPreferenceMenu renders heading and actions", () => {
|
||||
const { document } = new JSDOM("<!DOCTYPE html>").window;
|
||||
@@ -84,3 +85,116 @@ test("createTagPreferenceMenu forwards actions to callback", () => {
|
||||
assert.equal(events[1].action, TAG_PREFERENCE_ACTIONS.REMOVE_DISINTEREST);
|
||||
assert.equal(events[1].detail.normalizedTag, "art");
|
||||
});
|
||||
|
||||
test("handleTagPreferenceMenuAction publishes hashtag updates", async () => {
|
||||
const store = {
|
||||
interests: new Set(),
|
||||
disinterests: new Set(),
|
||||
};
|
||||
|
||||
const publishCalls = [];
|
||||
const service = {
|
||||
addInterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
store.disinterests.delete(normalized);
|
||||
const had = store.interests.has(normalized);
|
||||
store.interests.add(normalized);
|
||||
return !had;
|
||||
},
|
||||
removeInterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return store.interests.delete(normalized);
|
||||
},
|
||||
addDisinterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
store.interests.delete(normalized);
|
||||
const had = store.disinterests.has(normalized);
|
||||
store.disinterests.add(normalized);
|
||||
return !had;
|
||||
},
|
||||
removeDisinterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return store.disinterests.delete(normalized);
|
||||
},
|
||||
getInterests() {
|
||||
return Array.from(store.interests).sort();
|
||||
},
|
||||
getDisinterests() {
|
||||
return Array.from(store.disinterests).sort();
|
||||
},
|
||||
async publish(payload) {
|
||||
publishCalls.push(payload);
|
||||
return { ok: true };
|
||||
},
|
||||
on() {
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
const app = Object.create(Application.prototype);
|
||||
app.hashtagPreferences = service;
|
||||
app.pubkey = "f".repeat(64);
|
||||
app.safeDecodeNpub = () => null;
|
||||
app.describeHashtagPreferencesError = (_error, fallback = "") => fallback;
|
||||
app.showError = () => {};
|
||||
app.refreshTagPreferenceUi = () => {};
|
||||
app.normalizeHexPubkey = Application.prototype.normalizeHexPubkey;
|
||||
app.normalizeHashtagPreferenceList =
|
||||
Application.prototype.normalizeHashtagPreferenceList;
|
||||
app.createHashtagPreferencesSnapshot =
|
||||
Application.prototype.createHashtagPreferencesSnapshot;
|
||||
app.computeHashtagPreferencesSignature =
|
||||
Application.prototype.computeHashtagPreferencesSignature;
|
||||
app.getHashtagPreferences = Application.prototype.getHashtagPreferences;
|
||||
app.updateCachedHashtagPreferences =
|
||||
Application.prototype.updateCachedHashtagPreferences;
|
||||
app.hashtagPreferencesSnapshot = app.createHashtagPreferencesSnapshot();
|
||||
app.hashtagPreferencesSnapshotSignature =
|
||||
app.computeHashtagPreferencesSignature(app.hashtagPreferencesSnapshot);
|
||||
app.hashtagPreferencesPublishInFlight = false;
|
||||
app.hashtagPreferencesPublishPromise = null;
|
||||
app.persistHashtagPreferencesFromMenu =
|
||||
Application.prototype.persistHashtagPreferencesFromMenu;
|
||||
app.handleTagPreferenceMenuAction =
|
||||
Application.prototype.handleTagPreferenceMenuAction;
|
||||
|
||||
await app.handleTagPreferenceMenuAction(
|
||||
TAG_PREFERENCE_ACTIONS.ADD_INTEREST,
|
||||
{ tag: "nostr" },
|
||||
);
|
||||
|
||||
assert.equal(publishCalls.length, 1);
|
||||
assert.deepEqual(publishCalls[0], { pubkey: app.pubkey });
|
||||
|
||||
await app.handleTagPreferenceMenuAction(
|
||||
TAG_PREFERENCE_ACTIONS.REMOVE_INTEREST,
|
||||
{ tag: "nostr" },
|
||||
);
|
||||
|
||||
assert.equal(publishCalls.length, 2);
|
||||
assert.deepEqual(publishCalls[1], { pubkey: app.pubkey });
|
||||
});
|
||||
Reference in new issue
Block a user