diff --git a/src/components/Common/Tooltip.tsx b/src/components/Common/Tooltip.tsx new file mode 100644 index 00000000..738641c6 --- /dev/null +++ b/src/components/Common/Tooltip.tsx @@ -0,0 +1,59 @@ +import { FC, ReactNode } from "react"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; +import { Info, LucideIcon } from "lucide-react"; + +interface TooltipProps { + children: ReactNode; + trigger?: string | LucideIcon; + className?: string; + position?: + | "top" + | "bottom" + | "left" + | "right" + | "top start" + | "top end" + | "bottom start" + | "bottom end" + | "left start" + | "left end" + | "right start" + | "right end"; +} + +export const Tooltip: FC = ({ + children, + trigger = Info, + className, + position = "right start", +}) => { + const renderTrigger = () => { + if (typeof trigger === "string") { + return ( + + {trigger} + + ); + } else { + const Icon = trigger; + return ( + + ); + } + }; + + return ( + + + + {renderTrigger()} + + + {children} + + + ); +}; diff --git a/src/components/Modals/ContactInfoModal.tsx b/src/components/Modals/ContactInfoModal.tsx index b807c326..12ad63cc 100644 --- a/src/components/Modals/ContactInfoModal.tsx +++ b/src/components/Modals/ContactInfoModal.tsx @@ -1,85 +1,385 @@ -import { FC } from "react"; +import { FC, useState, useEffect } from "react"; import { OneOnOneConversation } from "../../types/all"; import { AvatarHash } from "../icons/AvatarHash"; +import { Tooltip } from "../Common/Tooltip"; import clsx from "clsx"; +import { useMessagingStore } from "../../store/messaging.store"; +import { Pencil, CheckCircle } from "lucide-react"; +import { validateAlias } from "../../utils/alias-validator"; +import { Button } from "../Common/Button"; +import { WarningBlock } from "../Common/WarningBlock"; +import { ALIAS_LENGTH } from "../../config/constants"; +import { useSelfStash } from "../../hooks/useSelfStash"; type ContactInfoModalProps = { oooc: OneOnOneConversation; onClose: () => void; }; -export const ContactInfoModal: FC = ({ oooc }) => ( -
e.stopPropagation()}> -
-
-
- - {oooc.contact.name?.trim()?.slice(0, 2)?.toUpperCase() && ( - - {oooc.contact.name.trim().slice(0, 2).toUpperCase()} - - )} -
-
-
- {oooc.contact.name || "No nickname"} -
-
Contact
-
-
- {/* Indented content below avatar/nickname/contact */} -
- {" "} - {/* pl-14 aligns with avatar+gap */} -
-
- Address +export const ContactInfoModal: FC = ({ oooc }) => { + const [editingAlias, setEditingAlias] = useState<"my" | "their" | null>(null); + const [error, setError] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [aliasWasEdited, setAliasWasEdited] = useState(false); + + // subscribe to live conversation data from messaging store + const liveOooc = useMessagingStore((state) => + state.oneOnOneConversations.find( + (o) => o.conversation.id === oooc.conversation.id + ) + ); + + // use live data if available, fallback to prop + const currentOooc = liveOooc || oooc; + + const [myAliasValue, setMyAliasValue] = useState( + currentOooc.conversation.myAlias + ); + const [theirAliasValue, setTheirAliasValue] = useState( + currentOooc.conversation.theirAlias ?? "" + ); + + const updateConversationAliases = useMessagingStore( + (state) => state.updateConversationAliases + ); + + // use self stash hook for saving handshake with updated aliases + const { + isCreating: isCreatingSelfStash, + isCompleted: selfStashCompleted, + hasSufficientFunds, + handleCreateSelfStash, + } = useSelfStash({ + partnerAddress: currentOooc.contact.kaspaAddress, + ourAlias: currentOooc.conversation.myAlias, + theirAlias: currentOooc.conversation.theirAlias ?? "", + }); + + // sync local state when conversation updates from store + useEffect(() => { + setMyAliasValue(currentOooc.conversation.myAlias); + setTheirAliasValue(currentOooc.conversation.theirAlias ?? ""); + }, [ + currentOooc.conversation.myAlias, + currentOooc.conversation.theirAlias, + currentOooc.conversation.id, + ]); + + // auto-hide self stash section after successful save + useEffect(() => { + if (selfStashCompleted) { + const timer = setTimeout(() => { + setAliasWasEdited(false); + }, 10000); + + return () => clearTimeout(timer); + } + }, [selfStashCompleted]); + + const handleSaveAlias = async (type: "my" | "their") => { + setError(null); + setIsSaving(true); + + try { + const aliasToSave = type === "my" ? myAliasValue : theirAliasValue; + + // validate alias format + const validationError = validateAlias(aliasToSave); + if (validationError) { + setError(validationError); + setIsSaving(false); + return; + } + + await updateConversationAliases( + currentOooc.conversation.id, + type === "my" ? myAliasValue : undefined, + type === "their" ? theirAliasValue : undefined + ); + + setEditingAlias(null); + setAliasWasEdited(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to update alias"); + } finally { + setIsSaving(false); + } + }; + + const handleCancelEdit = (type: "my" | "their") => { + if (type === "my") { + setMyAliasValue(currentOooc.conversation.myAlias); + } else { + setTheirAliasValue(currentOooc.conversation.theirAlias ?? ""); + } + setError(null); + setEditingAlias(null); + }; + + return ( +
e.stopPropagation()}> +
+
+
+ + {currentOooc.contact.name?.trim()?.slice(0, 2)?.toUpperCase() && ( + + {currentOooc.contact.name.trim().slice(0, 2).toUpperCase()} + + )}
-
- {oooc.contact.kaspaAddress} +
+
+ {currentOooc.contact.name || "No nickname"} +
+
Contact
- {oooc.contact.name && ( + {/* Indented content below avatar/nickname/contact */} +
+ {" "} + {/* pl-14 aligns with avatar+gap */}
- Nickname + Address
- {oooc.contact.name} + {currentOooc.contact.kaspaAddress}
- )} -
-
- Messages -
-
- {oooc.events.length || 0} messages -
-
-
-
- Last Activity + {currentOooc.contact.name && ( +
+
+ Nickname +
+
+ {currentOooc.contact.name} +
+
+ )} + {!editingAlias ? ( + <> +
+
+ Messages +
+
+ {currentOooc.events.length || 0} messages +
+
+
+
+ Last Activity +
+
+ {currentOooc.conversation.lastActivityAt.toLocaleString()} +
+
+ + ) : ( + + Only edit the alias if you're certain on the new values + + )} + {/* My Alias Section */} +
+ + This is sent from you to the contact. They use this to identify + messages from you. + + {editingAlias === "my" ? ( +
+
+ setMyAliasValue(e.target.value)} + className="min-h-10 w-full rounded border border-[var(--secondary-border)] bg-[var(--bg-secondary)] px-3 py-2 font-mono text-base text-[var(--text-primary)]" + placeholder="Enter alias" + disabled={isSaving} + /> +
+
+ + +
+
+ ) : ( +
+
+ {currentOooc.conversation.myAlias} +
+ +
+ )}
-
- {oooc.conversation.lastActivityAt.toLocaleString()} + {/* Their Alias Section */} +
+ + Your contacts sent alias. Kasia uses this to 'scan' for messages + for you. + + + {editingAlias === "their" ? ( +
+
+ setTheirAliasValue(e.target.value)} + className="min-h-10 w-full rounded border border-[var(--secondary-border)] bg-[var(--bg-secondary)] px-3 py-2 font-mono text-base text-[var(--text-primary)]" + placeholder="Enter their alias" + disabled={isSaving} + /> +
+
+ + +
+
+ ) : ( +
+
+ {currentOooc.conversation.theirAlias ?? "N/A"} +
+ + +
+ )}
+ {/* Error display */} + {error && ( +
+ {error} +
+ )} + {/* Self Stash Section - Shows after alias edits */} + {aliasWasEdited && ( +
+
+
+
+ +

+ Aliases updated +

+
+

+ Save this change to your handshake on-chain so it syncs + across devices. +

+ + {!selfStashCompleted ? ( + + ) : ( +
+ + + Handshake saved successfully! + +
+ )} + + {!hasSufficientFunds && !selfStashCompleted && ( +

+ Insufficient funds. You need at least 0.2 KAS to save + handshake. +

+ )} +
+
+
+ )}
-
-); + ); +}; diff --git a/src/components/Modals/OffChainHandshakeModal.tsx b/src/components/Modals/OffChainHandshakeModal.tsx index b86f46a1..e0e48bcb 100644 --- a/src/components/Modals/OffChainHandshakeModal.tsx +++ b/src/components/Modals/OffChainHandshakeModal.tsx @@ -6,13 +6,13 @@ import { Clipboard, QrCode, CheckCircle } from "lucide-react"; import { useFeatureFlagsStore } from "../../store/featureflag.store"; import { useMessagingStore } from "../../store/messaging.store"; import { useUiStore } from "../../store/ui.store"; -import { useWalletStore } from "../../store/wallet.store"; import { toast } from "../../utils/toast-helper"; import { pasteFromClipboard } from "../../utils/clipboard"; import { Button } from "../Common/Button"; -import clsx from "clsx"; -import { Address, kaspaToSompi } from "kaspa-wasm"; +import { clsx } from "clsx"; +import { Address } from "kaspa-wasm"; import { ALIAS_LENGTH } from "../../config/constants"; +import { useSelfStash } from "../../hooks/useSelfStash"; interface OffChainHandshakeModalProps { isOpen: boolean; @@ -30,9 +30,6 @@ export const OffChainHandshakeModal: React.FC = ({ const [theirAliasForUs, setTheirAliasForUs] = useState(""); const [isCompleted, setIsCompleted] = useState(false); const [isCreating, setIsCreating] = useState(false); - const [isCreatingSelfStash, setIsCreatingSelfStash] = - useState(false); - const [selfStashCompleted, setSelfStashCompleted] = useState(false); const [isQrOpen, setIsQrOpen] = useState(false); const [activeQrSection, setActiveQrSection] = useState(null); const [hideTitles, setHideTitles] = useState(false); @@ -44,19 +41,22 @@ export const OffChainHandshakeModal: React.FC = ({ const cameraEnabled = useFeatureFlagsStore((s) => s.flags.enabledcamera); const { createOffChainHandshake: createOffChainHandshake, - createSelfStash, - setOpenedRecipient, generateUniqueAlias, } = useMessagingStore(); const { openModal, setQrScannerCallback, modals } = useUiStore(); - const balanceMature = useWalletStore((s) => s.balance?.mature); - const maxDustAmount = kaspaToSompi("0.19")!; - - // check if user has sufficient funds for self stash (0.2 KAS minimum) - const hasSufficientFundsForSelfStash = balanceMature - ? balanceMature >= BigInt(maxDustAmount) - : false; + // use self stash hook for handling handshake save logic + const { + isCreating: isCreatingSelfStash, + isCompleted: selfStashCompleted, + hasSufficientFunds: hasSufficientFundsForSelfStash, + handleCreateSelfStash, + } = useSelfStash({ + partnerAddress, + ourAlias: ourAliasForPartner, + theirAlias: theirAliasForUs, + onCompleted: onClose, + }); // validate partner address (so we know the alias is right) const validatePartnerAddress = ( @@ -223,41 +223,6 @@ export const OffChainHandshakeModal: React.FC = ({ } }; - // create a self stash transaction using the store function - const handleCreateSelfStash = async () => { - setIsCreatingSelfStash(true); - try { - // create single self-stash for off-chain handshake (contains both aliases) - await createSelfStash({ - type: "initiation", - partnerAddress, - ourAlias: ourAliasForPartner, - theirAlias: theirAliasForUs, - }); - - setSelfStashCompleted(true); - - // select the newly created conversation - setOpenedRecipient(partnerAddress); - - toast.success("Handshake Saved"); - - // close the modal after a brief delay to let user see the success message - setTimeout(() => { - onClose(); - }, 1500); - } catch (error) { - console.error("Failed to create self stash:", error); - toast.error( - error instanceof Error - ? error.message - : "Failed to save off-chain handshake" - ); - } finally { - setIsCreatingSelfStash(false); - } - }; - const titleRow = (opts: { hidden: boolean; section?: string; diff --git a/src/hooks/useSelfStash.ts b/src/hooks/useSelfStash.ts new file mode 100644 index 00000000..6ee3795f --- /dev/null +++ b/src/hooks/useSelfStash.ts @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { useMessagingStore } from "../store/messaging.store"; +import { useWalletStore } from "../store/wallet.store"; +import { toast } from "../utils/toast-helper"; +import { kaspaToSompi } from "kaspa-wasm"; + +interface UseSelfStashProps { + partnerAddress: string; + ourAlias: string; + theirAlias: string; + onCompleted?: () => void; +} + +interface UseSelfStashReturn { + isCreating: boolean; + isCompleted: boolean; + hasSufficientFunds: boolean; + handleCreateSelfStash: () => Promise; +} + +export const useSelfStash = ({ + partnerAddress, + ourAlias, + theirAlias, + onCompleted, +}: UseSelfStashProps): UseSelfStashReturn => { + const [isCreating, setIsCreating] = useState(false); + const [isCompleted, setIsCompleted] = useState(false); + + const { createSelfStash, setOpenedRecipient } = useMessagingStore(); + const balanceMature = useWalletStore((s) => s.balance?.mature); + + // check if user has sufficient funds for self stash (0.2 KAS minimum) + const maxDustAmount = kaspaToSompi("0.2")!; + const hasSufficientFunds = balanceMature + ? balanceMature >= BigInt(maxDustAmount) + : false; + + const handleCreateSelfStash = async () => { + if (!partnerAddress || !ourAlias || !theirAlias) { + toast.error("Missing required handshake data"); + return; + } + + setIsCreating(true); + try { + // create single self-stash for off-chain handshake (contains both aliases) + await createSelfStash({ + type: "initiation", + partnerAddress, + ourAlias, + theirAlias, + }); + + setIsCompleted(true); + + // select the newly created conversation + setOpenedRecipient(partnerAddress); + + toast.success("Handshake Saved"); + + // call the completion callback if provided + if (onCompleted) { + setTimeout(onCompleted, 1500); + } + } catch (error) { + console.error("Failed to create self stash:", error); + toast.error( + error instanceof Error + ? error.message + : "Failed to save off-chain handshake" + ); + } finally { + setIsCreating(false); + } + }; + + return { + isCreating, + isCompleted, + hasSufficientFunds, + handleCreateSelfStash, + }; +}; diff --git a/src/service/block-processor-service.ts b/src/service/block-processor-service.ts index 5d86ade7..c91e4e3e 100644 --- a/src/service/block-processor-service.ts +++ b/src/service/block-processor-service.ts @@ -36,9 +36,10 @@ export class BlockProcessorService extends EventEmitter<{ // ordering matters, Set is safe as per MDN documentation // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set private processedTransactionIds: Set = new Set(); - private monitoredConversations: Set = new Set(); // Store monitored aliases - private monitoredAddresses: Map = new Map(); // Store address -> alias mappings - private readonly MAX_TRANSACTION_IDS_RETENTION_COUNT = 1_000; // Prevent unlimited growth + private monitoredConversations: Set = new Set(); + private monitoredAddresses: Map = new Map(); + private readonly MAX_TRANSACTION_IDS_RETENTION_COUNT = 1_000; + private boundProcessBlockAdded: (event: IBlockAdded) => Promise; constructor( private readonly rpc: RpcClient, @@ -47,19 +48,18 @@ export class BlockProcessorService extends EventEmitter<{ ) { super(); + this.boundProcessBlockAdded = this.processBlockAdded.bind(this); this.init(); } private async init() { - this.rpc.addEventListener("block-added", this.processBlockAdded.bind(this)); + this.rpc.addEventListener("block-added", this.boundProcessBlockAdded); await this.rpc.subscribeBlockAdded(); } - stop() { - this.rpc.removeEventListener( - "block-added", - this.processBlockAdded.bind(this) - ); + async stop() { + this.rpc.removeEventListener("block-added", this.boundProcessBlockAdded); + await this.rpc.unsubscribeBlockAdded(); } private async processBlockAdded(_event: IBlockAdded) { diff --git a/src/service/conversation-manager-service.ts b/src/service/conversation-manager-service.ts index c1a173ac..6aebd6ac 100644 --- a/src/service/conversation-manager-service.ts +++ b/src/service/conversation-manager-service.ts @@ -269,6 +269,14 @@ export class ConversationManagerService { : null; } + public getConversationWithContactByConversationId( + conversationId: string + ): { conversation: Conversation; contact: Contact } | null { + return ( + this.conversationWithContactByConversationId.get(conversationId) || null + ); + } + public getActiveConversationsWithContact(): { conversation: ActiveConversation; contact: Contact; diff --git a/src/store/live.store.ts b/src/store/live.store.ts index 109361d1..1f53031d 100644 --- a/src/store/live.store.ts +++ b/src/store/live.store.ts @@ -15,7 +15,7 @@ interface LiveState { walletReceiveAddressString: string | undefined; start: (rpc: RpcClient, walletReceiveAddressString: string) => void; - stop: () => void; + stop: () => Promise; } export const useLiveStore = create((set, get) => { @@ -76,7 +76,7 @@ export const useLiveStore = create((set, get) => { boundOnRawKasiaTransactionReceived ); }, - stop() { + async stop() { get().rpc?.removeEventListener("disconnect", boundOnStop); get().rpc?.addEventListener("connect", boundOnStart); @@ -85,7 +85,7 @@ export const useLiveStore = create((set, get) => { boundOnRawKasiaTransactionReceived ); - get().blockProcessorService?.stop(); + await get().blockProcessorService?.stop(); get().saars?.stop(); set({ saars: undefined, diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts index f44843fd..62101715 100644 --- a/src/store/messaging.store.ts +++ b/src/store/messaging.store.ts @@ -49,6 +49,7 @@ import { } from "../service/import-export-service"; import { useNetworkStore } from "./network.store"; import { historicalLoader_loadSendAndReceivedHandshake } from "../utils/historical-loader"; +import { validateAlias } from "../utils/alias-validator"; interface MessagingState { isLoaded: boolean; @@ -113,6 +114,12 @@ interface MessagingState { // Nickname management setContactNickname: (address: string, nickname?: string) => Promise; removeContactNickname: (address: string) => Promise; + // Alias management + updateConversationAliases: ( + conversationId: string, + myAlias?: string, + theirAlias?: string + ) => Promise; // self stash management createSelfStash: (handshakeData: { @@ -431,6 +438,129 @@ export const useMessagingStore = create((set, g) => { return; } } + // ======= + + // if (!oooc) { + // oooc = { + // conversation: conversationWithContact.conversation, + // contact: conversationWithContact.contact, + // events: [], + // }; + // } else { + // // update the conversation reference with the latest from manager + // // this ensures alias updates from self-stash are reflected (newer than db aliases) + // oooc.conversation = conversationWithContact.conversation; + // oooc.contact = conversationWithContact.contact; + // } + + // const kasiaHandshakesToPersist: Handshake[] = []; + + // if (lastSentHS) { + // kasiaHandshakesToPersist.push({ + // __type: "handshake", + // amount: 0.2, + // contactId: oooc.contact.id, + // conversationId: oooc.conversation.id, + // content: "Handshake Sent", + // fromMe: true, + // createdAt: new Date(Number(lastSentHS.selfStash.block_time)), + // tenantId: unlockedWallet.id, + // transactionId: lastSentHS.selfStash.tx_id, + // id: `${unlockedWallet.id}_${lastSentHS.selfStash.tx_id}`, + // fee: 0, + // }); + // } + + // if (lastReceivedHS) { + // kasiaHandshakesToPersist.push({ + // __type: "handshake", + // amount: 0.2, + // contactId: oooc.contact.id, + // conversationId: oooc.conversation.id, + // content: JSON.stringify(lastReceivedHS.payload), + // fromMe: false, + // createdAt: new Date(Number(lastReceivedHS.handshake.block_time)), + // tenantId: unlockedWallet.id, + // transactionId: lastReceivedHS.handshake.tx_id, + // id: `${unlockedWallet.id}_${lastReceivedHS.handshake.tx_id}`, + // fee: 0, + // }); + // } + + // console.log("saving", { + // kasiaHandshakeToPersist: kasiaHandshakesToPersist, + // }); + + // for (const kasiaHSToPersist of kasiaHandshakesToPersist) { + // try { + // await repositories.handshakeRepository.saveHandshake( + // kasiaHSToPersist + // ); + // } catch (error) { + // console.error( + // "Messaging Store - Error while persisting handshake", + // error + // ); + // continue; + // } + + // oooc.events.push(kasiaHSToPersist); + // } + + // oooc.events.sort( + // (a, b) => a.createdAt.getTime() - b.createdAt.getTime() + // ); + + // ooocsByAddress.set(senderAddress, oooc); + // }; + + // await Promise.all([...uniqueSenderAddresses].map(processOneSender)); + + // repositories.metadataRepository.store({ + // lastSavedHandshakeBlockTime: lastProcessedSavedHandshakeBlockTime, + // lastHandshakeBlockTime: lastProcessedHandhshakeBlockTime, + // }); + + // console.log("Loading Strategy - handshake history reconciliation loaded"); + + // set({ + // oneOnOneConversations: Array.from(ooocsByAddress.values()), + // }); + + // // 5. lazily trigger historical polling of events for each conversation + // console.log("Lazy historical polling of events for each conversation"); + + // Promise.all( + // Array.from(ooocsByAddress.values()).map(async (oooc) => { + // // optimization possibility: fetch only events that are after last known conversation event + + // // also included previously unknown handhshakes message history fetching + // // this is useful mainly on a new device, where we have no history of received messages + + // // include current conversation participant's alias + // const resolvedUnknownHandshakesAlisesForThisConversation = + // resolvedUnknownReceivedHandshakesAliasesBySenderAddress[ + // oooc.contact.kaspaAddress + // ] ?? new Set(); + // if (oooc.conversation.theirAlias) { + // resolvedUnknownHandshakesAlisesForThisConversation.add( + // oooc.conversation.theirAlias + // ); + // } + + // return _fetchHistoricalForConversation( + // oooc, + // resolvedUnknownHandshakesAlisesForThisConversation, + // address, + // repositories + // ); + // }) + // ); + + // set({ + // isLoaded: true, + // }); + // >>>>>>> 0da279b (feat: temp alias) }, stop() { _historicalSyncer = null!; @@ -891,11 +1021,6 @@ export const useMessagingStore = create((set, g) => { updatedConversationWithContacts[ooocToUpdateIndex] = updatedConversation; - console.log( - "updatedConversationWithContacts", - updatedConversationWithContacts - ); - return { oneOnOneConversations: updatedConversationWithContacts }; }); } finally { @@ -1344,6 +1469,77 @@ export const useMessagingStore = create((set, g) => { return g().setContactNickname(address, undefined); }, + updateConversationAliases: async ( + conversationId: string, + myAlias?: string, + theirAlias?: string + ) => { + const manager = g().conversationManager; + if (!manager) { + throw new Error("Conversation manager not initialized"); + } + + // validate em + if (myAlias !== undefined && myAlias !== "") { + const validationError = validateAlias(myAlias); + if (validationError) { + throw new Error(`Invalid myAlias: ${validationError}`); + } + } + + if (theirAlias !== undefined && theirAlias !== "") { + const validationError = validateAlias(theirAlias); + if (validationError) { + throw new Error(`Invalid theirAlias: ${validationError}`); + } + } + + const oneOnOneConversationIndex = g().oneOnOneConversations.findIndex( + (oooc) => oooc.conversation.id === conversationId + ); + + if (oneOnOneConversationIndex === -1) { + throw new Error("Conversation not found"); + } + + // prepare the update object with only the fields that are provided + const updates: Pick & Partial = { + id: conversationId, + }; + + if (myAlias !== undefined && myAlias !== "") { + updates.myAlias = myAlias; + } + + if (theirAlias !== undefined && theirAlias !== "") { + updates.theirAlias = theirAlias; + } + + // update via conversation manager (includes persistence) + await manager.updateConversation(updates); + + // get the updated conversation from manager + const updatedConversationWithContact = + manager.getConversationWithContactByConversationId(conversationId); + + if (!updatedConversationWithContact) { + throw new Error("Failed to get updated conversation"); + } + + // update local state efficiently using map + const updatedConversations = g().oneOnOneConversations.map( + (oooc, index) => + index === oneOnOneConversationIndex + ? { + ...oooc, + conversation: updatedConversationWithContact.conversation, + } + : oooc + ); + + set({ oneOnOneConversations: updatedConversations }); + }, + async ingestRawResolvedKasiaTransaction(tx) { // note: ideally we wouldn't have "too much" cross-store dependency // or at least in a determined way that would prevent bi-directional dependencies diff --git a/src/utils/alias-validator.ts b/src/utils/alias-validator.ts index 11744fe6..053aa06e 100644 --- a/src/utils/alias-validator.ts +++ b/src/utils/alias-validator.ts @@ -1,3 +1,5 @@ +import { ALIAS_LENGTH } from "../config/constants"; + export type Alias = string & { __brand: "Alias" }; export const isAlias = (value: unknown): value is Alias => { @@ -7,3 +9,15 @@ export const isAlias = (value: unknown): value is Alias => { return /^[0-9a-f]{12}$/.test(value); }; + +export const validateAlias = (alias: string): string | null => { + if (!/^[0-9a-fA-F]+$/.test(alias)) { + return "Alias must be a hexadecimal string"; + } + + if (alias.length !== ALIAS_LENGTH * 2) { + return `Alias must be exactly ${ALIAS_LENGTH * 2} characters`; + } + + return null; // valid +}; diff --git a/src/utils/historical-loader.ts b/src/utils/historical-loader.ts index c3d3bdfd..a822728a 100644 --- a/src/utils/historical-loader.ts +++ b/src/utils/historical-loader.ts @@ -244,6 +244,11 @@ export const historicalLoader_loadSendAndReceivedHandshake = async ( contact: conversationWithContact.contact, events: [], }; + } else { + // update the conversation reference with the latest from manager + // this ensures alias updates from self-stash are reflected (newer than db aliases) + oooc.conversation = conversationWithContact.conversation; + oooc.contact = conversationWithContact.contact; } const kasiaHandshakesToPersist: Handshake[] = [];