diff --git a/src/index.html b/src/index.html index 438a781..eb0ee56 100644 --- a/src/index.html +++ b/src/index.html @@ -11732,7 +11732,10 @@ customArt: { width: Math.round(bbox.w * 96), height: Math.round(bbox.h * 96), - background: '#ffffff', + // Transparent so the extracted design composites cleanly + // into any host. Users can pick a solid fill later via + // the customart background control if they want one. + background: 'transparent', }, layers: clonedLayers, layerGroups: {}, @@ -11939,23 +11942,57 @@ toastError('Finish the current embedded-design edit first.'); return; } + // Linked layers don't carry a payload locally — the source + // lives on relays. Open the cached resolved payload for + // editing and remember the linked context so finishDesignObjectEdit + // can republish the source under the same d-tag on save. + const isLinked = layer.design?.kind === 'linked'; + let editingPayload = null; + let linkedContext = null; + if (isLinked) { + const spec = { ref: layer.design.ref, pinnedEventId: layer.design.pinnedEventId }; + const cached = linkedDesigns.peek(spec); + if (!cached?.payload) { + toastError('Linked source is still loading or unavailable. Refresh the layer and try again.'); + return; + } + if (layer.design.pinnedEventId) { + toastError('Unpin this layer to edit its source — pinned versions are frozen.'); + return; + } + const sourcePubkey = cached.sourceEvent?.pubkey; + const myPubkey = state.community?.myPubkey || ''; + if (sourcePubkey && myPubkey && sourcePubkey !== myPubkey) { + toastError("Only the source author can edit a linked design. Fork or unlink it to edit a local copy."); + return; + } + editingPayload = cached.payload; + linkedContext = { ref: layer.design.ref, sourceEvent: cached.sourceEvent }; + } else { + editingPayload = layer.design?.payload || {}; + } _designObjectEdit = { hostPayload: currentEditableDesignPayload(), layerId: layer.id, - layerName: layer.name || designObjectTitle(layer.design?.payload) + layerName: layer.name || designObjectTitle(editingPayload), + isLinked, + linkedContext, }; try { - loadProjectFromText(JSON.stringify(layer.design?.payload || {})); + loadProjectFromText(JSON.stringify(editingPayload)); } catch (err) { _designObjectEdit = null; - toastError(`Could not open embedded design: ${err?.message || err}`); + toastError(`Could not open design: ${err?.message || err}`); return; } paintDesignObjectBanner(); - toastSuccess('Editing embedded design object. Use Done editing object to write it back.'); + toastSuccess(isLinked + ? '🔗 Editing linked source — Done republishes it to every host that links it.' + : 'Editing embedded design object. Use Done editing object to write it back.' + ); } - function finishDesignObjectEdit(saveChanges) { + async function finishDesignObjectEdit(saveChanges) { if (!_designObjectEdit) return; const edit = _designObjectEdit; const updatedPayload = saveChanges ? currentEditableDesignPayload() : null; @@ -11967,19 +12004,91 @@ } _designObjectEdit = null; const layer = getLayer(edit.layerId); - if (saveChanges && layer && layer.type === 'design') { - pushHistory('update design object'); - layer.design = { mode: normalizeTemplateMode(updatedPayload), payload: clonePayload(updatedPayload) }; - layer.name = updatedPayload?.meta?.title || edit.layerName || layer.name; - setLayerSelection([layer.id], layer.id); - render(); - toastSuccess('Embedded design object updated.'); + if (saveChanges && layer && layer.type === 'design' && updatedPayload) { + if (edit.isLinked && edit.linkedContext) { + await _republishLinkedDesignSource(layer, updatedPayload, edit.linkedContext, edit.layerName); + } else { + pushHistory('update design object'); + layer.design = { mode: normalizeTemplateMode(updatedPayload), payload: clonePayload(updatedPayload) }; + layer.name = updatedPayload?.meta?.title || edit.layerName || layer.name; + setLayerSelection([layer.id], layer.id); + render(); + toastSuccess('Embedded design object updated.'); + } } else { toastSuccess('Returned to parent design.'); } paintDesignObjectBanner(); } + // Republish a linked-design source under the same d-tag with + // the edited payload, then seed the resolver caches with the + // new signed event so every linked layer on this canvas paints + // the updated version on the next render. Only safe for the + // source author — beginDesignObjectEdit blocks non-authors + // from reaching this path. + async function _republishLinkedDesignSource(layer, updatedPayload, linkedContext, prevLayerName) { + const myPubkey = state.community?.myPubkey || ''; + if (!myPubkey) { + toastError('Sign in with Nostr first.'); + return; + } + const ref = linkedContext.ref; + const parsed = linkedDesigns._parseAddressCoord(ref); + if (!parsed || parsed.pubkey !== myPubkey) { + toastError('Only the source author can republish a linked design.'); + return; + } + const dTag = parsed.dTag; + const mode = normalizeTemplateMode(updatedPayload); + const payload = clonePayload(updatedPayload); + stripLinkedDesignEphemera(payload); + let attributionTags = []; + try { + const built = await _buildLinkedAttributionTags(payload, myPubkey); + attributionTags = [...built.contributorTags, ...built.linkTags]; + } catch (err) { + console.warn('[linked-edit] attribution build failed', err); + } + const typeTag = typeTagForMode(mode); + const tags = [ + ['d', dTag], + ...getTemplateTags(payload, typeTag), + ...attributionTags, + ]; + const evt = { + kind: 30078, + created_at: Math.floor(Date.now() / 1000), + tags, + content: JSON.stringify(payload), + }; + const publishingToast = toast('Re-publishing linked source…', { duration: 30000 }); + let signed; + try { signed = await publishCommunityEvent(evt); } + catch (err) { + publishingToast?.dismiss?.(); + toastError(err?.message || 'Could not re-publish the linked source.'); + return; + } + publishingToast?.dismiss?.(); + if (!signed?.id) { + toastError('Republish succeeded but the relay did not return the event id.'); + return; + } + pushHistory('republish linked source'); + const key = linkedDesigns.keyFor({ ref, pinnedEventId: null }); + if (key) linkedDesigns.clearCache(key); + try { await linkedDesigns.seed({ ref, pinnedEventId: null }, signed); } + catch (err) { console.warn('[linked-edit] cache seed failed', err); } + // Refresh the layer name from the (possibly edited) payload title. + const newTitle = updatedPayload?.meta?.title?.trim(); + if (newTitle) layer.name = '🔗 ' + newTitle; + else if (prevLayerName) layer.name = prevLayerName; + setLayerSelection([layer.id], layer.id); + render(); + toastSuccess('🔗 Linked source updated — every host that links it shows the new version on next refresh.'); + } + function paintDesignObjectBanner() { const banner = $('designObjectBanner'); const title = $('designObjectTitle');