Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m6s
Details
CI/CD / deploy (push) Successful in 3m6s
Details
This commit is contained in:
commit
5fc7b4d6b1
|
|
@ -166,6 +166,7 @@ export class CommunitySync extends EventTarget {
|
||||||
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
#initialSyncFired = false;
|
#initialSyncFired = false;
|
||||||
#wsUrl: string | null = null;
|
#wsUrl: string | null = null;
|
||||||
|
#localDID: string = '';
|
||||||
|
|
||||||
// ── Undo/Redo state ──
|
// ── Undo/Redo state ──
|
||||||
#undoStack: UndoEntry[] = [];
|
#undoStack: UndoEntry[] = [];
|
||||||
|
|
@ -195,6 +196,11 @@ export class CommunitySync extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the local user's DID so forgotten-shape filtering is per-user. */
|
||||||
|
setLocalDID(did: string): void {
|
||||||
|
this.#localDID = did;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load document and sync state from offline cache.
|
* Load document and sync state from offline cache.
|
||||||
* Call BEFORE connect() to show cached content immediately.
|
* Call BEFORE connect() to show cached content immediately.
|
||||||
|
|
@ -878,9 +884,14 @@ export class CommunitySync extends EventTarget {
|
||||||
for (const [id, shapeData] of Object.entries(shapes)) {
|
for (const [id, shapeData] of Object.entries(shapes)) {
|
||||||
const d = shapeData as Record<string, unknown>;
|
const d = shapeData as Record<string, unknown>;
|
||||||
if (d.deleted === true) continue; // Deleted: not in DOM
|
if (d.deleted === true) continue; // Deleted: not in DOM
|
||||||
this.#applyShapeToDOM(shapeData);
|
// Skip shapes this user has forgotten — one delete = gone from their view
|
||||||
// If forgotten (faded), emit state-changed so canvas can apply visual
|
|
||||||
const fb = d.forgottenBy;
|
const fb = d.forgottenBy;
|
||||||
|
if (this.#localDID && fb && typeof fb === 'object'
|
||||||
|
&& (fb as Record<string, number>)[this.#localDID]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.#applyShapeToDOM(shapeData);
|
||||||
|
// If forgotten by others (but not this user), emit state-changed for fade visual
|
||||||
if (fb && typeof fb === 'object' && Object.keys(fb).length > 0) {
|
if (fb && typeof fb === 'object' && Object.keys(fb).length > 0) {
|
||||||
this.dispatchEvent(new CustomEvent("shape-state-changed", {
|
this.dispatchEvent(new CustomEvent("shape-state-changed", {
|
||||||
detail: { shapeId: id, state: 'forgotten', data: shapeData }
|
detail: { shapeId: id, state: 'forgotten', data: shapeData }
|
||||||
|
|
@ -945,6 +956,14 @@ export class CommunitySync extends EventTarget {
|
||||||
const d = shapeData as Record<string, unknown>;
|
const d = shapeData as Record<string, unknown>;
|
||||||
const state = this.getShapeVisualState(shapeId);
|
const state = this.getShapeVisualState(shapeId);
|
||||||
|
|
||||||
|
// Skip shapes this user has forgotten — don't create/update DOM
|
||||||
|
const fb = d.forgottenBy;
|
||||||
|
if (this.#localDID && fb && typeof fb === 'object'
|
||||||
|
&& (fb as Record<string, number>)[this.#localDID]) {
|
||||||
|
this.#removeShapeFromDOM(shapeId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === 'deleted') {
|
if (state === 'deleted') {
|
||||||
// Hard-deleted: remove from DOM
|
// Hard-deleted: remove from DOM
|
||||||
this.#removeShapeFromDOM(shapeId);
|
this.#removeShapeFromDOM(shapeId);
|
||||||
|
|
@ -952,7 +971,7 @@ export class CommunitySync extends EventTarget {
|
||||||
detail: { shapeId, state: 'deleted', data: shapeData }
|
detail: { shapeId, state: 'deleted', data: shapeData }
|
||||||
}));
|
}));
|
||||||
} else if (state === 'forgotten') {
|
} else if (state === 'forgotten') {
|
||||||
// Forgotten: keep in DOM, emit state change for fade visual
|
// Forgotten by others: keep in DOM, emit state change for fade visual
|
||||||
this.#applyShapeToDOM(shapeData);
|
this.#applyShapeToDOM(shapeData);
|
||||||
this.dispatchEvent(new CustomEvent("shape-state-changed", {
|
this.dispatchEvent(new CustomEvent("shape-state-changed", {
|
||||||
detail: { shapeId, state: 'forgotten', data: shapeData }
|
detail: { shapeId, state: 'forgotten', data: shapeData }
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ export class FolkEmbed extends FolkShape {
|
||||||
|
|
||||||
#url: string | null = null;
|
#url: string | null = null;
|
||||||
#error: string | null = null;
|
#error: string | null = null;
|
||||||
|
#embedRendered = false;
|
||||||
|
|
||||||
get url() {
|
get url() {
|
||||||
return this.#url;
|
return this.#url;
|
||||||
|
|
@ -320,15 +321,34 @@ export class FolkEmbed extends FolkShape {
|
||||||
this.dispatchEvent(new CustomEvent("close"));
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// If URL is already set, render embed
|
// Defer embed rendering until shape enters viewport
|
||||||
if (this.#url) {
|
if (this.#url) {
|
||||||
const embedUrl = transformUrl(this.#url);
|
if (this.hasBeenVisible) {
|
||||||
this.#renderEmbed(content, urlInputContainer, titleText, headerTitle, this.#url, embedUrl);
|
const embedUrl = transformUrl(this.#url);
|
||||||
|
this.#renderEmbed(content, urlInputContainer, titleText, headerTitle, this.#url, embedUrl);
|
||||||
|
} else {
|
||||||
|
// Show URL text as lightweight placeholder
|
||||||
|
titleText.textContent = getDisplayTitle(this.#url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override onBecameVisible(): void {
|
||||||
|
if (this.#url && !this.#embedRendered) {
|
||||||
|
const root = this.renderRoot as ShadowRoot;
|
||||||
|
const content = root.querySelector(".content") as HTMLElement;
|
||||||
|
const urlInputContainer = root.querySelector(".url-input-container") as HTMLElement;
|
||||||
|
const titleText = root.querySelector(".title-text") as HTMLElement;
|
||||||
|
const headerTitle = root.querySelector(".header-title") as HTMLElement;
|
||||||
|
if (content && urlInputContainer && titleText && headerTitle) {
|
||||||
|
const embedUrl = transformUrl(this.#url);
|
||||||
|
this.#renderEmbed(content, urlInputContainer, titleText, headerTitle, this.#url, embedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#renderEmbed(
|
#renderEmbed(
|
||||||
content: HTMLElement,
|
content: HTMLElement,
|
||||||
urlInputContainer: HTMLElement,
|
urlInputContainer: HTMLElement,
|
||||||
|
|
@ -337,6 +357,7 @@ export class FolkEmbed extends FolkShape {
|
||||||
originalUrl: string,
|
originalUrl: string,
|
||||||
embedUrl: string | null
|
embedUrl: string | null
|
||||||
) {
|
) {
|
||||||
|
this.#embedRendered = true;
|
||||||
// Update header
|
// Update header
|
||||||
titleText.textContent = getDisplayTitle(originalUrl);
|
titleText.textContent = getDisplayTitle(originalUrl);
|
||||||
const favicon = document.createElement("img");
|
const favicon = document.createElement("img");
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ export class FolkFeed extends FolkShape {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.#buildUI();
|
this.#buildUI();
|
||||||
this.#fetchFeed();
|
// Defer API calls until shape is visible (onBecameVisible handles it)
|
||||||
this.#startAutoRefresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|
@ -73,6 +72,11 @@ export class FolkFeed extends FolkShape {
|
||||||
this.#stopAutoRefresh();
|
this.#stopAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override onBecameVisible(): void {
|
||||||
|
this.#fetchFeed();
|
||||||
|
this.#startAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
|
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
|
||||||
super.attributeChangedCallback(name, oldVal, newVal);
|
super.attributeChangedCallback(name, oldVal, newVal);
|
||||||
if (["source-module", "feed-id", "feed-filter", "max-items"].includes(name)) {
|
if (["source-module", "feed-id", "feed-filter", "max-items"].includes(name)) {
|
||||||
|
|
|
||||||
|
|
@ -634,6 +634,7 @@ export class FolkRApp extends FolkShape {
|
||||||
#refreshTimer: ReturnType<typeof setInterval> | null = null;
|
#refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
#modeToggleBtn: HTMLButtonElement | null = null;
|
#modeToggleBtn: HTMLButtonElement | null = null;
|
||||||
#resolvedPorts: PortDescriptor[] | null = null;
|
#resolvedPorts: PortDescriptor[] | null = null;
|
||||||
|
#contentLoaded = false;
|
||||||
|
|
||||||
get moduleId() { return this.#moduleId; }
|
get moduleId() { return this.#moduleId; }
|
||||||
set moduleId(value: string) {
|
set moduleId(value: string) {
|
||||||
|
|
@ -815,9 +816,13 @@ export class FolkRApp extends FolkShape {
|
||||||
// Register for enabled-state broadcasts
|
// Register for enabled-state broadcasts
|
||||||
FolkRApp.#instances.add(this);
|
FolkRApp.#instances.add(this);
|
||||||
|
|
||||||
// Load content
|
// Defer heavy content loading until shape enters viewport
|
||||||
if (this.#moduleId) {
|
if (this.#moduleId) {
|
||||||
this.#renderContent();
|
if (this.hasBeenVisible) {
|
||||||
|
this.#renderContent();
|
||||||
|
} else {
|
||||||
|
this.#showLoading();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.#showPicker();
|
this.#showPicker();
|
||||||
}
|
}
|
||||||
|
|
@ -825,8 +830,8 @@ export class FolkRApp extends FolkShape {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
super.disconnectedCallback?.();
|
super.disconnectedCallback();
|
||||||
FolkRApp.#instances.delete(this);
|
FolkRApp.#instances.delete(this);
|
||||||
if (this.#messageHandler) {
|
if (this.#messageHandler) {
|
||||||
window.removeEventListener("message", this.#messageHandler);
|
window.removeEventListener("message", this.#messageHandler);
|
||||||
|
|
@ -838,6 +843,23 @@ export class FolkRApp extends FolkShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override onBecameVisible(): void {
|
||||||
|
if (this.#moduleId && !this.#contentLoaded) {
|
||||||
|
this.#renderContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#showLoading() {
|
||||||
|
if (!this.#contentEl) return;
|
||||||
|
const meta = MODULE_META[this.#moduleId];
|
||||||
|
this.#contentEl.innerHTML = `
|
||||||
|
<div class="rapp-loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>${meta?.name || this.#moduleId}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
#buildSwitcher(switcherEl: HTMLElement) {
|
#buildSwitcher(switcherEl: HTMLElement) {
|
||||||
const enabledSet = FolkRApp.enabledModuleIds
|
const enabledSet = FolkRApp.enabledModuleIds
|
||||||
?? ((window as any).__rspaceEnabledModules ? new Set((window as any).__rspaceEnabledModules as string[]) : null);
|
?? ((window as any).__rspaceEnabledModules ? new Set((window as any).__rspaceEnabledModules as string[]) : null);
|
||||||
|
|
@ -955,16 +977,9 @@ export class FolkRApp extends FolkShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Derive the base module URL path, accounting for subdomain routing */
|
/** Derive the base module URL path — spaces are always subdomains */
|
||||||
#getModulePath(): string {
|
#getModulePath(): string {
|
||||||
if (!this.#spaceSlug) {
|
return `/${this.#moduleId}`;
|
||||||
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
|
||||||
if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
|
|
||||||
}
|
|
||||||
const space = this.#spaceSlug || "demo";
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const onSubdomain = hostname.split(".").length >= 3 && hostname.startsWith(space + ".");
|
|
||||||
return onSubdomain ? `/${this.#moduleId}` : `/${space}/${this.#moduleId}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if this shape's module is currently disabled */
|
/** Check if this shape's module is currently disabled */
|
||||||
|
|
@ -1014,6 +1029,8 @@ export class FolkRApp extends FolkShape {
|
||||||
#renderContent() {
|
#renderContent() {
|
||||||
if (!this.#contentEl || !this.#moduleId) return;
|
if (!this.#contentEl || !this.#moduleId) return;
|
||||||
|
|
||||||
|
this.#contentLoaded = true;
|
||||||
|
|
||||||
// Block rendering if module is disabled
|
// Block rendering if module is disabled
|
||||||
if (this.#isModuleDisabled()) { this.#showDisabled(); return; }
|
if (this.#isModuleDisabled()) { this.#showDisabled(); return; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,32 @@ export class FolkShape extends FolkElement {
|
||||||
static tagName = "folk-shape";
|
static tagName = "folk-shape";
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
|
// ── Viewport-based lazy loading ──
|
||||||
|
static #visibilityObserver: IntersectionObserver | null = null;
|
||||||
|
static #ensureObserver(): IntersectionObserver {
|
||||||
|
if (!FolkShape.#visibilityObserver) {
|
||||||
|
FolkShape.#visibilityObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const shape = entry.target as FolkShape;
|
||||||
|
if (entry.isIntersecting && !shape.#hasBeenVisible) {
|
||||||
|
shape.#hasBeenVisible = true;
|
||||||
|
shape.onBecameVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '500px' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FolkShape.#visibilityObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hasBeenVisible = false;
|
||||||
|
get hasBeenVisible(): boolean { return this.#hasBeenVisible; }
|
||||||
|
|
||||||
|
/** Called once when shape first enters viewport (+500px margin). Override to defer heavy init. */
|
||||||
|
protected onBecameVisible(): void {}
|
||||||
|
|
||||||
#internals = this.attachInternals();
|
#internals = this.attachInternals();
|
||||||
#attrWidth: Dimension = 0;
|
#attrWidth: Dimension = 0;
|
||||||
#attrHeight: Dimension = 0;
|
#attrHeight: Dimension = 0;
|
||||||
|
|
@ -466,6 +492,16 @@ export class FolkShape extends FolkElement {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
FolkShape.#ensureObserver().observe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
FolkShape.#visibilityObserver?.unobserve(this);
|
||||||
|
}
|
||||||
|
|
||||||
getTransformDOMRect() {
|
getTransformDOMRect() {
|
||||||
return this.#readonlyRect;
|
return this.#readonlyRect;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -875,9 +875,13 @@ export class RStackIdentity extends HTMLElement {
|
||||||
<input class="input" id="auth-signin-username" type="text" placeholder="Username or email" autocomplete="username webauthn" maxlength="64" />
|
<input class="input" id="auth-signin-username" type="text" placeholder="Username or email" autocomplete="username webauthn" maxlength="64" />
|
||||||
`}
|
`}
|
||||||
<div class="actions actions--stack">
|
<div class="actions actions--stack">
|
||||||
${showPicker ? '' : `<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>`}
|
${showPicker ? '' : `
|
||||||
|
<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>
|
||||||
|
<button class="btn btn--outline" data-action="send-magic-link">📧 Send Magic Link</button>
|
||||||
|
`}
|
||||||
<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</button>
|
<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="magic-link-msg" style="display:none;margin-top:0.5rem;font-size:0.85rem;text-align:center"></div>
|
||||||
<div class="error" id="auth-error"></div>
|
<div class="error" id="auth-error"></div>
|
||||||
<div class="learn-more">Powered by <a href="https://ridentity.online" target="_blank" rel="noopener">EncryptID</a></div>
|
<div class="learn-more">Powered by <a href="https://ridentity.online" target="_blank" rel="noopener">EncryptID</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -890,6 +894,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
<h2>Create your EncryptID</h2>
|
<h2>Create your EncryptID</h2>
|
||||||
<p>Set up a secure, passwordless identity.</p>
|
<p>Set up a secure, passwordless identity.</p>
|
||||||
<input class="input" id="auth-username" type="text" placeholder="Choose a username" autocomplete="username webauthn" maxlength="32" />
|
<input class="input" id="auth-username" type="text" placeholder="Choose a username" autocomplete="username webauthn" maxlength="32" />
|
||||||
|
<input class="input" id="auth-email" type="email" placeholder="Email (optional — for account recovery)" autocomplete="email" maxlength="128" style="margin-top:-0.5rem" />
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn--secondary" data-action="switch-signin">Back</button>
|
<button class="btn btn--secondary" data-action="switch-signin">Back</button>
|
||||||
<button class="btn btn--primary" data-action="register">🔐 Create Passkey</button>
|
<button class="btn btn--primary" data-action="register">🔐 Create Passkey</button>
|
||||||
|
|
@ -921,10 +926,13 @@ export class RStackIdentity extends HTMLElement {
|
||||||
if (pickerBtn) { pickerBtn.style.opacity = "0.6"; pickerBtn.style.pointerEvents = "none"; const arrow = pickerBtn.querySelector(".account-pick-arrow"); if (arrow) arrow.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px"></span>'; }
|
if (pickerBtn) { pickerBtn.style.opacity = "0.6"; pickerBtn.style.pointerEvents = "none"; const arrow = pickerBtn.querySelector(".account-pick-arrow"); if (arrow) arrow.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px"></span>'; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Send as email or username depending on input format
|
||||||
|
const isEmail = loginUsername.includes("@");
|
||||||
|
const authBody = isEmail ? { email: loginUsername } : { username: loginUsername };
|
||||||
const startRes = await fetch(`${ENCRYPTID_URL}/api/auth/start`, {
|
const startRes = await fetch(`${ENCRYPTID_URL}/api/auth/start`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: loginUsername }),
|
body: JSON.stringify(authBody),
|
||||||
});
|
});
|
||||||
if (!startRes.ok) throw new Error("Failed to start authentication");
|
if (!startRes.ok) throw new Error("Failed to start authentication");
|
||||||
const { options: serverOptions, userFound } = await startRes.json();
|
const { options: serverOptions, userFound } = await startRes.json();
|
||||||
|
|
@ -981,11 +989,40 @@ export class RStackIdentity extends HTMLElement {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMagicLink = async () => {
|
||||||
|
const input = overlay.querySelector("#auth-signin-username") as HTMLInputElement | null;
|
||||||
|
const msgEl = overlay.querySelector("#magic-link-msg") as HTMLElement | null;
|
||||||
|
const errEl = overlay.querySelector("#auth-error") as HTMLElement | null;
|
||||||
|
const value = input?.value.trim() || "";
|
||||||
|
if (!value || !value.includes("@")) {
|
||||||
|
if (errEl) errEl.textContent = "Enter an email address to receive a magic link.";
|
||||||
|
input?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errEl) errEl.textContent = "";
|
||||||
|
const btn = overlay.querySelector('[data-action="send-magic-link"]') as HTMLButtonElement | null;
|
||||||
|
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Sending...'; }
|
||||||
|
try {
|
||||||
|
await fetch(`${ENCRYPTID_URL}/api/auth/magic-link`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email: value }),
|
||||||
|
});
|
||||||
|
if (msgEl) { msgEl.style.display = ""; msgEl.style.color = "#22c55e"; msgEl.textContent = "Login link sent! Check your inbox."; }
|
||||||
|
} catch {
|
||||||
|
if (msgEl) { msgEl.style.display = ""; msgEl.style.color = "#f87171"; msgEl.textContent = "Failed to send. Try again."; }
|
||||||
|
} finally {
|
||||||
|
if (btn) { btn.disabled = false; btn.innerHTML = "📧 Send Magic Link"; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
const usernameInput = overlay.querySelector("#auth-username") as HTMLInputElement;
|
const usernameInput = overlay.querySelector("#auth-username") as HTMLInputElement;
|
||||||
|
const emailInput = overlay.querySelector("#auth-email") as HTMLInputElement | null;
|
||||||
const errEl = overlay.querySelector("#auth-error") as HTMLElement;
|
const errEl = overlay.querySelector("#auth-error") as HTMLElement;
|
||||||
const btn = overlay.querySelector('[data-action="register"]') as HTMLButtonElement;
|
const btn = overlay.querySelector('[data-action="register"]') as HTMLButtonElement;
|
||||||
const username = usernameInput.value.trim();
|
const username = usernameInput.value.trim();
|
||||||
|
const email = emailInput?.value.trim() || "";
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
errEl.textContent = "Please enter a username.";
|
errEl.textContent = "Please enter a username.";
|
||||||
|
|
@ -1037,6 +1074,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
},
|
},
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
|
...(email ? { email } : {}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await completeRes.json().catch(() => null);
|
const data = await completeRes.json().catch(() => null);
|
||||||
|
|
@ -1062,6 +1100,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
callbacks?.onCancel?.();
|
callbacks?.onCancel?.();
|
||||||
});
|
});
|
||||||
overlay.querySelector('[data-action="signin"]')?.addEventListener("click", handleSignIn);
|
overlay.querySelector('[data-action="signin"]')?.addEventListener("click", handleSignIn);
|
||||||
|
overlay.querySelector('[data-action="send-magic-link"]')?.addEventListener("click", handleMagicLink);
|
||||||
overlay.querySelector('[data-action="register"]')?.addEventListener("click", handleRegister);
|
overlay.querySelector('[data-action="register"]')?.addEventListener("click", handleRegister);
|
||||||
overlay.querySelector('[data-action="switch-register"]')?.addEventListener("click", () => {
|
overlay.querySelector('[data-action="switch-register"]')?.addEventListener("click", () => {
|
||||||
mode = "register";
|
mode = "register";
|
||||||
|
|
@ -1099,6 +1138,13 @@ export class RStackIdentity extends HTMLElement {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
|
overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
|
||||||
|
if ((e as KeyboardEvent).key === "Enter") {
|
||||||
|
// Tab to email field if empty, otherwise register
|
||||||
|
const emailInput = overlay.querySelector("#auth-email") as HTMLInputElement | null;
|
||||||
|
if (emailInput && !emailInput.value.trim()) { emailInput.focus(); } else { handleRegister(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
overlay.querySelector("#auth-email")?.addEventListener("keydown", (e) => {
|
||||||
if ((e as KeyboardEvent).key === "Enter") handleRegister();
|
if ((e as KeyboardEvent).key === "Enter") handleRegister();
|
||||||
});
|
});
|
||||||
overlay.querySelector("#auth-signin-username")?.addEventListener("keydown", (e) => {
|
overlay.querySelector("#auth-signin-username")?.addEventListener("keydown", (e) => {
|
||||||
|
|
|
||||||
|
|
@ -2563,6 +2563,9 @@
|
||||||
window.__communitySync?.disconnect?.();
|
window.__communitySync?.disconnect?.();
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-sync DID for forgotten-shape filtering after auth changes
|
||||||
|
window.__communitySync?.setLocalDID?.(getLocalDID());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load module list for app switcher and tab bar + menu
|
// Load module list for app switcher and tab bar + menu
|
||||||
|
|
@ -3217,6 +3220,7 @@
|
||||||
// Initialize offline store and CommunitySync
|
// Initialize offline store and CommunitySync
|
||||||
const offlineStore = new OfflineStore();
|
const offlineStore = new OfflineStore();
|
||||||
const sync = new CommunitySync(communitySlug, offlineStore);
|
const sync = new CommunitySync(communitySlug, offlineStore);
|
||||||
|
sync.setLocalDID(getLocalDID());
|
||||||
window.__communitySync = sync;
|
window.__communitySync = sync;
|
||||||
|
|
||||||
// Wire history panel to CommunitySync doc
|
// Wire history panel to CommunitySync doc
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue