+
{
kClassName="fill-[#ffffff]"
/>
-
- Why do I need a Kaspa wallet?
+
+ Why do I need a Seed Phrase?
@@ -90,11 +90,11 @@ export const TrustMessage: FC = () => {
openWhy ? "mt-2 max-h-40 opacity-100" : "max-h-0 opacity-0"
}`}
>
-
- Kasia is messaging app that protects your privacy. Your Kaspa wallet
- acts as your secure login – no email, phone number, or personal
- details needed. Messages are encrypted and sequenced on the Kaspa
- blockDAG, making them completely private and decentralized.
+
+ Kasia is a messaging app that protects your privacy. Your seed
+ phrase is your secure login—no email, phone number, or personal
+ details required. It proves ownership of your identity and allows
+ you to recover access if your device is lost or replaced.
Kasia is community built by volunteers.
From d07f61bcc3b8ead7186b218b404ef00080da05fe Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 30 Jan 2026 18:28:06 +1100
Subject: [PATCH 59/81] chore: update locked flow wallet terminology
---
src/components/Modals/DeleteWalletModal.tsx | 8 ++++----
src/components/WalletLockedFlow/Create.tsx | 10 +++++-----
src/components/WalletLockedFlow/Home.tsx | 8 ++++----
src/components/WalletLockedFlow/Import.tsx | 8 ++++----
src/components/WalletLockedFlow/SeedDisplay.tsx | 7 ++++---
src/components/WalletLockedFlow/Unlock.tsx | 8 ++++----
6 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/src/components/Modals/DeleteWalletModal.tsx b/src/components/Modals/DeleteWalletModal.tsx
index deaacd44..440f7142 100644
--- a/src/components/Modals/DeleteWalletModal.tsx
+++ b/src/components/Modals/DeleteWalletModal.tsx
@@ -46,11 +46,11 @@ export const DeleteWalletModal: FC = ({
- Delete Wallet
+ Delete Account
- Are you sure you want to delete the wallet{" "}
+ Are you sure you want to delete the accounts{" "}
"{wallet.name}"
@@ -58,8 +58,8 @@ export const DeleteWalletModal: FC = ({
- This action cannot be undone. All wallet data will be permanently
- removed.
+ This action cannot be undone. All account data and funds will be
+ permanently removed.
diff --git a/src/components/WalletLockedFlow/Create.tsx b/src/components/WalletLockedFlow/Create.tsx
index 84cf1451..9e3f4683 100644
--- a/src/components/WalletLockedFlow/Create.tsx
+++ b/src/components/WalletLockedFlow/Create.tsx
@@ -71,8 +71,8 @@ export const CreateWallet = ({ onSuccess, onBack }: CreateWalletProps) => {
onSuccess(id, mnemonic);
} catch (err) {
- console.error("Wallet creation error:", err);
- setError(err instanceof Error ? err.message : "Failed to create wallet");
+ console.error("Account creation error:", err);
+ setError(err instanceof Error ? err.message : "Failed to create account");
} finally {
setIsCreating(false);
if (passwordRef.current?.value) passwordRef.current.value = "";
@@ -91,17 +91,17 @@ export const CreateWallet = ({ onSuccess, onBack }: CreateWalletProps) => {
return (
<>
-
Create Wallet
+
Create Account
- Wallet Name
+ Account Name
onStepChange("create")}>
- Create Wallet
+ Create Account
onStepChange("import")}>
- Import Wallet
+ Import Account
>
diff --git a/src/components/WalletLockedFlow/Import.tsx b/src/components/WalletLockedFlow/Import.tsx
index 03e79a94..d5cc5c49 100644
--- a/src/components/WalletLockedFlow/Import.tsx
+++ b/src/components/WalletLockedFlow/Import.tsx
@@ -63,22 +63,22 @@ export const Import = ({ onSuccess, onBack }: ImportWalletProps) => {
return (
<>
-
Import Wallet
+
Import Account
- Wallet Name
+ Account Name
diff --git a/src/components/WalletLockedFlow/SeedDisplay.tsx b/src/components/WalletLockedFlow/SeedDisplay.tsx
index e6859954..7e9275d8 100644
--- a/src/components/WalletLockedFlow/SeedDisplay.tsx
+++ b/src/components/WalletLockedFlow/SeedDisplay.tsx
@@ -18,7 +18,7 @@ export const SeedPhraseDisplay = ({
return (
<>
-
Wallet Created
+
Account Created
Please save your mnemonic phrase securely:
@@ -27,7 +27,8 @@ export const SeedPhraseDisplay = ({
Please keep your seed phrase safe, if you lose your seed phrase there
is no recovery.
- Remember: Anyone with your seed phrase can access your wallet.
+ Remember: Anyone with your seed phrase can access your account and
+ messages.
- Back to Wallets
+ Back to Accounts
>
);
diff --git a/src/components/WalletLockedFlow/Unlock.tsx b/src/components/WalletLockedFlow/Unlock.tsx
index 71c7e7c4..20e9bebe 100644
--- a/src/components/WalletLockedFlow/Unlock.tsx
+++ b/src/components/WalletLockedFlow/Unlock.tsx
@@ -5,7 +5,7 @@ import { Wallet } from "../../types/wallet.type";
import { Button } from "../Common/Button";
import { WalletFlowErrorMessage } from "./WalletFlowErrorMessage";
import { Loader2 } from "lucide-react";
-import clsx from "clsx";
+
import {
StartSessionInvalidPasswordException,
useOrchestrator,
@@ -50,7 +50,7 @@ export const Unlock = ({
const onUnlockWallet = async () => {
const pass = passwordRef.current?.value;
if (!selectedWalletId || !pass) {
- setError("Please enter your wallet password");
+ setError("Please enter your account password");
return;
}
setError(null);
@@ -72,7 +72,7 @@ export const Unlock = ({
const msg =
err instanceof StartSessionInvalidPasswordException
? "Incorrect password. Please try again."
- : "Failed to unlock wallet. Please try again.";
+ : "Failed to unlock account. Please try again.";
setError(msg);
} finally {
setUnlocking(false);
@@ -128,7 +128,7 @@ export const Unlock = ({
{unlocking ? (
- Unlocking Wallet…
+ Unlocking Account
From 48a69ebadfc5c9466c39ecf6c9aeac5be48297c8 Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 30 Jan 2026 18:39:17 +1100
Subject: [PATCH 60/81] feat: copy update inside app
---
src/components/Modals/SettingsModal.tsx | 46 +++++++++++-----------
src/components/Modals/UtxoCompound.tsx | 2 +-
src/components/Modals/Wallet.tsx | 2 +-
src/components/Modals/WalletWithdrawal.tsx | 2 +-
4 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/src/components/Modals/SettingsModal.tsx b/src/components/Modals/SettingsModal.tsx
index 4312b81f..96bf50ed 100644
--- a/src/components/Modals/SettingsModal.tsx
+++ b/src/components/Modals/SettingsModal.tsx
@@ -238,18 +238,18 @@ export const SettingsModal: React.FC
= ({
const handleNameChange = async () => {
if (!selectedWalletId) {
- setNameChangeError("No wallet selected");
+ setNameChangeError("No account selected");
return;
}
// Basic validation (real-time validation handles most cases)
if (!newWalletName.trim()) {
- setNameChangeError("Please enter a wallet name");
+ setNameChangeError("Please enter an account name");
return;
}
if (newWalletName.trim().length < 2) {
- setNameChangeError("Wallet name must be at least 2 characters long");
+ setNameChangeError("Account name must be at least 2 characters long");
return;
}
@@ -272,7 +272,7 @@ export const SettingsModal: React.FC = ({
}, 2000);
} catch (error) {
setNameChangeError(
- error instanceof Error ? error.message : "Failed to change wallet name"
+ error instanceof Error ? error.message : "Failed to change account name"
);
} finally {
setIsChangingName(false);
@@ -346,7 +346,7 @@ export const SettingsModal: React.FC = ({
const currentWallet = wallets.find((w) => w.id === selectedWalletId);
if (currentWallet && newWalletName.trim() === currentWallet.name) {
- setNameChangeError("This is already your current wallet name");
+ setNameChangeError("This is already your current account name");
return;
}
@@ -358,7 +358,7 @@ export const SettingsModal: React.FC = ({
);
if (nameExists) {
- setNameChangeError("A wallet with this name already exists");
+ setNameChangeError("An account with this name already exists");
return;
}
@@ -480,7 +480,7 @@ export const SettingsModal: React.FC = ({
)}
- {/* Change Wallet Name */}
+ {/* Change Account Name */}
= ({
- Change Wallet Name
+ Change Account Name
- Update your wallet's display name
+ Update your accounts's display name
@@ -541,7 +541,7 @@ export const SettingsModal: React.FC
= ({
- Change Wallet Name
+ Change Account Name
@@ -549,29 +549,29 @@ export const SettingsModal: React.FC
= ({
{nameChangeSuccess ? (
- Wallet name changed successfully!
+ Account name changed successfully!
- Your wallet name has been updated.
+ Your account name has been updated.
) : (
- Your wallet password has been updated.
+ Your account password has been updated.
) : (
diff --git a/src/components/Modals/UtxoCompound.tsx b/src/components/Modals/UtxoCompound.tsx
index c982c8be..caec6a67 100644
--- a/src/components/Modals/UtxoCompound.tsx
+++ b/src/components/Modals/UtxoCompound.tsx
@@ -109,7 +109,7 @@ export const UtxoCompound: FC
= ({
Compound UTXOs
- Combine multiple UTXOs into fewer, larger ones to optimize wallet
+ Combine multiple UTXOs into fewer, larger ones to optimize account
performance. This creates a tx and sends all funds automatically to
yourself. Network fees will apply.
diff --git a/src/components/Modals/Wallet.tsx b/src/components/Modals/Wallet.tsx
index 3a565198..d7c21d50 100644
--- a/src/components/Modals/Wallet.tsx
+++ b/src/components/Modals/Wallet.tsx
@@ -44,7 +44,7 @@ export const Wallet = () => {
- Wallet
+ Account
{unlockedWalletName}
diff --git a/src/components/Modals/WalletWithdrawal.tsx b/src/components/Modals/WalletWithdrawal.tsx
index b7faf39c..1e5f8f88 100644
--- a/src/components/Modals/WalletWithdrawal.tsx
+++ b/src/components/Modals/WalletWithdrawal.tsx
@@ -229,7 +229,7 @@ export const WalletWithdrawal: FC = () => {
variant="secondary"
className="!w-full sm:w-auto"
>
- Back to Wallet
+ Back to Account
Date: Thu, 4 Dec 2025 11:34:30 +0300
Subject: [PATCH 61/81] feat: add status to sent direct messages
---
.../MessagesPane/Directs/DirectDisplay.tsx | 1 +
src/service/block-processor-service.ts | 33 +++++++++-
src/store/db.store.ts | 10 ----
src/store/messaging.store.ts | 60 ++++++++++++++++++-
src/store/repository/db.ts | 22 ++++++-
src/store/repository/message.repository.ts | 3 +
6 files changed, 113 insertions(+), 16 deletions(-)
diff --git a/src/components/MessagesPane/Directs/DirectDisplay.tsx b/src/components/MessagesPane/Directs/DirectDisplay.tsx
index 8b639a84..052e88c7 100644
--- a/src/components/MessagesPane/Directs/DirectDisplay.tsx
+++ b/src/components/MessagesPane/Directs/DirectDisplay.tsx
@@ -127,6 +127,7 @@ export const DirectDisplay: FC = ({
generateBubbleClasses({
isOutgoing,
groupPosition,
+ status: event.__type === "message" ? event.status : "confirmed",
})
)}
>
diff --git a/src/service/block-processor-service.ts b/src/service/block-processor-service.ts
index 5d86ade7..c1a979ad 100644
--- a/src/service/block-processor-service.ts
+++ b/src/service/block-processor-service.ts
@@ -2,6 +2,7 @@ import { IBlockAdded, ITransaction, RpcClient } from "kaspa-wasm";
import { BlockAddedData, Header } from "../types/all";
import { SenderAndAcceptanceResolutionService } from "./sender-and-acceptance-resolution-service";
import { useMessagingStore } from "../store/messaging.store";
+import { useWalletStore } from "../store/wallet.store";
import {
isKasiaTransaction,
ParsedKaspaMessagePayload,
@@ -104,13 +105,41 @@ export class BlockProcessorService extends EventEmitter<{
// mark as processed optimistically
this.processedTransactionIds.add(txId);
+ /*
+ * Temp hacky solution to mark out pending messages as confirmed
+ * This is planned to be unified with a single pending set so we can
+ * align with broadcast message update below
+ * We should wait until kaspa core finally implements vccv2, taking them ages
+ */
+
if (
await useDBStore.getState().repositories.doesKasiaEventExistsById(txId)
) {
- console.log(`Transaction ${txId} already processed`);
+ // try to flip pending direct message to confirmed (even if already stored)
+ const unlockedWallet = useWalletStore.getState().unlockedWallet;
+ const repositories = useDBStore.getState().repositories;
+ if (unlockedWallet && repositories) {
+ const messageId = `${unlockedWallet.id}_${txId}`;
+ try {
+ const existingMessage =
+ await repositories.messageRepository.getMessage(messageId);
+ if (existingMessage.status === "pending") {
+ await useMessagingStore
+ .getState()
+ .updateMessageStatus(
+ txId,
+ "confirmed",
+ repositories,
+ existingMessage
+ );
+ }
+ } catch {
+ console.log("Error updating direct status to 'confirmed'");
+ }
+ }
return;
}
-
+ //TO DO: Payments as well
/*
* Temp hacky solution to mark out pending broadcasts as confirmed
* This is planned to be unified with a single pending set so we can extend
diff --git a/src/store/db.store.ts b/src/store/db.store.ts
index 51da3051..a5740074 100644
--- a/src/store/db.store.ts
+++ b/src/store/db.store.ts
@@ -1,16 +1,6 @@
import { create } from "zustand";
import { KasiaDB, openDatabase, Repositories } from "./repository/db";
import { UnlockedWallet } from "../types/wallet.type";
-import { v4 } from "uuid";
-import {
- LEGACY_STORAGE_KEY,
- loadLegacyMessages,
-} from "../service/storage-encryption";
-import { PROTOCOL } from "../config/protocol";
-import { FileData, Message } from "./repository/message.repository";
-import { Handshake } from "./repository/handshake.repository";
-import { Payment } from "./repository/payment.repository";
-import { LegacyMessage } from "../types/legacy";
interface DBState {
db: KasiaDB | undefined;
diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts
index f44843fd..bc9a65c9 100644
--- a/src/store/messaging.store.ts
+++ b/src/store/messaging.store.ts
@@ -125,6 +125,14 @@ interface MessagingState {
// Hydration
hydrateOneonOneConversations: () => Promise;
+
+ // Message status management
+ updateMessageStatus: (
+ transactionId: string,
+ status: "pending" | "confirmed" | "failed",
+ repositories: Repositories,
+ existingMessage: Message
+ ) => Promise;
}
export const useMessagingStore = create((set, g) => {
@@ -821,6 +829,8 @@ export const useMessagingStore = create((set, g) => {
transaction.content.indexOf(":") + 1
);
+ const isFromMeMessage =
+ transaction.senderAddress === address.toString();
const message: Message = {
__type: "message",
amount: transaction.amount,
@@ -828,11 +838,13 @@ export const useMessagingStore = create((set, g) => {
conversationId: existingConversationWithContact.conversation.id,
content: decryptedContent ?? "",
createdAt: transaction.createdAt,
- fromMe: transaction.senderAddress === address.toString(),
+ fromMe: isFromMeMessage,
id: `${unlockedWallet.id}_${transaction.transactionId}`,
tenantId: unlockedWallet.id,
transactionId: transaction.transactionId,
fee: transaction.fee,
+ // outgoing messages start as pending until confirmed on chain
+ status: isFromMeMessage ? "pending" : "confirmed",
};
await repositories.messageRepository.saveMessage(message);
@@ -1478,5 +1490,51 @@ export const useMessagingStore = create((set, g) => {
console.log("self stash created successfully:", txId);
return txId;
},
+
+ updateMessageStatus: async (
+ transactionId,
+ status,
+ repositories,
+ existingMessage
+ ) => {
+ if (!repositories || !transactionId || !existingMessage) {
+ console.warn("Message status update failure: missing props");
+ return;
+ }
+
+ try {
+ // update status in db
+ await repositories.messageRepository.saveMessage({
+ ...existingMessage,
+ status,
+ });
+
+ // update in-memory state
+ set((state) => {
+ const updatedConversations = state.oneOnOneConversations.map(
+ (oooc) => ({
+ ...oooc,
+ events: oooc.events.map((event) => {
+ if (
+ event.__type === "message" &&
+ event.transactionId === transactionId
+ ) {
+ return { ...event, status };
+ }
+ return event;
+ }),
+ })
+ );
+
+ return { oneOnOneConversations: updatedConversations };
+ });
+ } catch (error) {
+ // message might not exist yet (incoming from network) - that's ok
+ console.debug(
+ `Could not update message status for ${transactionId}:`,
+ error
+ );
+ }
+ },
};
});
diff --git a/src/store/repository/db.ts b/src/store/repository/db.ts
index 6033a479..96dc2b35 100644
--- a/src/store/repository/db.ts
+++ b/src/store/repository/db.ts
@@ -23,7 +23,7 @@ import {
} from "./broadcast-channel.repository";
import { MetaRespository } from "./meta.repository";
-const CURRENT_DB_VERSION = 3;
+const CURRENT_DB_VERSION = 4;
export class DBNotFoundException extends Error {
constructor() {
@@ -117,7 +117,7 @@ export type KasiaDB = IDBPDatabase;
export const openDatabase = async (): Promise => {
return openDB("kasia-db", CURRENT_DB_VERSION, {
- upgrade(db, oldVersion, newVersion, transaction) {
+ async upgrade(db, oldVersion, newVersion, transaction) {
console.log(
`[DB] - Identity database upgrade from version ${oldVersion} to ${newVersion}`
);
@@ -260,9 +260,25 @@ export const openDatabase = async (): Promise => {
}
if (oldVersion <= 3) {
+ // Migration to v4: Add status field to existing messages
+ const messagesStore = transaction.objectStore("messages");
+ let cursor = await messagesStore.openCursor();
+
+ while (cursor) {
+ const message = cursor.value;
+ // add default status to existing messages that don't have it
+ if (!message.status) {
+ message.status = "confirmed";
+ await cursor.update(message);
+ }
+ cursor = await cursor.continue();
+ }
+
+ console.log("[DB] - Migrated to v4: Added status field to messages");
+ }
+ if (oldVersion <= 4) {
// HERE next migration, first increase CURRENT_DB_VERSION then implement with oldVersion <= CURRENT_DB_VERSION - 1
// add more if branching for each next version
- // BROADCAST CHANNELS
}
},
});
diff --git a/src/store/repository/message.repository.ts b/src/store/repository/message.repository.ts
index 2d0b266b..fa25fbf2 100644
--- a/src/store/repository/message.repository.ts
+++ b/src/store/repository/message.repository.ts
@@ -15,6 +15,7 @@ export type DbMessage = {
createdAt: Date;
transactionId: TransactionId;
contactId: string;
+ status: "pending" | "confirmed" | "failed";
/**
* encrypted data shaped as `json(MessageBag)`
*/
@@ -191,6 +192,7 @@ export class MessageRepository {
conversationId: message.conversationId,
createdAt: message.createdAt,
contactId: message.contactId,
+ status: message.status,
encryptedData: encryptXChaCha20Poly1305(
JSON.stringify({
fileData: message.fileData,
@@ -217,6 +219,7 @@ export class MessageRepository {
conversationId: dbMessage.conversationId,
createdAt: dbMessage.createdAt,
transactionId: dbMessage.transactionId,
+ status: dbMessage.status,
fee: messageBag.fee,
fileData: messageBag.fileData,
amount: messageBag.amount,
From 032437b0baabb2e22aead0ccb82c8349aa64d29c Mon Sep 17 00:00:00 2001
From: plagalPatent <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 6 Dec 2025 05:06:34 +1100
Subject: [PATCH 62/81] feat: add status to direct messages
---
.../MessagesPane/Directs/DirectDisplay.tsx | 5 ++-
src/service/block-processor-service.ts | 38 ++++++++++++-------
src/store/messaging.store.ts | 33 ++++++++++------
src/store/repository/db.ts | 19 +++++++++-
src/store/repository/payment.repository.ts | 3 ++
src/utils/message-payload.ts | 1 -
6 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/src/components/MessagesPane/Directs/DirectDisplay.tsx b/src/components/MessagesPane/Directs/DirectDisplay.tsx
index 052e88c7..86b9e613 100644
--- a/src/components/MessagesPane/Directs/DirectDisplay.tsx
+++ b/src/components/MessagesPane/Directs/DirectDisplay.tsx
@@ -127,7 +127,10 @@ export const DirectDisplay: FC = ({
generateBubbleClasses({
isOutgoing,
groupPosition,
- status: event.__type === "message" ? event.status : "confirmed",
+ status:
+ event.__type === "message" || event.__type === "payment"
+ ? event.status
+ : "confirmed",
})
)}
>
diff --git a/src/service/block-processor-service.ts b/src/service/block-processor-service.ts
index c1a979ad..dd4e5be6 100644
--- a/src/service/block-processor-service.ts
+++ b/src/service/block-processor-service.ts
@@ -104,6 +104,9 @@ export class BlockProcessorService extends EventEmitter<{
// mark as processed optimistically
this.processedTransactionIds.add(txId);
+ const parsed = parseKaspaMessagePayload(tx.payload);
+
+ const messageType = parsed.type;
/*
* Temp hacky solution to mark out pending messages as confirmed
@@ -111,7 +114,6 @@ export class BlockProcessorService extends EventEmitter<{
* align with broadcast message update below
* We should wait until kaspa core finally implements vccv2, taking them ages
*/
-
if (
await useDBStore.getState().repositories.doesKasiaEventExistsById(txId)
) {
@@ -119,27 +121,39 @@ export class BlockProcessorService extends EventEmitter<{
const unlockedWallet = useWalletStore.getState().unlockedWallet;
const repositories = useDBStore.getState().repositories;
if (unlockedWallet && repositories) {
- const messageId = `${unlockedWallet.id}_${txId}`;
+ const eventId = `${unlockedWallet.id}_${txId}`;
try {
- const existingMessage =
- await repositories.messageRepository.getMessage(messageId);
- if (existingMessage.status === "pending") {
+ // try message first, fallback to payment
+ let existingEvent;
+ switch (messageType) {
+ case "comm":
+ existingEvent =
+ await repositories.messageRepository.getMessage(eventId);
+ break;
+ case "payment":
+ existingEvent =
+ await repositories.paymentRepository.getPayment(eventId);
+ break;
+ default:
+ console.error(`Unknown message type: ${messageType}`);
+ return;
+ }
+ if (existingEvent?.status === "pending") {
await useMessagingStore
.getState()
- .updateMessageStatus(
+ .updateEventStatus(
txId,
"confirmed",
repositories,
- existingMessage
+ existingEvent
);
}
- } catch {
- console.log("Error updating direct status to 'confirmed'");
+ } catch (e) {
+ console.log("Error updating direct status to 'confirmed':", e);
}
}
return;
}
- //TO DO: Payments as well
/*
* Temp hacky solution to mark out pending broadcasts as confirmed
* This is planned to be unified with a single pending set so we can extend
@@ -166,10 +180,6 @@ export class BlockProcessorService extends EventEmitter<{
// try to resolve sender
const resolvedSenderData = await this.saars.askResolution(txId);
const resolvedSenderAddress = resolvedSenderData.sender.toString();
-
- const parsed = parseKaspaMessagePayload(tx.payload);
-
- const messageType = parsed.type;
const targetAlias = parsed.alias;
const isCommForUs =
diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts
index bc9a65c9..5f971ba8 100644
--- a/src/store/messaging.store.ts
+++ b/src/store/messaging.store.ts
@@ -127,11 +127,11 @@ interface MessagingState {
hydrateOneonOneConversations: () => Promise;
// Message status management
- updateMessageStatus: (
+ updateEventStatus: (
transactionId: string,
status: "pending" | "confirmed" | "failed",
repositories: Repositories,
- existingMessage: Message
+ existingMessage: Message | Payment
) => Promise;
}
@@ -805,6 +805,7 @@ export const useMessagingStore = create((set, g) => {
tenantId: unlockedWallet.id,
transactionId: transaction.transactionId,
fee: transaction.fee,
+ status: "pending",
};
await repositories.paymentRepository.savePayment(payment);
@@ -1491,23 +1492,31 @@ export const useMessagingStore = create((set, g) => {
return txId;
},
- updateMessageStatus: async (
+ updateEventStatus: async (
transactionId,
status,
repositories,
- existingMessage
+ existingEvent
) => {
- if (!repositories || !transactionId || !existingMessage) {
+ if (!repositories || !transactionId || !existingEvent) {
console.warn("Message status update failure: missing props");
return;
}
-
try {
- // update status in db
- await repositories.messageRepository.saveMessage({
- ...existingMessage,
- status,
- });
+ switch (existingEvent.__type) {
+ case "message":
+ await repositories.messageRepository.saveMessage({
+ ...existingEvent,
+ status,
+ });
+ break;
+ case "payment":
+ await repositories.paymentRepository.savePayment({
+ ...existingEvent,
+ status,
+ });
+ break;
+ }
// update in-memory state
set((state) => {
@@ -1516,7 +1525,7 @@ export const useMessagingStore = create((set, g) => {
...oooc,
events: oooc.events.map((event) => {
if (
- event.__type === "message" &&
+ event.__type === existingEvent.__type &&
event.transactionId === transactionId
) {
return { ...event, status };
diff --git a/src/store/repository/db.ts b/src/store/repository/db.ts
index 96dc2b35..cd120317 100644
--- a/src/store/repository/db.ts
+++ b/src/store/repository/db.ts
@@ -260,7 +260,7 @@ export const openDatabase = async (): Promise => {
}
if (oldVersion <= 3) {
- // Migration to v4: Add status field to existing messages
+ // Migration to v4: Add status field to existing messages and payments
const messagesStore = transaction.objectStore("messages");
let cursor = await messagesStore.openCursor();
@@ -274,7 +274,22 @@ export const openDatabase = async (): Promise => {
cursor = await cursor.continue();
}
- console.log("[DB] - Migrated to v4: Added status field to messages");
+ const paymentsStore = transaction.objectStore("payments");
+ let paymentCursor = await paymentsStore.openCursor();
+
+ while (paymentCursor) {
+ const payment = paymentCursor.value;
+ // add default status to existing payments that don't have it
+ if (!payment.status) {
+ payment.status = "confirmed";
+ await paymentCursor.update(payment);
+ }
+ paymentCursor = await paymentCursor.continue();
+ }
+
+ console.log(
+ "[DB] - Migrated to v4: Added status field to messages and payments"
+ );
}
if (oldVersion <= 4) {
// HERE next migration, first increase CURRENT_DB_VERSION then implement with oldVersion <= CURRENT_DB_VERSION - 1
diff --git a/src/store/repository/payment.repository.ts b/src/store/repository/payment.repository.ts
index cbd79db6..88015ca8 100644
--- a/src/store/repository/payment.repository.ts
+++ b/src/store/repository/payment.repository.ts
@@ -15,6 +15,7 @@ export type DbPayment = {
createdAt: Date;
transactionId: TransactionId;
contactId: string;
+ status: "pending" | "confirmed" | "failed";
/**
* encrypted data shaped as `json(PaymentBag)`
*/
@@ -180,6 +181,7 @@ export class PaymentRepository {
conversationId: payment.conversationId,
createdAt: payment.createdAt,
contactId: payment.contactId,
+ status: payment.status,
encryptedData: encryptXChaCha20Poly1305(
JSON.stringify({
amount: payment.amount,
@@ -204,6 +206,7 @@ export class PaymentRepository {
contactId: dbPayment.contactId,
conversationId: dbPayment.conversationId,
createdAt: dbPayment.createdAt,
+ status: dbPayment.status,
transactionId: dbPayment.transactionId,
fee: paymentBag.fee,
amount: paymentBag.amount,
diff --git a/src/utils/message-payload.ts b/src/utils/message-payload.ts
index 94ca5acc..bab44674 100644
--- a/src/utils/message-payload.ts
+++ b/src/utils/message-payload.ts
@@ -1,7 +1,6 @@
import { PROTOCOL } from "../config/protocol";
import { hexToString } from "./format";
import { ITransaction } from "wasm/kaspa";
-import { ExplorerTransaction } from "../types/transactions";
export type ParsedKaspaMessagePayload = {
type: string;
From 2f5823303bdb8da619d9bbcbaa80bb367c87328c Mon Sep 17 00:00:00 2001
From: plagalPatent <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 6 Dec 2025 06:32:13 +1100
Subject: [PATCH 63/81] chore: refactor - extract Tx status type
---
.../Utilities/Utils/BubbleClassGenerator.tsx | 3 ++-
src/service/block-processor-service.ts | 9 +++------
src/store/broadcast.store.ts | 3 ++-
src/store/messaging.store.ts | 3 ++-
src/store/repository/message.repository.ts | 3 ++-
src/store/repository/payment.repository.ts | 3 ++-
src/types/all.ts | 2 ++
7 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/components/MessagesPane/Utilities/Utils/BubbleClassGenerator.tsx b/src/components/MessagesPane/Utilities/Utils/BubbleClassGenerator.tsx
index 058af4c8..bd28f716 100644
--- a/src/components/MessagesPane/Utilities/Utils/BubbleClassGenerator.tsx
+++ b/src/components/MessagesPane/Utilities/Utils/BubbleClassGenerator.tsx
@@ -1,4 +1,5 @@
import clsx from "clsx";
+import { TransactionStatus } from "../../../../types/all";
type GroupPosition = "single" | "top" | "middle" | "bottom";
type BubbleClassOptions = {
@@ -6,7 +7,7 @@ type BubbleClassOptions = {
groupPosition: GroupPosition;
className?: string;
noBubble?: boolean;
- status?: "pending" | "confirmed" | "failed";
+ status?: TransactionStatus;
customBorderColor?: string;
};
diff --git a/src/service/block-processor-service.ts b/src/service/block-processor-service.ts
index dd4e5be6..c61eceda 100644
--- a/src/service/block-processor-service.ts
+++ b/src/service/block-processor-service.ts
@@ -109,9 +109,8 @@ export class BlockProcessorService extends EventEmitter<{
const messageType = parsed.type;
/*
- * Temp hacky solution to mark out pending messages as confirmed
- * This is planned to be unified with a single pending set so we can
- * align with broadcast message update below
+ * Temp solution to mark out pending messages and payments as confirmed
+ * This is planned to be refactored once there are some node updates
* We should wait until kaspa core finally implements vccv2, taking them ages
*/
if (
@@ -123,7 +122,6 @@ export class BlockProcessorService extends EventEmitter<{
if (unlockedWallet && repositories) {
const eventId = `${unlockedWallet.id}_${txId}`;
try {
- // try message first, fallback to payment
let existingEvent;
switch (messageType) {
case "comm":
@@ -156,8 +154,7 @@ export class BlockProcessorService extends EventEmitter<{
}
/*
* Temp hacky solution to mark out pending broadcasts as confirmed
- * This is planned to be unified with a single pending set so we can extend
- * the same functionality to outgoing chat messages too
+ * Currently only updates in memory broadcasts
*/
const broadcastStore = useBroadcastStore.getState();
const existingPendingMessage = broadcastStore.findMessageByTxId(txId);
diff --git a/src/store/broadcast.store.ts b/src/store/broadcast.store.ts
index 2a799dd0..ea563f97 100644
--- a/src/store/broadcast.store.ts
+++ b/src/store/broadcast.store.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import { TransactionStatus } from "../types/all";
import { useDBStore } from "./db.store";
import { BroadcastChannel } from "./repository/broadcast-channel.repository";
import { v4 } from "uuid";
@@ -12,7 +13,7 @@ export type BroadcastMessage = {
content: string;
timestamp: Date;
transactionId: string | null;
- status: "pending" | "confirmed" | "failed";
+ status: TransactionStatus;
};
// Add normalization utility at the top level
diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts
index 5f971ba8..fa7b0b00 100644
--- a/src/store/messaging.store.ts
+++ b/src/store/messaging.store.ts
@@ -3,6 +3,7 @@ import {
KasiaConversationEvent,
KasiaTransaction,
OneOnOneConversation,
+ TransactionStatus,
} from "../types/all";
import { Contact } from "./repository/contact.repository";
import {
@@ -129,7 +130,7 @@ interface MessagingState {
// Message status management
updateEventStatus: (
transactionId: string,
- status: "pending" | "confirmed" | "failed",
+ status: TransactionStatus,
repositories: Repositories,
existingMessage: Message | Payment
) => Promise;
diff --git a/src/store/repository/message.repository.ts b/src/store/repository/message.repository.ts
index fa25fbf2..a7275ada 100644
--- a/src/store/repository/message.repository.ts
+++ b/src/store/repository/message.repository.ts
@@ -1,5 +1,6 @@
import { encryptXChaCha20Poly1305, decryptXChaCha20Poly1305 } from "kaspa-wasm";
import { TransactionId } from "../../types/transactions";
+import { TransactionStatus } from "../../types/all";
import { DBNotFoundException, KasiaDB } from "./db";
export type DbMessage = {
@@ -15,7 +16,7 @@ export type DbMessage = {
createdAt: Date;
transactionId: TransactionId;
contactId: string;
- status: "pending" | "confirmed" | "failed";
+ status: TransactionStatus;
/**
* encrypted data shaped as `json(MessageBag)`
*/
diff --git a/src/store/repository/payment.repository.ts b/src/store/repository/payment.repository.ts
index 88015ca8..41304c33 100644
--- a/src/store/repository/payment.repository.ts
+++ b/src/store/repository/payment.repository.ts
@@ -1,5 +1,6 @@
import { encryptXChaCha20Poly1305, decryptXChaCha20Poly1305 } from "kaspa-wasm";
import { TransactionId } from "../../types/transactions";
+import { TransactionStatus } from "../../types/all";
import { KasiaDB, DBNotFoundException } from "./db";
export type DbPayment = {
@@ -15,7 +16,7 @@ export type DbPayment = {
createdAt: Date;
transactionId: TransactionId;
contactId: string;
- status: "pending" | "confirmed" | "failed";
+ status: TransactionStatus;
/**
* encrypted data shaped as `json(PaymentBag)`
*/
diff --git a/src/types/all.ts b/src/types/all.ts
index 09601eae..b6a7985f 100644
--- a/src/types/all.ts
+++ b/src/types/all.ts
@@ -163,3 +163,5 @@ export type OneOnOneConversation = {
export type KasiaConversationEvent = Message | Payment | Handshake;
export type ConnectionMode = "manual" | "auto";
+
+export type TransactionStatus = "pending" | "confirmed" | "failed";
From b900050b83d2cd52493d3e49577603cdf3ce6adf Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 30 Jan 2026 19:58:04 +1100
Subject: [PATCH 64/81] fix: incorrect payment status
---
src/store/messaging.store.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts
index fa7b0b00..9f003ba1 100644
--- a/src/store/messaging.store.ts
+++ b/src/store/messaging.store.ts
@@ -794,6 +794,7 @@ export const useMessagingStore = create((set, g) => {
paymentContent = "";
}
}
+ const paymentStatus = isFromMe ? "pending" : "confirmed";
const payment: Payment = {
__type: "payment",
amount: transaction.amount,
@@ -806,7 +807,7 @@ export const useMessagingStore = create((set, g) => {
tenantId: unlockedWallet.id,
transactionId: transaction.transactionId,
fee: transaction.fee,
- status: "pending",
+ status: paymentStatus,
};
await repositories.paymentRepository.savePayment(payment);
From 098ad0dd2ac99451303ef75bb1f40a7f22d9b2aa Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Wed, 15 Oct 2025 13:12:50 +0300
Subject: [PATCH 65/81] feat: blocklists squashed
wip: block lists
refactor: tidy settings menu
feat: further bcast
chore: color, component name
fix: misc
- tidy up var names
- some styling
- load block list before handshakes
- really make sure we dont get handshakes for blocked messages
feat: add generic confirmation modal
refactor: block core functionality
- all blocks are now block and delete
- lift method to store
feat: add default channel
feat: move blocked button on directs to contact list
feat: multiple items
- post rebase cleanup
- delete blocked-store for tenant ID
- add import export for blocked addresses
- bring settings modal into modal system
style: transition new chat instead of present new modal
style(fix): remove redundant conditional on locked home
---
src/components/Common/BlockUnblockButton.tsx | 100 ++++
src/components/ContactMenu.tsx | 56 ++-
src/components/Layout/DesktopMenu.tsx | 13 +-
src/components/Layout/ModalHost.tsx | 26 +-
src/components/Layout/SlideOutMenu.tsx | 11 +-
.../Broadcasts/BroadcastMessagesList.tsx | 227 ++++++----
.../Modals/BroadcastParticipantInfo.tsx | 59 ---
.../Modals/BroadcastParticipantInfoModal.tsx | 104 +++++
src/components/Modals/ConfirmationModal.tsx | 69 +++
src/components/Modals/ContactInfoModal.tsx | 144 +++---
src/components/Modals/NewChatForm.tsx | 426 +++++++++---------
src/components/Modals/SettingsModal.tsx | 92 ++--
.../Modals/SubSettings/BlockList.tsx | 134 ++++++
.../SideBarPane/Directs/ContactList.tsx | 14 +-
src/containers/WalletLockedFlowContainer.tsx | 2 +-
src/hooks/useOrchestrator.ts | 35 +-
src/service/block-processor-service.ts | 14 +
src/service/conversation-manager-service.ts | 47 +-
src/service/import-export-service.ts | 13 +-
src/service/storage-encryption.ts | 1 +
src/store/blocklist.store.ts | 195 ++++++++
src/store/messaging.store.ts | 45 +-
.../repository/blocked-address.repository.ts | 191 ++++++++
src/store/repository/db.ts | 87 +++-
src/store/ui.store.ts | 19 +-
src/utils/historical-loader.ts | 11 +
26 files changed, 1619 insertions(+), 516 deletions(-)
create mode 100644 src/components/Common/BlockUnblockButton.tsx
delete mode 100644 src/components/Modals/BroadcastParticipantInfo.tsx
create mode 100644 src/components/Modals/BroadcastParticipantInfoModal.tsx
create mode 100644 src/components/Modals/ConfirmationModal.tsx
create mode 100644 src/components/Modals/SubSettings/BlockList.tsx
create mode 100644 src/store/blocklist.store.ts
create mode 100644 src/store/repository/blocked-address.repository.ts
diff --git a/src/components/Common/BlockUnblockButton.tsx b/src/components/Common/BlockUnblockButton.tsx
new file mode 100644
index 00000000..539d657c
--- /dev/null
+++ b/src/components/Common/BlockUnblockButton.tsx
@@ -0,0 +1,100 @@
+import { useState } from "react";
+import { Ban } from "lucide-react";
+import {
+ Popover,
+ PopoverButton,
+ PopoverPanel,
+ Transition,
+} from "@headlessui/react";
+import { MoreHorizontal } from "lucide-react";
+import { useBlocklistStore } from "../../store/blocklist.store";
+import { toast } from "../../utils/toast-helper";
+
+interface BlockUnblockButtonProps {
+ address: string;
+ onBlock?: () => void;
+ onUnblock?: () => void;
+ className?: string;
+}
+
+export const BlockUnblockButton: React.FC = ({
+ address,
+ onBlock,
+ onUnblock,
+ className = "ml-auto pt-2",
+}) => {
+ const [isBlocking, setIsBlocking] = useState(false);
+ const blocklistStore = useBlocklistStore();
+ const isBlocked = blocklistStore.blockedAddresses.has(address);
+
+ const handleBlock = async () => {
+ try {
+ setIsBlocking(true);
+ if (isBlocked) {
+ await blocklistStore.unblockAddress(address);
+ toast.success("Unblocked");
+ onUnblock?.();
+ } else {
+ if (onBlock) {
+ onBlock();
+ } else {
+ // Default behavior: block and delete data
+ await blocklistStore.blockAddressAndDeleteData(address);
+ toast.success("Blocked");
+ }
+ }
+ } catch (error) {
+ console.error("Error blocking/unblocking:", error);
+ toast.error("Failed to update block status");
+ } finally {
+ setIsBlocking(false);
+ }
+ };
+
+ return (
+
+
+ {({ close }) => (
+ <>
+
+
+
+
+
+
+ {
+ handleBlock();
+ close();
+ }}
+ disabled={isBlocking}
+ className="focus:bg-secondary-bg active:bg-secondary-bg flex w-full cursor-pointer items-center justify-start gap-2 px-4 py-2 text-[var(--text-primary)] hover:bg-[var(--kas-primary)] active:scale-90 active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+
+ {isBlocking
+ ? isBlocked
+ ? "Block"
+ : "Unblock"
+ : isBlocked
+ ? "Unblock"
+ : "Block"}
+
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/src/components/ContactMenu.tsx b/src/components/ContactMenu.tsx
index 22d34fc6..49ab5989 100644
--- a/src/components/ContactMenu.tsx
+++ b/src/components/ContactMenu.tsx
@@ -1,11 +1,15 @@
import { useState } from "react";
-import { Pencil, Info, Copy, Check, UserCog } from "lucide-react";
+import { Pencil, Info, Copy, Check, UserCog, Ban } from "lucide-react";
import clsx from "clsx";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { EditNicknamePopover } from "./EditNicknamePopover";
import { copyToClipboard } from "../utils/clipboard";
import { OneOnOneConversation } from "../types/all";
import { ModalType } from "../store/ui.store";
+import { useBlocklistStore } from "../store/blocklist.store";
+import { useUiStore } from "../store/ui.store";
+import { useMessagingStore } from "../store/messaging.store";
+import { toast } from "../utils/toast-helper";
interface ContactMenuProps {
oneOnOneConversation: OneOnOneConversation | null;
@@ -27,6 +31,43 @@ export const ContactMenu: React.FC = ({
const [isCopying, setIsCopying] = useState(false);
const [isEditingInPopover, setIsEditingInPopover] = useState(false);
const [popoverEditValue, setPopoverEditValue] = useState("");
+ const [isBlocking, setIsBlocking] = useState(false);
+ const blocklistStore = useBlocklistStore();
+ const uiStore = useUiStore();
+ const messagingStore = useMessagingStore();
+
+ const address =
+ oneOnOneConversation?.contact.kaspaAddress ?? openedRecipient ?? "";
+ const isBlocked = blocklistStore.blockedAddresses.has(address);
+
+ const handleBlock = () => {
+ // set up confirmation modal
+ uiStore.setConfirmationConfig({
+ title: "Block Contact",
+ message: "This will block and delete ALL messages with this contact",
+ confirmText: "Block",
+ cancelText: "Cancel",
+ onConfirm: async () => {
+ try {
+ setIsBlocking(true);
+ await blocklistStore.blockAddressAndDeleteData(address);
+
+ // close modal and navigate away
+ messagingStore.setOpenedRecipient(null);
+
+ toast.success("Contact blocked and all conversations deleted");
+ } catch (error) {
+ console.error("Error blocking contact:", error);
+ toast.error("Failed to block contact");
+ } finally {
+ setIsBlocking(false);
+ }
+ },
+ });
+
+ // open it
+ uiStore.openModal("confirm");
+ };
return (
@@ -112,6 +153,19 @@ export const ContactMenu: React.FC = ({
onCancel={() => setIsEditingInPopover(false)}
/>
)}
+ {!isBlocked && (
+ {
+ handleBlock();
+ close();
+ }}
+ disabled={isBlocking}
+ className="focus:bg-secondary-bg active:bg-secondary-bg flex w-full cursor-pointer items-center justify-start gap-2 px-4 py-2 text-[var(--text-primary)] hover:bg-[var(--kas-primary)] active:scale-90 active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+
+ {isBlocking ? "Blocking..." : "Block"}
+
+ )}
>
diff --git a/src/components/Layout/DesktopMenu.tsx b/src/components/Layout/DesktopMenu.tsx
index 079e7f58..0238046b 100644
--- a/src/components/Layout/DesktopMenu.tsx
+++ b/src/components/Layout/DesktopMenu.tsx
@@ -1,7 +1,6 @@
-import { FC, useState } from "react";
+import { FC } from "react";
import clsx from "clsx";
import { PanelLeftOpen, Settings, ArrowLeft, User, Wallet } from "lucide-react";
-import { SettingsModal } from "../Modals/SettingsModal";
import { useUiStore } from "../../store/ui.store";
import { useWalletStore } from "../../store/wallet.store";
@@ -19,7 +18,6 @@ export const DesktopMenu: FC
= ({
}) => {
const openModal = useUiStore((s) => s.openModal);
const lockWallet = useWalletStore((s) => s.lock);
- const [showSettingsModal, setShowSettingsModal] = useState(false);
return (
@@ -59,7 +57,7 @@ export const DesktopMenu: FC = ({
{/* settings */}
setShowSettingsModal(true)}
+ onClick={() => openModal("settings-unlocked")}
className="hover:bg-primary-bg/50 cursor-pointer rounded p-2 hover:text-[var(--kas-primary)] focus:outline-none active:scale-90 active:opacity-80"
aria-label="Settings"
>
@@ -90,7 +88,7 @@ export const DesktopMenu: FC = ({
{/* settings */}
setShowSettingsModal(true)}
+ onClick={() => openModal("settings-unlocked")}
className="hover:bg-primary-bg/50 cursor-pointer rounded p-2 hover:text-[var(--kas-primary)] focus:outline-none active:scale-90 active:opacity-80"
aria-label="Settings"
>
@@ -147,11 +145,6 @@ export const DesktopMenu: FC = ({
)}
-
- setShowSettingsModal(false)}
- />
);
};
diff --git a/src/components/Layout/ModalHost.tsx b/src/components/Layout/ModalHost.tsx
index 9b82d5de..a45ac1dd 100644
--- a/src/components/Layout/ModalHost.tsx
+++ b/src/components/Layout/ModalHost.tsx
@@ -7,14 +7,16 @@ import { Wallet } from "../Modals/Wallet";
import { WalletSeedRetreiveDisplay } from "../Modals/WalletSeedRetreiveDisplay";
import { WalletWithdrawal } from "../Modals/WalletWithdrawal";
import { LockedSettingsModal } from "../Modals/LockedSettingsModal";
+import { SettingsModal } from "../Modals/SettingsModal";
import { ContactInfoModal } from "../Modals/ContactInfoModal";
import { NewChatForm } from "../Modals/NewChatForm";
import { LoaderCircle } from "lucide-react";
import { ImagePresenter } from "../Modals/ImagePresenter";
-import { BroadcastParticipantInfo } from "../Modals/BroadcastParticipantInfo";
+import { BroadcastParticipantInfoModal } from "../Modals/BroadcastParticipantInfoModal";
import { QrScannerModal } from "../Modals/QrScannerModal";
import { OffChainHandshakeModal } from "../Modals/OffChainHandshakeModal";
import { DeleteWalletModal } from "../Modals/DeleteWalletModal";
+import { ConfirmationModal } from "../Modals/ConfirmationModal";
import { useBroadcastStore } from "../../store/broadcast.store";
import { KASPA_DONATION_ADDRESS } from "../../config/constants";
@@ -93,6 +95,14 @@ export const ModalHost = () => {
)}
+ {/* Unlocked Settings Modal */}
+ {modals["settings-unlocked"] && (
+
closeModal("settings-unlocked")}
+ />
+ )}
+
{/* Contact Info Modal */}
{modals["contact-info-modal"] && oneOnOneConversation && (
{
setSelectedParticipant(null);
}}
>
- {
- closeModal("broadcast-participant-info");
- setSelectedParticipant(null);
- }}
/>
)}
@@ -163,6 +169,14 @@ export const ModalHost = () => {
onClose={() => closeModal("delete")}
/>
)}
+
+ {/* Confirmation Modal */}
+ {modals.confirm && (
+ closeModal("confirm")}
+ />
+ )}
>
);
};
diff --git a/src/components/Layout/SlideOutMenu.tsx b/src/components/Layout/SlideOutMenu.tsx
index 5b821fa7..89282092 100644
--- a/src/components/Layout/SlideOutMenu.tsx
+++ b/src/components/Layout/SlideOutMenu.tsx
@@ -10,9 +10,7 @@ import {
} from "lucide-react";
import { Settings } from "lucide-react";
import { useUiStore } from "../../store/ui.store";
-import { SettingsModal } from "../Modals/SettingsModal";
import { isIndexerDisabled } from "../../utils/indexer-settings";
-import { ConnectionIndicator } from "../Common/ConnectionIndicator";
type SlideOutMenuProps = {
address?: string;
@@ -29,7 +27,6 @@ export const SlideOutMenu: FC = ({
const setSettingsOpen = useUiStore((s) => s.setSettingsOpen);
const { openModal } = useUiStore();
- const [showSettingsModal, setShowSettingsModal] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
@@ -150,7 +147,7 @@ export const SlideOutMenu: FC = ({
{/* Sign Out Section */}
setShowSettingsModal(true)}
+ onClick={() => openModal("settings-unlocked")}
className="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-4 transition-colors active:bg-gray-700"
>
@@ -171,12 +168,6 @@ export const SlideOutMenu: FC = ({
-
- {/* Settings Modal */}
- setShowSettingsModal(false)}
- />
>
);
};
diff --git a/src/components/MessagesPane/Broadcasts/BroadcastMessagesList.tsx b/src/components/MessagesPane/Broadcasts/BroadcastMessagesList.tsx
index 117620a0..27c18e8b 100644
--- a/src/components/MessagesPane/Broadcasts/BroadcastMessagesList.tsx
+++ b/src/components/MessagesPane/Broadcasts/BroadcastMessagesList.tsx
@@ -1,120 +1,147 @@
-import { FC, memo } from "react";
+import { FC } from "react";
import { BroadcastMessage } from "../../../store/broadcast.store";
import { BroadcastDisplay } from "./BroadcastDisplay";
import { DateSeparator } from "../../DateSeparator";
import { isToday } from "../../../utils/message-date-format";
+import { useBlocklistStore } from "../../../store/blocklist.store";
+export const BLOCKED_PLACEHOLDER = "Blocked Contact";
interface BroadcastMessagesListProps {
messages: BroadcastMessage[];
walletAddress: string;
}
-export const BroadcastMessagesList: FC = memo(
- ({ messages, walletAddress }) => {
- if (!messages.length) {
- return (
-
- No messages in this channel yet...
-
- );
- }
+export const BroadcastMessagesList: FC = ({
+ messages,
+ walletAddress,
+}) => {
+ const blockedAddresses = useBlocklistStore((state) => state.blockedAddresses);
+ const broadcastBlockedDisplayMode = useBlocklistStore(
+ (state) => state.broadcastBlockedDisplayMode
+ );
- // compute last index of outgoing and messages from each sender
- const lastMessageIndices = new Map();
- messages.forEach((message, idx) => {
- lastMessageIndices.set(message.senderAddress, idx);
- });
+ // filter or map blocked messages based on display mode
+ const processedMessages = messages
+ .map((message) => {
+ const isSenderBlocked = blockedAddresses.has(message.senderAddress);
+ if (isSenderBlocked && broadcastBlockedDisplayMode === "hide") {
+ return null; // hide completely
+ }
+ if (isSenderBlocked && broadcastBlockedDisplayMode === "placeholder") {
+ return {
+ ...message,
+ content: BLOCKED_PLACEHOLDER,
+ isBlockedPlaceholder: true,
+ };
+ }
+ return { ...message, isBlockedPlaceholder: false };
+ })
+ .filter(Boolean) as (BroadcastMessage & {
+ isBlockedPlaceholder: boolean;
+ })[];
- const firstTodayIdx = messages.findIndex((message) =>
- isToday(message.timestamp)
+ if (!processedMessages.length) {
+ return (
+
+ No messages in this channel yet...
+
);
+ }
- return (
- <>
- {messages.map((message, idx) => {
- const isFirst = idx === 0;
- const isOutgoing = message.senderAddress === walletAddress;
- const showTimestamp =
- idx === (lastMessageIndices.get(message.senderAddress) || -1);
- const previousMessage = messages[idx - 1];
- const nextMessage = messages[idx + 1];
- const dateObj = message.timestamp;
+ // compute last index of outgoing and messages from each sender
+ const lastMessageIndices = new Map();
+ processedMessages.forEach((message, idx) => {
+ lastMessageIndices.set(message.senderAddress, idx);
+ });
+
+ const firstTodayIdx = processedMessages.findIndex((message) =>
+ isToday(message.timestamp)
+ );
- // is this the first message of today?
- const isFirstToday = idx === firstTodayIdx && isToday(dateObj);
+ return (
+ <>
+ {processedMessages.map((message, idx) => {
+ const isFirst = idx === 0;
+ const isOutgoing = message.senderAddress === walletAddress;
+ const showTimestamp =
+ idx === (lastMessageIndices.get(message.senderAddress) || -1);
+ const previousMessage = processedMessages[idx - 1];
+ const nextMessage = processedMessages[idx + 1];
+ const dateObj = message.timestamp;
- // show date time stamp if:
- // - this is the first message of today
- // - or, this is not the first message and there's a 5min+ gap
- // - or, this is the first message
- const showSeparator =
- isFirstToday ||
- (idx > 0 &&
- previousMessage &&
- message.timestamp.getTime() -
- previousMessage.timestamp.getTime() >
- 5 * 60 * 1000) ||
- (idx === 0 && !isToday(dateObj));
+ // is this the first message of today?
+ const isFirstToday = idx === firstTodayIdx && isToday(dateObj);
- // if there's a separator, treat as new group
- const isPrevSameSender =
- !showSeparator &&
+ // show date time stamp if:
+ // - this is the first message of today
+ // - or, this is not the first message and there's a 5min+ gap
+ // - or, this is the first message
+ const showSeparator =
+ isFirstToday ||
+ (idx > 0 &&
previousMessage &&
- previousMessage.senderAddress === message.senderAddress;
- const isNextSameSender =
- nextMessage &&
- // if the next message has a separator, it's not same group
- !(
- (idx + 1 === firstTodayIdx && isToday(nextMessage.timestamp)) ||
- (idx + 1 > 0 &&
- messages[idx + 1 - 1] &&
- nextMessage.timestamp.getTime() -
- messages[idx + 1 - 1].timestamp.getTime() >
- 30 * 60 * 1000) ||
- (idx + 1 === 0 && !isToday(nextMessage.timestamp))
- ) &&
- nextMessage.senderAddress === message.senderAddress;
+ message.timestamp.getTime() - previousMessage.timestamp.getTime() >
+ 5 * 60 * 1000) ||
+ (idx === 0 && !isToday(dateObj));
- const isSingleInGroup = !isPrevSameSender && !isNextSameSender;
- const isTopOfGroup = !isPrevSameSender && isNextSameSender;
- const isBottomOfGroup = isPrevSameSender && !isNextSameSender;
+ // if there's a separator, treat as new group
+ const isPrevSameSender =
+ !showSeparator &&
+ previousMessage &&
+ previousMessage.senderAddress === message.senderAddress;
+ const isNextSameSender =
+ nextMessage &&
+ // if the next message has a separator, it's not same group
+ !(
+ (idx + 1 === firstTodayIdx && isToday(nextMessage.timestamp)) ||
+ (idx + 1 > 0 &&
+ processedMessages[idx + 1 - 1] &&
+ nextMessage.timestamp.getTime() -
+ processedMessages[idx + 1 - 1].timestamp.getTime() >
+ 30 * 60 * 1000) ||
+ (idx + 1 === 0 && !isToday(nextMessage.timestamp))
+ ) &&
+ nextMessage.senderAddress === message.senderAddress;
- return (
-
- {showSeparator &&
- (isFirstToday ? (
-
- Today
-
- {dateObj.toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
- ) : (
-
- ))}
-
-
- );
- })}
- >
- );
- }
-);
+ const isSingleInGroup = !isPrevSameSender && !isNextSameSender;
+ const isTopOfGroup = !isPrevSameSender && isNextSameSender;
+ const isBottomOfGroup = isPrevSameSender && !isNextSameSender;
+
+ return (
+
+ {showSeparator &&
+ (isFirstToday ? (
+
+ Today
+
+ {dateObj.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+ ) : (
+
+ ))}
+
+
+ );
+ })}
+ >
+ );
+};
diff --git a/src/components/Modals/BroadcastParticipantInfo.tsx b/src/components/Modals/BroadcastParticipantInfo.tsx
deleted file mode 100644
index 2d30ba6d..00000000
--- a/src/components/Modals/BroadcastParticipantInfo.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { FC } from "react";
-import { AvatarHash } from "../icons/AvatarHash";
-import { CopyableValueWithQR } from "./CopyableValueWithQR";
-import clsx from "clsx";
-
-type BroadcastParticipantInfoProps = {
- address: string;
- nickname?: string;
- onClose: () => void;
-};
-
-export const BroadcastParticipantInfo: FC = ({
- address,
- nickname,
-}) => {
- return (
- e.stopPropagation()}>
-
- {/* Avatar and basic info */}
-
-
-
- {nickname && (
-
- {nickname.slice(0, 2).toUpperCase()}
-
- )}
-
-
-
- {nickname || "No nickname"}
-
-
- Broadcast Participant
-
-
-
-
- {/* Address section */}
-
-
-
- );
-};
diff --git a/src/components/Modals/BroadcastParticipantInfoModal.tsx b/src/components/Modals/BroadcastParticipantInfoModal.tsx
new file mode 100644
index 00000000..43a3765e
--- /dev/null
+++ b/src/components/Modals/BroadcastParticipantInfoModal.tsx
@@ -0,0 +1,104 @@
+import { FC } from "react";
+import { AvatarHash } from "../icons/AvatarHash";
+import { CopyableValueWithQR } from "./CopyableValueWithQR";
+import clsx from "clsx";
+import { useBlocklistStore } from "../../store/blocklist.store";
+import { useUiStore } from "../../store/ui.store";
+import { BlockUnblockButton } from "../Common/BlockUnblockButton";
+import { toast } from "../../utils/toast-helper";
+
+type BroadcastParticipantInfoModalProps = {
+ address: string;
+ nickname?: string;
+};
+
+export const BroadcastParticipantInfoModal: FC<
+ BroadcastParticipantInfoModalProps
+> = ({ address, nickname }) => {
+ const blocklistStore = useBlocklistStore();
+ const uiStore = useUiStore();
+ const isBlocked = blocklistStore.blockedAddresses.has(address);
+
+ const handleBlockWithConfirmation = () => {
+ // set up confirmation modal
+ uiStore.setConfirmationConfig({
+ title: "Block Participant",
+ message: `This will block ${nickname || "this participant"} and delete any existing direct messages with them.`,
+ confirmText: "Block",
+ cancelText: "Cancel",
+ onConfirm: async () => {
+ try {
+ await blocklistStore.blockAddressAndDeleteData(address);
+ toast.success("Participant blocked");
+ } catch (error) {
+ console.error("Error blocking participant:", error);
+ toast.error("Failed to block participant");
+ }
+ },
+ });
+
+ // open confirmation modal
+ uiStore.openModal("confirm");
+ };
+
+ return (
+ e.stopPropagation()}>
+
+ {/* Avatar and basic info */}
+
+
+
+ {nickname && (
+
+ {nickname.slice(0, 2).toUpperCase()}
+
+ )}
+
+
+
+
+ {nickname || "No nickname"}
+
+ {/* actions menu */}
+
+
+
+ Broadcast Participant
+
+
+
+
+ {/* Address section */}
+
+
+ {/* block status */}
+ {isBlocked && (
+
+
+ This participant is blocked
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Modals/ConfirmationModal.tsx b/src/components/Modals/ConfirmationModal.tsx
new file mode 100644
index 00000000..17ac7f22
--- /dev/null
+++ b/src/components/Modals/ConfirmationModal.tsx
@@ -0,0 +1,69 @@
+import { FC, useEffect } from "react";
+import { Modal } from "../Common/modal";
+import { Button } from "../Common/Button";
+import { useUiStore } from "../../store/ui.store";
+
+interface ConfirmationModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export const ConfirmationModal: FC = ({
+ isOpen,
+ onClose,
+}) => {
+ const confirmationConfig = useUiStore((s) => s.confirmationConfig);
+ const setConfirmationConfig = useUiStore((s) => s.setConfirmationConfig);
+
+ // Clear confirmation config when modal closes
+ useEffect(() => {
+ if (!isOpen) {
+ setConfirmationConfig(null);
+ }
+ }, [isOpen, setConfirmationConfig]);
+
+ const handleConfirm = () => {
+ if (confirmationConfig?.onConfirm) {
+ confirmationConfig.onConfirm();
+ }
+ setConfirmationConfig(null);
+ onClose();
+ };
+
+ const handleCancel = () => {
+ if (confirmationConfig?.onCancel) {
+ confirmationConfig.onCancel();
+ }
+ setConfirmationConfig(null);
+ onClose();
+ };
+
+ if (!isOpen || !confirmationConfig) return null;
+
+ return (
+
+
+
+ {confirmationConfig.title}
+
+
+
+ {confirmationConfig.message}
+
+
+
+
+ {confirmationConfig.cancelText || "Cancel"}
+
+
+ {confirmationConfig.confirmText || "Confirm"}
+
+
+
+
+ );
+};
diff --git a/src/components/Modals/ContactInfoModal.tsx b/src/components/Modals/ContactInfoModal.tsx
index b807c326..c569795d 100644
--- a/src/components/Modals/ContactInfoModal.tsx
+++ b/src/components/Modals/ContactInfoModal.tsx
@@ -2,84 +2,106 @@ import { FC } from "react";
import { OneOnOneConversation } from "../../types/all";
import { AvatarHash } from "../icons/AvatarHash";
import clsx from "clsx";
+import { useBlocklistStore } from "../../store/blocklist.store";
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,
+ onClose,
+}) => {
+ const blocklistStore = useBlocklistStore();
+
+ const isBlocked = blocklistStore.blockedAddresses.has(
+ oooc.contact.kaspaAddress
+ );
+
+ return (
+ e.stopPropagation()}>
+
+
+
+
+ {oooc.contact.name?.trim()?.slice(0, 2)?.toUpperCase() && (
+
+ {oooc.contact.name.trim().slice(0, 2).toUpperCase()}
+
+ )}
-
- {oooc.contact.kaspaAddress}
+
+
+
+ {oooc.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}
+ {oooc.contact.kaspaAddress}
- )}
-
-
- Messages
-
-
- {oooc.events.length || 0} messages
-
-
-
-
- Last Activity
+ {oooc.contact.name && (
+
+
+ Nickname
+
+
+ {oooc.contact.name}
+
+
+ )}
+
+
+ Messages
+
+
+ {oooc.events.length || 0} messages
+
-
- {oooc.conversation.lastActivityAt.toLocaleString()}
+
+
+ Last Activity
+
+
+ {oooc.conversation.lastActivityAt.toLocaleString()}
+
+ {/* block status */}
+ {isBlocked && (
+
+
+ This contact is blocked
+
+
+ )}
-
-);
+ );
+};
diff --git a/src/components/Modals/NewChatForm.tsx b/src/components/Modals/NewChatForm.tsx
index a516b874..3bb38383 100644
--- a/src/components/Modals/NewChatForm.tsx
+++ b/src/components/Modals/NewChatForm.tsx
@@ -347,27 +347,224 @@ export const NewChatForm: React.FC
= ({ onClose }) => {
}
};
- if (showConfirmation) {
- let recipientDisplay;
- if (
- detectedRecipientInputValueFormat === "kns" &&
- resolvedRecipientAddress
- ) {
- recipientDisplay = (
-
-
{recipientInputValue}
-
-
-
+ // helper function for smooth transitions
+ const blockVisible = (hidden: boolean) =>
+ clsx(
+ "transition-all duration-300 ease-in-out",
+ hidden
+ ? "pointer-events-none max-h-0 overflow-hidden opacity-0"
+ : "pointer-events-auto max-h-screen opacity-100"
+ );
+
+ let recipientDisplay;
+ if (detectedRecipientInputValueFormat === "kns" && resolvedRecipientAddress) {
+ recipientDisplay = (
+
+
{recipientInputValue}
+
+
- );
- } else {
- recipientDisplay =
;
- }
+
+ );
+ } else {
+ recipientDisplay =
;
+ }
+
+ return (
+ <>
+ {/* Form Section */}
+
+
Start New Conversation
+
+
- return (
- <>
-
Confirm Handshake
+ {/* Confirmation Section */}
+
+
Confirm Handshake
Recipient:
@@ -422,196 +619,7 @@ export const NewChatForm: React.FC = ({ onClose }) => {
Back
- >
- );
- }
-
- return (
- <>
-
Start New Conversation
-
-
-
- Recipient Address
-
-
-
- setRecipientInputValue(e.target.value.toLowerCase())
- }
- placeholder="Kaspa address or KNS domain"
- disabled={isLoading}
- required
- autoComplete="off"
- />
-
-
-
-
- {cameraEnabled && (
-
-
-
- )}
-
-
-
- {isResolvingKns && detectedRecipientInputValueFormat === "kns" && (
-
- Resolving KNS domain...
-
- )}
- {resolvedRecipientAddress &&
- detectedRecipientInputValueFormat === "kns" &&
- !isResolvingKns &&
- !knsError && (
-
-
-
-
- )}
- {knsError &&
- detectedRecipientInputValueFormat === "kns" &&
- !isResolvingKns && (
-
- {knsError}
-
- )}
- {isCheckingRecipient && (
-
- Checking recipient balance...
-
- )}
- {recipientWarning && (
-
- {recipientWarning}
-
- )}
-
-
-
-
- Handshake Amount (KAS)
-
-
handleAmountChange(e.target.value)}
- placeholder="0.2"
- disabled={isLoading}
- />
-
- handleQuickAmount("0.2")}
- disabled={isLoading}
- >
- 0.2
-
- handleQuickAmount("0.5")}
- disabled={isLoading}
- >
- 0.5
-
- handleQuickAmount("1")}
- disabled={isLoading}
- >
- 1
-
-
-
- Default: 0.2 KAS. Higher amounts help recipients respond even if
- they have no KAS. This creates a better experience for newcomers to
- Kasia.
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- {isLoading ? "Initiating..." : "Start Chat"}
-
-
- Cancel
-
-
-
+
>
);
};
diff --git a/src/components/Modals/SettingsModal.tsx b/src/components/Modals/SettingsModal.tsx
index 96bf50ed..fe7ea62b 100644
--- a/src/components/Modals/SettingsModal.tsx
+++ b/src/components/Modals/SettingsModal.tsx
@@ -44,6 +44,7 @@ import { HoldToDelete } from "../Common/HoldToDelete";
import { AppVersion } from "../App/AppVersion";
import { toast } from "../../utils/toast-helper";
import { Donations } from "../Common/Donations";
+import { BlockList } from "./SubSettings/BlockList";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
@@ -95,7 +96,13 @@ export const SettingsModal: React.FC
= ({
{ id: "security", label: "Security", icon: Shield },
// only show if there are >0 flips
...(Object.keys(flips).length > 0
- ? [{ id: "extras", label: "Extra", icon: RectangleEllipsis }]
+ ? [
+ {
+ id: "extras",
+ label: isMobile ? "Feat." : "Features",
+ icon: RectangleEllipsis,
+ },
+ ]
: []),
...(devMode
? [
@@ -130,6 +137,9 @@ export const SettingsModal: React.FC = ({
// Delete all messages state
const [showDeleteAll, setShowDeleteAll] = useState(false);
+ // Blocklist state
+ const [showBlocklist, setShowBlocklist] = useState(false);
+
// Custom theme state
const [showCustomTheme, setShowCustomTheme] = useState(false);
@@ -366,18 +376,7 @@ export const SettingsModal: React.FC = ({
setNameChangeError("");
}, [newWalletName, wallets, selectedWalletId, showNameChange]);
- useEffect(() => {
- if (isOpen) {
- document.body.classList.add("settings-modal-open");
- } else {
- document.body.classList.remove("settings-modal-open");
- }
- return () => {
- document.body.classList.remove("settings-modal-open");
- };
- }, [isOpen]);
-
- // Reset custom theme state when switching away from custom theme
+ // reset custom theme state when switching away from custom theme
useEffect(() => {
if (theme !== "custom") {
setShowCustomTheme(false);
@@ -432,9 +431,9 @@ export const SettingsModal: React.FC = ({
isMobile && activeTab === tab.id,
"text-primary bg-primary-bg border-kas-secondary rounded-lg border":
!isMobile && activeTab === tab.id,
- "text-muted-foreground hover:text-primary border-b-2 border-transparent":
+ "hover:text-primary border-b-2 border-transparent":
isMobile && activeTab !== tab.id,
- "text-muted-foreground hover:text-primary border border-transparent":
+ "hover:text-primary border border-transparent":
!isMobile && activeTab !== tab.id,
}
)}
@@ -474,7 +473,7 @@ export const SettingsModal: React.FC = ({
Your Wallet:
-
@@ -506,7 +505,7 @@ export const SettingsModal: React.FC = ({
Import / Export Messages
-
+
Backup or restore your message history
@@ -524,7 +523,7 @@ export const SettingsModal: React.FC = ({
Delete All Messages
-
+
Permanently remove all conversations and data
@@ -536,7 +535,7 @@ export const SettingsModal: React.FC = ({
@@ -617,7 +616,7 @@ export const SettingsModal: React.FC
= ({
setShowImportExport(false)}
- className="hover:text-primary text-muted-foreground cursor-pointer p-1 transition-colors"
+ className="hover:text-primary cursor-pointer p-1 transition-colors"
>
@@ -632,7 +631,7 @@ export const SettingsModal: React.FC
= ({
setShowDeleteAll(false)}
- className="hover:text-primary text-muted-foreground cursor-pointer p-1 transition-colors"
+ className="hover:text-primary cursor-pointer p-1 transition-colors"
>
@@ -651,7 +650,7 @@ export const SettingsModal: React.FC
= ({
Confirm Deletion
-
+
Click and hold the delete button below to confirm you
want to permanently delete all messages.
@@ -735,7 +734,7 @@ export const SettingsModal: React.FC
= ({
setShowCustomTheme(false)}
- className="hover:text-primary text-muted-foreground cursor-pointer p-1 transition-colors"
+ className="hover:text-primary cursor-pointer p-1 transition-colors"
>
@@ -781,13 +780,13 @@ export const SettingsModal: React.FC
= ({
Current Network
-
+
{networkStore.network}{" "}
@@ -796,7 +795,7 @@ export const SettingsModal: React.FC
= ({
: "(Disconnected)"}
{networkStore.nodeUrl && (
-
+
{networkStore.nodeUrl}
@@ -808,7 +807,7 @@ export const SettingsModal: React.FC
= ({
)}
{activeTab === "security" && (
- {!showPasswordChange ? (
+ {!showPasswordChange && !showBlocklist ? (
<>
Security
@@ -850,14 +849,28 @@ export const SettingsModal: React.FC = ({
+
+ {/* Blocklist */}
+ setShowBlocklist(true)}
+ className="bg-primary-bg hover:bg-primary-bg/50 border-primary-border flex w-full cursor-pointer items-center gap-3 rounded-2xl border p-4 transition-all duration-200 active:rounded-4xl"
+ >
+
+
+
Blocklist
+
+ Manage blocked addresses and privacy settings
+
+
+
>
- ) : (
+ ) : showPasswordChange ? (
<>
@@ -950,12 +963,25 @@ export const SettingsModal: React.FC
= ({
)}
>
- )}
+ ) : showBlocklist ? (
+ <>
+
+
setShowBlocklist(false)}
+ className="hover:text-primary cursor-pointer p-1 transition-colors"
+ >
+
+
+
Blocklist
+
+
+ >
+ ) : null}
)}
{activeTab === "extras" && (
-
Extra
+
Features
{/* Warning */}
@@ -979,7 +1005,7 @@ export const SettingsModal: React.FC
= ({
{item.label}
-
diff --git a/src/components/Modals/SubSettings/BlockList.tsx b/src/components/Modals/SubSettings/BlockList.tsx
new file mode 100644
index 00000000..4ff63331
--- /dev/null
+++ b/src/components/Modals/SubSettings/BlockList.tsx
@@ -0,0 +1,134 @@
+import React, { useState } from "react";
+import { useBlocklistStore } from "../../../store/blocklist.store";
+import { X, ChevronDown, ChevronUp } from "lucide-react";
+import clsx from "clsx";
+import { toast } from "../../../utils/toast-helper";
+import { BLOCKED_PLACEHOLDER } from "../../../components/MessagesPane/Broadcasts/BroadcastMessagesList";
+
+export const BlockList: React.FC = () => {
+ const blocklistStore = useBlocklistStore();
+ const [isBroadcastExpanded, setIsBroadcastExpanded] = useState(false);
+
+ return (
+
+ {/* broadcast display mode toggle - collapsible */}
+
+ {/* Collapsible Header */}
+
setIsBroadcastExpanded(!isBroadcastExpanded)}
+ className="hover:bg-secondary-bg -my-2 flex w-full cursor-pointer items-center justify-between rounded-lg p-2 transition-colors"
+ >
+
+ Broadcast Display Mode
+
+ {isBroadcastExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Collapsible Content */}
+
+
+ Choose how to display messages from blocked participants in
+ broadcasts
+
+
+
+ blocklistStore.setBroadcastBlockedDisplayMode("placeholder")
+ }
+ className={clsx(
+ "flex w-full cursor-pointer items-center gap-2 rounded-lg border p-3 transition-all active:rounded-4xl",
+ blocklistStore.broadcastBlockedDisplayMode === "placeholder"
+ ? "bg-kas-secondary/10 border-kas-secondary"
+ : "bg-primary-bg border-primary-border hover:bg-primary-bg/50"
+ )}
+ >
+
+
Show Placeholder
+
+ Display "{BLOCKED_PLACEHOLDER}"
+
+
+
+
+ blocklistStore.setBroadcastBlockedDisplayMode("hide")
+ }
+ className={clsx(
+ "flex w-full cursor-pointer items-center gap-2 rounded-lg border p-3 transition-all active:rounded-4xl",
+ blocklistStore.broadcastBlockedDisplayMode === "hide"
+ ? "bg-kas-secondary/10 border-kas-secondary"
+ : "bg-primary-bg border-primary-border hover:bg-primary-bg/50"
+ )}
+ >
+
+
Hide Completely
+
+ Don't show messages from blocked participants
+
+
+
+
+
+
+
+ {/* blocked addresses list */}
+
+
+
+ Blocked Addresses ({blocklistStore.blockedAddressList.length})
+
+
+
+ {blocklistStore.blockedAddressList.length === 0 ? (
+
+ No blocked addresses
+
+ ) : (
+
+ {blocklistStore.blockedAddressList.map((blocked) => (
+
+
+
+ {blocked.kaspaAddress}
+
+ {blocked.reason && (
+
+ {blocked.reason}
+
+ )}
+
+
{
+ try {
+ await blocklistStore.unblockAddress(blocked.kaspaAddress);
+ toast.success("Address unblocked");
+ } catch (error) {
+ console.error("Error unblocking address:", error);
+ toast.error("Failed to unblock address");
+ }
+ }}
+ className="ml-2 cursor-pointer p-1 transition-colors hover:text-red-500"
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/SideBarPane/Directs/ContactList.tsx b/src/components/SideBarPane/Directs/ContactList.tsx
index e0aeb171..3a956744 100644
--- a/src/components/SideBarPane/Directs/ContactList.tsx
+++ b/src/components/SideBarPane/Directs/ContactList.tsx
@@ -20,17 +20,17 @@ export const ContactList: FC = ({
setMobileView,
isMobile,
}) => {
- const messageStore = useMessagingStore();
-
- const contacts = messageStore.oneOnOneConversations.map(
- (oooc) => oooc.contact
+ const oneOnOneConversations = useMessagingStore(
+ (state) => state.oneOnOneConversations
);
+
+ const contacts = oneOnOneConversations.map((oooc) => oooc.contact);
// order contacts by last activity (most recent first)
const orderedContacts = contacts.sort((a, b) => {
- const conversationA = messageStore.oneOnOneConversations.find(
+ const conversationA = oneOnOneConversations.find(
(oooc) => oooc.contact.id === a.id
);
- const conversationB = messageStore.oneOnOneConversations.find(
+ const conversationB = oneOnOneConversations.find(
(oooc) => oooc.contact.id === b.id
);
@@ -69,7 +69,7 @@ export const ContactList: FC = ({
});
// then, add contacts from messages that match content
- messageStore.oneOnOneConversations.forEach((oneOnOneConversation) => {
+ oneOnOneConversations.forEach((oneOnOneConversation) => {
oneOnOneConversation.events.forEach((event) => {
if (event.content.includes(q)) {
// only add if not already present
diff --git a/src/containers/WalletLockedFlowContainer.tsx b/src/containers/WalletLockedFlowContainer.tsx
index dc9a0e41..4b2e1c3a 100644
--- a/src/containers/WalletLockedFlowContainer.tsx
+++ b/src/containers/WalletLockedFlowContainer.tsx
@@ -136,7 +136,7 @@ export const WalletLockedFlowContainer = ({
isMobile
? [
"fixed top-safe bottom-safe w-full max-h-screen overflow-y-auto flex flex-col p-4",
- (step.type === "home" && wallets.length <= 2) ||
+ step.type === "home" ||
step.type === "create" ||
step.type === "unlock"
? "justify-center"
diff --git a/src/hooks/useOrchestrator.ts b/src/hooks/useOrchestrator.ts
index 91b7a26a..058a94ea 100644
--- a/src/hooks/useOrchestrator.ts
+++ b/src/hooks/useOrchestrator.ts
@@ -9,6 +9,7 @@ import { UnlockedWallet } from "../types/wallet.type";
import { useLiveStore } from "../store/live.store";
import { useBroadcastStore } from "../store/broadcast.store";
import { useFeatureFlagsStore, FeatureFlags } from "../store/featureflag.store";
+import { useBlocklistStore } from "../store/blocklist.store";
import { getEffectiveIndexerUrl } from "../utils/indexer-settings";
export type ConnectOpts = {
@@ -123,6 +124,17 @@ export const useOrchestrator = () => {
conversationManager: null,
});
+ // load blocked addresses BEFORE messaging store to prevent rehydrating blocked contacts
+ try {
+ await useBlocklistStore.getState().loadBlockedAddresses();
+ console.log("Blocked addresses loaded");
+ } catch (error) {
+ console.error(
+ "Failed to load blocked addresses during initialization:",
+ error
+ );
+ }
+
await messagingStore.load(receivedAddressString);
// load broadcast channels async so we can start processing them straight away
@@ -132,7 +144,25 @@ export const useOrchestrator = () => {
useBroadcastStore
.getState()
.loadChannels()
- .then(() => console.log("Broadcast channels loaded"))
+ .then(async () => {
+ console.log("Broadcast channels loaded");
+
+ // Add default broadcast channel if not already added
+ const defaultsLoaded = localStorage.getItem(
+ "kasia-load-defaults-true"
+ );
+ if (!defaultsLoaded) {
+ try {
+ await useBroadcastStore
+ .getState()
+ .addChannel("kasia-general", "default");
+ localStorage.setItem("kasia-load-defaults-true", "true");
+ console.log("Added default broadcast channel: kasia-general");
+ } catch (error) {
+ console.error("Failed to add default broadcast channel:", error);
+ }
+ }
+ })
.catch((error) =>
console.error(
"Failed to load broadcast channels during initialization:",
@@ -183,6 +213,9 @@ export const useOrchestrator = () => {
// clear broadcast store to prevent channel/message bleed
broadcastStore.reset();
+
+ // clear blocklist store to prevent address list bleed
+ useBlocklistStore.getState().reset();
};
return { connect, startSession, onPause, onResume };
diff --git a/src/service/block-processor-service.ts b/src/service/block-processor-service.ts
index c61eceda..f05fa8cd 100644
--- a/src/service/block-processor-service.ts
+++ b/src/service/block-processor-service.ts
@@ -18,6 +18,7 @@ import EventEmitter from "eventemitter3";
import { devMode } from "../config/dev-mode";
import { useBroadcastStore } from "../store/broadcast.store";
import { getTransactionId } from "../types/transactions";
+import { useBlocklistStore } from "../store/blocklist.store";
export type RawResolvedKasiaTransaction = {
id: string;
@@ -179,6 +180,19 @@ export class BlockProcessorService extends EventEmitter<{
const resolvedSenderAddress = resolvedSenderData.sender.toString();
const targetAlias = parsed.alias;
+ // check if sender is blocked - reject direct messages, allow broadcasts to be filtered by UI
+ const blocklistStore = useBlocklistStore.getState();
+ const isSenderBlocked = blocklistStore.isBlocked(resolvedSenderAddress);
+
+ if (isSenderBlocked && messageType !== PROTOCOL.headers.BROADCAST.type) {
+ if (devMode) {
+ console.log(
+ `Block Processor - Rejecting message from blocked address: ${resolvedSenderAddress}`
+ );
+ }
+ return; // don't process messages from blocked addresses
+ }
+
const isCommForUs =
messageType === PROTOCOL.headers.COMM.type &&
targetAlias &&
diff --git a/src/service/conversation-manager-service.ts b/src/service/conversation-manager-service.ts
index c1a173ac..027d18e5 100644
--- a/src/service/conversation-manager-service.ts
+++ b/src/service/conversation-manager-service.ts
@@ -14,6 +14,7 @@ import {
} from "../store/repository/conversation.repository";
import { Contact } from "../store/repository/contact.repository";
import { Handshake } from "../store/repository/handshake.repository";
+import { useBlocklistStore } from "../store/blocklist.store";
export class ConversationManagerService {
private static readonly STORAGE_KEY_PREFIX = "encrypted_conversations";
@@ -168,6 +169,15 @@ export class ConversationManagerService {
payload: HandshakePayload
): Promise {
try {
+ // check if sender is blocked before processing handshake
+ const blocklistStore = useBlocklistStore.getState();
+ if (blocklistStore.isBlocked(senderAddress)) {
+ console.log(
+ `Conversation Manager - Rejecting handshake from blocked address: ${senderAddress}`
+ );
+ return; // don't process handshakes from blocked addresses
+ }
+
// STEP 1 – look up strictly by sender address only
const existingConversationAndContactByAddress =
this.getConversationWithContactByAddress(senderAddress);
@@ -435,6 +445,13 @@ export class ConversationManagerService {
recipientAddress: string,
initiatedByMe: boolean
): Promise<{ conversation: Conversation; contact: Contact }> {
+ // prevent creating conversations with blocked addresses
+ if (this.isAddressBlocked(recipientAddress)) {
+ throw new Error(
+ `Cannot create conversation with blocked address: ${recipientAddress}`
+ );
+ }
+
const contact = await this.repositories.contactRepository
.getContactByKaspaAddress(recipientAddress)
.catch(async (error) => {
@@ -482,6 +499,13 @@ export class ConversationManagerService {
payload: SavedHandshakePayload,
transactionId: string
): Promise<{ conversation: Conversation; contact: Contact }> {
+ // check if address is blocked before processing
+ if (this.isAddressBlocked(payload.recipientAddress)) {
+ throw new Error(
+ `Cannot hydrate blocked address: ${payload.recipientAddress}`
+ );
+ }
+
const contact = await this.repositories.contactRepository
.getContactByKaspaAddress(payload.recipientAddress)
.catch(async (error) => {
@@ -580,13 +604,17 @@ export class ConversationManagerService {
}
private isValidKaspaAddress(address: string): boolean {
- // Check for both mainnet and testnet address formats
+ // check for both mainnet and testnet address formats
return (
(address.startsWith("kaspa:") || address.startsWith("kaspatest:")) &&
address.length > 10
);
}
+ private isAddressBlocked(address: string): boolean {
+ return useBlocklistStore.getState().isBlocked(address);
+ }
+
/**
* assumption: conversation does not exist yet
* -> you should call this method only if you are sure that the conversation does not exist yet
@@ -595,6 +623,12 @@ export class ConversationManagerService {
payload: HandshakePayload,
senderAddress: string
) {
+ // ignore handshakes from blocked addresses
+ if (this.isAddressBlocked(senderAddress)) {
+ console.log(`Ignoring handshake from blocked address: ${senderAddress}`);
+ return;
+ }
+
const isMyNewAliasValid = isAlias(payload.theirAlias);
const myAlias = this.generateUniqueAlias();
@@ -778,7 +812,14 @@ export class ConversationManagerService {
ourAliasForPartner: string,
theirAliasForUs: string
): Promise<{ conversationId: string; contactId: string }> {
- // Check if contact already exists - for offline handshakes, we should only create new contacts
+ // prevent creating offline handshakes with blocked addresses
+ if (this.isAddressBlocked(partnerAddress)) {
+ throw new Error(
+ `Cannot create handshake with blocked address: ${partnerAddress}`
+ );
+ }
+
+ // check if contact already exists - for offline handshakes, we should only create new contacts
let contact: Contact;
try {
await this.repositories.contactRepository.getContactByKaspaAddress(
@@ -787,7 +828,7 @@ export class ConversationManagerService {
throw new Error(`Cannot create handshake. Contact already exists.`);
} catch (error) {
if (error instanceof DBNotFoundException) {
- // Contact doesn't exist, create a new one
+ // contact doesn't exist, create a new one
const newContact = {
id: uuidv4(),
kaspaAddress: partnerAddress,
diff --git a/src/service/import-export-service.ts b/src/service/import-export-service.ts
index c6b77b89..0eb7b8b4 100644
--- a/src/service/import-export-service.ts
+++ b/src/service/import-export-service.ts
@@ -6,6 +6,7 @@ import type { Handshake } from "../store/repository/handshake.repository";
import type { Payment } from "../store/repository/payment.repository";
import type { DecryptionTrial } from "../store/repository/decryption-trial.repository";
import type { SavedHandshake } from "../store/repository/saved-handshake.repository";
+import type { BlockedAddress } from "../store/repository/blocked-address.repository";
/**
* Normalize string dates to Date objects in the backup data
@@ -44,6 +45,9 @@ const normalizeDates = (backup: BackupV2): BackupV2 => {
savedHandshakes: backup.savedHandshakes.map((sh) =>
normalizeDateField(sh, "createdAt")
),
+ blockedAddresses: backup.blockedAddresses.map((blockedAddress) =>
+ normalizeDateField(blockedAddress, "timestamp")
+ ),
};
};
@@ -61,10 +65,11 @@ export type BackupV2 = {
payments: Payment[];
decryptionTrials: DecryptionTrial[];
savedHandshakes: SavedHandshake[];
+ blockedAddresses: BlockedAddress[];
};
/**
- * Export contacts, conversations, messages, handshakes, payments and self handshakes,
+ * Export contacts, conversations, messages, handshakes, payments, self handshakes, and blocked addresses,
*/
export const exportData = async (
repositories: Repositories
@@ -77,6 +82,7 @@ export const exportData = async (
payments,
decryptionTrials,
savedHandshakes,
+ blockedAddresses,
] = await Promise.all([
repositories.messageRepository.getMessages(),
repositories.contactRepository.getContacts(),
@@ -85,6 +91,7 @@ export const exportData = async (
repositories.paymentRepository.getPayments(),
repositories.decryptionTrialRepository.getDecryptionTrials(),
repositories.savedHandshakeRepository.getAllSavedHandshakes(),
+ repositories.blockedAddressRepository.getBlockedAddresses(),
]);
return {
@@ -96,6 +103,7 @@ export const exportData = async (
payments,
decryptionTrials,
savedHandshakes,
+ blockedAddresses,
};
};
@@ -122,6 +130,9 @@ export const importData = async (
repositories.savedHandshakeRepository.saveBulk(
normalizedBackup.savedHandshakes
),
+ repositories.blockedAddressRepository.saveBulk(
+ normalizedBackup.blockedAddresses
+ ),
]);
} catch (error) {
// If any operation fails, the transaction will be aborted
diff --git a/src/service/storage-encryption.ts b/src/service/storage-encryption.ts
index 401e60b2..f6a363d9 100644
--- a/src/service/storage-encryption.ts
+++ b/src/service/storage-encryption.ts
@@ -38,6 +38,7 @@ export async function reEncryptMessagesForWallet(
repositories.messageRepository.reEncrypt(newPassword),
repositories.paymentRepository.reEncrypt(newPassword),
repositories.broadcastChannelRepository.reEncrypt(newPassword),
+ repositories.blockedAddressRepository.reEncrypt(newPassword),
]);
console.log(`Successfully reEncrypted messages for wallet ${walletId}`);
diff --git a/src/store/blocklist.store.ts b/src/store/blocklist.store.ts
new file mode 100644
index 00000000..e707e32a
--- /dev/null
+++ b/src/store/blocklist.store.ts
@@ -0,0 +1,195 @@
+import { create } from "zustand";
+import { useDBStore } from "./db.store";
+import { useMessagingStore } from "./messaging.store";
+import { BlockedAddress } from "./repository/blocked-address.repository";
+import { v4 } from "uuid";
+
+interface BlocklistState {
+ blockedAddresses: Set;
+ blockedAddressList: BlockedAddress[];
+ isLoaded: boolean;
+
+ loadBlockedAddresses: () => Promise;
+ blockAddressAndDeleteData: (
+ address: string,
+ reason?: string
+ ) => Promise;
+ unblockAddress: (address: string) => Promise;
+ isBlocked: (address: string) => boolean;
+ reset: () => void;
+
+ // broadcast display settings
+ broadcastBlockedDisplayMode: "hide" | "placeholder";
+ setBroadcastBlockedDisplayMode: (mode: "hide" | "placeholder") => void;
+}
+
+const BROADCAST_DISPLAY_MODE_KEY = "kasia_broadcast_blocked_display_mode";
+
+// get initial display mode from localStorage
+const getInitialDisplayMode = (): "hide" | "placeholder" => {
+ if (typeof window !== "undefined") {
+ const saved = localStorage.getItem(BROADCAST_DISPLAY_MODE_KEY);
+ if (saved === "hide" || saved === "placeholder") {
+ return saved;
+ }
+ }
+ return "placeholder"; // default to placeholder
+};
+
+export const useBlocklistStore = create((set, get) => ({
+ blockedAddresses: new Set(),
+ blockedAddressList: [],
+ isLoaded: false,
+
+ loadBlockedAddresses: async () => {
+ try {
+ const repositories = useDBStore.getState().repositories;
+ if (!repositories) {
+ console.warn(
+ "Repositories not initialized, cannot load blocked addresses"
+ );
+ return;
+ }
+
+ const blockedAddresses =
+ await repositories.blockedAddressRepository.getBlockedAddresses();
+
+ const addressSet = new Set(
+ blockedAddresses.map((addr) => addr.kaspaAddress)
+ );
+
+ set({
+ blockedAddresses: addressSet,
+ blockedAddressList: blockedAddresses,
+ isLoaded: true,
+ });
+ } catch (error) {
+ console.error("Failed to load blocked addresses:", error);
+ set({
+ blockedAddresses: new Set(),
+ blockedAddressList: [],
+ isLoaded: true,
+ });
+ }
+ },
+
+ blockAddressAndDeleteData: async (address: string, reason?: string) => {
+ try {
+ const repositories = useDBStore.getState().repositories;
+ if (!repositories) {
+ throw new Error("Repositories not initialized");
+ }
+
+ // First block the address
+ {
+ // check if already blocked
+ if (get().blockedAddresses.has(address)) {
+ console.log(`Address ${address} is already blocked`);
+ } else {
+ const newBlockedAddress: Omit = {
+ id: v4(),
+ kaspaAddress: address,
+ timestamp: new Date(),
+ reason,
+ };
+
+ await repositories.blockedAddressRepository.saveBlockedAddress(
+ newBlockedAddress
+ );
+
+ // update in-memory state
+ set((state) => {
+ const newSet = new Set(state.blockedAddresses);
+ newSet.add(address);
+ return {
+ blockedAddresses: newSet,
+ blockedAddressList: [
+ ...state.blockedAddressList,
+ { ...newBlockedAddress, tenantId: repositories.tenantId },
+ ],
+ };
+ });
+
+ console.log(`Blocked address: ${address}`);
+ }
+ }
+
+ // Try to find a contact with this address
+ const contact = await repositories.contactRepository
+ .getContactByKaspaAddress(address)
+ .catch(() => null);
+
+ if (contact) {
+ // Delete all messages, conversation, and contact data
+ await repositories.deleteAllDataForContact(contact.id, {
+ deleteConversation: true,
+ deleteContact: true,
+ });
+
+ // Remove the conversation from in-memory store
+ useMessagingStore.setState((state) => ({
+ oneOnOneConversations: state.oneOnOneConversations.filter(
+ (conversation) => conversation.contact.id !== contact.id
+ ),
+ }));
+
+ console.log(
+ `Blocked address and deleted all data for contact: ${address}`
+ );
+ } else {
+ console.log(`Blocked address (no contact found to delete): ${address}`);
+ }
+ } catch (error) {
+ console.error("Failed to block address and delete data:", error);
+ throw error;
+ }
+ },
+
+ unblockAddress: async (address: string) => {
+ try {
+ const repositories = useDBStore.getState().repositories;
+ if (!repositories) {
+ throw new Error("Repositories not initialized");
+ }
+
+ await repositories.blockedAddressRepository.deleteBlockedAddress(address);
+
+ // update in-memory state
+ set((state) => {
+ const newSet = new Set(state.blockedAddresses);
+ newSet.delete(address);
+ return {
+ blockedAddresses: newSet,
+ blockedAddressList: state.blockedAddressList.filter(
+ (addr) => addr.kaspaAddress !== address
+ ),
+ };
+ });
+
+ console.log(`Unblocked address: ${address}`);
+ } catch (error) {
+ console.error("Failed to unblock address:", error);
+ throw error;
+ }
+ },
+
+ isBlocked: (address: string) => {
+ return get().blockedAddresses.has(address);
+ },
+
+ reset: () => {
+ set({
+ blockedAddresses: new Set(),
+ blockedAddressList: [],
+ isLoaded: false,
+ });
+ },
+
+ // broadcast display settings
+ broadcastBlockedDisplayMode: getInitialDisplayMode(),
+
+ setBroadcastBlockedDisplayMode: (mode: "hide" | "placeholder") => {
+ set({ broadcastBlockedDisplayMode: mode });
+ localStorage.setItem(BROADCAST_DISPLAY_MODE_KEY, mode);
+ },
+}));
diff --git a/src/store/messaging.store.ts b/src/store/messaging.store.ts
index 9f003ba1..e19d5c81 100644
--- a/src/store/messaging.store.ts
+++ b/src/store/messaging.store.ts
@@ -49,6 +49,7 @@ import {
importData,
} from "../service/import-export-service";
import { useNetworkStore } from "./network.store";
+import { useBlocklistStore } from "./blocklist.store";
import { historicalLoader_loadSendAndReceivedHandshake } from "../utils/historical-loader";
interface MessagingState {
@@ -446,6 +447,7 @@ export const useMessagingStore = create((set, g) => {
},
hydrateOneonOneConversations: async () => {
const repositories = useDBStore.getState().repositories;
+ const blocklistStore = useBlocklistStore.getState();
const conversationWithContacts =
g().conversationManager?.getAllConversationsWithContact();
@@ -454,18 +456,25 @@ export const useMessagingStore = create((set, g) => {
return;
}
- const oneOnOneConversationPromises = conversationWithContacts.map(
- async ({
- contact,
- conversation,
- }): Promise => {
- const events = await repositories.getKasiaEventsByConversationId(
- conversation.id
- );
-
- return { conversation, contact, events };
- }
+ // filter out blocked contacts before hydrating
+ const unBlockedConversationWithContacts = conversationWithContacts.filter(
+ ({ contact }) =>
+ !blocklistStore.blockedAddresses.has(contact.kaspaAddress)
);
+
+ const oneOnOneConversationPromises =
+ unBlockedConversationWithContacts.map(
+ async ({
+ contact,
+ conversation,
+ }): Promise => {
+ const events = await repositories.getKasiaEventsByConversationId(
+ conversation.id
+ );
+
+ return { conversation, contact, events };
+ }
+ );
const oneOnOneConversations = await Promise.all(
oneOnOneConversationPromises
);
@@ -520,6 +529,15 @@ export const useMessagingStore = create((set, g) => {
? transaction.recipientAddress
: transaction.senderAddress;
+ // check if participant is blocked - skip processing
+ const blocklistStore = useBlocklistStore.getState();
+ if (blocklistStore.blockedAddresses.has(participantAddress)) {
+ console.log(
+ `Skipping transaction from blocked address: ${participantAddress}`
+ );
+ continue;
+ }
+
if (
await repositories.doesKasiaEventExistsById(
`${unlockedWallet.id}_${transaction.transactionId}`
@@ -953,6 +971,7 @@ export const useMessagingStore = create((set, g) => {
repositories.handshakeRepository.deleteTenant(walletTenant),
repositories.messageRepository.deleteTenant(walletTenant),
repositories.savedHandshakeRepository.deleteTenant(walletTenant),
+ repositories.blockedAddressRepository.deleteTenant(walletTenant),
]);
// 3. Reset metadata
@@ -965,6 +984,8 @@ export const useMessagingStore = create((set, g) => {
isCreatingNewChat: false,
});
+ useBlocklistStore.getState().reset();
+
// 5. Clear and reinitialize conversation manager
const manager = g().conversationManager;
if (manager) {
@@ -1002,6 +1023,8 @@ export const useMessagingStore = create((set, g) => {
await g()?.conversationManager?.loadConversations();
await g().hydrateOneonOneConversations();
+
+ await useBlocklistStore.getState().loadBlockedAddresses();
},
conversationManager: null,
initiateHandshake: async (
diff --git a/src/store/repository/blocked-address.repository.ts b/src/store/repository/blocked-address.repository.ts
new file mode 100644
index 00000000..5f37805c
--- /dev/null
+++ b/src/store/repository/blocked-address.repository.ts
@@ -0,0 +1,191 @@
+import { encryptXChaCha20Poly1305, decryptXChaCha20Poly1305 } from "kaspa-wasm";
+import { KasiaDB, DBNotFoundException } from "./db";
+
+export type DbBlockedAddress = {
+ /**
+ * `uuidv4()`
+ */
+ id: string;
+ /**
+ * tenant is the selected wallet
+ */
+ tenantId: string;
+ timestamp: Date;
+ /**
+ * encrypted data shaped as `json(BlockedAddressBag)`
+ */
+ encryptedData: string;
+};
+
+export type BlockedAddressBag = {
+ kaspaAddress: string;
+ reason?: string;
+};
+
+export type BlockedAddress = BlockedAddressBag &
+ Omit;
+
+export class BlockedAddressRepository {
+ constructor(
+ readonly db: KasiaDB,
+ readonly tenantId: string,
+ readonly walletPassword: string
+ ) {}
+
+ async getBlockedAddressByKaspaAddress(
+ kaspaAddress: string
+ ): Promise {
+ const result = await this.getBlockedAddresses().then((addresses) => {
+ return addresses.find((addr) => addr.kaspaAddress === kaspaAddress);
+ });
+
+ if (!result) {
+ throw new DBNotFoundException();
+ }
+
+ return result;
+ }
+
+ async getBlockedAddresses(): Promise {
+ return this.db
+ .getAllFromIndex("blockedAddresses", "by-tenant-id", this.tenantId)
+ .then((dbBlockedAddresses) => {
+ return dbBlockedAddresses.map((dbBlockedAddress) => {
+ return this._dbBlockedAddressToBlockedAddress(dbBlockedAddress);
+ });
+ });
+ }
+
+ async isAddressBlocked(kaspaAddress: string): Promise {
+ try {
+ await this.getBlockedAddressByKaspaAddress(kaspaAddress);
+ return true;
+ } catch (error) {
+ if (error instanceof DBNotFoundException) {
+ return false;
+ }
+ throw error;
+ }
+ }
+
+ async saveBlockedAddress(
+ blockedAddress: Omit
+ ): Promise {
+ // check if already blocked
+ const isBlocked = await this.isAddressBlocked(blockedAddress.kaspaAddress);
+ if (isBlocked) {
+ throw new Error("Address is already blocked");
+ }
+
+ return this.db.put(
+ "blockedAddresses",
+ this._blockedAddressToDbBlockedAddress({
+ ...blockedAddress,
+ tenantId: this.tenantId,
+ })
+ );
+ }
+
+ async deleteBlockedAddress(kaspaAddress: string): Promise {
+ const blockedAddress =
+ await this.getBlockedAddressByKaspaAddress(kaspaAddress);
+ await this.db.delete("blockedAddresses", blockedAddress.id);
+ }
+
+ async saveBulk(
+ blockedAddresses: Omit[]
+ ): Promise {
+ const tx = this.db.transaction("blockedAddresses", "readwrite");
+ const store = tx.objectStore("blockedAddresses");
+
+ for (const blockedAddress of blockedAddresses) {
+ // check if address already exists
+ const existing = await store.get(blockedAddress.id);
+ if (!existing) {
+ await store.put(
+ this._blockedAddressToDbBlockedAddress({
+ ...blockedAddress,
+ tenantId: this.tenantId,
+ })
+ );
+ }
+ }
+
+ await tx.done;
+ }
+
+ async reEncrypt(newPassword: string): Promise {
+ const transaction = this.db.transaction("blockedAddresses", "readwrite");
+ const store = transaction.objectStore("blockedAddresses");
+ const index = store.index("by-tenant-id");
+ const cursor = await index.openCursor(IDBKeyRange.only(this.tenantId));
+ if (!cursor) {
+ return;
+ }
+
+ do {
+ const dbBlockedAddress = cursor.value;
+ // decrypt with old password
+ const decryptedData = decryptXChaCha20Poly1305(
+ dbBlockedAddress.encryptedData,
+ this.walletPassword
+ );
+ // re-encrypt with new password
+ const reEncryptedData = encryptXChaCha20Poly1305(
+ decryptedData,
+ newPassword
+ );
+ // update in database
+ await cursor.update({
+ ...dbBlockedAddress,
+ encryptedData: reEncryptedData,
+ });
+ } while (await cursor.continue());
+ }
+
+ async deleteTenant(tenantId: string): Promise {
+ const keys = await this.db.getAllKeysFromIndex(
+ "blockedAddresses",
+ "by-tenant-id",
+ tenantId
+ );
+
+ await Promise.all(keys.map((k) => this.db.delete("blockedAddresses", k)));
+ }
+
+ private _blockedAddressToDbBlockedAddress(
+ blockedAddress: BlockedAddress
+ ): DbBlockedAddress {
+ return {
+ id: blockedAddress.id,
+ encryptedData: encryptXChaCha20Poly1305(
+ JSON.stringify({
+ kaspaAddress: blockedAddress.kaspaAddress,
+ reason: blockedAddress.reason,
+ } satisfies BlockedAddressBag),
+ this.walletPassword
+ ),
+ timestamp: blockedAddress.timestamp,
+ tenantId: this.tenantId,
+ };
+ }
+
+ private _dbBlockedAddressToBlockedAddress(
+ dbBlockedAddress: DbBlockedAddress
+ ): BlockedAddress {
+ const blockedAddressBag = JSON.parse(
+ decryptXChaCha20Poly1305(
+ dbBlockedAddress.encryptedData,
+ this.walletPassword
+ )
+ ) as BlockedAddressBag;
+
+ return {
+ tenantId: dbBlockedAddress.tenantId,
+ id: dbBlockedAddress.id,
+ timestamp: dbBlockedAddress.timestamp,
+ kaspaAddress: blockedAddressBag.kaspaAddress,
+ reason: blockedAddressBag.reason,
+ };
+ }
+}
diff --git a/src/store/repository/db.ts b/src/store/repository/db.ts
index cd120317..68a4be82 100644
--- a/src/store/repository/db.ts
+++ b/src/store/repository/db.ts
@@ -22,8 +22,12 @@ import {
BroadcastChannelRepository,
} from "./broadcast-channel.repository";
import { MetaRespository } from "./meta.repository";
+import {
+ DbBlockedAddress,
+ BlockedAddressRepository,
+} from "./blocked-address.repository";
-const CURRENT_DB_VERSION = 4;
+const CURRENT_DB_VERSION = 5;
export class DBNotFoundException extends Error {
constructor() {
@@ -111,6 +115,14 @@ export interface KasiaDBSchema extends DBSchema {
"by-tenant-id": string;
};
};
+ blockedAddresses: {
+ key: string;
+ value: DbBlockedAddress;
+ indexes: {
+ "by-tenant-id": string;
+ "by-tenant-id-timestamp": [string, Date];
+ };
+ };
}
export type KasiaDB = IDBPDatabase;
@@ -260,6 +272,20 @@ export const openDatabase = async (): Promise => {
}
if (oldVersion <= 3) {
+ // blocked addresses
+ const blockedAddressesStore = db.createObjectStore("blockedAddresses", {
+ keyPath: "id",
+ });
+ blockedAddressesStore.createIndex("by-tenant-id", "tenantId");
+ blockedAddressesStore.createIndex("by-tenant-id-timestamp", [
+ "tenantId",
+ "timestamp",
+ ]);
+
+ console.log("Database schema upgraded to v4 - blocked addresses");
+ }
+
+ if (oldVersion <= 4) {
// Migration to v4: Add status field to existing messages and payments
const messagesStore = transaction.objectStore("messages");
let cursor = await messagesStore.openCursor();
@@ -291,7 +317,7 @@ export const openDatabase = async (): Promise => {
"[DB] - Migrated to v4: Added status field to messages and payments"
);
}
- if (oldVersion <= 4) {
+ if (oldVersion <= 5) {
// HERE next migration, first increase CURRENT_DB_VERSION then implement with oldVersion <= CURRENT_DB_VERSION - 1
// add more if branching for each next version
}
@@ -308,6 +334,7 @@ export class Repositories {
public readonly handshakeRepository: HandshakeRepository;
public readonly savedHandshakeRepository: SavedHandhshakeRepository;
public readonly broadcastChannelRepository: BroadcastChannelRepository;
+ public readonly blockedAddressRepository: BlockedAddressRepository;
public readonly metadataRepository: MetaRespository;
constructor(
@@ -359,6 +386,12 @@ export class Repositories {
walletPassword
);
+ this.blockedAddressRepository = new BlockedAddressRepository(
+ db,
+ tenantId,
+ walletPassword
+ );
+
this.metadataRepository = new MetaRespository(tenantId);
}
@@ -392,4 +425,54 @@ export class Repositories {
]);
return !!message || !!payment || !!handshake;
}
+
+ /**
+ * delete all messages, payments, handshakes and optionally conversation and contact for a given contact
+ */
+ async deleteAllDataForContact(
+ contactId: string,
+ options?: { deleteConversation?: boolean; deleteContact?: boolean }
+ ): Promise {
+ try {
+ // get the conversation for this contact
+ const conversation = await this.conversationRepository
+ .getConversationByContactId(contactId)
+ .catch(() => null);
+
+ if (conversation) {
+ // delete all messages, payments, and handshakes for this conversation
+ const [messages, payments, handshakes] = await Promise.all([
+ this.messageRepository.getMessagesByConversationId(conversation.id),
+ this.paymentRepository.getPaymentsByConversationId(conversation.id),
+ this.handshakeRepository.getHanshakesByConversationId(
+ conversation.id
+ ),
+ ]);
+
+ // delete all events
+ await Promise.all([
+ ...messages.map((m) => this.messageRepository.deleteMessage(m.id)),
+ ...payments.map((p) => this.paymentRepository.deletePayment(p.id)),
+ ...handshakes.map((h) =>
+ this.handshakeRepository.deleteHandshake(h.id)
+ ),
+ ]);
+
+ // delete conversation if requested
+ if (options?.deleteConversation) {
+ await this.conversationRepository.deleteConversation(conversation.id);
+ }
+ }
+
+ // delete contact if requested
+ if (options?.deleteContact) {
+ await this.contactRepository.deleteContact(contactId);
+ }
+
+ console.log(`Deleted all data for contact ${contactId}`);
+ } catch (error) {
+ console.error("Error deleting contact data:", error);
+ throw error;
+ }
+ }
}
diff --git a/src/store/ui.store.ts b/src/store/ui.store.ts
index 175fdaec..dc909c5c 100644
--- a/src/store/ui.store.ts
+++ b/src/store/ui.store.ts
@@ -15,6 +15,7 @@ export type ModalType =
| "delete"
| "seed"
| "settings"
+ | "settings-unlocked"
| "contact-info-modal"
| "image"
| "new-chat"
@@ -22,7 +23,8 @@ export type ModalType =
| "broadcast-participant-info"
| "qr-scanner"
| "offchain-handshake"
- | "donation";
+ | "donation"
+ | "confirm";
type Theme = "light" | "dark" | "system" | "custom";
type UiState = {
@@ -65,6 +67,17 @@ type UiState = {
// Delete wallet modal state
pendingDeleteWalletId: string | null;
setPendingDeleteWalletId: (id: string | null) => void;
+
+ // Confirmation modal state
+ confirmationConfig: {
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ onConfirm: () => void;
+ onCancel?: () => void;
+ } | null;
+ setConfirmationConfig: (config: UiState["confirmationConfig"]) => void;
};
// Get initial theme from localStorage or default to system
@@ -224,4 +237,8 @@ export const useUiStore = create()((set, get) => ({
// Delete wallet state
pendingDeleteWalletId: null,
setPendingDeleteWalletId: (id) => set({ pendingDeleteWalletId: id }),
+
+ // Confirmation modal state
+ confirmationConfig: null,
+ setConfirmationConfig: (config) => set({ confirmationConfig: config }),
}));
diff --git a/src/utils/historical-loader.ts b/src/utils/historical-loader.ts
index c3d3bdfd..6bb22a70 100644
--- a/src/utils/historical-loader.ts
+++ b/src/utils/historical-loader.ts
@@ -14,6 +14,7 @@ import {
import { MetadataV1 } from "../store/repository/meta.repository";
import { ConversationManagerService } from "../service/conversation-manager-service";
import { Handshake } from "../store/repository/handshake.repository";
+import { useBlocklistStore } from "../store/blocklist.store";
export const historicalLoader_loadSendAndReceivedHandshake = async (
repositories: Repositories,
@@ -122,6 +123,15 @@ export const historicalLoader_loadSendAndReceivedHandshake = async (
continue;
}
+ // if handshake is in blocked list, ignore
+ const blocklistStore = useBlocklistStore.getState();
+ if (blocklistStore.blockedAddresses.has(handshake.sender)) {
+ console.log(
+ `Skipping historical handshake processing for blocked address: ${handshake.sender}`
+ );
+ continue;
+ }
+
try {
const encryptedMessage = new EncryptedMessage(handshake.message_payload);
@@ -138,6 +148,7 @@ export const historicalLoader_loadSendAndReceivedHandshake = async (
// only consider the last saved handshake for our recipient
// since we loop by last first, we can skip this iteration if already exist
const handshakes = lastReceivedHSBySenderAddress.get(handshake.sender);
+
if (!handshakes) {
lastReceivedHSBySenderAddress.set(handshake.sender, {
handshake,
From d48ece3ec65b060c061670671ce78979f1824906 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 9 Jan 2026 09:50:05 +0300
Subject: [PATCH 66/81] fix: clear in-memory address state
---
src/service/conversation-manager-service.ts | 27 +++++++++++++++++++++
src/store/blocklist.store.ts | 5 ++++
2 files changed, 32 insertions(+)
diff --git a/src/service/conversation-manager-service.ts b/src/service/conversation-manager-service.ts
index 027d18e5..388c9a9b 100644
--- a/src/service/conversation-manager-service.ts
+++ b/src/service/conversation-manager-service.ts
@@ -350,6 +350,33 @@ export class ConversationManagerService {
return true;
}
+ // clears in-memory (non db) state for a conversation by address
+ // needed edge case of blocking > ublocking and tryint to re add contact
+ public clearConversationByAddress(address: string): boolean {
+ const conversationId = this.addressToConversation.get(address);
+ if (!conversationId) return false;
+
+ const conversationWithContact =
+ this.conversationWithContactByConversationId.get(conversationId);
+ if (!conversationWithContact) {
+ // just clear the address mapping if conversation not found
+ this.addressToConversation.delete(address);
+ return true;
+ }
+
+ const { conversation } = conversationWithContact;
+
+ // clear all in-memory maps
+ this.conversationWithContactByConversationId.delete(conversationId);
+ this.addressToConversation.delete(address);
+ this.aliasToConversation.delete(conversation.myAlias);
+ if (conversation.theirAlias) {
+ this.aliasToConversation.delete(conversation.theirAlias);
+ }
+
+ return true;
+ }
+
public async updateConversation(
conversation: Pick & Partial
) {
diff --git a/src/store/blocklist.store.ts b/src/store/blocklist.store.ts
index e707e32a..1f4b3c39 100644
--- a/src/store/blocklist.store.ts
+++ b/src/store/blocklist.store.ts
@@ -133,6 +133,11 @@ export const useBlocklistStore = create((set, get) => ({
),
}));
+ // clear conversation manager's in-memory (non db) state for this address
+ const conversationManager =
+ useMessagingStore.getState().conversationManager;
+ conversationManager?.clearConversationByAddress(address);
+
console.log(
`Blocked address and deleted all data for contact: ${address}`
);
From 891a8bc00789081ab5dc45eb06ce0e43acd4e127 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 9 Jan 2026 09:57:34 +0300
Subject: [PATCH 67/81] style: minor bg color
---
src/components/Modals/SubSettings/BlockList.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Modals/SubSettings/BlockList.tsx b/src/components/Modals/SubSettings/BlockList.tsx
index 4ff63331..323fdcc1 100644
--- a/src/components/Modals/SubSettings/BlockList.tsx
+++ b/src/components/Modals/SubSettings/BlockList.tsx
@@ -98,7 +98,7 @@ export const BlockList: React.FC = () => {
{blocklistStore.blockedAddressList.map((blocked) => (
From 533770b5469c46ac9f00e09088ec412f138b3963 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 29 Nov 2025 12:11:04 +0300
Subject: [PATCH 68/81] feat: add react markdown and sani
---
package-lock.json | 1291 +++++++++++++++++++++++++++++++++++++++++++--
package.json | 4 +-
2 files changed, 1244 insertions(+), 51 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 56d21e31..bb288fd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,8 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "7.12.0",
+ "react-markdown": "^10.1.0",
+ "rehype-sanitize": "^6.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
@@ -4156,6 +4158,15 @@
"@types/deep-eql": "*"
}
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"dev": true,
@@ -4165,11 +4176,44 @@
"version": "1.0.8",
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
@@ -4190,7 +4234,6 @@
},
"node_modules/@types/react": {
"version": "19.1.0",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -4214,6 +4257,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/uuid": {
"version": "10.0.0",
"dev": true,
@@ -4438,6 +4487,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
"node_modules/@vitejs/plugin-react-swc": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz",
@@ -4845,6 +4900,16 @@
"@babel/types": "^7.26.0"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"dev": true,
@@ -5070,6 +5135,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chai": {
"version": "5.2.1",
"dev": true,
@@ -5100,6 +5175,46 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/check-error": {
"version": "2.1.1",
"dev": true,
@@ -5187,6 +5302,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@@ -5539,7 +5664,6 @@
},
"node_modules/csstype": {
"version": "3.1.3",
- "devOptional": true,
"license": "MIT"
},
"node_modules/data-view-buffer": {
@@ -5592,7 +5716,6 @@
},
"node_modules/debug": {
"version": "4.4.1",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5613,6 +5736,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-eql": {
"version": "5.0.2",
"dev": true,
@@ -5710,7 +5846,6 @@
},
"node_modules/dequal": {
"version": "2.0.3",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -5728,6 +5863,19 @@
"node": ">=8"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"license": "MIT"
@@ -6232,6 +6380,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/estree-walker": {
"version": "2.0.2",
"dev": true,
@@ -6257,6 +6415,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
@@ -6883,6 +7047,61 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"dev": true,
@@ -6896,6 +7115,16 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/husky": {
"version": "9.1.7",
"dev": true,
@@ -6959,6 +7188,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"dev": true,
@@ -6972,6 +7207,30 @@
"node": ">= 0.4"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"dev": true,
@@ -7091,6 +7350,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-docker": {
"version": "3.0.0",
"dev": true,
@@ -7162,6 +7431,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-inside-container": {
"version": "1.0.0",
"dev": true,
@@ -7239,6 +7518,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"dev": true,
@@ -7996,66 +8287,671 @@
"dev": true,
"license": "MIT"
},
- "node_modules/loupe": {
- "version": "3.2.0",
- "dev": true,
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.525.0",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT"
},
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "dev": true,
- "license": "ISC",
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
"dependencies": {
- "yallist": "^3.0.2"
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/lucide-react": {
- "version": "0.525.0",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/lz-string": {
- "version": "1.5.0",
- "dev": true,
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "bin": {
- "lz-string": "bin/bin.js"
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/magic-string": {
- "version": "0.30.17",
- "dev": true,
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "dev": true,
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/mdn-data": {
- "version": "2.12.2",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
- "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
- "dev": true,
- "license": "CC0-1.0"
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
@@ -8149,7 +9045,6 @@
},
"node_modules/ms": {
"version": "2.1.3",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -8381,6 +9276,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"license": "MIT",
@@ -9790,6 +10710,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"dev": true,
@@ -9873,6 +10803,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-router": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
@@ -10006,6 +10963,53 @@
"node": ">=6"
}
},
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"license": "MIT",
@@ -10445,6 +11449,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"dev": true,
@@ -10558,6 +11572,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/stringify-object": {
"version": "3.3.0",
"dev": true,
@@ -10616,6 +11644,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/stylehacks": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.7.tgz",
@@ -10874,6 +11920,26 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -11095,6 +12161,25 @@
"node": ">=4"
}
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/unique-string": {
"version": "2.0.0",
"dev": true,
@@ -11106,6 +12191,74 @@
"node": ">=8"
}
},
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/universalify": {
"version": "2.0.1",
"dev": true,
@@ -11178,6 +12331,34 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
@@ -12088,6 +13269,16 @@
}
}
},
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"vendors/tauri-plugin-biometry": {
"name": "@choochmeque/tauri-plugin-biometry-api",
"version": "0.2.1",
@@ -12328,4 +13519,4 @@
"license": "ISC"
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index f58143fa..56654af3 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,8 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "7.12.0",
+ "react-markdown": "^10.1.0",
+ "rehype-sanitize": "^6.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
@@ -84,4 +86,4 @@
"@rollup/rollup-linux-x64-gnu": "4.42.0",
"lightningcss-linux-x64-gnu": "1.30.1"
}
-}
+}
\ No newline at end of file
From d1329715856a308e0d25dc6176be208cc24bd3d2 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 29 Nov 2025 12:14:35 +0300
Subject: [PATCH 69/81] feat: add markdown rendering on flag
---
.../Utilities/Content/MessageContent.tsx | 21 ++++++++++++++++---
src/config/constants.ts | 2 ++
2 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
index 4a09c285..7d0d686b 100644
--- a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
@@ -1,6 +1,9 @@
import { FC, useState, useRef, useEffect } from "react";
import clsx from "clsx";
+import ReactMarkdown from "react-markdown";
+import rehypeSanitize from "rehype-sanitize";
import { parseMessageForDisplay } from "../../../../utils/message-format";
+import { MARKDOWN_PREFIX } from "../../../../config/constants";
type MessageContentProps = {
content: string;
@@ -39,9 +42,21 @@ export const MessageContent: FC
= ({
);
}
- // render plain text with newlines as and \\n as literal text
+ // render content (markdown or plain text) with expand/collapse functionality
if (typeof content === "string") {
- const parsedContent = parseMessageForDisplay(content);
+ // check if content starts with markdown flag and process content
+ const isMarkdown = content.startsWith(MARKDOWN_PREFIX);
+ const displayContent = isMarkdown
+ ? content.slice(MARKDOWN_PREFIX.length)
+ : content;
+
+ const renderedContent = isMarkdown ? (
+
+ {displayContent}
+
+ ) : (
+ parseMessageForDisplay(displayContent)
+ );
return (
@@ -61,7 +76,7 @@ export const MessageContent: FC = ({
: "none",
}}
>
- {parsedContent}
+ {renderedContent}
{shouldCollapse && (
diff --git a/src/config/constants.ts b/src/config/constants.ts
index 53f6c6f2..1b1f3bea 100644
--- a/src/config/constants.ts
+++ b/src/config/constants.ts
@@ -31,6 +31,8 @@ export const MAX_CHAT_INPUT_CHAR = 18000;
// broadcast channel name length limit, at 36 due to having parity with a uuid char length
export const MAX_BROADCAST_CHANNEL_NAME = 36;
+export const MARKDOWN_PREFIX = "§";
+
// Triggers a warn in the wallet to withdraw to cold storage.
// Settings at 30KAS
export const BALANCE_WARN = BigInt(30 * 100_000_000);
From 31a5648b238fecbf815f13ba49481a85946cb1ec Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 29 Nov 2025 12:15:13 +0300
Subject: [PATCH 70/81] feat: add featflag to settings and prepend to message
if enabled
---
src/hooks/MessageComposer/useBroadcastComposer.ts | 14 ++++++++++++--
src/hooks/MessageComposer/useMessageComposer.ts | 12 ++++++++++++
src/store/featureflag.store.ts | 15 ++++++++++++++-
3 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/src/hooks/MessageComposer/useBroadcastComposer.ts b/src/hooks/MessageComposer/useBroadcastComposer.ts
index 9a10312d..0dd4087d 100644
--- a/src/hooks/MessageComposer/useBroadcastComposer.ts
+++ b/src/hooks/MessageComposer/useBroadcastComposer.ts
@@ -1,11 +1,18 @@
import { useComposerStore } from "../../store/message-composer.store";
import { useWalletStore } from "../../store/wallet.store";
import { useBroadcastStore } from "../../store/broadcast.store";
+import {
+ useFeatureFlagsStore,
+ FeatureFlags,
+} from "../../store/featureflag.store";
+import { MARKDOWN_PREFIX } from "../../config/constants";
import { toast } from "../../utils/toast-helper";
export const useBroadcastComposer = (channelName?: string) => {
const { sendState, setSendState, clearDraft } = useComposerStore();
const { addMessage, updateMessageStatus } = useBroadcastStore();
+ const { flags } = useFeatureFlagsStore();
+ const markdownEnabled = flags[FeatureFlags.MARKDOWN];
const draft = useComposerStore((s) =>
channelName ? s.drafts[channelName] || "" : ""
@@ -34,8 +41,11 @@ export const useBroadcastComposer = (channelName?: string) => {
try {
setSendState({ status: "loading" });
+ // prepend markdown prefix if markdown is enabled
+ const messageToSend = markdownEnabled ? MARKDOWN_PREFIX + draft : draft;
+
const txId = await walletStore.sendBroadcastWithContext({
- message: draft,
+ message: messageToSend,
channelName: channelName.toLowerCase(),
priorityFee: useComposerStore.getState().priority,
});
@@ -46,7 +56,7 @@ export const useBroadcastComposer = (channelName?: string) => {
id: txId,
channelName: channelName.toLowerCase(),
senderAddress: walletStore.address!.toString(),
- content: draft,
+ content: messageToSend,
timestamp: new Date(),
transactionId: txId,
status: "pending",
diff --git a/src/hooks/MessageComposer/useMessageComposer.ts b/src/hooks/MessageComposer/useMessageComposer.ts
index d70b2905..b96c8cea 100644
--- a/src/hooks/MessageComposer/useMessageComposer.ts
+++ b/src/hooks/MessageComposer/useMessageComposer.ts
@@ -1,6 +1,11 @@
import { useComposerStore } from "../../store/message-composer.store";
import { useWalletStore } from "../../store/wallet.store";
import { useMessagingStore } from "../../store/messaging.store";
+import {
+ useFeatureFlagsStore,
+ FeatureFlags,
+} from "../../store/featureflag.store";
+import { MARKDOWN_PREFIX } from "../../config/constants";
import { Address } from "kaspa-wasm";
import { toast } from "../../utils/toast-helper";
import { unknownErrorToErrorLike } from "../../utils/errors";
@@ -19,6 +24,8 @@ export const useMessageComposer = (feeState: FeeState, recipient?: string) => {
setAttachment,
clearDraft,
} = useComposerStore();
+ const { flags } = useFeatureFlagsStore();
+ const markdownEnabled = flags[FeatureFlags.MARKDOWN];
const draft = useComposerStore((s) =>
recipient ? s.drafts[recipient] || "" : ""
);
@@ -84,6 +91,11 @@ export const useMessageComposer = (feeState: FeeState, recipient?: string) => {
let messageToSend = draft;
let fileDataForStorage: FileData | null = null;
+ // prepend markdown prefix if markdown is enabled and there's text content
+ if (markdownEnabled && draft && !attachment) {
+ messageToSend = MARKDOWN_PREFIX + draft;
+ }
+
if (attachment) {
messageToSend = attachment.content; // always send attachment payload
try {
diff --git a/src/store/featureflag.store.ts b/src/store/featureflag.store.ts
index d232c39a..d8f66ee4 100644
--- a/src/store/featureflag.store.ts
+++ b/src/store/featureflag.store.ts
@@ -1,11 +1,18 @@
import { create } from "zustand";
-import { Coins, LucideIcon, Radio, Camera } from "lucide-react";
+import {
+ Coins,
+ LucideIcon,
+ Radio,
+ Camera,
+ MessageSquareText,
+} from "lucide-react";
import { cameraPermissionService } from "../service/camera-permission-service";
export enum FeatureFlags {
BROADCAST = "broadcast",
CUSTOM_FEE = "customfee",
ENABLED_CAMERA = "enabledcamera",
+ MARKDOWN = "markdown",
}
export type FeatureFlagsTable = Record;
@@ -14,6 +21,7 @@ const defaultFeatureFlagsTable: FeatureFlagsTable = {
[FeatureFlags.BROADCAST]: false,
[FeatureFlags.CUSTOM_FEE]: false,
[FeatureFlags.ENABLED_CAMERA]: false,
+ [FeatureFlags.MARKDOWN]: false,
};
export interface FeatureDescription {
@@ -42,6 +50,11 @@ const featureFlips: FeatureFlips = {
desc: `Unencrypted open messages.\nLive messages only, no storage.\nReminder: Broadcasts are unencrypted.`,
icon: Radio,
},
+ [FeatureFlags.MARKDOWN]: {
+ label: "Markdown Messages",
+ desc: `Enable markdown formatting for all messages sent.`,
+ icon: MessageSquareText,
+ },
};
const useFeatureFlagsStore = create<{
From 626de3469595b6710a68dfadb4286269c760fa40 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sat, 29 Nov 2025 12:29:20 +0300
Subject: [PATCH 71/81] feat: all prefix to message inputs / construction
---
package-lock.json | 58 +++++++++++++++++++
package.json | 1 +
.../Composing/Broadcast/BroadcastComposer.tsx | 16 ++++-
.../Composing/Directs/DirectComposer.tsx | 21 +++++--
.../Composing/Utilities/InputBasic.tsx | 15 ++++-
.../Utilities/Content/MessageContent.tsx | 20 +++++--
.../MessageComposer/useBroadcastComposer.ts | 3 +-
.../MessageComposer/useMessageComposer.ts | 11 ----
src/store/featureflag.store.ts | 2 +-
9 files changed, 121 insertions(+), 26 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index bb288fd6..093d5136 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"react-router": "7.12.0",
"react-markdown": "^10.1.0",
"rehype-sanitize": "^6.0.0",
+ "remark-breaks": "^4.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
@@ -8341,6 +8342,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -8425,6 +8454,20 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-newline-to-break": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz",
+ "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-find-and-replace": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-phrasing": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
@@ -10977,6 +11020,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark-breaks": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
+ "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-newline-to-break": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
diff --git a/package.json b/package.json
index 56654af3..87a64c67 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"react-router": "7.12.0",
"react-markdown": "^10.1.0",
"rehype-sanitize": "^6.0.0",
+ "remark-breaks": "^4.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
diff --git a/src/components/MessagesPane/Composing/Broadcast/BroadcastComposer.tsx b/src/components/MessagesPane/Composing/Broadcast/BroadcastComposer.tsx
index b2040bba..d4748ff6 100644
--- a/src/components/MessagesPane/Composing/Broadcast/BroadcastComposer.tsx
+++ b/src/components/MessagesPane/Composing/Broadcast/BroadcastComposer.tsx
@@ -11,6 +11,11 @@ import { MessageInput } from "../Utilities/MessageInput";
import { FeeDisplay } from "../Utilities/FeeDisplay";
import { useFeeEstimate } from "../../../../hooks/MessageComposer/useFeeEstimate";
import { useIsMobile } from "../../../../hooks/useIsMobile";
+import {
+ useFeatureFlagsStore,
+ FeatureFlags,
+} from "../../../../store/featureflag.store";
+import { MARKDOWN_PREFIX } from "../../../../config/constants";
export const BroadcastComposer = ({ channelName }: { channelName: string }) => {
const setDraft = useComposerStore((s: ComposerState) => s.setDraft);
@@ -26,6 +31,9 @@ export const BroadcastComposer = ({ channelName }: { channelName: string }) => {
const messageInputRef = useRef(null);
const isMobile = useIsMobile();
+ const { flags } = useFeatureFlagsStore();
+ const markdownEnabled = flags[FeatureFlags.MARKDOWN];
+
// readines check
const canBroadcast = !!channelName && !!sendBroadcast;
@@ -71,7 +79,13 @@ export const BroadcastComposer = ({ channelName }: { channelName: string }) => {
{
const cameraInputRef = useRef(null);
const isMobile = useIsMobile();
+ const { flags } = useFeatureFlagsStore();
+ const cameraEnabled = flags[FeatureFlags.ENABLED_CAMERA];
+ const markdownEnabled = flags[FeatureFlags.MARKDOWN];
+
const feeState = useFeeEstimate({
toSelf: true,
recipient,
@@ -64,10 +71,6 @@ export const DirectComposer = ({ recipient }: { recipient?: string }) => {
(conversation.status === "active" ||
(conversation.status === "pending" && conversation.initiatedByMe));
- // Check if camera feature is enabled
- const { flags } = useFeatureFlagsStore();
- const cameraEnabled = flags[FeatureFlags.ENABLED_CAMERA];
-
const guardReady = () => {
if (!canCompose) {
toast.error("Accept or send handshake to chat");
@@ -219,7 +222,13 @@ export const DirectComposer = ({ recipient }: { recipient?: string }) => {
(
const [showScroll, setShowScroll] = useState(false);
const isMobile = useIsMobile();
+ const markdownEnabled = useFeatureFlagsStore(
+ (state) => state.flags.markdown
+ );
+
const handleChange = (e: React.ChangeEvent) => {
- onChange(e.target.value);
+ let newValue = e.target.value;
+
+ // add markdown prefix if markdown is enabled
+ if (markdownEnabled) {
+ newValue = MARKDOWN_PREFIX + newValue;
+ }
+
+ onChange(newValue);
// auto-resize logic - measure with minimal height
const originalHeight = e.target.style.height;
diff --git a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
index 7d0d686b..abeeee09 100644
--- a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
@@ -1,10 +1,17 @@
import { FC, useState, useRef, useEffect } from "react";
import clsx from "clsx";
import ReactMarkdown from "react-markdown";
-import rehypeSanitize from "rehype-sanitize";
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
+import remarkBreaks from "remark-breaks";
import { parseMessageForDisplay } from "../../../../utils/message-format";
import { MARKDOWN_PREFIX } from "../../../../config/constants";
+// extend default schema to ensure br tags are allowed
+const sanitizeSchema = {
+ ...defaultSchema,
+ tagNames: [...(defaultSchema.tagNames || []), "br"],
+};
+
type MessageContentProps = {
content: string;
isDecrypting: boolean;
@@ -51,9 +58,14 @@ export const MessageContent: FC = ({
: content;
const renderedContent = isMarkdown ? (
-
- {displayContent}
-
+
+
+ {displayContent}
+
+
) : (
parseMessageForDisplay(displayContent)
);
diff --git a/src/hooks/MessageComposer/useBroadcastComposer.ts b/src/hooks/MessageComposer/useBroadcastComposer.ts
index 0dd4087d..a853416c 100644
--- a/src/hooks/MessageComposer/useBroadcastComposer.ts
+++ b/src/hooks/MessageComposer/useBroadcastComposer.ts
@@ -41,8 +41,7 @@ export const useBroadcastComposer = (channelName?: string) => {
try {
setSendState({ status: "loading" });
- // prepend markdown prefix if markdown is enabled
- const messageToSend = markdownEnabled ? MARKDOWN_PREFIX + draft : draft;
+ const messageToSend = draft;
const txId = await walletStore.sendBroadcastWithContext({
message: messageToSend,
diff --git a/src/hooks/MessageComposer/useMessageComposer.ts b/src/hooks/MessageComposer/useMessageComposer.ts
index b96c8cea..c1749c87 100644
--- a/src/hooks/MessageComposer/useMessageComposer.ts
+++ b/src/hooks/MessageComposer/useMessageComposer.ts
@@ -1,10 +1,6 @@
import { useComposerStore } from "../../store/message-composer.store";
import { useWalletStore } from "../../store/wallet.store";
import { useMessagingStore } from "../../store/messaging.store";
-import {
- useFeatureFlagsStore,
- FeatureFlags,
-} from "../../store/featureflag.store";
import { MARKDOWN_PREFIX } from "../../config/constants";
import { Address } from "kaspa-wasm";
import { toast } from "../../utils/toast-helper";
@@ -24,8 +20,6 @@ export const useMessageComposer = (feeState: FeeState, recipient?: string) => {
setAttachment,
clearDraft,
} = useComposerStore();
- const { flags } = useFeatureFlagsStore();
- const markdownEnabled = flags[FeatureFlags.MARKDOWN];
const draft = useComposerStore((s) =>
recipient ? s.drafts[recipient] || "" : ""
);
@@ -91,11 +85,6 @@ export const useMessageComposer = (feeState: FeeState, recipient?: string) => {
let messageToSend = draft;
let fileDataForStorage: FileData | null = null;
- // prepend markdown prefix if markdown is enabled and there's text content
- if (markdownEnabled && draft && !attachment) {
- messageToSend = MARKDOWN_PREFIX + draft;
- }
-
if (attachment) {
messageToSend = attachment.content; // always send attachment payload
try {
diff --git a/src/store/featureflag.store.ts b/src/store/featureflag.store.ts
index d8f66ee4..991cb45b 100644
--- a/src/store/featureflag.store.ts
+++ b/src/store/featureflag.store.ts
@@ -52,7 +52,7 @@ const featureFlips: FeatureFlips = {
},
[FeatureFlags.MARKDOWN]: {
label: "Markdown Messages",
- desc: `Enable markdown formatting for all messages sent.`,
+ desc: `Enable markdown formatting for all messages.\nReminder: Messages will cost slightly more due to markdown formatting`,
icon: MessageSquareText,
},
};
From 6b54bff58611bdb27ab4e395a1c9cfac6e11acd3 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sun, 30 Nov 2025 05:30:22 +0300
Subject: [PATCH 72/81] feat: add new line support
- move markdown rendered to own component
---
.../Utilities/Content/MarkdownRenderer.tsx | 27 +++++++++++++++++++
.../Utilities/Content/MessageContent.tsx | 24 +++++------------
2 files changed, 33 insertions(+), 18 deletions(-)
create mode 100644 src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
new file mode 100644
index 00000000..58288f71
--- /dev/null
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import ReactMarkdown from "react-markdown";
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
+import remarkBreaks from "remark-breaks";
+
+// extend default schema to ensure br tags are allowed
+const sanitizeSchema = {
+ ...defaultSchema,
+ tagNames: [...(defaultSchema.tagNames || []), "br"],
+};
+
+interface MarkdownRendererProps {
+ content: string;
+}
+
+export const MarkdownRenderer: React.FC = ({
+ content,
+}) => (
+
+
+ {content}
+
+
+);
diff --git a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
index abeeee09..32b17c2b 100644
--- a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
@@ -1,16 +1,8 @@
import { FC, useState, useRef, useEffect } from "react";
import clsx from "clsx";
-import ReactMarkdown from "react-markdown";
-import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
-import remarkBreaks from "remark-breaks";
import { parseMessageForDisplay } from "../../../../utils/message-format";
import { MARKDOWN_PREFIX } from "../../../../config/constants";
-
-// extend default schema to ensure br tags are allowed
-const sanitizeSchema = {
- ...defaultSchema,
- tagNames: [...(defaultSchema.tagNames || []), "br"],
-};
+import { MarkdownRenderer } from "./MarkdownRenderer";
type MessageContentProps = {
content: string;
@@ -49,7 +41,7 @@ export const MessageContent: FC = ({
);
}
- // render content (markdown or plain text) with expand/collapse functionality
+ // render content (markdown or plain text)
if (typeof content === "string") {
// check if content starts with markdown flag and process content
const isMarkdown = content.startsWith(MARKDOWN_PREFIX);
@@ -57,15 +49,11 @@ export const MessageContent: FC = ({
? content.slice(MARKDOWN_PREFIX.length)
: content;
+ // preserve multiple consecutive newlines by adding nbsp between consecutive newlines only
+ const formattedContent = displayContent.replace(/\n\n/g, "\n \n");
+
const renderedContent = isMarkdown ? (
-
-
- {displayContent}
-
-
+
) : (
parseMessageForDisplay(displayContent)
);
From a974d4a27e200092d9070195fe66dd774b6217bc Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sun, 30 Nov 2025 09:37:11 +0300
Subject: [PATCH 73/81] fix: ability to open links + highlight em
---
.../Utilities/Content/MarkdownRenderer.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
index 58288f71..94ff972f 100644
--- a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -3,10 +3,14 @@ import ReactMarkdown from "react-markdown";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import remarkBreaks from "remark-breaks";
-// extend default schema to ensure br tags are allowed
+// extend default schema to ensure br tags are allowed and links can open in new tabs
const sanitizeSchema = {
...defaultSchema,
tagNames: [...(defaultSchema.tagNames || []), "br"],
+ attributes: {
+ ...defaultSchema.attributes,
+ a: [...(defaultSchema.attributes?.a || []), "target", "rel"],
+ },
};
interface MarkdownRendererProps {
@@ -16,10 +20,17 @@ interface MarkdownRendererProps {
export const MarkdownRenderer: React.FC = ({
content,
}) => (
-
+
(
+
+ {children}
+
+ ),
+ }}
>
{content}
From 9048f5ed6b616514fbec1255da31b818484ad6c0 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sun, 30 Nov 2025 12:52:01 +0300
Subject: [PATCH 74/81] feat: add sub feature flags ability and render
- add conditions on broadcast image and link rendering
- add alert to link navigation
- refactor/chore: remove rehype and clean up md renderer
---
package-lock.json | 29 ---
.../Broadcasts/BroadcastDisplay.tsx | 1 +
.../Utilities/Content/MarkdownRenderer.tsx | 54 +++--
.../Utilities/Content/MessageContent.tsx | 21 +-
src/components/Modals/SettingsModal.tsx | 215 ++++++++++++++----
src/store/featureflag.store.ts | 49 +++-
6 files changed, 267 insertions(+), 102 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 093d5136..47d52af0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7048,21 +7048,6 @@
"node": ">= 0.4"
}
},
- "node_modules/hast-util-sanitize": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
- "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "@ungap/structured-clone": "^1.0.0",
- "unist-util-position": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -11006,20 +10991,6 @@
"node": ">=6"
}
},
- "node_modules/rehype-sanitize": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
- "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "hast-util-sanitize": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/remark-breaks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
diff --git a/src/components/MessagesPane/Broadcasts/BroadcastDisplay.tsx b/src/components/MessagesPane/Broadcasts/BroadcastDisplay.tsx
index 89c79af9..21b06047 100644
--- a/src/components/MessagesPane/Broadcasts/BroadcastDisplay.tsx
+++ b/src/components/MessagesPane/Broadcasts/BroadcastDisplay.tsx
@@ -114,6 +114,7 @@ export const BroadcastDisplay: FC
= ({
{renderTimestamp()}
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
index 94ff972f..de8cef6d 100644
--- a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -1,35 +1,55 @@
import React from "react";
import ReactMarkdown from "react-markdown";
-import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import remarkBreaks from "remark-breaks";
-// extend default schema to ensure br tags are allowed and links can open in new tabs
-const sanitizeSchema = {
- ...defaultSchema,
- tagNames: [...(defaultSchema.tagNames || []), "br"],
- attributes: {
- ...defaultSchema.attributes,
- a: [...(defaultSchema.attributes?.a || []), "target", "rel"],
- },
-};
-
interface MarkdownRendererProps {
content: string;
+ enableMdLinks?: boolean;
+ enableMdImages?: boolean;
}
export const MarkdownRenderer: React.FC = ({
content,
+ enableMdLinks = false,
+ enableMdImages = false,
}) => (
(
-
- {children}
-
- ),
+ a: ({ href, children, ...props }) =>
+ enableMdLinks ? (
+ {
+ if (
+ !confirm(
+ `You are navigating away from Kasia to ${href}. Continue?`
+ )
+ ) {
+ e.preventDefault();
+ }
+ }}
+ {...props}
+ >
+ {children}
+
+ ) : (
+
+ KASIA PERMISSION - Links Not Enabled | Link Text: {children} |
+ Link: {href}
+
+ ),
+ img: ({ src, alt, ...props }) =>
+ enableMdImages ? (
+
+ ) : (
+
+ [KASIA PERMISSION - Image Links Not Enabled: {src}]
+
+ ),
}}
>
{content}
diff --git a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
index 32b17c2b..f1657a01 100644
--- a/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MessageContent.tsx
@@ -3,22 +3,33 @@ import clsx from "clsx";
import { parseMessageForDisplay } from "../../../../utils/message-format";
import { MARKDOWN_PREFIX } from "../../../../config/constants";
import { MarkdownRenderer } from "./MarkdownRenderer";
+import { useFeatureFlagsStore } from "../../../../store/featureflag.store";
type MessageContentProps = {
content: string;
isDecrypting: boolean;
+ isBroadcast?: boolean;
isOutgoing?: boolean;
};
export const MessageContent: FC = ({
content,
isDecrypting,
+ isBroadcast = false,
isOutgoing = true,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [shouldCollapse, setShouldCollapse] = useState(false);
const contentRef = useRef(null);
+ const imageLinksEnabled = useFeatureFlagsStore(
+ (state) => state.flags.broadcast_image_links
+ );
+
+ const clickableLinksEnabled = useFeatureFlagsStore(
+ (state) => state.flags.broadcast_links
+ );
+
// check if content should be collapsed based on line count, length or some combo
useEffect(() => {
if (typeof content === "string" && !isDecrypting) {
@@ -49,11 +60,15 @@ export const MessageContent: FC = ({
? content.slice(MARKDOWN_PREFIX.length)
: content;
- // preserve multiple consecutive newlines by adding nbsp between consecutive newlines only
- const formattedContent = displayContent.replace(/\n\n/g, "\n \n");
+ // preserve multiple consecutive newlines by replacing them with non-breaking spaces
+ const formattedContent = displayContent.replace(/\n{2,}/g, "\n\u00A0\n");
const renderedContent = isMarkdown ? (
-
+
) : (
parseMessageForDisplay(displayContent)
);
diff --git a/src/components/Modals/SettingsModal.tsx b/src/components/Modals/SettingsModal.tsx
index fe7ea62b..c6d1ad38 100644
--- a/src/components/Modals/SettingsModal.tsx
+++ b/src/components/Modals/SettingsModal.tsx
@@ -982,61 +982,180 @@ export const SettingsModal: React.FC = ({
{activeTab === "extras" && (
Features
-
-
- {/* Warning */}
-
- Some of these features are in beta or expose you to external
- content
-
- {Object.entries(flips).map(([flagKey, item]) => (
-
- setFlag(
- flagKey as FeatureFlags,
- !flags[flagKey as FeatureFlags]
- )
- }
- className="border-primary-border bg-primary-bg hover:bg-primary-bg/50 my-2 cursor-pointer rounded-2xl border p-4 transition-colors"
- >
-
-
-
- {item.label}
-
-
- {item.desc}
+
+
Extra
+
+
+ {/* Warning */}
+
+ Some of these features are in beta or expose you to
+ external content
+
+ {Object.entries(flips).map(([flagKey, item]) => (
+
+ setFlag(
+ flagKey as FeatureFlags,
+ !flags[flagKey as FeatureFlags]
+ )
+ }
+ className="border-primary-border bg-primary-bg hover:bg-primary-bg/50 my-2 cursor-pointer rounded-2xl border p-4 transition-colors"
+ >
+
+
+
+ {item.label}
+
+
+ {item.desc}
+
-
-
- setFlag(flagKey as FeatureFlags, enabled)
- }
- className={clsx(
- "relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full transition-colors",
- {
- "bg-kas-secondary":
- flags[flagKey as FeatureFlags],
- "bg-gray-300": !flags[flagKey as FeatureFlags],
+
+ setFlag(flagKey as FeatureFlags, enabled)
}
- )}
- >
-
-
+ >
+
+
+
-
- ))}
+ ))}
+
+ {Object.entries(flips)
+ .filter(([, item]) => !item.parent) // only render parent flags at top level
+ .map(([flagKey, item]) => {
+ const isEnabled = flags[flagKey as FeatureFlags];
+
+ // find child flags for this parent
+ const childFlags = Object.entries(flips).filter(
+ ([, childItem]) => childItem.parent === flagKey
+ );
+
+ return (
+
+ {/* Parent Flag */}
+
+ setFlag(flagKey as FeatureFlags, !isEnabled)
+ }
+ className="border-primary-border bg-primary-bg hover:bg-primary-bg/50 cursor-pointer rounded-2xl border p-4 transition-colors"
+ >
+
+
+
+ {item.label}
+
+
+ {item.desc}
+
+
+
+ setFlag(flagKey as FeatureFlags, enabled)
+ }
+ className={clsx(
+ "relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full transition-colors",
+ {
+ "bg-kas-secondary": isEnabled,
+ "bg-gray-300": !isEnabled,
+ }
+ )}
+ >
+
+
+
+
+
+ {/* Child flags - only show when parent is enabled */}
+ {isEnabled && childFlags.length > 0 && (
+
+ {childFlags.map(([childKey, childItem]) => {
+ const isChildEnabled =
+ flags[childKey as FeatureFlags];
+ return (
+
+ setFlag(
+ childKey as FeatureFlags,
+ !isChildEnabled
+ )
+ }
+ className="border-primary-border bg-primary-bg/80 hover:bg-primary-bg/60 cursor-pointer rounded-xl border p-3 transition-colors"
+ >
+
+
+
+ {childItem.label}
+
+
+ {childItem.desc}
+
+
+
+ setFlag(
+ childKey as FeatureFlags,
+ enabled
+ )
+ }
+ className={clsx(
+ "relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition-colors",
+ {
+ "bg-kas-secondary": isChildEnabled,
+ "bg-gray-300": !isChildEnabled,
+ }
+ )}
+ >
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
)}
diff --git a/src/store/featureflag.store.ts b/src/store/featureflag.store.ts
index 991cb45b..832cfd88 100644
--- a/src/store/featureflag.store.ts
+++ b/src/store/featureflag.store.ts
@@ -5,11 +5,15 @@ import {
Radio,
Camera,
MessageSquareText,
+ Image,
+ Link,
} from "lucide-react";
import { cameraPermissionService } from "../service/camera-permission-service";
export enum FeatureFlags {
BROADCAST = "broadcast",
+ BROADCAST_IMAGE_LINKS = "broadcast_image_links",
+ BROADCAST_LINKS = "broadcast_links",
CUSTOM_FEE = "customfee",
ENABLED_CAMERA = "enabledcamera",
MARKDOWN = "markdown",
@@ -19,6 +23,8 @@ export type FeatureFlagsTable = Record
;
const defaultFeatureFlagsTable: FeatureFlagsTable = {
[FeatureFlags.BROADCAST]: false,
+ [FeatureFlags.BROADCAST_IMAGE_LINKS]: false,
+ [FeatureFlags.BROADCAST_LINKS]: false,
[FeatureFlags.CUSTOM_FEE]: false,
[FeatureFlags.ENABLED_CAMERA]: false,
[FeatureFlags.MARKDOWN]: false,
@@ -28,6 +34,7 @@ export interface FeatureDescription {
label: string;
desc: string;
icon: LucideIcon;
+ parent?: FeatureFlags; // if set, this is a child flag rendered under the closet parent up the list
}
export type FeatureFlips = Record;
@@ -40,6 +47,11 @@ const featureFlips: FeatureFlips = {
"\nNote: Photos are encrypted but sent on chain.",
icon: Camera,
},
+ [FeatureFlags.MARKDOWN]: {
+ label: "Send Markdown Messages",
+ desc: `Enable markdown formatting for all outgoing messages.\nReminder: Messages will cost slightly more due to markdown formatting`,
+ icon: MessageSquareText,
+ },
[FeatureFlags.CUSTOM_FEE]: {
label: "Custom Priority Fee",
desc: "Turn on to set custom priority fee in chat.",
@@ -50,10 +62,17 @@ const featureFlips: FeatureFlips = {
desc: `Unencrypted open messages.\nLive messages only, no storage.\nReminder: Broadcasts are unencrypted.`,
icon: Radio,
},
- [FeatureFlags.MARKDOWN]: {
- label: "Markdown Messages",
- desc: `Enable markdown formatting for all messages.\nReminder: Messages will cost slightly more due to markdown formatting`,
- icon: MessageSquareText,
+ [FeatureFlags.BROADCAST_IMAGE_LINKS]: {
+ label: "Broadcasts - Enable Image Links",
+ desc: "Allow embedded image links to render.\nWarning: This is unfiltered and potentially contains unwanted content.",
+ icon: Image,
+ parent: FeatureFlags.BROADCAST,
+ },
+ [FeatureFlags.BROADCAST_LINKS]: {
+ label: "Broadcasts - Enable Links",
+ desc: "Make markdown links clickable.\nWarning: Links are dangerous if clicked.",
+ icon: Link,
+ parent: FeatureFlags.BROADCAST,
},
};
@@ -61,8 +80,9 @@ const useFeatureFlagsStore = create<{
flags: FeatureFlagsTable;
flips: FeatureFlips;
setFlag: (key: FeatureFlags, value: boolean) => void;
+ isFlagEnabled: (key: FeatureFlags) => boolean;
}>((set, get) => {
- // hydrate features here, else take default value
+ // hydrate from localStorage
let initialFlags = defaultFeatureFlagsTable;
try {
const stored = JSON.parse(
@@ -79,6 +99,16 @@ const useFeatureFlagsStore = create<{
setFlag: (key, value) => {
const updated = { ...get().flags, [key]: value };
+
+ // if disabling a parent flag, also disable all its children
+ if (!value) {
+ for (const [childKey, childFlip] of Object.entries(featureFlips)) {
+ if (childFlip.parent === key) {
+ updated[childKey as FeatureFlags] = false;
+ }
+ }
+ }
+
set({ flags: updated });
// clear camera permission if camera feature is disabled
@@ -88,6 +118,15 @@ const useFeatureFlagsStore = create<{
localStorage.setItem("kasia-feature-flags", JSON.stringify(updated));
},
+
+ isFlagEnabled: (key) => {
+ const flip = featureFlips[key];
+ // if this flag has a parent, it's only effective if parent is also enabled
+ if (flip.parent) {
+ return get().flags[flip.parent] && get().flags[key];
+ }
+ return get().flags[key];
+ },
};
});
From 2499d2f5ae1b8f3ed790fb485e62483eb610e93e Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Mon, 1 Dec 2025 10:03:54 +0300
Subject: [PATCH 75/81] feat: update direct card message preview
---
.../SideBarPane/Directs/ContactCard.tsx | 26 ++++++++++++----
src/utils/markdown.ts | 30 +++++++++++++++++++
2 files changed, 51 insertions(+), 5 deletions(-)
create mode 100644 src/utils/markdown.ts
diff --git a/src/components/SideBarPane/Directs/ContactCard.tsx b/src/components/SideBarPane/Directs/ContactCard.tsx
index 19d7bd69..d536b80a 100644
--- a/src/components/SideBarPane/Directs/ContactCard.tsx
+++ b/src/components/SideBarPane/Directs/ContactCard.tsx
@@ -3,9 +3,11 @@ import { decodePayload } from "../../../utils/format";
import { useMessagingStore } from "../../../store/messaging.store";
import { AvatarHash } from "../../icons/AvatarHash";
import { PROTOCOL } from "../../../config/protocol";
+import { MARKDOWN_PREFIX } from "../../../config/constants";
import clsx from "clsx";
import { Contact } from "../../../store/repository/contact.repository";
import { getFileTypeFromContent } from "../../../utils/parse-message";
+import { stripMarkdown } from "../../../utils/markdown";
function getMessagePreview(content: string) {
// check if it's a file or image message
@@ -20,15 +22,29 @@ function getMessagePreview(content: string) {
}
}
- // For regular messages, try to decode if it's encrypted
+ let messageContent: string;
if (content.startsWith(PROTOCOL.prefix.string)) {
const decoded = decodePayload(content);
- return decoded
- ? decoded.slice(0, 40) + (decoded.length > 40 ? "..." : "")
- : "Encrypted message";
+ if (!decoded) return "Encrypted message";
+ messageContent = decoded;
+ } else {
+ messageContent = content;
}
- return content.slice(0, 40) + (content.length > 40 ? "..." : "");
+ const isMarkdown = messageContent.startsWith(MARKDOWN_PREFIX);
+ if (isMarkdown) {
+ const processedContent = messageContent.slice(MARKDOWN_PREFIX.length);
+ // strip off the markdown flags, so we can just render the content 'plain' in the preview
+ const plainTextContent = stripMarkdown(processedContent);
+ return (
+ plainTextContent.slice(0, 40) +
+ (plainTextContent.length > 40 ? "..." : "")
+ );
+ }
+
+ return (
+ messageContent.slice(0, 40) + (messageContent.length > 40 ? "..." : "")
+ );
}
export const ContactCard: FC<{
diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts
new file mode 100644
index 00000000..61b703fa
--- /dev/null
+++ b/src/utils/markdown.ts
@@ -0,0 +1,30 @@
+// strip markdown formatting and return plain text
+export function stripMarkdown(markdown: string): string {
+ return (
+ markdown
+ // remove code blocks
+ .replace(/```[\s\S]*?```/g, "[code block]")
+ .replace(/`[^`\n]+`/g, "[code]")
+ // remove links and images
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") // images
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links
+ // remove headers
+ .replace(/^#+\s*/gm, "")
+ // remove bold/italic
+ .replace(/\*\*\*([^*]+)\*\*\*/g, "$1") // bold italic
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // bold
+ .replace(/\*([^*]+)\*/g, "$1") // italic
+ .replace(/__([^_]+)__/g, "$1") // underline bold
+ .replace(/_([^_]+)_/g, "$1") // underline italic
+ // remove strikethrough
+ .replace(/~~([^~]+)~~/g, "$1")
+ // remove blockquotes
+ .replace(/^>\s*/gm, "")
+ // remove list markers
+ .replace(/^[\s]*[-*+]\s+/gm, "")
+ .replace(/^[\s]*\d+\.\s+/gm, "")
+ // clean up extra whitespace
+ .replace(/\n{2,}/g, " ")
+ .trim()
+ );
+}
From a42d8cae04d9c951d976fe4b7fb7eb9694fdb4b2 Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:46:25 +0300
Subject: [PATCH 76/81] chore: pin versions
---
package-lock.json | 34 +++++++++++++++++++++++++++++++---
package.json | 5 ++---
2 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 47d52af0..5af23f61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,9 +27,8 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "7.12.0",
- "react-markdown": "^10.1.0",
- "rehype-sanitize": "^6.0.0",
- "remark-breaks": "^4.0.0",
+ "react-markdown": "10.1.0",
+ "remark-breaks": "4.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
@@ -7048,6 +7047,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -10991,6 +11005,20 @@
"node": ">=6"
}
},
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-breaks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
diff --git a/package.json b/package.json
index 87a64c67..4227458d 100644
--- a/package.json
+++ b/package.json
@@ -41,10 +41,9 @@
"qrcode": "1.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "react-markdown": "10.1.0",
"react-router": "7.12.0",
- "react-markdown": "^10.1.0",
- "rehype-sanitize": "^6.0.0",
- "remark-breaks": "^4.0.0",
+ "remark-breaks": "4.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
"zustand": "5.0.5"
From 6535423bb11e04b5051e30c629878875305e099e Mon Sep 17 00:00:00 2001
From: HocusLocust <182385655+HocusLocusTee@users.noreply.github.com>
Date: Wed, 3 Dec 2025 09:21:40 +0300
Subject: [PATCH 77/81] chore: add comment re:sanitization
---
.../MessagesPane/Utilities/Content/MarkdownRenderer.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
index de8cef6d..59b9ddb5 100644
--- a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -1,6 +1,8 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkBreaks from "remark-breaks";
+// NOTE: In its current vanilla form, our use of react-markdown is safe from XSS attack
+// If adding more plugins or raw HTML support, add rehype-sanitize or find a way to sanitize properly
interface MarkdownRendererProps {
content: string;
From 2c3dbe0efa8338942cea97078b0cd816f6cafe19 Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 30 Jan 2026 19:15:02 +1100
Subject: [PATCH 78/81] chore: rebase and input guard
---
package-lock.json | 42 ++-----------------
.../Composing/Utilities/InputBasic.tsx | 4 +-
2 files changed, 6 insertions(+), 40 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5af23f61..651bf1c3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,8 +26,8 @@
"qrcode": "1.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
- "react-router": "7.12.0",
"react-markdown": "10.1.0",
+ "react-router": "7.12.0",
"remark-breaks": "4.0.0",
"tauri-plugin-app-events-api": "0.2.0",
"uuid": "11.1.0",
@@ -7047,21 +7047,6 @@
"node": ">= 0.4"
}
},
- "node_modules/hast-util-sanitize": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
- "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "@ungap/structured-clone": "^1.0.0",
- "unist-util-position": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -7807,10 +7792,6 @@
"version": "1.4.0",
"license": "Apache-2.0"
},
- "node_modules/kasia": {
- "resolved": "",
- "link": true
- },
"node_modules/kaspa-wasm": {
"resolved": "wasm",
"link": true
@@ -11005,20 +10986,6 @@
"node": ">=6"
}
},
- "node_modules/rehype-sanitize": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
- "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "hast-util-sanitize": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/remark-breaks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
@@ -13341,8 +13308,7 @@
"version": "0.2.1",
"license": "MIT",
"dependencies": {
- "@tauri-apps/api": "^2.8.0",
- "kasia": "file:../.."
+ "@tauri-apps/api": "^2.8.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^12.0.0",
@@ -13572,8 +13538,8 @@
},
"wasm": {
"name": "kaspa-wasm",
- "version": "1.0.1",
+ "version": "1.1.0-rc.2",
"license": "ISC"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/components/MessagesPane/Composing/Utilities/InputBasic.tsx b/src/components/MessagesPane/Composing/Utilities/InputBasic.tsx
index 17cf934e..a65b9df0 100644
--- a/src/components/MessagesPane/Composing/Utilities/InputBasic.tsx
+++ b/src/components/MessagesPane/Composing/Utilities/InputBasic.tsx
@@ -52,9 +52,9 @@ export const InputBasic = forwardRef(
const handleChange = (e: React.ChangeEvent) => {
let newValue = e.target.value;
- // add markdown prefix if markdown is enabled
+ // add markdown prefix if markdown is enabled and there's real content
if (markdownEnabled) {
- newValue = MARKDOWN_PREFIX + newValue;
+ newValue = newValue.trim().length > 0 ? MARKDOWN_PREFIX + newValue : "";
}
onChange(newValue);
From b7a5f35f3e214feed8f4d6a1efb9f796fc7cbd48 Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Fri, 30 Jan 2026 19:36:23 +1100
Subject: [PATCH 79/81] fix: make sure md embedded links are clean (allow list)
---
.../Utilities/Content/MarkdownRenderer.tsx | 59 +++++++++++++------
1 file changed, 42 insertions(+), 17 deletions(-)
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
index 59b9ddb5..173b6aa0 100644
--- a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -10,6 +10,25 @@ interface MarkdownRendererProps {
enableMdImages?: boolean;
}
+const isSafeHref = (href?: string) => {
+ if (!href) return false;
+ const normalized = href.trim().toLowerCase();
+ return (
+ normalized.startsWith("http://") ||
+ normalized.startsWith("https://") ||
+ normalized.startsWith("mailto:") ||
+ normalized.startsWith("www.")
+ );
+};
+
+const normalizeHref = (href?: string) => {
+ if (!href) return undefined;
+ const trimmed = href.trim();
+ return trimmed.toLowerCase().startsWith("www.")
+ ? `https://${trimmed}`
+ : trimmed;
+};
+
export const MarkdownRenderer: React.FC = ({
content,
enableMdLinks = false,
@@ -21,23 +40,29 @@ export const MarkdownRenderer: React.FC = ({
components={{
a: ({ href, children, ...props }) =>
enableMdLinks ? (
- {
- if (
- !confirm(
- `You are navigating away from Kasia to ${href}. Continue?`
- )
- ) {
- e.preventDefault();
- }
- }}
- {...props}
- >
- {children}
-
+ isSafeHref(href) ? (
+ {
+ if (
+ !confirm(
+ `You are navigating away from Kasia to ${normalizeHref(href)}. Continue?`
+ )
+ ) {
+ e.preventDefault();
+ }
+ }}
+ {...props}
+ >
+ {children}
+
+ ) : (
+
+ UNSAFE LINK | Link Text: {children} | Link: {href}
+
+ )
) : (
KASIA PERMISSION - Links Not Enabled | Link Text: {children} |
From b5f9bab238e9e4d38906e25b2f5683473be18426 Mon Sep 17 00:00:00 2001
From: HocusLocusTee <182385655+HocusLocusTee@users.noreply.github.com>
Date: Sun, 1 Feb 2026 14:56:35 +1100
Subject: [PATCH 80/81] fix: assign proptypes and update copy
---
.../Utilities/Content/MarkdownRenderer.tsx | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
index 173b6aa0..1a0474af 100644
--- a/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
+++ b/src/components/MessagesPane/Utilities/Content/MarkdownRenderer.tsx
@@ -38,7 +38,11 @@ export const MarkdownRenderer: React.FC = ({
+ a: ({
+ href,
+ children,
+ ...props
+ }: React.AnchorHTMLAttributes) =>
enableMdLinks ? (
isSafeHref(href) ? (
= ({
) : (
- UNSAFE LINK | Link Text: {children} | Link: {href}
+ Kasia Safety Guard - Not Standard Link | Link Text: {children} |
+ Link: {href}
)
) : (
- KASIA PERMISSION - Links Not Enabled | Link Text: {children} |
+ Kasia Safety Guard - Links Not Enabled | Link Text: {children} |
Link: {href}
),
- img: ({ src, alt, ...props }) =>
+ img: ({
+ src,
+ alt,
+ ...props
+ }: React.ImgHTMLAttributes) =>
enableMdImages ? (
) : (
From 0243bfe72c760cb773e896d44ab2dc247c470e55 Mon Sep 17 00:00:00 2001
From: IzioDev <9900846+IzioDev@users.noreply.github.com>
Date: Sun, 1 Feb 2026 06:24:44 +0100
Subject: [PATCH 81/81] chore: improve cf deployment flow
---
DEPLOY.md | 4 ++++
wrangler.toml | 5 +++++
2 files changed, 9 insertions(+)
create mode 100644 wrangler.toml
diff --git a/DEPLOY.md b/DEPLOY.md
index ccbf9186..adc6c056 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -7,3 +7,7 @@
4. upload the `.zip` file and click `Save and Deploy`
Alternatively, if you wish to deploy a "staging" or "beta" version, you can tick `preview` while uploading the compressed build file. This will create a new application that will be up for testing, prior publishing it to production.
+
+## Alternatively with Wrangler
+- `npx wrangler pages deploy dist --project-name kasia --branch master`
+- or for staging: `npx wrangler pages deploy dist --project-name kasia --branch staging`
diff --git a/wrangler.toml b/wrangler.toml
new file mode 100644
index 00000000..878b77ea
--- /dev/null
+++ b/wrangler.toml
@@ -0,0 +1,5 @@
+"$schema" = "./node_modules/wrangler/config-schema.json"
+
+name = "kasia"
+pages_build_output_dir = "dist"
+compatibility_date = "2026-02-01"