aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-06 10:06:53 -0800
committerAdam Soltys <[email protected]>2026-03-06 10:06:53 -0800
commit47316e28150953909c71adfd1e9100c4e08024b1 (patch)
tree5f383db13a8145fac312b53199d01174386aed61 /src/lib
parent7e45e1b48c382f29c2d3bd114a4751a0f6b89d51 (diff)
downloadcoinos-ui-47316e28150953909c71adfd1e9100c4e08024b1.tar.gz
coinos-ui-47316e28150953909c71adfd1e9100c4e08024b1.zip
chat fixes
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/marmot.ts37
-rw-r--r--src/lib/messaging.ts7
-rw-r--r--src/lib/mls-relay.ts66
3 files changed, 73 insertions, 37 deletions
diff --git a/src/lib/marmot.ts b/src/lib/marmot.ts
index ed2143c3..2eecbdb1 100644
--- a/src/lib/marmot.ts
+++ b/src/lib/marmot.ts
@@ -164,31 +164,30 @@ class CoinosNostrNetwork implements NostrNetworkInterface {
// IndexedDB-backed KeyValueStoreBackend
// ---------------------------------------------------------------------------
-const IDB_NAME = "coinos-marmot";
const IDB_VERSION = 1;
+const STORE_NAMES = ["group-state", "key-packages", "invites-received", "invites-unread", "invites-seen"];
+
+let _idbPubkey: string | null = null;
+let _idbCache: IDBDatabase | null = null;
+
+function openMarmotDb(pubkey?: string): Promise<IDBDatabase> {
+ const pk = pubkey || _idbPubkey || "default";
+ if (_idbCache && _idbPubkey === pk) return Promise.resolve(_idbCache);
-function openMarmotDb(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
- const req = indexedDB.open(IDB_NAME, IDB_VERSION);
+ const dbName = `coinos-marmot-${pk.slice(0, 16)}`;
+ const req = indexedDB.open(dbName, IDB_VERSION);
req.onupgradeneeded = () => {
const db = req.result;
- if (!db.objectStoreNames.contains("group-state")) {
- db.createObjectStore("group-state");
- }
- if (!db.objectStoreNames.contains("key-packages")) {
- db.createObjectStore("key-packages");
- }
- if (!db.objectStoreNames.contains("invites-received")) {
- db.createObjectStore("invites-received");
- }
- if (!db.objectStoreNames.contains("invites-unread")) {
- db.createObjectStore("invites-unread");
- }
- if (!db.objectStoreNames.contains("invites-seen")) {
- db.createObjectStore("invites-seen");
+ for (const name of STORE_NAMES) {
+ if (!db.objectStoreNames.contains(name)) db.createObjectStore(name);
}
};
- req.onsuccess = () => resolve(req.result);
+ req.onsuccess = () => {
+ _idbPubkey = pk;
+ _idbCache = req.result;
+ resolve(req.result);
+ };
req.onerror = () => reject(req.error);
});
}
@@ -316,6 +315,8 @@ export function getMarmotClient(pubkey: string): MarmotClient {
_client = null;
_inviteReader = null;
_currentPubkey = pubkey;
+ _idbPubkey = pubkey;
+ _idbCache = null;
const signer = new CoinosEventSigner(pubkey);
const groupStateBackend = new IDBGroupStateBackend();
diff --git a/src/lib/messaging.ts b/src/lib/messaging.ts
index 37a4c22e..ae91709c 100644
--- a/src/lib/messaging.ts
+++ b/src/lib/messaging.ts
@@ -68,10 +68,5 @@ export async function resolveUser(pubkey: string): Promise<any> {
/** Get a display name for a user info object. */
export function displayName(info: any): string {
- return info?.coinosUsername || info?.nostrName || shortPubkey(info?.pubkey || "");
-}
-
-function shortPubkey(pk: string): string {
- if (pk.length <= 16) return pk;
- return pk.slice(0, 8) + "…" + pk.slice(-4);
+ return info?.coinosUsername || info?.nostrName || "Anonymous";
}
diff --git a/src/lib/mls-relay.ts b/src/lib/mls-relay.ts
index 6a280a3e..c8ac2241 100644
--- a/src/lib/mls-relay.ts
+++ b/src/lib/mls-relay.ts
@@ -11,22 +11,19 @@ import { getMarmotClient, getInviteReader, DM_RELAYS } from "$lib/marmot";
const pool = new SimplePool();
-/** Extract member pubkeys from an MLS group's ratchet tree. */
-function getGroupMembers(group: any): string[] {
- const state = group.state;
+/** Extract member pubkeys from an MLS group's ratchet tree (pass group.state). */
+function getGroupMembers(state: any): string[] {
if (!state?.ratchetTree) return [];
const pubkeys = new Set<string>();
for (const node of state.ratchetTree) {
- if (node?.nodeType === 1 && node.leaf?.credential?.credentialType === 0) {
- // Basic credential: identity is the pubkey bytes
- const identity = node.leaf.credential.identity;
- if (identity) {
- const hex = typeof identity === "string"
- ? identity
- : bytesToHex(new Uint8Array(identity));
- if (hex.length === 64) pubkeys.add(hex);
- }
- }
+ if (!node?.leaf?.credential?.identity) continue;
+ const identity = node.leaf.credential.identity;
+ const hex = identity instanceof Uint8Array
+ ? bytesToHex(identity)
+ : typeof identity === "string" ? identity
+ : bytesToHex(new Uint8Array(identity));
+ // identity may be 32 raw bytes (-> 64 hex) or already 64 hex chars
+ if (hex.length === 64) pubkeys.add(hex);
}
return Array.from(pubkeys);
}
@@ -307,6 +304,30 @@ export async function subscribeToMessages(
}
});
+ // Fetch historical welcomes so we join groups created while we were offline
+ try {
+ const historicalWraps = await pool.querySync(relays, {
+ kinds: [1059],
+ "#p": [user.pubkey],
+ limit: 100,
+ });
+ for (const event of historicalWraps) {
+ try {
+ const ingested = await inviteReader.ingestEvent(event as NostrEvent);
+ if (ingested) await inviteReader.decryptGiftWraps();
+ } catch {}
+ }
+ // Re-load groups after processing welcomes
+ await client.loadAllGroups();
+ for (const g of client.groups) {
+ registerGroupIds(g);
+ const r = g.relays;
+ if (r) r.forEach((url: string) => allRelayUrls.add(url));
+ }
+ } catch (e) {
+ console.error("[mls] Failed to fetch historical welcomes:", e);
+ }
+
for (const url of allRelayUrls) {
try {
const relay = await Relay.connect(url);
@@ -444,6 +465,25 @@ async function handleGroupMessageEvent(
export async function fetchGroupHistory(user: any, relays: string[] = DM_RELAYS): Promise<void> {
const client = getMarmotClient(user.pubkey);
+ const inviteReader = getInviteReader(user.pubkey);
+
+ // Process any pending welcomes first so we know about all our groups
+ try {
+ const wraps = await pool.querySync(relays, {
+ kinds: [1059],
+ "#p": [user.pubkey],
+ limit: 100,
+ });
+ for (const event of wraps) {
+ try {
+ const ingested = await inviteReader.ingestEvent(event as NostrEvent);
+ if (ingested) await inviteReader.decryptGiftWraps();
+ } catch {}
+ }
+ } catch (e) {
+ console.error("[mls] Failed to fetch welcomes in history:", e);
+ }
+
await client.loadAllGroups();
const groups = client.groups;