diff --git a/components/profile-modal.html b/components/profile-modal.html
index 30ce10ab..6a6103cb 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -230,13 +230,22 @@
Loading...
npub1...
-
- View channel
-
+
+
+
+
+
Edit Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configure storage to enable uploads.
+
+
+
+
+
+
+
+
+
+
+
+
Configure storage to enable uploads.
+
+
+
+
+
+
+
+
+
+
+
+
+
{};
@@ -189,6 +190,12 @@ const SERVICE_CONTRACT = [
? fallbackMessage.trim()
: "Failed to login. Please try again.",
},
+ {
+ key: "r2Service",
+ type: "object",
+ description: "Service for handling file uploads (R2/S3).",
+ optional: true,
+ },
{
key: "nostrService",
type: "object",
@@ -1297,7 +1304,11 @@ export class ProfileModalController {
document.getElementById("profileNavHistory") || null;
this.navButtons.admin = document.getElementById("profileNavAdmin") || null;
+ this.profileEditBtn = document.getElementById("profileEditBtn") || null;
+ this.profileEditBackBtn = document.getElementById("profileEditBackBtn") || null;
+
this.panes.account = document.getElementById("profilePaneAccount") || null;
+ this.panes.edit = document.getElementById("profilePaneEdit") || null;
this.panes.relays = document.getElementById("profilePaneRelays") || null;
this.panes.wallet = document.getElementById("profilePaneWallet") || null;
this.panes.storage = document.getElementById("profilePaneStorage") || null;
@@ -1528,6 +1539,29 @@ export class ProfileModalController {
this.adminAddBlacklistButton = this.addBlacklistButton;
this.adminBlacklistInput = this.blacklistInput;
+ 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();
}
@@ -2806,6 +2840,68 @@ 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.updateMessagesReloadState();
}
@@ -5943,6 +6039,171 @@ export class ProfileModalController {
}
}
+ handleEditProfile() {
+ this.selectPane("edit");
+ void this.populateEditPane();
+ }
+
+ 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);
+ }
+
+ 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(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 content = JSON.stringify(profile);
+ const event = {
+ kind: 0,
+ created_at: Math.floor(Date.now() / 1000),
+ pubkey,
+ tags: [],
+ content,
+ };
+
+ 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 populateStoragePane() {
const storageService = this.services.storageService;
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());