@howler/cli 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +216 -121
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -841,6 +841,28 @@ import {
|
|
|
841
841
|
SessionCipher,
|
|
842
842
|
SignalProtocolAddress
|
|
843
843
|
} from "@privacyresearch/libsignal-protocol-typescript";
|
|
844
|
+
async function withDecryptSessionLock(sessionKey, work) {
|
|
845
|
+
const previous = decryptSessionLocks.get(sessionKey) ?? Promise.resolve();
|
|
846
|
+
let release;
|
|
847
|
+
const current = new Promise((resolve) => {
|
|
848
|
+
release = resolve;
|
|
849
|
+
});
|
|
850
|
+
decryptSessionLocks.set(sessionKey, previous.then(() => current));
|
|
851
|
+
await previous;
|
|
852
|
+
try {
|
|
853
|
+
return await work();
|
|
854
|
+
} finally {
|
|
855
|
+
release();
|
|
856
|
+
const active = decryptSessionLocks.get(sessionKey);
|
|
857
|
+
if (active) {
|
|
858
|
+
void active.finally(() => {
|
|
859
|
+
if (decryptSessionLocks.get(sessionKey) === active) {
|
|
860
|
+
decryptSessionLocks.delete(sessionKey);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
844
866
|
async function getOrCreateSession(recipientUserId, deviceId = 1) {
|
|
845
867
|
const address = new SignalProtocolAddress(recipientUserId, deviceId);
|
|
846
868
|
const addressKey = `${recipientUserId}.${deviceId}`;
|
|
@@ -899,35 +921,37 @@ async function encryptForRecipient(data, recipientUserId, deviceId = 1) {
|
|
|
899
921
|
async function decryptFromSender(ciphertext, senderUserId, deviceId = 1) {
|
|
900
922
|
const address = new SignalProtocolAddress(senderUserId, deviceId);
|
|
901
923
|
const addressKey = `${senderUserId}.${deviceId}`;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
924
|
+
return withDecryptSessionLock(addressKey, async () => {
|
|
925
|
+
const existingSession = await signalStore.loadSession(addressKey);
|
|
926
|
+
if (define_import_meta_env_default.DEV) console.log(`[Signal] Decrypting from ${senderUserId.slice(0, 8)}.${deviceId}, type: ${ciphertext.type}, hasSession: ${!!existingSession}`);
|
|
927
|
+
const sessionCipher = new SessionCipher(signalStore, address);
|
|
928
|
+
const binaryBody = base64ToBinaryString(ciphertext.body);
|
|
929
|
+
let plaintext;
|
|
930
|
+
try {
|
|
931
|
+
if (ciphertext.type === 3) {
|
|
932
|
+
if (define_import_meta_env_default.DEV) console.log("[Signal] Decrypting PreKeyWhisperMessage (type 3) - will establish session");
|
|
933
|
+
plaintext = await sessionCipher.decryptPreKeyWhisperMessage(binaryBody, "binary");
|
|
934
|
+
} else {
|
|
935
|
+
if (!existingSession) {
|
|
936
|
+
console.error("[Signal] No existing session for WhisperMessage (type 1) - this will fail");
|
|
937
|
+
}
|
|
938
|
+
plaintext = await sessionCipher.decryptWhisperMessage(binaryBody, "binary");
|
|
914
939
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
940
|
+
} catch (err) {
|
|
941
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
942
|
+
console.error(`[Signal] Decryption failed:`, {
|
|
943
|
+
error: errorMsg,
|
|
944
|
+
sender: `${senderUserId.slice(0, 8)}.${deviceId}`,
|
|
945
|
+
messageType: ciphertext.type,
|
|
946
|
+
hadSession: !!existingSession
|
|
947
|
+
});
|
|
948
|
+
if (errorMsg.includes("Bad MAC") && existingSession) {
|
|
949
|
+
console.error("[Signal] Bad MAC with existing session - session may be corrupted");
|
|
950
|
+
}
|
|
951
|
+
throw err;
|
|
927
952
|
}
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
return plaintext;
|
|
953
|
+
return plaintext;
|
|
954
|
+
});
|
|
931
955
|
}
|
|
932
956
|
async function encryptMessage(message, recipientUserId) {
|
|
933
957
|
const encoder = new TextEncoder();
|
|
@@ -949,12 +973,14 @@ async function resetSessionWith(userId) {
|
|
|
949
973
|
await signalStore.removeAllSessions(userId);
|
|
950
974
|
if (define_import_meta_env_default.DEV) console.log("[Signal] Session reset complete");
|
|
951
975
|
}
|
|
976
|
+
var decryptSessionLocks;
|
|
952
977
|
var init_crypto = __esm({
|
|
953
978
|
"../../packages/core/src/lib/signal/crypto.ts"() {
|
|
954
979
|
"use strict";
|
|
955
980
|
init_define_import_meta_env();
|
|
956
981
|
init_store();
|
|
957
982
|
init_supabaseKeys();
|
|
983
|
+
decryptSessionLocks = /* @__PURE__ */ new Map();
|
|
958
984
|
}
|
|
959
985
|
});
|
|
960
986
|
|
|
@@ -2244,6 +2270,7 @@ __export(messages_exports, {
|
|
|
2244
2270
|
clearDecryptedAudioCache: () => clearDecryptedAudioCache,
|
|
2245
2271
|
connectHowlerToParent: () => connectHowlerToParent,
|
|
2246
2272
|
decryptTextOnlyTranscript: () => decryptTextOnlyTranscript,
|
|
2273
|
+
decryptTextOnlyTranscriptBatch: () => decryptTextOnlyTranscriptBatch,
|
|
2247
2274
|
getBlobUrlCount: () => getBlobUrlCount,
|
|
2248
2275
|
getConversation: () => getConversation,
|
|
2249
2276
|
getConversations: () => getConversations,
|
|
@@ -2824,41 +2851,45 @@ async function getDecryptedTranscript(howlerId) {
|
|
|
2824
2851
|
const { getTranscriptCacheProvider: getTranscriptCacheProvider2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
2825
2852
|
return getTranscriptCacheProvider2().getCachedTranscript(howlerId);
|
|
2826
2853
|
}
|
|
2827
|
-
|
|
2828
|
-
const { getTranscriptCacheProvider: getTranscriptCacheProvider2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
2829
|
-
const provider = getTranscriptCacheProvider2();
|
|
2830
|
-
const cached = await provider.getCachedTranscript(howlerId);
|
|
2831
|
-
if (cached) return cached;
|
|
2832
|
-
const { data: howler, error } = await db.from("howlers").select("id, sender_id, transcript, encryption_metadata").eq("id", howlerId).single();
|
|
2833
|
-
if (error || !howler) return null;
|
|
2834
|
-
const meta = parseEncryptionMetadata(howler.encryption_metadata);
|
|
2835
|
-
if (!meta?.transcriptEncrypted || !meta.transcriptIv) return null;
|
|
2836
|
-
if (!howler.transcript || typeof howler.transcript !== "string") return null;
|
|
2837
|
-
const { data: { user } } = await db.auth.getUser();
|
|
2838
|
-
if (!user) return null;
|
|
2839
|
-
const { getLocalDeviceId: getLocalDeviceId2 } = await Promise.resolve().then(() => (init_device(), device_exports));
|
|
2840
|
-
const myDeviceId = getLocalDeviceId2();
|
|
2841
|
-
if (!myDeviceId) return null;
|
|
2854
|
+
function resolveTranscriptDecryptionTarget(howler, meta, currentUserId, myDeviceId) {
|
|
2842
2855
|
let keyToDecrypt = meta.encryptedKey;
|
|
2843
2856
|
let sessionUserId = howler.sender_id;
|
|
2844
2857
|
const sessionDeviceId = meta.senderDeviceId ?? 1;
|
|
2845
2858
|
if (meta.encryptedKeys && meta.encryptedKeys.length > 0) {
|
|
2846
2859
|
const myKey = meta.encryptedKeys.find(
|
|
2847
|
-
(k) => k.userId ===
|
|
2860
|
+
(k) => k.userId === currentUserId && k.deviceId === myDeviceId
|
|
2848
2861
|
);
|
|
2849
2862
|
if (!myKey) {
|
|
2850
2863
|
const deviceList = meta.encryptedKeys.map(
|
|
2851
2864
|
(k) => `${k.userId.slice(0, 8)}.${k.deviceId}`
|
|
2852
2865
|
).join(", ");
|
|
2853
|
-
console.warn(`[E2E] No key for device ${myDeviceId} in [${deviceList}] \u2014 message ${
|
|
2866
|
+
console.warn(`[E2E] No key for device ${myDeviceId} in [${deviceList}] \u2014 message ${howler.id.slice(0, 8)}`);
|
|
2854
2867
|
return null;
|
|
2855
2868
|
}
|
|
2856
2869
|
keyToDecrypt = { type: myKey.type, body: myKey.body, registrationId: myKey.registrationId };
|
|
2857
|
-
sessionUserId =
|
|
2870
|
+
sessionUserId = currentUserId === howler.sender_id ? currentUserId : howler.sender_id;
|
|
2858
2871
|
}
|
|
2872
|
+
return {
|
|
2873
|
+
encryptedKey: keyToDecrypt,
|
|
2874
|
+
sessionUserId,
|
|
2875
|
+
sessionDeviceId
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
async function decryptTextOnlyTranscriptRow(howler, currentUserId, myDeviceId, provider) {
|
|
2879
|
+
const cached = await provider.getCachedTranscript(howler.id);
|
|
2880
|
+
if (cached) return cached;
|
|
2881
|
+
const meta = parseEncryptionMetadata(howler.encryption_metadata);
|
|
2882
|
+
if (!meta?.transcriptEncrypted || !meta.transcriptIv) return null;
|
|
2883
|
+
if (!howler.transcript || typeof howler.transcript !== "string") return null;
|
|
2884
|
+
const decryptionTarget = resolveTranscriptDecryptionTarget(howler, meta, currentUserId, myDeviceId);
|
|
2885
|
+
if (!decryptionTarget) return null;
|
|
2859
2886
|
try {
|
|
2860
2887
|
const { decryptFromSender: decryptFromSender2, base64ToArrayBuffer: base64ToArrayBuffer5, decryptWithAES: decryptWithAES2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
|
|
2861
|
-
const keyBytes = await decryptFromSender2(
|
|
2888
|
+
const keyBytes = await decryptFromSender2(
|
|
2889
|
+
decryptionTarget.encryptedKey,
|
|
2890
|
+
decryptionTarget.sessionUserId,
|
|
2891
|
+
decryptionTarget.sessionDeviceId
|
|
2892
|
+
);
|
|
2862
2893
|
const aesKey = await crypto.subtle.importKey(
|
|
2863
2894
|
"raw",
|
|
2864
2895
|
keyBytes,
|
|
@@ -2871,14 +2902,46 @@ async function decryptTextOnlyTranscript(howlerId) {
|
|
|
2871
2902
|
const plaintext = await decryptWithAES2(ciphertext, aesKey, iv);
|
|
2872
2903
|
const json = new TextDecoder().decode(plaintext);
|
|
2873
2904
|
const transcript = JSON.parse(json);
|
|
2874
|
-
await provider.cacheTranscript(
|
|
2875
|
-
console.log("[E2E] Decrypted text-only transcript:",
|
|
2905
|
+
await provider.cacheTranscript(howler.id, transcript);
|
|
2906
|
+
console.log("[E2E] Decrypted text-only transcript:", howler.id);
|
|
2876
2907
|
return transcript;
|
|
2877
2908
|
} catch (err) {
|
|
2878
|
-
console.error("[E2E] Failed to decrypt text-only transcript:",
|
|
2909
|
+
console.error("[E2E] Failed to decrypt text-only transcript:", howler.id, err);
|
|
2879
2910
|
return null;
|
|
2880
2911
|
}
|
|
2881
2912
|
}
|
|
2913
|
+
async function decryptTextOnlyTranscriptBatch(messages) {
|
|
2914
|
+
if (messages.length === 0) return /* @__PURE__ */ new Map();
|
|
2915
|
+
const { getTranscriptCacheProvider: getTranscriptCacheProvider2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
2916
|
+
const provider = getTranscriptCacheProvider2();
|
|
2917
|
+
const { data: { user } } = await db.auth.getUser();
|
|
2918
|
+
if (!user) return /* @__PURE__ */ new Map();
|
|
2919
|
+
const { getLocalDeviceId: getLocalDeviceId2 } = await Promise.resolve().then(() => (init_device(), device_exports));
|
|
2920
|
+
const myDeviceId = getLocalDeviceId2();
|
|
2921
|
+
if (!myDeviceId) return /* @__PURE__ */ new Map();
|
|
2922
|
+
const decrypted = /* @__PURE__ */ new Map();
|
|
2923
|
+
for (const message of messages) {
|
|
2924
|
+
const transcript = await decryptTextOnlyTranscriptRow(
|
|
2925
|
+
message,
|
|
2926
|
+
user.id,
|
|
2927
|
+
myDeviceId,
|
|
2928
|
+
provider
|
|
2929
|
+
);
|
|
2930
|
+
if (transcript) {
|
|
2931
|
+
decrypted.set(message.id, transcript);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return decrypted;
|
|
2935
|
+
}
|
|
2936
|
+
async function decryptTextOnlyTranscript(howlerId) {
|
|
2937
|
+
const { getTranscriptCacheProvider: getTranscriptCacheProvider2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
2938
|
+
const cached = await getTranscriptCacheProvider2().getCachedTranscript(howlerId);
|
|
2939
|
+
if (cached) return cached;
|
|
2940
|
+
const { data: howler, error } = await db.from("howlers").select("id, sender_id, transcript, encryption_metadata").eq("id", howlerId).single();
|
|
2941
|
+
if (error || !howler) return null;
|
|
2942
|
+
const batch = await decryptTextOnlyTranscriptBatch([howler]);
|
|
2943
|
+
return batch.get(howlerId) ?? null;
|
|
2944
|
+
}
|
|
2882
2945
|
var db, PUBLIC_USER_COLUMNS, blobUrlCache, blobUrlCount;
|
|
2883
2946
|
var init_messages = __esm({
|
|
2884
2947
|
"../../packages/core/src/lib/messages/index.ts"() {
|
|
@@ -29637,21 +29700,6 @@ async function getGroupMessages(groupId, limit = 50, beforeTimestamp) {
|
|
|
29637
29700
|
const trimmed = messages.slice(0, limit).reverse();
|
|
29638
29701
|
return { messages: trimmed, hasMore };
|
|
29639
29702
|
}
|
|
29640
|
-
async function tryDecrypt(msg) {
|
|
29641
|
-
if (!msg.isEncrypted || msg.recording_url === "text://plain") return msg;
|
|
29642
|
-
try {
|
|
29643
|
-
const transcript = await decryptTextOnlyTranscript(msg.id);
|
|
29644
|
-
if (transcript && transcript.length > 0) {
|
|
29645
|
-
return {
|
|
29646
|
-
...msg,
|
|
29647
|
-
transcript: JSON.stringify(transcript)
|
|
29648
|
-
};
|
|
29649
|
-
}
|
|
29650
|
-
} catch (err) {
|
|
29651
|
-
console.error("[TUI] Decrypt failed:", msg.id, err);
|
|
29652
|
-
}
|
|
29653
|
-
return msg;
|
|
29654
|
-
}
|
|
29655
29703
|
function isCacheableTranscriptJson(transcript) {
|
|
29656
29704
|
if (!transcript) return false;
|
|
29657
29705
|
try {
|
|
@@ -29661,16 +29709,88 @@ function isCacheableTranscriptJson(transcript) {
|
|
|
29661
29709
|
return false;
|
|
29662
29710
|
}
|
|
29663
29711
|
}
|
|
29712
|
+
function shouldResolveTranscript(msg) {
|
|
29713
|
+
return msg.isEncrypted && msg.recording_url !== "text://plain";
|
|
29714
|
+
}
|
|
29664
29715
|
function useMessages(conversation, currentUserId) {
|
|
29665
29716
|
const [messages, setMessages] = useState3([]);
|
|
29666
29717
|
const [loading, setLoading] = useState3(!!conversation);
|
|
29667
29718
|
const [error, setError] = useState3(null);
|
|
29668
29719
|
const [hasMore, setHasMore] = useState3(false);
|
|
29669
|
-
const
|
|
29720
|
+
const resolvedIds = useRef3(/* @__PURE__ */ new Set());
|
|
29721
|
+
const inFlightIds = useRef3(/* @__PURE__ */ new Set());
|
|
29722
|
+
const activeConversationId = useRef3(conversation?.id ?? null);
|
|
29670
29723
|
const transcriptCache = useRef3(/* @__PURE__ */ new Map());
|
|
29671
29724
|
const rememberResolvedTranscript = useCallback2((msg) => {
|
|
29672
|
-
if (!isCacheableTranscriptJson(msg.transcript)) return;
|
|
29725
|
+
if (!isCacheableTranscriptJson(msg.transcript)) return false;
|
|
29673
29726
|
transcriptCache.current.set(msg.id, msg.transcript);
|
|
29727
|
+
resolvedIds.current.add(msg.id);
|
|
29728
|
+
inFlightIds.current.delete(msg.id);
|
|
29729
|
+
return true;
|
|
29730
|
+
}, []);
|
|
29731
|
+
const applyCachedTranscripts = useCallback2((nextMessages) => {
|
|
29732
|
+
return nextMessages.map((message) => {
|
|
29733
|
+
const cached = transcriptCache.current.get(message.id);
|
|
29734
|
+
return cached ? { ...message, transcript: cached } : message;
|
|
29735
|
+
});
|
|
29736
|
+
}, []);
|
|
29737
|
+
const prewarmTranscriptCache = useCallback2(async (nextMessages) => {
|
|
29738
|
+
const messagesToCheck = nextMessages.filter(
|
|
29739
|
+
(message) => shouldResolveTranscript(message) && !transcriptCache.current.has(message.id)
|
|
29740
|
+
);
|
|
29741
|
+
if (messagesToCheck.length === 0) return;
|
|
29742
|
+
const cachedEntries = await Promise.all(
|
|
29743
|
+
messagesToCheck.map(async (message) => {
|
|
29744
|
+
const transcript = await getDecryptedTranscript(message.id);
|
|
29745
|
+
return { id: message.id, transcript };
|
|
29746
|
+
})
|
|
29747
|
+
);
|
|
29748
|
+
for (const entry of cachedEntries) {
|
|
29749
|
+
if (!entry.transcript) continue;
|
|
29750
|
+
transcriptCache.current.set(entry.id, JSON.stringify(entry.transcript));
|
|
29751
|
+
resolvedIds.current.add(entry.id);
|
|
29752
|
+
}
|
|
29753
|
+
}, []);
|
|
29754
|
+
const resolveMessagesInBackground = useCallback2((nextMessages) => {
|
|
29755
|
+
const targetConversationId = activeConversationId.current;
|
|
29756
|
+
const messagesToResolve = nextMessages.filter(
|
|
29757
|
+
(message) => shouldResolveTranscript(message) && !transcriptCache.current.has(message.id) && !resolvedIds.current.has(message.id) && !inFlightIds.current.has(message.id)
|
|
29758
|
+
);
|
|
29759
|
+
if (messagesToResolve.length === 0) return;
|
|
29760
|
+
for (const message of messagesToResolve) {
|
|
29761
|
+
inFlightIds.current.add(message.id);
|
|
29762
|
+
}
|
|
29763
|
+
void decryptTextOnlyTranscriptBatch(
|
|
29764
|
+
messagesToResolve.map((message) => ({
|
|
29765
|
+
id: message.id,
|
|
29766
|
+
sender_id: message.sender_id,
|
|
29767
|
+
transcript: message.transcript,
|
|
29768
|
+
encryption_metadata: message.encryption_metadata
|
|
29769
|
+
}))
|
|
29770
|
+
).then((decrypted) => {
|
|
29771
|
+
const updatedTranscripts = /* @__PURE__ */ new Map();
|
|
29772
|
+
for (const message of messagesToResolve) {
|
|
29773
|
+
inFlightIds.current.delete(message.id);
|
|
29774
|
+
const transcript = decrypted.get(message.id);
|
|
29775
|
+
if (!transcript) continue;
|
|
29776
|
+
const json = JSON.stringify(transcript);
|
|
29777
|
+
transcriptCache.current.set(message.id, json);
|
|
29778
|
+
resolvedIds.current.add(message.id);
|
|
29779
|
+
updatedTranscripts.set(message.id, json);
|
|
29780
|
+
}
|
|
29781
|
+
if (updatedTranscripts.size === 0 || targetConversationId !== activeConversationId.current) return;
|
|
29782
|
+
setMessages(
|
|
29783
|
+
(prev) => prev.map((message) => {
|
|
29784
|
+
const transcript = updatedTranscripts.get(message.id);
|
|
29785
|
+
return transcript ? { ...message, transcript } : message;
|
|
29786
|
+
})
|
|
29787
|
+
);
|
|
29788
|
+
}).catch((err) => {
|
|
29789
|
+
for (const message of messagesToResolve) {
|
|
29790
|
+
inFlightIds.current.delete(message.id);
|
|
29791
|
+
}
|
|
29792
|
+
console.error("[TUI] Background transcript decrypt failed:", err);
|
|
29793
|
+
});
|
|
29674
29794
|
}, []);
|
|
29675
29795
|
const fetchMessages = useCallback2(
|
|
29676
29796
|
(beforeTimestamp) => {
|
|
@@ -29689,7 +29809,7 @@ function useMessages(conversation, currentUserId) {
|
|
|
29689
29809
|
isGroup ? "(group path)" : "(1:1 path)"
|
|
29690
29810
|
);
|
|
29691
29811
|
const promise = isGroup ? getGroupMessages(conversation.id, 50, beforeTimestamp) : getConversation(conversation.id, 50, beforeTimestamp);
|
|
29692
|
-
promise.then((result) => {
|
|
29812
|
+
promise.then(async (result) => {
|
|
29693
29813
|
console.error(
|
|
29694
29814
|
"[TUI] Fetched",
|
|
29695
29815
|
result.messages.length,
|
|
@@ -29697,10 +29817,9 @@ function useMessages(conversation, currentUserId) {
|
|
|
29697
29817
|
conversation.name,
|
|
29698
29818
|
"hasMore=" + result.hasMore
|
|
29699
29819
|
);
|
|
29700
|
-
|
|
29701
|
-
|
|
29702
|
-
|
|
29703
|
-
});
|
|
29820
|
+
await prewarmTranscriptCache(result.messages);
|
|
29821
|
+
if (activeConversationId.current !== conversation.id) return;
|
|
29822
|
+
const withCached = applyCachedTranscripts(result.messages);
|
|
29704
29823
|
if (beforeTimestamp) {
|
|
29705
29824
|
setMessages((prev) => {
|
|
29706
29825
|
const ids = new Set(prev.map((m) => m.id));
|
|
@@ -29714,37 +29833,22 @@ function useMessages(conversation, currentUserId) {
|
|
|
29714
29833
|
}
|
|
29715
29834
|
setHasMore(result.hasMore);
|
|
29716
29835
|
setLoading(false);
|
|
29717
|
-
|
|
29718
|
-
(m) => m.isEncrypted && !decryptedIds.current.has(m.id)
|
|
29719
|
-
);
|
|
29720
|
-
const needsDecrypt = encrypted.filter(
|
|
29721
|
-
(m) => !transcriptCache.current.has(m.id)
|
|
29722
|
-
);
|
|
29723
|
-
if (needsDecrypt.length > 0) {
|
|
29724
|
-
Promise.all(needsDecrypt.map(tryDecrypt)).then((decrypted) => {
|
|
29725
|
-
const decryptedMap = /* @__PURE__ */ new Map();
|
|
29726
|
-
for (const msg of decrypted) {
|
|
29727
|
-
decryptedIds.current.add(msg.id);
|
|
29728
|
-
rememberResolvedTranscript(msg);
|
|
29729
|
-
decryptedMap.set(msg.id, msg);
|
|
29730
|
-
}
|
|
29731
|
-
setMessages(
|
|
29732
|
-
(prev) => prev.map((m) => decryptedMap.get(m.id) ?? m)
|
|
29733
|
-
);
|
|
29734
|
-
});
|
|
29735
|
-
}
|
|
29836
|
+
resolveMessagesInBackground(result.messages);
|
|
29736
29837
|
}).catch((err) => {
|
|
29838
|
+
if (activeConversationId.current !== conversation.id) return;
|
|
29737
29839
|
const message = err instanceof Error ? err.message : String(err);
|
|
29738
29840
|
console.error("[TUI] Message fetch failed:", conversation?.name, message);
|
|
29739
29841
|
setError(message);
|
|
29740
29842
|
setLoading(false);
|
|
29741
29843
|
});
|
|
29742
29844
|
},
|
|
29743
|
-
[conversation]
|
|
29845
|
+
[applyCachedTranscripts, conversation, prewarmTranscriptCache, resolveMessagesInBackground]
|
|
29744
29846
|
);
|
|
29745
29847
|
useEffect3(() => {
|
|
29848
|
+
activeConversationId.current = conversation?.id ?? null;
|
|
29746
29849
|
if (conversation) {
|
|
29747
|
-
|
|
29850
|
+
resolvedIds.current.clear();
|
|
29851
|
+
inFlightIds.current.clear();
|
|
29748
29852
|
fetchMessages();
|
|
29749
29853
|
} else {
|
|
29750
29854
|
setMessages([]);
|
|
@@ -29753,45 +29857,37 @@ function useMessages(conversation, currentUserId) {
|
|
|
29753
29857
|
}
|
|
29754
29858
|
}, [conversation, fetchMessages]);
|
|
29755
29859
|
const handleNewMessage = useCallback2((msg) => {
|
|
29756
|
-
if (msg
|
|
29757
|
-
|
|
29758
|
-
|
|
29759
|
-
|
|
29760
|
-
setMessages((prev) => {
|
|
29761
|
-
if (prev.some((m) => m.id === processed.id)) return prev;
|
|
29762
|
-
return [...prev, processed];
|
|
29763
|
-
});
|
|
29860
|
+
if (shouldResolveTranscript(msg) && rememberResolvedTranscript(msg)) {
|
|
29861
|
+
setMessages((prev) => {
|
|
29862
|
+
if (prev.some((m) => m.id === msg.id)) return prev;
|
|
29863
|
+
return [...prev, msg];
|
|
29764
29864
|
});
|
|
29765
29865
|
return;
|
|
29766
29866
|
}
|
|
29867
|
+
const cachedTranscript = transcriptCache.current.get(msg.id);
|
|
29868
|
+
const withCachedTranscript = cachedTranscript ? { ...msg, transcript: cachedTranscript } : msg;
|
|
29767
29869
|
setMessages((prev) => {
|
|
29768
|
-
if (prev.some((m) => m.id ===
|
|
29769
|
-
return [...prev,
|
|
29870
|
+
if (prev.some((m) => m.id === withCachedTranscript.id)) return prev;
|
|
29871
|
+
return [...prev, withCachedTranscript];
|
|
29770
29872
|
});
|
|
29771
|
-
|
|
29873
|
+
resolveMessagesInBackground([msg]);
|
|
29874
|
+
}, [rememberResolvedTranscript, resolveMessagesInBackground]);
|
|
29772
29875
|
const handleUpdateMessage = useCallback2(
|
|
29773
29876
|
(id, updates) => {
|
|
29877
|
+
const existing = messages.find((message) => message.id === id);
|
|
29878
|
+
const updatedMessage = existing ? { ...existing, ...updates } : null;
|
|
29774
29879
|
setMessages(
|
|
29775
29880
|
(prev) => prev.map((m) => m.id === id ? { ...m, ...updates } : m)
|
|
29776
29881
|
);
|
|
29777
|
-
if (
|
|
29778
|
-
|
|
29779
|
-
|
|
29780
|
-
|
|
29781
|
-
|
|
29782
|
-
|
|
29783
|
-
transcriptCache.current.delete(id);
|
|
29784
|
-
tryDecrypt(msg).then((processed) => {
|
|
29785
|
-
rememberResolvedTranscript(processed);
|
|
29786
|
-
setMessages(
|
|
29787
|
-
(p) => p.map((m) => m.id === id ? processed : m)
|
|
29788
|
-
);
|
|
29789
|
-
});
|
|
29790
|
-
return prev;
|
|
29791
|
-
});
|
|
29882
|
+
if (updatedMessage && typeof updates.transcript === "string" && shouldResolveTranscript(updatedMessage)) {
|
|
29883
|
+
transcriptCache.current.delete(id);
|
|
29884
|
+
resolvedIds.current.delete(id);
|
|
29885
|
+
inFlightIds.current.delete(id);
|
|
29886
|
+
if (rememberResolvedTranscript(updatedMessage)) return;
|
|
29887
|
+
resolveMessagesInBackground([updatedMessage]);
|
|
29792
29888
|
}
|
|
29793
29889
|
},
|
|
29794
|
-
[rememberResolvedTranscript]
|
|
29890
|
+
[messages, rememberResolvedTranscript, resolveMessagesInBackground]
|
|
29795
29891
|
);
|
|
29796
29892
|
useRealtimeMessages(
|
|
29797
29893
|
conversation,
|
|
@@ -29810,8 +29906,7 @@ function useMessages(conversation, currentUserId) {
|
|
|
29810
29906
|
fetchMessages();
|
|
29811
29907
|
}, [fetchMessages]);
|
|
29812
29908
|
const addMessage = useCallback2((msg) => {
|
|
29813
|
-
if (msg
|
|
29814
|
-
decryptedIds.current.add(msg.id);
|
|
29909
|
+
if (shouldResolveTranscript(msg) && isCacheableTranscriptJson(msg.transcript)) {
|
|
29815
29910
|
rememberResolvedTranscript(msg);
|
|
29816
29911
|
}
|
|
29817
29912
|
setMessages((prev) => {
|