Merge branch 'dev'
This commit is contained in:
commit
7f88d4f0bc
|
|
@ -161,6 +161,7 @@ export class CommunitySync extends EventTarget {
|
||||||
#offlineStore: OfflineStore | null = null;
|
#offlineStore: OfflineStore | null = null;
|
||||||
#saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
#saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
#initialSyncFired = false;
|
||||||
#wsUrl: string | null = null;
|
#wsUrl: string | null = null;
|
||||||
|
|
||||||
// ── Undo/Redo state ──
|
// ── Undo/Redo state ──
|
||||||
|
|
@ -292,6 +293,7 @@ export class CommunitySync extends EventTarget {
|
||||||
|
|
||||||
this.#ws.onclose = () => {
|
this.#ws.onclose = () => {
|
||||||
console.log(`[CommunitySync] Disconnected from ${this.#communitySlug}`);
|
console.log(`[CommunitySync] Disconnected from ${this.#communitySlug}`);
|
||||||
|
this.#initialSyncFired = false;
|
||||||
this.dispatchEvent(new CustomEvent("disconnected"));
|
this.dispatchEvent(new CustomEvent("disconnected"));
|
||||||
|
|
||||||
if (!this.#disconnectedIntentionally) {
|
if (!this.#disconnectedIntentionally) {
|
||||||
|
|
@ -844,10 +846,12 @@ export class CommunitySync extends EventTarget {
|
||||||
|
|
||||||
// Debounce the synced event — during initial sync negotiation, #applyDocToDOM()
|
// Debounce the synced event — during initial sync negotiation, #applyDocToDOM()
|
||||||
// is called for every Automerge sync message (100+ round-trips). Debounce to
|
// is called for every Automerge sync message (100+ round-trips). Debounce to
|
||||||
// fire once after the burst settles.
|
// fire once after the burst settles. Only fires once per connection cycle.
|
||||||
|
if (this.#initialSyncFired) return;
|
||||||
if (this.#syncedDebounceTimer) clearTimeout(this.#syncedDebounceTimer);
|
if (this.#syncedDebounceTimer) clearTimeout(this.#syncedDebounceTimer);
|
||||||
this.#syncedDebounceTimer = setTimeout(() => {
|
this.#syncedDebounceTimer = setTimeout(() => {
|
||||||
this.#syncedDebounceTimer = null;
|
this.#syncedDebounceTimer = null;
|
||||||
|
this.#initialSyncFired = true;
|
||||||
this.dispatchEvent(new CustomEvent("synced", { detail: { shapes } }));
|
this.dispatchEvent(new CustomEvent("synced", { detail: { shapes } }));
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -711,7 +711,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
let openSection: string | null = null;
|
let openSection: string | null = null;
|
||||||
|
|
||||||
// Account completion status
|
// Account completion status
|
||||||
let acctStatus: { email: boolean; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
|
let acctStatus: { email: boolean; emailAddress?: string | null; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
|
||||||
|
|
||||||
// Lazy-loaded data
|
// Lazy-loaded data
|
||||||
let guardians: { id: string; name: string; email?: string; status: string }[] = [];
|
let guardians: { id: string; name: string; email?: string; status: string }[] = [];
|
||||||
|
|
@ -819,10 +819,13 @@ export class RStackIdentity extends HTMLElement {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const emailDisplay = !isOpen && done && acctStatus?.emailAddress
|
||||||
|
? `<span style="color:var(--rs-text-secondary);font-size:0.85rem;margin-left:8px">${acctStatus.emailAddress}</span>`
|
||||||
|
: "";
|
||||||
return `
|
return `
|
||||||
<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
|
<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
|
||||||
<div class="account-section-header" data-section="email">
|
<div class="account-section-header" data-section="email">
|
||||||
<span>${statusDot(done)} ✉️ Email</span>
|
<span>${statusDot(done)} ✉️ Email${emailDisplay}</span>
|
||||||
<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
|
<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
|
||||||
</div>
|
</div>
|
||||||
${body}
|
${body}
|
||||||
|
|
@ -1087,7 +1090,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
body: JSON.stringify({ email: emailAddr, code }),
|
body: JSON.stringify({ email: emailAddr, code }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed");
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed");
|
||||||
if (acctStatus) acctStatus.email = true;
|
if (acctStatus) { acctStatus.email = true; acctStatus.emailAddress = emailAddr; }
|
||||||
openSection = null; render();
|
openSection = null; render();
|
||||||
this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } }));
|
this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ export async function cleanExpiredChallenges(): Promise<number> {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function setUserEmail(userId: string, email: string): Promise<void> {
|
export async function setUserEmail(userId: string, email: string): Promise<void> {
|
||||||
await sql`UPDATE users SET email = ${email} WHERE id = ${userId}`;
|
await sql`UPDATE users SET email = ${email}, profile_email = ${email}, updated_at = NOW() WHERE id = ${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserByEmail(email: string) {
|
export async function getUserByEmail(email: string) {
|
||||||
|
|
|
||||||
|
|
@ -3088,10 +3088,18 @@
|
||||||
let _tripCache = null; // { trips: [], detail: null, fetchedAt: 0 }
|
let _tripCache = null; // { trips: [], detail: null, fetchedAt: 0 }
|
||||||
const TRIP_CACHE_TTL = 60000; // 1 minute
|
const TRIP_CACHE_TTL = 60000; // 1 minute
|
||||||
|
|
||||||
|
function _authHeaders() {
|
||||||
|
try {
|
||||||
|
const s = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
|
||||||
|
if (s?.accessToken) return { 'Authorization': 'Bearer ' + s.accessToken };
|
||||||
|
} catch {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchTripData() {
|
async function fetchTripData() {
|
||||||
if (_tripCache && Date.now() - _tripCache.fetchedAt < TRIP_CACHE_TTL) return _tripCache;
|
if (_tripCache && Date.now() - _tripCache.fetchedAt < TRIP_CACHE_TTL) return _tripCache;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${communitySlug}/rtrips/api/trips`);
|
const res = await fetch(`/${communitySlug}/rtrips/api/trips`, { headers: _authHeaders() });
|
||||||
if (!res.ok) return { trips: [], detail: null, fetchedAt: Date.now() };
|
if (!res.ok) return { trips: [], detail: null, fetchedAt: Date.now() };
|
||||||
const trips = await res.json();
|
const trips = await res.json();
|
||||||
_tripCache = { trips, detail: null, fetchedAt: Date.now() };
|
_tripCache = { trips, detail: null, fetchedAt: Date.now() };
|
||||||
|
|
@ -3101,7 +3109,7 @@
|
||||||
|
|
||||||
async function fetchTripDetail(tripId) {
|
async function fetchTripDetail(tripId) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`);
|
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`, { headers: _authHeaders() });
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
|
|
@ -3145,7 +3153,7 @@
|
||||||
async function fetchNotesData() {
|
async function fetchNotesData() {
|
||||||
if (_notesCache && Date.now() - _notesCache.fetchedAt < TRIP_CACHE_TTL) return _notesCache;
|
if (_notesCache && Date.now() - _notesCache.fetchedAt < TRIP_CACHE_TTL) return _notesCache;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`);
|
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`, { headers: _authHeaders() });
|
||||||
if (!res.ok) return { notes: [], fetchedAt: Date.now() };
|
if (!res.ok) return { notes: [], fetchedAt: Date.now() };
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
_notesCache = { notes: data.notes || [], fetchedAt: Date.now() };
|
_notesCache = { notes: data.notes || [], fetchedAt: Date.now() };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue