From 0b58f5e011052c32092d730b4091524782bb7345 Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" <290147524+clagentic-builder[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:49:12 -0400 Subject: [PATCH] fix(emoji-picker): surface silent doUpload returns + fix stale grid refresh closure Custom emoji upload from the Set Project Icon picker's Custom tab could silently fail: empty-pendingFile and invalid-slug branches in doUpload() returned with no user feedback (no toast, no error). Additionally, the post-success/post-delete grid refresh called an orphaned renderCustomGrid closure captured before buildCustomPanel() rebuilt the panel, instead of relying on buildCustomPanel()'s own fresh render. - doUpload(): showToast on both previously-silent early-return paths - proceed()/delete handler: drop stale renderCustomGrid() call; buildCustomPanel() already repaints the live grid via its own closure No repro environment (headless browser) available in this sandbox to confirm which branch is load-bearing in production, so both are fixed as cheap, non-conflicting defense in depth per dispatch brief. --- lib/public/modules/emoji-picker.js | 31 +++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/public/modules/emoji-picker.js b/lib/public/modules/emoji-picker.js index 799543ef..9295108d 100644 --- a/lib/public/modules/emoji-picker.js +++ b/lib/public/modules/emoji-picker.js @@ -249,12 +249,24 @@ export function buildEmojiPicker(opts) { } function doUpload() { - if (!pendingFile) return; + // Both early returns below used to be silent — the control (rendered + // by showSlugConfirm) is only reachable once a file is already + // selected, but a defensive check that fires with zero user feedback + // is indistinguishable from the picker doing nothing at all. Surface + // both via the same toast used for server-side upload failures (line + // ~285) so "nothing happened" never happens silently again. + if (!pendingFile) { + showToast("Couldn't add custom emoji", "warn"); + return; + } var slug = slugInput.value.trim().toLowerCase() .replace(/[^a-z0-9_-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 64); - if (!slug || !/^[a-z0-9_-]{1,64}$/.test(slug)) return; + if (!slug || !/^[a-z0-9_-]{1,64}$/.test(slug)) { + showToast("Enter a valid name", "warn"); + return; + } // Check for duplicate in the already-fetched list var existing = customList || []; @@ -274,9 +286,14 @@ export function buildEmojiPicker(opts) { showToast(result.error || "Upload failed", "warn"); return; } - // Refresh the custom grid after upload + // Refresh the custom grid after upload. buildCustomPanel() already + // repaints the grid it just (re)built via its own fresh + // renderCustomGrid closure (see below) — calling the outer + // renderCustomGrid here re-renders the ORPHANED grid captured by + // this closure at the time doUpload's panel was built, which is + // detached from the DOM the moment buildCustomPanel() runs again. customList = null; - fetchCustomList(function () { buildCustomPanel(); renderCustomGrid(); }); + fetchCustomList(function () { buildCustomPanel(); }); }); } @@ -352,8 +369,12 @@ export function buildEmojiPicker(opts) { delBtn.addEventListener("click", function (e) { e.stopPropagation(); deleteCustomIcon(entry.slug).then(function () { + // Same stale-closure hazard as the upload success path above: + // buildCustomPanel() already repaints via its own fresh + // renderCustomGrid; the closure captured here is orphaned + // once buildCustomPanel() rebuilds the panel. customList = null; - fetchCustomList(function () { buildCustomPanel(); renderCustomGrid(); }); + fetchCustomList(function () { buildCustomPanel(); }); }); }); tile.appendChild(delBtn);