Add channel and copy actions for profile modal lists

This commit is contained in:
thePR0M3TH3AN committed 2025-10-20 14:17:15 -04:00
1 parent 51f5db2cec
commit e8d20cd8dd
4 files changed
+244 -26

No files matched your search

+8
View File
@@ -1618,6 +1618,14 @@
white-space: nowrap;
}
.profile-modal__remove-button {
@apply border-2;
}
.profile-modal__remove-button:focus-visible {
--tw-ring-color: var(--color-critical-strong);
}
.profile-modal__add-profile {
--tw-ring-color: var(--color-ring-alert);
@apply focus-ring relative flex w-full items-center justify-start gap-4 rounded-xl px-5 py-4 text-left text-text transition duration-base ease-in-out;
+4
View File
@@ -566,6 +566,10 @@ class Application {
authService: this.authService,
log: (...args) => this.log(...args),
closeAllMoreMenus: (options) => this.closeAllMoreMenus(options),
clipboard:
typeof navigator !== "undefined" && navigator?.clipboard
? navigator.clipboard
: null,
};
const profileModalState = {
+229 -25
View File
@@ -1640,6 +1640,173 @@ export class ProfileModalController {
return container;
}
createViewChannelButton({ targetNpub, displayNpub } = {}) {
const normalizedTarget =
typeof targetNpub === "string" && targetNpub.trim()
? targetNpub.trim()
: "";
if (!normalizedTarget) {
return null;
}
const button = document.createElement("button");
button.type = "button";
button.className = "btn-ghost focus-ring text-xs";
button.textContent = "View channel";
button.dataset.targetNpub = normalizedTarget;
if (displayNpub && typeof displayNpub === "string") {
button.setAttribute(
"aria-label",
`View channel ${displayNpub.trim() || normalizedTarget}`,
);
button.title = `View channel ${displayNpub.trim() || normalizedTarget}`;
}
button.addEventListener("click", () => {
this.callbacks.onChannelLink(button, this);
});
return button;
}
createCopyNpubButton({ targetNpub, displayNpub } = {}) {
const normalizedTarget =
typeof targetNpub === "string" && targetNpub.trim()
? targetNpub.trim()
: "";
if (!normalizedTarget) {
return null;
}
const button = document.createElement("button");
button.type = "button";
button.className = "btn-ghost focus-ring text-xs";
button.textContent = "Copy npub";
if (displayNpub && typeof displayNpub === "string") {
button.setAttribute(
"aria-label",
`Copy ${displayNpub.trim() || normalizedTarget}`,
);
button.title = `Copy ${displayNpub.trim() || normalizedTarget}`;
}
const handleCopy = async () => {
if (button.dataset.state === "loading") {
return;
}
button.dataset.state = "loading";
button.disabled = true;
button.setAttribute("aria-busy", "true");
try {
await this.copyNpubToClipboard(normalizedTarget, { displayNpub });
} finally {
button.disabled = false;
button.removeAttribute("aria-busy");
delete button.dataset.state;
}
};
button.addEventListener("click", () => {
void handleCopy();
});
return button;
}
createRemoveButton({
label = "Remove",
confirmMessage,
confirmValue,
onRemove,
} = {}) {
if (typeof onRemove !== "function") {
return null;
}
const button = document.createElement("button");
button.type = "button";
button.className = "btn-ghost focus-ring text-xs profile-modal__remove-button";
button.dataset.variant = "critical";
button.dataset.role = "remove";
button.textContent = label;
const handleRemove = async () => {
if (confirmMessage) {
const replacement =
typeof confirmValue === "string" && confirmValue.trim()
? confirmValue.trim()
: "this entry";
const prompt = confirmMessage.replace("{npub}", replacement);
if (!window.confirm(prompt)) {
return;
}
}
button.disabled = true;
button.setAttribute("aria-busy", "true");
try {
await onRemove(button);
} catch (error) {
userLogger.error("Failed to remove entry:", error);
} finally {
button.disabled = false;
button.removeAttribute("aria-busy");
}
};
button.addEventListener("click", () => {
void handleRemove();
});
return button;
}
async copyNpubToClipboard(npub, { displayNpub } = {}) {
const normalized =
typeof npub === "string" && npub.trim() ? npub.trim() : "";
if (!normalized) {
this.showError("Unable to copy npub. Invalid value provided.");
return { copied: false, reason: "invalid" };
}
const clipboard = (() => {
if (
this.services?.clipboard &&
typeof this.services.clipboard.writeText === "function"
) {
return this.services.clipboard;
}
if (
typeof navigator !== "undefined" &&
navigator?.clipboard &&
typeof navigator.clipboard.writeText === "function"
) {
return navigator.clipboard;
}
return null;
})();
if (!clipboard) {
this.showError("Copy to clipboard is not supported in this browser.");
return { copied: false, reason: "unsupported" };
}
try {
await clipboard.writeText(normalized);
this.showSuccess("npub copied to clipboard!");
return { copied: true };
} catch (error) {
userLogger.error("Failed to copy npub:", error);
this.showError("Failed to copy npub. Please try again.");
return { copied: false, error };
}
}
setupLayoutBreakpointObserver() {
if (
typeof window === "undefined" ||
@@ -2439,18 +2606,38 @@ export class ProfileModalController {
avatarSrc,
});
const actionBtn = document.createElement("button");
actionBtn.type = "button";
actionBtn.className = "btn-ghost focus-ring text-xs";
actionBtn.dataset.variant = "danger";
actionBtn.textContent = "Remove";
actionBtn.dataset.blockedHex = hex;
actionBtn.addEventListener("click", () => {
void this.handleRemoveBlockedCreator(hex);
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);
item.appendChild(actionBtn);
if (actions.childElementCount > 0) {
item.appendChild(actions);
}
this.blockList.appendChild(item);
});
@@ -3636,25 +3823,42 @@ export class ProfileModalController {
displayNpub,
avatarSrc,
});
item.appendChild(summary);
const actions = document.createElement("div");
actions.className =
"flex flex-wrap items-center justify-end gap-2 sm:flex-none";
const viewButton = this.createViewChannelButton({
targetNpub: encodedNpub,
displayNpub,
});
if (viewButton) {
actions.appendChild(viewButton);
}
const copyButton = this.createCopyNpubButton({
targetNpub: encodedNpub,
displayNpub,
});
if (copyButton) {
actions.appendChild(copyButton);
}
if (removable && typeof onRemove === "function") {
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "btn-ghost focus-ring text-xs";
removeBtn.textContent = removeLabel;
removeBtn.addEventListener("click", () => {
if (confirmMessage) {
const message = confirmMessage.replace("{npub}", displayNpub);
if (!window.confirm(message)) {
return;
}
}
removeBtn.disabled = true;
removeBtn.setAttribute("aria-busy", "true");
onRemove(npub, removeBtn);
const removeBtn = this.createRemoveButton({
label: removeLabel,
confirmMessage,
confirmValue: displayNpub,
onRemove: (button) => onRemove(npub, button),
});
item.appendChild(removeBtn);
if (removeBtn) {
actions.appendChild(removeBtn);
}
}
item.appendChild(summary);
if (actions.childElementCount > 0) {
item.appendChild(actions);
}
listEl.appendChild(item);
+3 -1
View File
@@ -683,7 +683,9 @@ test('admin mutations invoke accessControl stubs and update admin DOM', async ()
assert.equal(whitelistButton.hasAttribute('aria-busy'), false);
assert.ok(successMessages.includes('Added to the whitelist.'));
const removeButton = controller.whitelistList.querySelector('button');
const removeButton = controller.whitelistList.querySelector(
'button[data-role="remove"]',
);
await controller.handleAdminListMutation('whitelist', 'remove', 'npub1allow', removeButton);
const removeEvent = accessEvents.find((entry) => entry[0] === 'removeWhitelist');
assert.deepEqual(removeEvent, [