fix(shell): header button reliability — history, settings, comments
History panel: lazy doc acquisition from CommunitySync/offline runtime on open + listen for community-sync-ready event for late connections. Space settings: reposition panel after every re-render (async data loads were destroying inline positioning styles), clamp max-height to viewport, fix wrong global name (__rspaceCommunitySync → __communitySync). Comment bell: render DOM once, update badge without innerHTML churn, listen for community-sync-ready event, cache sync reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c35f39380e
commit
772e5e4352
|
|
@ -17,6 +17,7 @@ export class RStackCommentBell extends HTMLElement {
|
|||
#shadow: ShadowRoot;
|
||||
#count = 0;
|
||||
#pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
#syncRef: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -25,27 +26,42 @@ export class RStackCommentBell extends HTMLElement {
|
|||
|
||||
connectedCallback() {
|
||||
this.#render();
|
||||
// Try to pick up existing CommunitySync
|
||||
this.#syncRef = (window as any).__communitySync || null;
|
||||
this.#refreshCount();
|
||||
this.#pollTimer = setInterval(() => this.#refreshCount(), POLL_INTERVAL);
|
||||
window.addEventListener("comment-pins-changed", this.#onPinsChanged);
|
||||
// Listen for CommunitySync becoming available (may connect after mount)
|
||||
document.addEventListener("community-sync-ready", this.#onSyncReady);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.#pollTimer) clearInterval(this.#pollTimer);
|
||||
window.removeEventListener("comment-pins-changed", this.#onPinsChanged);
|
||||
document.removeEventListener("community-sync-ready", this.#onSyncReady);
|
||||
}
|
||||
|
||||
#onSyncReady = (e: Event) => {
|
||||
const sync = (e as CustomEvent).detail?.sync;
|
||||
if (sync) {
|
||||
this.#syncRef = sync;
|
||||
this.#refreshCount();
|
||||
}
|
||||
};
|
||||
|
||||
#onPinsChanged = () => {
|
||||
this.#refreshCount();
|
||||
};
|
||||
|
||||
#refreshCount() {
|
||||
const sync = (window as any).__communitySync;
|
||||
// Prefer cached ref, fall back to global
|
||||
const sync = this.#syncRef || (window as any).__communitySync;
|
||||
if (sync && !this.#syncRef) this.#syncRef = sync;
|
||||
const pins = sync?.doc?.commentPins;
|
||||
if (!pins) {
|
||||
if (this.#count !== 0) {
|
||||
this.#count = 0;
|
||||
this.#render();
|
||||
this.#updateBadge();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -54,23 +70,19 @@ export class RStackCommentBell extends HTMLElement {
|
|||
).length;
|
||||
if (newCount !== this.#count) {
|
||||
this.#count = newCount;
|
||||
this.#render();
|
||||
this.#updateBadge();
|
||||
}
|
||||
}
|
||||
|
||||
/** Full render — only called once in connectedCallback. */
|
||||
#render() {
|
||||
const badge =
|
||||
this.#count > 0
|
||||
? `<span class="badge">${this.#count > 99 ? "99+" : this.#count}</span>`
|
||||
: "";
|
||||
|
||||
this.#shadow.innerHTML = `
|
||||
<style>${STYLES}</style>
|
||||
<button class="comment-btn" aria-label="Leave Comment" title="Leave Comment (/)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"/>
|
||||
</svg>
|
||||
${badge}
|
||||
<span class="badge" style="display:none"></span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
|
|
@ -82,6 +94,18 @@ export class RStackCommentBell extends HTMLElement {
|
|||
});
|
||||
}
|
||||
|
||||
/** Lightweight badge update — no innerHTML churn. */
|
||||
#updateBadge() {
|
||||
const badge = this.#shadow.querySelector(".badge") as HTMLElement | null;
|
||||
if (!badge) return;
|
||||
if (this.#count > 0) {
|
||||
badge.textContent = this.#count > 99 ? "99+" : String(this.#count);
|
||||
badge.style.display = "";
|
||||
} else {
|
||||
badge.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
static define(tag = "rstack-comment-bell") {
|
||||
if (!customElements.get(tag)) customElements.define(tag, RStackCommentBell);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,20 +58,49 @@ export class RStackHistoryPanel extends HTMLElement {
|
|||
}
|
||||
};
|
||||
|
||||
private _syncReadyHandler = (e: Event) => {
|
||||
const sync = (e as CustomEvent).detail?.sync;
|
||||
if (sync?.doc) {
|
||||
this._doc = sync.doc;
|
||||
if (this._open) {
|
||||
this._refreshHistory();
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
||||
this._render();
|
||||
// Pick up CommunitySync when it connects (may be after mount)
|
||||
document.addEventListener("community-sync-ready", this._syncReadyHandler);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
document.removeEventListener("click", this._clickOutsideHandler, true);
|
||||
document.removeEventListener("community-sync-ready", this._syncReadyHandler);
|
||||
}
|
||||
|
||||
open() {
|
||||
this._open = true;
|
||||
// Lazy doc acquisition: try CommunitySync global first, then offline runtime
|
||||
if (!this._doc) {
|
||||
const sync = (window as any).__communitySync;
|
||||
if (sync?.doc) {
|
||||
this._doc = sync.doc;
|
||||
} else {
|
||||
const rt = (window as any).__rspaceOfflineRuntime;
|
||||
if (rt?.documentManager) {
|
||||
const docs = rt.documentManager.listAll?.() || [];
|
||||
if (docs.length > 0) {
|
||||
const doc = rt.documentManager.get(docs[0]);
|
||||
if (doc) this._doc = doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._refreshHistory();
|
||||
this._render();
|
||||
this._positionPanel();
|
||||
document.getElementById("history-btn")?.classList.add("active");
|
||||
document.addEventListener("click", this._clickOutsideHandler, true);
|
||||
}
|
||||
|
|
@ -88,13 +117,16 @@ export class RStackHistoryPanel extends HTMLElement {
|
|||
if (this._open) this.close(); else this.open();
|
||||
}
|
||||
|
||||
/** Position the panel below the history button, right-aligned to button. */
|
||||
/** Position the panel below the history button, right-aligned to button, clamped to viewport. */
|
||||
private _positionPanel() {
|
||||
const panel = this.shadowRoot?.querySelector(".panel") as HTMLElement | null;
|
||||
const btn = document.getElementById("history-btn");
|
||||
if (!panel || !btn) return;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
panel.style.top = `${rect.bottom + 6}px`;
|
||||
const topPos = rect.bottom + 6;
|
||||
panel.style.top = `${topPos}px`;
|
||||
// Clamp max-height so panel never overflows bottom of viewport
|
||||
panel.style.maxHeight = `${window.innerHeight - topPos - 8}px`;
|
||||
// Right-align to button's right edge, but clamp so left edge stays on screen
|
||||
const rightOffset = window.innerWidth - rect.right;
|
||||
panel.style.right = `${rightOffset}px`;
|
||||
|
|
@ -369,6 +401,8 @@ export class RStackHistoryPanel extends HTMLElement {
|
|||
`;
|
||||
|
||||
this._bindEvents();
|
||||
// Re-position after every render since innerHTML replacement destroys inline styles
|
||||
if (this._open) this._positionPanel();
|
||||
}
|
||||
|
||||
private _renderTimeMachineDetails() {
|
||||
|
|
|
|||
|
|
@ -115,22 +115,25 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
if (this._open) this.close(); else this.open();
|
||||
}
|
||||
|
||||
/** Position the panel below the settings button, left-aligned with screen clamping. */
|
||||
/** Position the panel below the settings button, clamped to viewport. */
|
||||
private _positionPanel() {
|
||||
const panel = this.shadowRoot?.querySelector(".panel") as HTMLElement | null;
|
||||
const btn = document.getElementById("settings-btn");
|
||||
if (!panel || !btn) return;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const panelWidth = Math.min(380, window.innerWidth * 0.9);
|
||||
const topPos = rect.bottom + 6;
|
||||
// Align panel's left edge with the button, but clamp so it stays on screen
|
||||
let left = rect.left;
|
||||
if (left + panelWidth > window.innerWidth - 8) {
|
||||
left = window.innerWidth - panelWidth - 8;
|
||||
}
|
||||
if (left < 8) left = 8;
|
||||
panel.style.top = `${rect.bottom + 6}px`;
|
||||
panel.style.top = `${topPos}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.right = "auto";
|
||||
// Clamp max-height so panel never overflows bottom of viewport
|
||||
panel.style.maxHeight = `${window.innerHeight - topPos - 8}px`;
|
||||
}
|
||||
|
||||
private async _loadData() {
|
||||
|
|
@ -140,7 +143,7 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
const token = getToken();
|
||||
|
||||
// Load community data from WS-synced doc (via global)
|
||||
const sync = (window as any).__rspaceCommunitySync;
|
||||
const sync = (window as any).__communitySync;
|
||||
if (sync?.doc) {
|
||||
const data = sync.doc;
|
||||
this._ownerDID = data.meta?.ownerDID || "";
|
||||
|
|
@ -297,6 +300,9 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
// Track whether this is a re-render (panel already exists)
|
||||
const isRerender = !!this.shadowRoot.querySelector(".panel");
|
||||
|
||||
const roleOptions = (currentRole: string) => {
|
||||
const roles = ["viewer", "member", "moderator", "admin"];
|
||||
return roles.map(r => `<option value="${r}" ${r === currentRole ? "selected" : ""}>${r}</option>`).join("");
|
||||
|
|
@ -469,6 +475,8 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
`;
|
||||
|
||||
this._bindEvents();
|
||||
// Re-position after every render since innerHTML replacement destroys inline styles
|
||||
this._positionPanel();
|
||||
}
|
||||
|
||||
private _bindEvents() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue