From e85e9519d2a55775ddda30b3d743158e4db6cb39 Mon Sep 17 00:00:00 2001
From: enes <mulahasanovic@outlook.com>
Date: Fri, 11 Oct 2024 15:24:32 +0200
Subject: [PATCH 01/28] feat: add private dm sending

---
 src/pages/create/index.tsx      |  33 ++++++-
 src/pages/sign/index.tsx        |  55 ++++++++++-
 src/types/errors/SendDMError.ts |  24 +++++
 src/utils/nostr.ts              | 166 ++++++++++++++++++++++++++++++--
 src/utils/relays.ts             |  13 ++-
 5 files changed, 278 insertions(+), 13 deletions(-)
 create mode 100644 src/types/errors/SendDMError.ts

diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 3374a08..de263e6 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -39,7 +39,9 @@ import {
   updateUsersAppData,
   uploadToFileStorage,
   DEFAULT_TOOLBOX,
-  settleAllFullfilfedPromises
+  settleAllFullfilfedPromises,
+  sendPrivateDirectMessage,
+  parseNostrEvent
 } from '../../utils'
 import { Container } from '../../components/Container'
 import fileListStyles from '../../components/FileList/style.module.scss'
@@ -62,6 +64,7 @@ import {
 } from '@fortawesome/free-solid-svg-icons'
 import { getSigitFile, SigitFile } from '../../utils/file.ts'
 import _ from 'lodash'
+import { SendDMError } from '../../types/errors/SendDMError.ts'
 
 export const CreatePage = () => {
   const navigate = useNavigate()
@@ -713,6 +716,34 @@ export const CreatePage = () => {
             toast.error('Failed to publish notifications')
           })
 
+        setLoadingSpinnerDesc('Sending DMs')
+        // Send DM to signers and viewers
+        // No need to send notification to self so remove it from the list
+        const receivers = (
+          signers.length > 0
+            ? [signers[0].pubkey]
+            : viewers.map((viewer) => viewer.pubkey)
+        ).filter((receiver) => receiver !== usersPubkey)
+
+        for (let i = 0; i < receivers.length; i++) {
+          const createSignatureEvent = await parseNostrEvent(
+            meta.createSignature
+          )
+          const { id } = createSignatureEvent
+          const r = receivers[i]
+          try {
+            await sendPrivateDirectMessage(
+              `Sigit created, visit ${window.location.origin}/#/${signers.length > 0 ? 'sign' : 'verify'}/${id}`,
+              npubToHex(r!)!
+            )
+          } catch (error) {
+            if (error instanceof SendDMError) {
+              toast.error(error.message)
+            }
+            console.error(error)
+          }
+        }
+
         navigate(appPrivateRoutes.sign, { state: { meta } })
       } else {
         const zip = new JSZip()
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index dbbcd98..be64a67 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -33,7 +33,9 @@ import {
   signEventForMetaFile,
   updateUsersAppData,
   findOtherUserMarks,
-  timeout
+  timeout,
+  sendPrivateDirectMessage,
+  parseNostrEvent
 } from '../../utils'
 import { Container } from '../../components/Container'
 import { DisplayMeta } from './internal/displayMeta'
@@ -53,6 +55,7 @@ import {
   SigitFile
 } from '../../utils/file.ts'
 import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
+import { SendDMError } from '../../types/errors/SendDMError.ts'
 
 enum SignedStatus {
   Fully_Signed,
@@ -720,6 +723,56 @@ export const SignPage = () => {
         toast.error('Failed to publish notifications')
       })
 
+    // Send DMs
+    setLoadingSpinnerDesc('Sending DMs')
+    const createSignatureEvent = await parseNostrEvent(meta.createSignature)
+    const { id } = createSignatureEvent
+
+    if (isLastSigner) {
+      // Final sign sends to everyone (creator, signers, viewers - /verify)
+      const areSent: boolean[] = Array(users.length).fill(false)
+      for (let i = 0; i < users.length; i++) {
+        try {
+          areSent[i] = await sendPrivateDirectMessage(
+            `Sigit completed, visit ${window.location.origin}/#/verify/${id}`,
+            npubToHex(users[i])!
+          )
+        } catch (error) {
+          if (error instanceof SendDMError) {
+            toast.error(error.message)
+          }
+          console.error(error)
+        }
+      }
+    } else {
+      // Notify the creator and the next signer (/sign).
+      try {
+        await sendPrivateDirectMessage(
+          `Sigit signed by ${usersNpub}, visit ${window.location.origin}/#/sign/${id}`,
+          npubToHex(submittedBy!)!
+        )
+      } catch (error) {
+        if (error instanceof SendDMError) {
+          toast.error(error.message)
+        }
+        console.error(error)
+      }
+
+      try {
+        const currentSignerIndex = signers.indexOf(usersNpub)
+        const nextSigner = signers[currentSignerIndex + 1]
+        await sendPrivateDirectMessage(
+          `You're the next signer, visit ${window.location.origin}/#/sign/${id}`,
+          npubToHex(nextSigner)!
+        )
+      } catch (error) {
+        if (error instanceof SendDMError) {
+          toast.error(error.message)
+        }
+        console.error(error)
+      }
+    }
+
     setIsLoading(false)
   }
 
diff --git a/src/types/errors/SendDMError.ts b/src/types/errors/SendDMError.ts
new file mode 100644
index 0000000..223823b
--- /dev/null
+++ b/src/types/errors/SendDMError.ts
@@ -0,0 +1,24 @@
+import { Jsonable } from '.'
+
+export enum SendDMErrorType {
+  'METADATA_FETCH_FAILED' = 'Sending DM failed. An error occured while fetching user metadata.',
+  'RELAY_READ_EMPTY' = `Sending DM failed. The user's relay read set is empty.`,
+  'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
+  'RELAY_PUBLISH_FAILED' = 'Sending DM failed. Publishing events failed.'
+}
+
+export class SendDMError extends Error {
+  public readonly context?: Jsonable
+
+  constructor(
+    message: string,
+    options: { cause?: Error; context?: Jsonable } = {}
+  ) {
+    const { cause, context } = options
+
+    super(message, { cause })
+    this.name = this.constructor.name
+
+    this.context = context
+  }
+}
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index ec8c97e..2046b38 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -6,6 +6,7 @@ import {
   EventTemplate,
   Filter,
   UnsignedEvent,
+  VerifiedEvent,
   finalizeEvent,
   generateSecretKey,
   getEventHash,
@@ -21,7 +22,8 @@ import { NIP05_REGEX } from '../constants'
 import {
   MetadataController,
   NostrController,
-  relayController
+  relayController,
+  RelayController
 } from '../controllers'
 import {
   updateProcessedGiftWraps,
@@ -35,6 +37,7 @@ import { parseJson, removeLeadingSlash } from './string'
 import { timeout } from './utils'
 import { getHash } from './hash'
 import { SIGIT_BLOSSOM } from './const.ts'
+import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError.ts'
 
 /**
  * Generates a `d` tag for userAppData
@@ -248,6 +251,12 @@ export const toUnixTimestamp = (date: number | Date) => {
 export const fromUnixTimestamp = (unix: number) => {
   return unix * 1000
 }
+export const randomTimeUpTo2DaysInThePast = (): number => {
+  const now = Date.now()
+  const twoDaysInMilliseconds = 2 * 24 * 60 * 60 * 1000
+  const randomPastTime = now - Math.floor(Math.random() * twoDaysInMilliseconds)
+  return toUnixTimestamp(randomPastTime)
+}
 
 /**
  * Generate nip44 conversation key
@@ -297,19 +306,21 @@ export const countLeadingZeroes = (hex: string) => {
 
 /**
  * Function to create a wrapped event with PoW
- * @param event Original event to be wrapped
+ * @param event  Original event to be wrapped (can be unsigned or verified)
  * @param receiver Public key of the receiver
- * @param difficulty PoW difficulty level (default is 20)
  * @returns
  */
 //
-export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
+export const createWrap = (
+  event: UnsignedEvent | VerifiedEvent,
+  receiver: string
+) => {
   // Generate a random secret key and its corresponding public key
   const randomKey = generateSecretKey()
   const pubkey = getPublicKey(randomKey)
 
   // Encrypt the event content using nip44 encryption
-  const content = nip44Encrypt(unsignedEvent, randomKey, receiver)
+  const content = nip44Encrypt(event, randomKey, receiver)
 
   // Initialize nonce and leadingZeroes for PoW calculation
   let nonce = 0
@@ -320,11 +331,12 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
   // eslint-disable-next-line no-constant-condition
   while (true) {
     // Create an unsigned event with the necessary fields
+    // TODO: kinds.GiftWrap (wrong kind number in nostr-tools 10/11/2024 at v2.7.2)
     const event: UnsignedEvent = {
       kind: 1059, // Event kind
       content, // Encrypted content
       pubkey, // Public key of the creator
-      created_at: unixNow(), // Current timestamp
+      created_at: randomTimeUpTo2DaysInThePast(),
       tags: [
         // Tags including receiver and nonce
         ['p', receiver],
@@ -989,3 +1001,145 @@ export const getProfileUsername = (
   truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
     length: 16
   })
+
+/**
+ * Modified {@link UnsignedEvent Unsigned Event} that includes an id
+ *
+ * Fields id and created_at are required.
+ * @see {@link UnsignedEvent}
+ * @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
+ */
+type UnsignedEventWithId = UnsignedEvent & {
+  id?: string
+}
+export const sendPrivateDirectMessage = async (
+  message: string,
+  receiver: string,
+  subject?: string
+) => {
+  // Instantiate the MetadataController to retrieve relay list metadata to look for preferred DM relays
+  const metadataController = MetadataController.getInstance()
+  const relaySet = await metadataController
+    .findRelayListMetadata(receiver)
+    .catch((err) => {
+      // Log an error if retrieving relay list metadata fails
+      console.log(
+        `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
+        err
+      )
+      return null
+    })
+
+  // Throw if metadata retrieval failed
+  if (!relaySet) {
+    throw new SendDMError(SendDMErrorType.METADATA_FETCH_FAILED, {
+      context: {
+        receiver
+      }
+    })
+  }
+
+  // Ensure relay list is not empty
+  if (relaySet.read.length === 0) {
+    throw new SendDMError(SendDMErrorType.RELAY_READ_EMPTY, {
+      context: {
+        receiver,
+        relaySet: JSON.stringify(relaySet)
+      }
+    })
+  }
+  // Get the direct message preferred relays list
+  // TODO: kinds.DirectMessageRelaysList  (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
+  // https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
+  const eventFilter: Filter = {
+    kinds: [10050],
+    authors: [receiver]
+  }
+  const preferredRelaysListEvents =
+    await RelayController.getInstance().fetchEvents(eventFilter, relaySet.read)
+
+  const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
+  const preferredRelaysList = preferredRelaysListEvents.reduce(
+    (previous: string[], current: Event) => {
+      const relaysList = current.tags
+        .filter((t) => isRelayTag(t) && !previous.includes(t[1]))
+        .map((t) => t[1])
+
+      return [...previous, ...relaysList]
+    },
+    []
+  )
+  // Empty preferred relays list
+  const finalRelaysList: string[] =
+    preferredRelaysList?.length > 0 ? preferredRelaysList : [...relaySet.write]
+
+  // Generate "sender"
+  const senderSecret = generateSecretKey()
+  const senderPubkey = getPublicKey(senderSecret)
+
+  // Prepare tags for the message
+  const tags: string[][] = [['p', receiver]]
+
+  // Conversation title
+  if (subject) tags.push(['subject', subject])
+
+  // Create private DM event containing the message and relevant metadata
+  // TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
+  const dm: UnsignedEventWithId = {
+    pubkey: senderPubkey,
+    created_at: unixNow(),
+    kind: 14,
+    tags,
+    content: message
+  }
+
+  // Calculate the hash based on the UnverifiedEvent
+  dm.id = getEventHash(dm)
+
+  // Encrypt the private dm using the sender secret and the receiver's public key
+  const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
+  if (!encryptedDm) {
+    throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
+      context: {
+        receiver,
+        message,
+        kind: dm.kind
+      }
+    })
+  }
+
+  // Seal the message
+  // TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
+  const sealedMessage: UnsignedEvent = {
+    kind: 13, // seal
+    pubkey: senderPubkey,
+    content: encryptedDm,
+    created_at: randomTimeUpTo2DaysInThePast(),
+    tags: [] // no tags
+  }
+
+  // Finalize and sign the sealed event
+  const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
+
+  // Encrypt the seal and gift wrap
+  const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
+
+  // Publish the finalized gift wrap event (the encrypted DM) to the relays
+  const publishedOnRelays = await relayController.publish(
+    finalizedGiftWrap,
+    finalRelaysList
+  )
+
+  // Handle cases where publishing to the relays failed
+  if (publishedOnRelays.length === 0) {
+    throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
+      context: {
+        receiver,
+        count: publishedOnRelays.length
+      }
+    })
+  }
+
+  // Return true indicating that the DM was successfully sent
+  return true
+}
diff --git a/src/utils/relays.ts b/src/utils/relays.ts
index bfef7aa..c7be280 100644
--- a/src/utils/relays.ts
+++ b/src/utils/relays.ts
@@ -97,20 +97,23 @@ const isOlderThanOneDay = (cachedAt: number) => {
 }
 
 const isRelayTag = (tag: string[]): boolean => tag[0] === 'r'
-
+const addRelay = (list: string[], relay: string) => {
+  // Only add if the list doesn't already include the relay
+  if (!list.includes(relay)) list.push(relay)
+}
 const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
   if (tag.length >= 3) {
     const marker = tag[2]
 
     if (marker === READ_MARKER) {
-      obj.read.push(tag[1])
+      addRelay(obj.read, tag[1])
     } else if (marker === WRITE_MARKER) {
-      obj.write.push(tag[1])
+      addRelay(obj.write, tag[1])
     }
   }
   if (tag.length === 2) {
-    obj.read.push(tag[1])
-    obj.write.push(tag[1])
+    addRelay(obj.read, tag[1])
+    addRelay(obj.write, tag[1])
   }
 
   return obj

From 3b4bf9aa29b337aa07e0aba9d06e89116482499c Mon Sep 17 00:00:00 2001
From: enes <mulahasanovic@outlook.com>
Date: Mon, 14 Oct 2024 11:58:52 +0200
Subject: [PATCH 02/28] fix: only send to next signer on create

---
 src/pages/create/index.tsx | 39 ++++++++++++++++++--------------------
 1 file changed, 18 insertions(+), 21 deletions(-)

diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index de263e6..40da254 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -716,31 +716,28 @@ export const CreatePage = () => {
             toast.error('Failed to publish notifications')
           })
 
+        // Send DM to the next signer
         setLoadingSpinnerDesc('Sending DMs')
-        // Send DM to signers and viewers
-        // No need to send notification to self so remove it from the list
-        const receivers = (
-          signers.length > 0
-            ? [signers[0].pubkey]
-            : viewers.map((viewer) => viewer.pubkey)
-        ).filter((receiver) => receiver !== usersPubkey)
+        if (signers.length > 0 && signers[0].pubkey !== usersPubkey) {
+          // No need to send notification to self so remove it from the list
+          const nextSigner = signers[0].pubkey
 
-        for (let i = 0; i < receivers.length; i++) {
-          const createSignatureEvent = await parseNostrEvent(
-            meta.createSignature
-          )
-          const { id } = createSignatureEvent
-          const r = receivers[i]
-          try {
-            await sendPrivateDirectMessage(
-              `Sigit created, visit ${window.location.origin}/#/${signers.length > 0 ? 'sign' : 'verify'}/${id}`,
-              npubToHex(r!)!
+          if (nextSigner) {
+            const createSignatureEvent = await parseNostrEvent(
+              meta.createSignature
             )
-          } catch (error) {
-            if (error instanceof SendDMError) {
-              toast.error(error.message)
+            const { id } = createSignatureEvent
+            try {
+              await sendPrivateDirectMessage(
+                `Sigit created, visit ${window.location.origin}/#/sign/${id}`,
+                npubToHex(nextSigner)!
+              )
+            } catch (error) {
+              if (error instanceof SendDMError) {
+                toast.error(error.message)
+              }
+              console.error(error)
             }
-            console.error(error)
           }
         }
 

From b04f4fb88d139cdd4050d56d592e41b79ae79a72 Mon Sep 17 00:00:00 2001
From: enes <mulahasanovic@outlook.com>
Date: Mon, 14 Oct 2024 13:19:44 +0200
Subject: [PATCH 03/28] refactor: show sent dm count, don't sent twice to
 creator

---
 src/pages/sign/index.tsx | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index be64a67..ed925e1 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -744,6 +744,12 @@ export const SignPage = () => {
           console.error(error)
         }
       }
+
+      if (areSent.some((r) => r)) {
+        toast.success(
+          `DMs sent ${areSent.filter((r) => r).length}/${users.length}`
+        )
+      }
     } else {
       // Notify the creator and the next signer (/sign).
       try {
@@ -758,18 +764,21 @@ export const SignPage = () => {
         console.error(error)
       }
 
-      try {
-        const currentSignerIndex = signers.indexOf(usersNpub)
-        const nextSigner = signers[currentSignerIndex + 1]
-        await sendPrivateDirectMessage(
-          `You're the next signer, visit ${window.location.origin}/#/sign/${id}`,
-          npubToHex(nextSigner)!
-        )
-      } catch (error) {
-        if (error instanceof SendDMError) {
-          toast.error(error.message)
+      // No need to notify creator twice, skipping
+      const currentSignerIndex = signers.indexOf(usersNpub)
+      const nextSigner = signers[currentSignerIndex + 1]
+      if (nextSigner !== submittedBy) {
+        try {
+          await sendPrivateDirectMessage(
+            `You're the next signer, visit ${window.location.origin}/#/sign/${id}`,
+            npubToHex(nextSigner)!
+          )
+        } catch (error) {
+          if (error instanceof SendDMError) {
+            toast.error(error.message)
+          }
+          console.error(error)
         }
-        console.error(error)
       }
     }
 

From 664ed9de06c05b9132a6e6da922982b001152bbe Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 24 Jan 2025 14:52:09 +0100
Subject: [PATCH 04/28] refactor(cache): remove unused cache service

---
 src/pages/settings/cache/index.tsx | 33 ------------
 src/services/cache/index.ts        | 86 ------------------------------
 src/services/cache/schema.ts       | 16 ------
 src/services/index.ts              |  1 -
 4 files changed, 136 deletions(-)
 delete mode 100644 src/services/cache/index.ts
 delete mode 100644 src/services/cache/schema.ts

diff --git a/src/pages/settings/cache/index.tsx b/src/pages/settings/cache/index.tsx
index 35cc95e..5dbf8af 100644
--- a/src/pages/settings/cache/index.tsx
+++ b/src/pages/settings/cache/index.tsx
@@ -1,4 +1,3 @@
-import ClearIcon from '@mui/icons-material/Clear'
 import InputIcon from '@mui/icons-material/Input'
 import IosShareIcon from '@mui/icons-material/IosShare'
 import {
@@ -9,36 +8,12 @@ import {
   ListSubheader,
   useTheme
 } from '@mui/material'
-import { useState } from 'react'
-import { toast } from 'react-toastify'
-import { localCache } from '../../../services'
-import { LoadingSpinner } from '../../../components/LoadingSpinner'
 import { Container } from '../../../components/Container'
 import { Footer } from '../../../components/Footer/Footer'
 
 export const CacheSettingsPage = () => {
   const theme = useTheme()
 
-  const [isLoading, setIsLoading] = useState(false)
-  const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
-
-  const handleClearData = async () => {
-    setIsLoading(true)
-    setLoadingSpinnerDesc('Clearing cache data')
-    localCache
-      .clearCacheData()
-      .then(() => {
-        toast.success('cleared cached data')
-      })
-      .catch((err) => {
-        console.log('An error occurred in clearing cache data', err)
-        toast.error(err.message || 'An error occurred in clearing cache data')
-      })
-      .finally(() => {
-        setIsLoading(false)
-      })
-  }
-
   const listItem = (label: string) => {
     return (
       <ListItemText
@@ -53,7 +28,6 @@ export const CacheSettingsPage = () => {
   return (
     <>
       <Container>
-        {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
         <List
           sx={{
             width: '100%',
@@ -87,13 +61,6 @@ export const CacheSettingsPage = () => {
             </ListItemIcon>
             {listItem('Import (coming soon)')}
           </ListItemButton>
-
-          <ListItemButton onClick={handleClearData}>
-            <ListItemIcon>
-              <ClearIcon sx={{ color: theme.palette.error.main }} />
-            </ListItemIcon>
-            {listItem('Clear Cache')}
-          </ListItemButton>
         </List>
       </Container>
       <Footer />
diff --git a/src/services/cache/index.ts b/src/services/cache/index.ts
deleted file mode 100644
index 957e45b..0000000
--- a/src/services/cache/index.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { IDBPDatabase, openDB } from 'idb'
-import { Event } from 'nostr-tools'
-import { CachedEvent } from '../../types'
-import { SchemaV2 } from './schema'
-
-class LocalCache {
-  // Static property to hold the single instance of LocalCache
-  private static instance: LocalCache | null = null
-  private db!: IDBPDatabase<SchemaV2>
-
-  // Private constructor to prevent direct instantiation
-  private constructor() {}
-
-  // Method to initialize the database
-  private async init() {
-    this.db = await openDB<SchemaV2>('sigit-cache', 2, {
-      upgrade(db, oldVersion) {
-        if (oldVersion < 1) {
-          db.createObjectStore('userMetadata', { keyPath: 'event.pubkey' })
-        }
-
-        if (oldVersion < 2) {
-          const v6 = db as unknown as IDBPDatabase<SchemaV2>
-
-          v6.createObjectStore('userRelayListMetadata', {
-            keyPath: 'event.pubkey'
-          })
-        }
-      }
-    })
-  }
-
-  // Static method to get the single instance of LocalCache
-  public static async getInstance(): Promise<LocalCache> {
-    // If the instance doesn't exist, create it
-    if (!LocalCache.instance) {
-      LocalCache.instance = new LocalCache()
-      await LocalCache.instance.init()
-    }
-    // Return the single instance of LocalCache
-    return LocalCache.instance
-  }
-
-  // Method to add user metadata
-  public async addUserMetadata(event: Event) {
-    await this.db.put('userMetadata', { event, cachedAt: Date.now() })
-  }
-
-  // Method to get user metadata by key
-  public async getUserMetadata(key: string): Promise<CachedEvent | null> {
-    const data = await this.db.get('userMetadata', key)
-    return data || null
-  }
-
-  // Method to delete user metadata by key
-  public async deleteUserMetadata(key: string) {
-    await this.db.delete('userMetadata', key)
-  }
-
-  public async addUserRelayListMetadata(event: Event) {
-    await this.db.put('userRelayListMetadata', { event, cachedAt: Date.now() })
-  }
-
-  public async getUserRelayListMetadata(
-    key: string
-  ): Promise<CachedEvent | null> {
-    const data = await this.db.get('userRelayListMetadata', key)
-    return data || null
-  }
-
-  public async deleteUserRelayListMetadata(key: string) {
-    await this.db.delete('userRelayListMetadata', key)
-  }
-
-  // Method to clear cache data
-  public async clearCacheData() {
-    // Clear the 'userMetadata' store in the IndexedDB database
-    await this.db.clear('userMetadata')
-
-    // Reload the current page to ensure any cached data is reset
-    window.location.reload()
-  }
-}
-
-// Export the single instance of LocalCache
-export const localCache = await LocalCache.getInstance()
diff --git a/src/services/cache/schema.ts b/src/services/cache/schema.ts
deleted file mode 100644
index bc21956..0000000
--- a/src/services/cache/schema.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { DBSchema } from 'idb'
-import { CachedEvent } from '../../types'
-
-export interface SchemaV1 extends DBSchema {
-  userMetadata: {
-    key: string
-    value: CachedEvent
-  }
-}
-
-export interface SchemaV2 extends SchemaV1 {
-  userRelayListMetadata: {
-    key: string
-    value: CachedEvent
-  }
-}
diff --git a/src/services/index.ts b/src/services/index.ts
index b8d275a..eb0b67f 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,2 +1 @@
-export * from './cache'
 export * from './signer'

From 4b5955fa9c4c2a5323cda0a1ae1ef93873eb6a93 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 24 Jan 2025 14:52:34 +0100
Subject: [PATCH 05/28] chore(ndk): bump ndk version

---
 package-lock.json | 196 +++++++++++++++++++---------------------------
 package.json      |   4 +-
 2 files changed, 83 insertions(+), 117 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 65fb09a..9d76e2f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,8 +20,8 @@
         "@mui/lab": "5.0.0-alpha.166",
         "@mui/material": "5.15.11",
         "@noble/hashes": "^1.4.0",
-        "@nostr-dev-kit/ndk": "2.10.0",
-        "@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
+        "@nostr-dev-kit/ndk": "2.11.0",
+        "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
         "@pdf-lib/fontkit": "^1.1.1",
         "@reduxjs/toolkit": "2.2.1",
         "axios": "^1.7.4",
@@ -1668,15 +1668,13 @@
       }
     },
     "node_modules/@noble/secp256k1": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.0.0.tgz",
-      "integrity": "sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ]
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz",
+      "integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
     },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
@@ -1714,19 +1712,19 @@
       }
     },
     "node_modules/@nostr-dev-kit/ndk": {
-      "version": "2.10.0",
-      "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
-      "integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.11.0.tgz",
+      "integrity": "sha512-FKIMtcVsVcquzrC+yir9lOXHCIHmQ3IKEVCMohqEB7N96HjP2qrI9s5utbjI3lkavFNF5tXg1Gp9ODEo7XCfLA==",
+      "license": "MIT",
       "dependencies": {
-        "@noble/curves": "^1.4.0",
-        "@noble/hashes": "^1.3.1",
-        "@noble/secp256k1": "^2.0.0",
-        "@scure/base": "^1.1.1",
-        "debug": "^4.3.4",
-        "light-bolt11-decoder": "^3.0.0",
-        "node-fetch": "^3.3.1",
+        "@noble/curves": "^1.6.0",
+        "@noble/hashes": "^1.5.0",
+        "@noble/secp256k1": "^2.1.0",
+        "@scure/base": "^1.1.9",
+        "debug": "^4.3.6",
+        "light-bolt11-decoder": "^3.2.0",
         "nostr-tools": "^2.7.1",
-        "tseep": "^1.1.1",
+        "tseep": "^1.2.2",
         "typescript-lru-cache": "^2.0.0",
         "utf8-buffer": "^1.0.0",
         "websocket-polyfill": "^0.0.3"
@@ -1736,17 +1734,41 @@
       }
     },
     "node_modules/@nostr-dev-kit/ndk-cache-dexie": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
-      "integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
+      "version": "2.5.9",
+      "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.9.tgz",
+      "integrity": "sha512-SZ5FjON0QPekiC7oW9Hy3JQxG0Oxxtud9LBa1q/A49JV/Qppv1x37nFHxi0XLxEbDgFTNYbaN27Zjfp2NPem2g==",
+      "license": "MIT",
       "dependencies": {
-        "@nostr-dev-kit/ndk": "2.10.0",
-        "debug": "^4.3.4",
-        "dexie": "^4.0.2",
+        "@nostr-dev-kit/ndk": "2.11.0",
+        "debug": "^4.3.7",
+        "dexie": "^4.0.8",
         "nostr-tools": "^2.4.0",
         "typescript-lru-cache": "^2.0.0"
       }
     },
+    "node_modules/@nostr-dev-kit/ndk-cache-dexie/node_modules/debug": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@nostr-dev-kit/ndk-cache-dexie/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
     "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
@@ -1772,6 +1794,15 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/@nostr-dev-kit/ndk/node_modules/@scure/base": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz",
+      "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
       "version": "2.10.4",
       "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
@@ -1829,6 +1860,24 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@scure/base": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
+      "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/@nostr-dev-kit/ndk/node_modules/tseep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz",
+      "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==",
+      "license": "MIT"
+    },
     "node_modules/@pdf-lib/fontkit": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
@@ -3784,14 +3833,6 @@
         "type": "^1.0.1"
       }
     },
-    "node_modules/data-uri-to-buffer": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
-      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
-      "engines": {
-        "node": ">= 12"
-      }
-    },
     "node_modules/debug": {
       "version": "4.3.6",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
@@ -4719,28 +4760,6 @@
         "reusify": "^1.0.4"
       }
     },
-    "node_modules/fetch-blob": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
-      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/jimmywarting"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/jimmywarting"
-        }
-      ],
-      "dependencies": {
-        "node-domexception": "^1.0.0",
-        "web-streams-polyfill": "^3.0.3"
-      },
-      "engines": {
-        "node": "^12.20 || >= 14.13"
-      }
-    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -4871,17 +4890,6 @@
         "node": ">= 6"
       }
     },
-    "node_modules/formdata-polyfill": {
-      "version": "4.0.10",
-      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
-      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
-      "dependencies": {
-        "fetch-blob": "^3.1.2"
-      },
-      "engines": {
-        "node": ">=12.20.0"
-      }
-    },
     "node_modules/fs-minipass": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -5761,9 +5769,10 @@
       }
     },
     "node_modules/light-bolt11-decoder": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
-      "integrity": "sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
+      "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
+      "license": "MIT",
       "dependencies": {
         "@scure/base": "1.1.1"
       }
@@ -6339,41 +6348,6 @@
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
       "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
     },
-    "node_modules/node-domexception": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
-      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/jimmywarting"
-        },
-        {
-          "type": "github",
-          "url": "https://paypal.me/jimmywarting"
-        }
-      ],
-      "engines": {
-        "node": ">=10.5.0"
-      }
-    },
-    "node_modules/node-fetch": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
-      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
-      "dependencies": {
-        "data-uri-to-buffer": "^4.0.0",
-        "fetch-blob": "^3.1.4",
-        "formdata-polyfill": "^4.0.10"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/node-fetch"
-      }
-    },
     "node_modules/node-gyp-build": {
       "version": "4.8.0",
       "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
@@ -8830,14 +8804,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/web-streams-polyfill": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
-      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
     "node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/package.json b/package.json
index 98bc510..a9b7583 100644
--- a/package.json
+++ b/package.json
@@ -30,8 +30,8 @@
     "@mui/lab": "5.0.0-alpha.166",
     "@mui/material": "5.15.11",
     "@noble/hashes": "^1.4.0",
-    "@nostr-dev-kit/ndk": "2.10.0",
-    "@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
+    "@nostr-dev-kit/ndk": "2.11.0",
+    "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
     "@pdf-lib/fontkit": "^1.1.1",
     "@reduxjs/toolkit": "2.2.1",
     "axios": "^1.7.4",

From efe3c2c9c77a81a6eb7045945d358e7cbf2ecf78 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 24 Jan 2025 14:53:17 +0100
Subject: [PATCH 06/28] refactor(dm): update private dm to use ndk

---
 src/hooks/useNDK.ts             | 137 +++++++++++++++++++++++++++++-
 src/pages/create/index.tsx      |   4 +-
 src/pages/sign/index.tsx        |   6 +-
 src/types/errors/SendDMError.ts |   3 +-
 src/utils/nostr.ts              | 143 --------------------------------
 5 files changed, 142 insertions(+), 151 deletions(-)

diff --git a/src/hooks/useNDK.ts b/src/hooks/useNDK.ts
index ad9e54f..2076a29 100644
--- a/src/hooks/useNDK.ts
+++ b/src/hooks/useNDK.ts
@@ -12,7 +12,9 @@ import {
 import _ from 'lodash'
 import {
   Event,
+  finalizeEvent,
   generateSecretKey,
+  getEventHash,
   getPublicKey,
   kinds,
   UnsignedEvent
@@ -40,17 +42,21 @@ import {
   getDTagForUserAppData,
   getUserAppDataFromBlossom,
   hexToNpub,
+  nip44Encrypt,
   parseJson,
+  randomTimeUpTo2DaysInThePast,
   SIGIT_RELAY,
   unixNow,
   uploadUserAppDataToBlossom
 } from '../utils'
+import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
 
 export const useNDK = () => {
   const dispatch = useAppDispatch()
   const {
     ndk,
     fetchEvent,
+    fetchEventFromUserRelays,
     fetchEventsFromUserRelays,
     publish,
     getNDKRelayList
@@ -503,10 +509,139 @@ export const useNDK = () => {
     [ndk, usersPubkey, getNDKRelayList]
   )
 
+  /**
+   * Modified {@link UnsignedEvent Unsigned Event} that includes an id
+   *
+   * Fields id and created_at are required.
+   * @see {@link UnsignedEvent}
+   * @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
+   */
+  type UnsignedEventWithId = UnsignedEvent & {
+    id?: string
+  }
+  const sendPrivateDirectMessage = useCallback(
+    async (message: string, receiver: string, subject?: string) => {
+      if (!receiver) throw new SendDMError(SendDMErrorType.MISSING_RECIEVER)
+
+      // Get the direct message preferred relays list
+      // https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
+      const preferredRelaysListEvent = await fetchEventFromUserRelays(
+        {
+          kinds: [NDKKind.DirectMessageReceiveRelayList],
+          authors: [receiver]
+        },
+        receiver,
+        UserRelaysType.Read
+      )
+
+      const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
+      const finalRelaysList: string[] = []
+      if (preferredRelaysListEvent) {
+        const preferredRelaysList = preferredRelaysListEvent.tags
+          .filter((t) => isRelayTag(t))
+          .map((t) => t[1])
+
+        finalRelaysList.push(...preferredRelaysList)
+      }
+
+      if (!finalRelaysList.length) {
+        // Get receiver's read relay list
+        const ndkRelayList = await getNDKRelayList(receiver).catch((err) => {
+          // Log an error if retrieving relay list metadata fails
+          console.log(
+            `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
+            err
+          )
+          return null
+        })
+        if (ndkRelayList?.readRelayUrls) {
+          finalRelaysList.push(...ndkRelayList.readRelayUrls)
+        }
+      }
+
+      if (!finalRelaysList.length) {
+        finalRelaysList.push(SIGIT_RELAY)
+      }
+
+      // Generate "sender"
+      const senderSecret = generateSecretKey()
+      const senderPubkey = getPublicKey(senderSecret)
+
+      // Prepare tags for the message
+      const tags: string[][] = [['p', receiver]]
+
+      // Conversation title
+      if (subject) tags.push(['subject', subject])
+
+      // Create private DM event containing the message and relevant metadata
+      // TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
+      const dm: UnsignedEventWithId = {
+        pubkey: senderPubkey,
+        created_at: unixNow(),
+        kind: 14,
+        tags,
+        content: message
+      }
+
+      // Calculate the hash based on the UnverifiedEvent
+      dm.id = getEventHash(dm)
+
+      // Encrypt the private dm using the sender secret and the receiver's public key
+      const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
+      if (!encryptedDm) {
+        throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
+          context: {
+            receiver,
+            message,
+            kind: dm.kind
+          }
+        })
+      }
+
+      // Seal the message
+      // TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
+      const sealedMessage: UnsignedEvent = {
+        kind: 13, // seal
+        pubkey: senderPubkey,
+        content: encryptedDm,
+        created_at: randomTimeUpTo2DaysInThePast(),
+        tags: [] // no tags
+      }
+
+      // Finalize and sign the sealed event
+      const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
+
+      // Encrypt the seal and gift wrap
+      const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
+
+      const ndkEvent = new NDKEvent(ndk, finalizedGiftWrap)
+
+      // Publish the finalized gift wrap event (the encrypted DM) to the relays
+      const publishedOnRelays = await ndkEvent.publish(
+        NDKRelaySet.fromRelayUrls(finalRelaysList, ndk, true)
+      )
+
+      // Handle cases where publishing to the relays failed
+      if (publishedOnRelays.size === 0) {
+        throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
+          context: {
+            receiver,
+            count: publishedOnRelays.size
+          }
+        })
+      }
+
+      // Return true indicating that the DM was successfully sent
+      return true
+    },
+    [fetchEventFromUserRelays, getNDKRelayList, ndk]
+  )
+
   return {
     getUsersAppData,
     subscribeForSigits,
     updateUsersAppData,
-    sendNotification
+    sendNotification,
+    sendPrivateDirectMessage
   }
 }
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 686f080..89a09b4 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -45,7 +45,6 @@ import {
   uploadToFileStorage,
   DEFAULT_TOOLBOX,
   settleAllFullfilfedPromises,
-  sendPrivateDirectMessage,
   parseNostrEvent,
   uploadMetaToFileStorage
 } from '../../utils'
@@ -89,7 +88,8 @@ export const CreatePage = () => {
   const navigate = useNavigate()
   const location = useLocation()
   const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
-  const { updateUsersAppData, sendNotification } = useNDK()
+  const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
+    useNDK()
 
   const { uploadedFiles } = location.state || {}
   const [currentFile, setCurrentFile] = useState<File>()
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index 309ee1f..e210cb9 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -29,7 +29,6 @@ import {
   unixNow,
   updateMarks,
   uploadMetaToFileStorage,
-  sendPrivateDirectMessage,
   parseNostrEvent
 } from '../../utils'
 import { CurrentUserMark, Mark } from '../../types/mark.ts'
@@ -44,7 +43,8 @@ export const SignPage = () => {
   const navigate = useNavigate()
   const location = useLocation()
   const params = useParams()
-  const { updateUsersAppData, sendNotification } = useNDK()
+  const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
+    useNDK()
 
   const usersAppData = useAppSelector((state) => state.userAppData)
 
@@ -607,7 +607,7 @@ export const SignPage = () => {
 
     // Send DMs
     setLoadingSpinnerDesc('Sending DMs')
-    const createSignatureEvent = await parseNostrEvent(meta.createSignature)
+    const createSignatureEvent = parseNostrEvent(meta.createSignature)
     const { id } = createSignatureEvent
 
     if (isLastSigner) {
diff --git a/src/types/errors/SendDMError.ts b/src/types/errors/SendDMError.ts
index 223823b..70cc94a 100644
--- a/src/types/errors/SendDMError.ts
+++ b/src/types/errors/SendDMError.ts
@@ -1,8 +1,7 @@
 import { Jsonable } from '.'
 
 export enum SendDMErrorType {
-  'METADATA_FETCH_FAILED' = 'Sending DM failed. An error occured while fetching user metadata.',
-  'RELAY_READ_EMPTY' = `Sending DM failed. The user's relay read set is empty.`,
+  'MISSING_RECIEVER' = 'Sending DM failed. Reciever is required.',
   'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
   'RELAY_PUBLISH_FAILED' = 'Sending DM failed. Publishing events failed.'
 }
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index 17030ad..3d8aa15 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -23,7 +23,6 @@ import { Meta, SignedEvent } from '../types'
 import { SIGIT_BLOSSOM } from './const.ts'
 import { getHash } from './hash'
 import { parseJson, removeLeadingSlash } from './string'
-import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError.ts'
 
 /**
  * Generates a `d` tag for userAppData
@@ -514,148 +513,6 @@ export const getProfileUsername = (
     length: 16
   })
 
-/**
- * Modified {@link UnsignedEvent Unsigned Event} that includes an id
- *
- * Fields id and created_at are required.
- * @see {@link UnsignedEvent}
- * @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
- */
-type UnsignedEventWithId = UnsignedEvent & {
-  id?: string
-}
-export const sendPrivateDirectMessage = async (
-  message: string,
-  receiver: string,
-  subject?: string
-) => {
-  // Instantiate the MetadataController to retrieve relay list metadata to look for preferred DM relays
-  const metadataController = MetadataController.getInstance()
-  const relaySet = await metadataController
-    .findRelayListMetadata(receiver)
-    .catch((err) => {
-      // Log an error if retrieving relay list metadata fails
-      console.log(
-        `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
-        err
-      )
-      return null
-    })
-
-  // Throw if metadata retrieval failed
-  if (!relaySet) {
-    throw new SendDMError(SendDMErrorType.METADATA_FETCH_FAILED, {
-      context: {
-        receiver
-      }
-    })
-  }
-
-  // Ensure relay list is not empty
-  if (relaySet.read.length === 0) {
-    throw new SendDMError(SendDMErrorType.RELAY_READ_EMPTY, {
-      context: {
-        receiver,
-        relaySet: JSON.stringify(relaySet)
-      }
-    })
-  }
-  // Get the direct message preferred relays list
-  // TODO: kinds.DirectMessageRelaysList  (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
-  // https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
-  const eventFilter: Filter = {
-    kinds: [10050],
-    authors: [receiver]
-  }
-  const preferredRelaysListEvents =
-    await RelayController.getInstance().fetchEvents(eventFilter, relaySet.read)
-
-  const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
-  const preferredRelaysList = preferredRelaysListEvents.reduce(
-    (previous: string[], current: Event) => {
-      const relaysList = current.tags
-        .filter((t) => isRelayTag(t) && !previous.includes(t[1]))
-        .map((t) => t[1])
-
-      return [...previous, ...relaysList]
-    },
-    []
-  )
-  // Empty preferred relays list
-  const finalRelaysList: string[] =
-    preferredRelaysList?.length > 0 ? preferredRelaysList : [...relaySet.write]
-
-  // Generate "sender"
-  const senderSecret = generateSecretKey()
-  const senderPubkey = getPublicKey(senderSecret)
-
-  // Prepare tags for the message
-  const tags: string[][] = [['p', receiver]]
-
-  // Conversation title
-  if (subject) tags.push(['subject', subject])
-
-  // Create private DM event containing the message and relevant metadata
-  // TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
-  const dm: UnsignedEventWithId = {
-    pubkey: senderPubkey,
-    created_at: unixNow(),
-    kind: 14,
-    tags,
-    content: message
-  }
-
-  // Calculate the hash based on the UnverifiedEvent
-  dm.id = getEventHash(dm)
-
-  // Encrypt the private dm using the sender secret and the receiver's public key
-  const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
-  if (!encryptedDm) {
-    throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
-      context: {
-        receiver,
-        message,
-        kind: dm.kind
-      }
-    })
-  }
-
-  // Seal the message
-  // TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
-  const sealedMessage: UnsignedEvent = {
-    kind: 13, // seal
-    pubkey: senderPubkey,
-    content: encryptedDm,
-    created_at: randomTimeUpTo2DaysInThePast(),
-    tags: [] // no tags
-  }
-
-  // Finalize and sign the sealed event
-  const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
-
-  // Encrypt the seal and gift wrap
-  const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
-
-  // Publish the finalized gift wrap event (the encrypted DM) to the relays
-  const publishedOnRelays = await relayController.publish(
-    finalizedGiftWrap,
-    finalRelaysList
-  )
-
-  // Handle cases where publishing to the relays failed
-  if (publishedOnRelays.length === 0) {
-    throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
-      context: {
-        receiver,
-        count: publishedOnRelays.length
-      }
-    })
-  }
-
-  // Return true indicating that the DM was successfully sent
-  return true
-}
-
 /**
  * Orders an array of NDKEvent objects chronologically based on their `created_at` property.
  *

From 37baf5709397e7099a735d6390cfceffff646f11 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 31 Jan 2025 19:32:28 +0100
Subject: [PATCH 07/28] fix(callback): login and private route redirect

Fix #229
---
 src/App.tsx                 | 20 +++------------
 src/layouts/Main.tsx        | 49 +++++++++++++++++--------------------
 src/pages/landing/index.tsx | 18 +++++++-------
 src/routes/PrivateRoute.tsx | 21 ++++++++++++++++
 src/routes/util.tsx         | 49 +++++++++++++++++++++++++++++++------
 src/utils/localStorage.ts   | 24 ------------------
 6 files changed, 97 insertions(+), 84 deletions(-)
 create mode 100644 src/routes/PrivateRoute.tsx

diff --git a/src/App.tsx b/src/App.tsx
index 3829ba6..d10dc0d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,8 +4,6 @@ import { Navigate, Route, Routes } from 'react-router-dom'
 import { useAppSelector, useAuth } from './hooks'
 
 import { MainLayout } from './layouts/Main'
-
-import { appPrivateRoutes, appPublicRoutes } from './routes'
 import {
   privateRoutes,
   publicRoutes,
@@ -16,7 +14,7 @@ import './App.scss'
 
 const App = () => {
   const { checkSession } = useAuth()
-  const authState = useAppSelector((state) => state.auth)
+  const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
 
   useEffect(() => {
     if (window.location.hostname === '0.0.0.0') {
@@ -29,19 +27,9 @@ const App = () => {
     checkSession()
   }, [checkSession])
 
-  const handleRootRedirect = () => {
-    if (authState.loggedIn) return appPrivateRoutes.homePage
-
-    const callbackPathEncoded = btoa(
-      window.location.href.split(`${window.location.origin}/#`)[1]
-    )
-
-    return `${appPublicRoutes.landingPage}?callbackPath=${callbackPathEncoded}`
-  }
-
   // Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true
   const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
-    return !authState.loggedIn || !r.hiddenWhenLoggedIn
+    return !isLoggedIn || !r.hiddenWhenLoggedIn
   })
 
   const privateRouteList = recursiveRouteRenderer(privateRoutes)
@@ -49,9 +37,9 @@ const App = () => {
   return (
     <Routes>
       <Route element={<MainLayout />}>
-        {authState?.loggedIn && privateRouteList}
         {publicRoutesList}
-        <Route path="*" element={<Navigate to={handleRootRedirect()} />} />
+        {privateRouteList}
+        <Route path="*" element={<Navigate to={'/'} />} />
       </Route>
     </Routes>
   )
diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx
index 85daf75..c91c986 100644
--- a/src/layouts/Main.tsx
+++ b/src/layouts/Main.tsx
@@ -1,16 +1,11 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
 import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
-
 import { getPublicKey, nip19 } from 'nostr-tools'
-
 import { init as initNostrLogin } from 'nostr-login'
 import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
-
 import { AppBar } from '../components/AppBar/AppBar'
 import { LoadingSpinner } from '../components/LoadingSpinner'
-
 import { NostrController } from '../controllers'
-
 import {
   useAppDispatch,
   useAppSelector,
@@ -19,7 +14,6 @@ import {
   useNDK,
   useNDKContext
 } from '../hooks'
-
 import {
   restoreState,
   setUserProfile,
@@ -30,9 +24,7 @@ import {
   setUserRobotImage
 } from '../store/actions'
 import { LoginMethod } from '../store/auth/types'
-
 import { getRoboHashPicture, loadState } from '../utils'
-
 import styles from './style.module.scss'
 
 export const MainLayout = () => {
@@ -53,29 +45,32 @@ export const MainLayout = () => {
   // Ref to track if `subscribeForSigits` has been called
   const hasSubscribed = useRef(false)
 
-  const navigateAfterLogin = (path: string) => {
-    const callbackPath = searchParams.get('callbackPath')
-
-    if (callbackPath) {
-      // base64 decoded path
-      const path = atob(callbackPath)
+  const navigateAfterLogin = useCallback(
+    (path: string) => {
+      const isCallback = window.location.hash.startsWith('#/?callbackPath=')
+      if (isCallback) {
+        const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
+        setSearchParams((prev) => {
+          prev.delete('callbackPath')
+          return prev
+        })
+        navigate(path)
+        return
+      }
       navigate(path)
-      return
-    }
-
-    navigate(path)
-  }
+    },
+    [navigate, setSearchParams]
+  )
 
   const login = useCallback(async () => {
-    dispatch(updateLoginMethod(LoginMethod.nostrLogin))
-
-    const nostrController = NostrController.getInstance()
-    const pubkey = await nostrController.capturePublicKey()
-
-    const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
-
-    if (redirectPath) {
+    try {
+      dispatch(updateLoginMethod(LoginMethod.nostrLogin))
+      const nostrController = NostrController.getInstance()
+      const pubkey = await nostrController.capturePublicKey()
+      const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
       navigateAfterLogin(redirectPath)
+    } catch (error) {
+      console.error(`Error occured during login`, error)
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [dispatch])
diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx
index 773b923..5d24bdf 100644
--- a/src/pages/landing/index.tsx
+++ b/src/pages/landing/index.tsx
@@ -1,7 +1,5 @@
 import { Box, Button } from '@mui/material'
-import { useEffect } from 'react'
-import { Outlet, useLocation } from 'react-router-dom'
-import { saveVisitedLink } from '../../utils'
+import { Outlet } from 'react-router-dom'
 import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
 import { Container } from '../../components/Container'
 import styles from './style.module.scss'
@@ -20,13 +18,19 @@ import {
 import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
 import { Footer } from '../../components/Footer/Footer'
 import { launch as launchNostrLoginDialog } from 'nostr-login'
+import { useDidMount } from '../../hooks'
 
 export const LandingPage = () => {
-  const location = useLocation()
-
   const onSignInClick = async () => {
     launchNostrLoginDialog()
   }
+  useDidMount(() => {
+    const isCallback = window.location.hash.startsWith('#/?callbackPath=')
+    // Open nostr login if detect callback
+    if (isCallback) {
+      onSignInClick()
+    }
+  })
 
   const cards = [
     {
@@ -101,10 +105,6 @@ export const LandingPage = () => {
     }
   ]
 
-  useEffect(() => {
-    saveVisitedLink(location.pathname, location.search)
-  }, [location])
-
   return (
     <div className={styles.background}>
       <div
diff --git a/src/routes/PrivateRoute.tsx b/src/routes/PrivateRoute.tsx
new file mode 100644
index 0000000..410ecea
--- /dev/null
+++ b/src/routes/PrivateRoute.tsx
@@ -0,0 +1,21 @@
+import { Navigate, useLocation } from 'react-router-dom'
+import { useAppSelector } from '../hooks'
+import { appPublicRoutes } from '.'
+
+export function PrivateRoute({ children }: { children: JSX.Element }) {
+  const location = useLocation()
+  const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
+  if (!isLoggedIn) {
+    return (
+      <Navigate
+        to={{
+          pathname: appPublicRoutes.landingPage,
+          search: `?callbackPath=${btoa(location.pathname)}`
+        }}
+        replace
+      />
+    )
+  }
+
+  return children
+}
diff --git a/src/routes/util.tsx b/src/routes/util.tsx
index 8773b81..2e1be26 100644
--- a/src/routes/util.tsx
+++ b/src/routes/util.tsx
@@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
 import { SettingsPage } from '../pages/settings/Settings'
 import { SignPage } from '../pages/sign'
 import { VerifyPage } from '../pages/verify'
+import { PrivateRoute } from './PrivateRoute'
 
 /**
  * Helper type allows for extending react-router-dom's **RouteProps** with generic type
@@ -70,34 +71,66 @@ export const publicRoutes: PublicRouteProps[] = [
 export const privateRoutes = [
   {
     path: appPrivateRoutes.homePage,
-    element: <HomePage />
+    element: (
+      <PrivateRoute>
+        <HomePage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.create,
-    element: <CreatePage />
+    element: (
+      <PrivateRoute>
+        <CreatePage />
+      </PrivateRoute>
+    )
   },
   {
     path: `${appPrivateRoutes.sign}/:id?`,
-    element: <SignPage />
+    element: (
+      <PrivateRoute>
+        <SignPage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.settings,
-    element: <SettingsPage />
+    element: (
+      <PrivateRoute>
+        <SettingsPage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.profileSettings,
-    element: <ProfileSettingsPage />
+    element: (
+      <PrivateRoute>
+        <ProfileSettingsPage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.cacheSettings,
-    element: <CacheSettingsPage />
+    element: (
+      <PrivateRoute>
+        <CacheSettingsPage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.relays,
-    element: <RelaysPage />
+    element: (
+      <PrivateRoute>
+        <RelaysPage />
+      </PrivateRoute>
+    )
   },
   {
     path: appPrivateRoutes.nostrLogin,
-    element: <NostrLoginPage />
+    element: (
+      <PrivateRoute>
+        <NostrLoginPage />
+      </PrivateRoute>
+    )
   }
 ]
diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts
index 472e092..8196e35 100644
--- a/src/utils/localStorage.ts
+++ b/src/utils/localStorage.ts
@@ -26,30 +26,6 @@ export const clearState = () => {
   localStorage.removeItem('state')
 }
 
-export const saveVisitedLink = (pathname: string, search: string) => {
-  localStorage.setItem(
-    'visitedLink',
-    JSON.stringify({
-      pathname,
-      search
-    })
-  )
-}
-
-export const getVisitedLink = () => {
-  const visitedLink = localStorage.getItem('visitedLink')
-  if (!visitedLink) return null
-
-  try {
-    return JSON.parse(visitedLink) as {
-      pathname: string
-      search: string
-    }
-  } catch {
-    return null
-  }
-}
-
 export const saveAuthToken = (token: string) => {
   localStorage.setItem('authToken', token)
 }

From e405b735f7ca03e6c1fabe8ddaf4ba3e3c6cec6d Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Feb 2025 14:37:08 +0100
Subject: [PATCH 08/28] fix(dm): always add sigit relay when sending private
 DMs

---
 src/hooks/useNDK.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/hooks/useNDK.ts b/src/hooks/useNDK.ts
index 2076a29..c6ec41d 100644
--- a/src/hooks/useNDK.ts
+++ b/src/hooks/useNDK.ts
@@ -559,7 +559,7 @@ export const useNDK = () => {
         }
       }
 
-      if (!finalRelaysList.length) {
+      if (!finalRelaysList.includes(SIGIT_RELAY)) {
         finalRelaysList.push(SIGIT_RELAY)
       }
 

From 1474fafde7b76f465d895ef0457960c0704ec444 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Feb 2025 14:49:25 +0100
Subject: [PATCH 09/28] fix(dm): don't send private DM twice to same signer

---
 src/pages/sign/index.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx
index e210cb9..a1a3039 100644
--- a/src/pages/sign/index.tsx
+++ b/src/pages/sign/index.tsx
@@ -633,7 +633,8 @@ export const SignPage = () => {
         )
       }
     } else {
-      // Notify the creator and the next signer (/sign).
+      // Notify the creator and
+      // the next signer (/sign).
       try {
         await sendPrivateDirectMessage(
           `Sigit signed by ${usersNpub}, visit ${window.location.origin}/#/sign/${id}`,
@@ -648,12 +649,12 @@ export const SignPage = () => {
 
       // No need to notify creator twice, skipping
       const currentSignerIndex = signers.indexOf(usersNpub)
-      const nextSigner = signers[currentSignerIndex + 1]
+      const nextSigner = npubToHex(signers[currentSignerIndex + 1])
       if (nextSigner !== submittedBy) {
         try {
           await sendPrivateDirectMessage(
             `You're the next signer, visit ${window.location.origin}/#/sign/${id}`,
-            npubToHex(nextSigner)!
+            nextSigner!
           )
         } catch (error) {
           if (error instanceof SendDMError) {

From 6f4b41d84b7968286bd3aa7503afe23375e58fdf Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Feb 2025 17:16:45 +0100
Subject: [PATCH 10/28] fix(login): remove default login redirect

---
 src/hooks/useAuth.ts | 5 ++---
 src/layouts/Main.tsx | 4 ++--
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
index e0e75fb..7199a51 100644
--- a/src/hooks/useAuth.ts
+++ b/src/hooks/useAuth.ts
@@ -56,8 +56,7 @@ export const useAuth = () => {
    * method will be chosen (extension or keys)
    *
    * @param pubkey of the user trying to login
-   * @returns url to redirect if authentication successfull
-   * or error if otherwise
+   * @returns url to redirect if user has no relays set
    */
   const authAndGetMetadataAndRelaysMap = useCallback(
     async (pubkey: string) => {
@@ -108,7 +107,7 @@ export const useAuth = () => {
         dispatch(setRelayMapAction(relayMap))
       }
 
-      return appPrivateRoutes.homePage
+      return
     },
     [
       dispatch,
diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx
index c91c986..2106931 100644
--- a/src/layouts/Main.tsx
+++ b/src/layouts/Main.tsx
@@ -46,7 +46,7 @@ export const MainLayout = () => {
   const hasSubscribed = useRef(false)
 
   const navigateAfterLogin = useCallback(
-    (path: string) => {
+    (path: string | undefined) => {
       const isCallback = window.location.hash.startsWith('#/?callbackPath=')
       if (isCallback) {
         const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
@@ -57,7 +57,7 @@ export const MainLayout = () => {
         navigate(path)
         return
       }
-      navigate(path)
+      if (path) navigate(path)
     },
     [navigate, setSearchParams]
   )

From f7d0718b7820ebc66310f572b0ceb6e91f5393bd Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Feb 2025 19:14:25 +0100
Subject: [PATCH 11/28] fix(search): tim input, add timeout

Fixes #308
---
 src/pages/create/index.tsx | 60 +++++++++++++++++++-------------------
 1 file changed, 30 insertions(+), 30 deletions(-)

diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index d5424a9..2d476ae 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -45,7 +45,8 @@ import {
   uploadToFileStorage,
   DEFAULT_TOOLBOX,
   settleAllFullfilfedPromises,
-  uploadMetaToFileStorage
+  uploadMetaToFileStorage,
+  timeout
 } from '../../utils'
 import { Container } from '../../components/Container'
 import fileListStyles from '../../components/FileList/style.module.scss'
@@ -79,6 +80,7 @@ import { useNDKContext } from '../../hooks/useNDKContext.ts'
 import { useNDK } from '../../hooks/useNDK.ts'
 import { useImmer } from 'use-immer'
 import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
+import { TimeoutError } from '../../types/errors/TimeoutError.ts'
 
 type FoundUser = NostrEvent & { npub: string }
 
@@ -162,8 +164,8 @@ export const CreatePage = () => {
     return pubkey
   }
 
-  const handleSearchUsers = async (searchValue?: string) => {
-    const searchString = searchValue || userSearchInput || undefined
+  const handleSearchUsers = async () => {
+    const searchString = userSearchInput || undefined
 
     if (!searchString) return
 
@@ -171,14 +173,17 @@ export const CreatePage = () => {
 
     const searchTerm = searchString.trim()
 
-    fetchEventsFromUserRelays(
-      {
-        kinds: [0],
-        search: searchTerm
-      },
-      usersPubkey,
-      UserRelaysType.Write
-    )
+    Promise.race([
+      fetchEventsFromUserRelays(
+        {
+          kinds: [0],
+          search: searchTerm
+        },
+        usersPubkey,
+        UserRelaysType.Write
+      ),
+      timeout(30000)
+    ])
       .then((events) => {
         const nostrEvents = events.map((event) => event.rawEvent())
 
@@ -216,6 +221,9 @@ export const CreatePage = () => {
           toast.info('No user found with the provided search term')
       })
       .catch((error) => {
+        if (error instanceof TimeoutError) {
+          toast.error('Search timed out. Please try again.')
+        }
         console.error(error)
       })
       .finally(() => {
@@ -245,22 +253,23 @@ export const CreatePage = () => {
 
       // If pasted user npub of nip05 is present, we just add the user to the counterparts list
       if (pastedUserNpubOrNip05) {
-        setUserInput(pastedUserNpubOrNip05)
+        setUserInput(pastedUserNpubOrNip05.trim())
         setPastedUserNpubOrNip05(undefined)
       } else {
-        // Otherwize if search already provided some results, user must manually click the search button
+        // Otherwise if search already provided some results, user must manually click the search button
         if (!foundUsers.length) {
+          const searchTerm = userSearchInput.trim()
           // If it's NIP05 (includes @ or is a valid domain) send request to .well-known
           const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
-          if (domainRegex.test(userSearchInput)) {
+          if (searchTerm.startsWith('_@') || domainRegex.test(searchTerm)) {
             setSearchUsersLoading(true)
 
-            const pubkey = await handleSearchUserNip05(userSearchInput)
+            const pubkey = await handleSearchUserNip05(searchTerm)
 
             setSearchUsersLoading(false)
 
             if (pubkey) {
-              setUserInput(userSearchInput)
+              setUserInput(searchTerm)
             } else {
               toast.error(`No user found with the NIP05: ${userSearchInput}`)
             }
@@ -411,7 +420,7 @@ export const CreatePage = () => {
 
     setUserSearchInput('')
 
-    if (input.startsWith('npub')) {
+    if (input.startsWith('npub1')) {
       return handleAddNpubUser(input)
     }
 
@@ -1034,17 +1043,8 @@ export const CreatePage = () => {
     }
 
     // Seems like it's npub format
-    if (value.startsWith('npub')) {
-      // We will try to convert npub to hex and if it's successfull that means
-      // npub is valid
-      const validHexPubkey = npubToHex(value)
-
-      if (validHexPubkey) {
-        // Arm the manual user npub add after enter is hit, we don't want to trigger search
-        setPastedUserNpubOrNip05(value)
-      } else {
-        disarmAddOnEnter()
-      }
+    if (value.trim().startsWith('npub1')) {
+      setPastedUserNpubOrNip05(value.trim())
     } else {
       // Disarm the add user on enter hit, and trigger search after 1 second
       disarmAddOnEnter()
@@ -1204,7 +1204,7 @@ export const CreatePage = () => {
                 {!pastedUserNpubOrNip05 ? (
                   <Button
                     disabled={!userSearchInput || searchUsersLoading}
-                    onClick={() => handleSearchUsers()}
+                    onClick={handleSearchUsers}
                     variant="contained"
                     aria-label="Add"
                     className={styles.counterpartToggleButton}
@@ -1218,7 +1218,7 @@ export const CreatePage = () => {
                 ) : (
                   <Button
                     onClick={() => {
-                      setUserInput(userSearchInput)
+                      setUserInput(userSearchInput.trim())
                     }}
                     variant="contained"
                     aria-label="Add"

From 13b88516cac858dd481ba4974509de5b35dffdb1 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Mon, 17 Feb 2025 19:06:19 +0100
Subject: [PATCH 12/28] feat(draft): serialize sigit and save/load to local
 storage

---
 src/types/draft.ts |  19 ++++++++
 src/types/index.ts |   2 +
 src/utils/draft.ts | 112 +++++++++++++++++++++++++++++++++++++++++++++
 src/utils/index.ts |   1 +
 4 files changed, 134 insertions(+)
 create mode 100644 src/types/draft.ts
 create mode 100644 src/utils/draft.ts

diff --git a/src/types/draft.ts b/src/types/draft.ts
new file mode 100644
index 0000000..900d87e
--- /dev/null
+++ b/src/types/draft.ts
@@ -0,0 +1,19 @@
+import { SigitFile } from '../utils/file'
+import { User } from './core'
+import { DrawnField } from './drawing'
+
+export interface SigitFileDraft {
+  name: string
+  file: string
+  pages: DrawnField[][]
+}
+export interface SigitDraft {
+  title: string
+  users: User[]
+  files: SigitFile[]
+}
+export interface SerializedSigitDraft {
+  title: string
+  users: User[]
+  files: SigitFileDraft[]
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 5c5b715..40b240b 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -4,3 +4,5 @@ export * from './nostr'
 export * from './relay'
 export * from './zip'
 export * from './event'
+export * from './drawing'
+export * from './draft'
diff --git a/src/utils/draft.ts b/src/utils/draft.ts
new file mode 100644
index 0000000..4e9ae56
--- /dev/null
+++ b/src/utils/draft.ts
@@ -0,0 +1,112 @@
+import {
+  DrawnField,
+  SerializedSigitDraft,
+  SigitDraft,
+  SigitFileDraft
+} from '../types'
+import {
+  getMediaType,
+  extractFileExtension,
+  toFile,
+  getSigitFile
+} from './file'
+
+let saveSigitDraftTimeout: number | null = null
+const serializeSigitDraft = async (
+  draft: SigitDraft
+): Promise<SerializedSigitDraft> => {
+  const serializedFiles = draft.files.map((file) => {
+    return new Promise<SigitFileDraft>((resolve, reject) => {
+      const reader = new FileReader()
+      reader.onload = () => {
+        const pages = file.pages
+          ? file.pages.map((page) =>
+              page.drawnFields.map(
+                (field) =>
+                  ({
+                    left: field.left,
+                    top: field.top,
+                    width: field.width,
+                    height: field.height,
+                    type: field.type,
+                    counterpart: field.counterpart
+                  }) as DrawnField
+              )
+            )
+          : []
+        resolve({
+          name: file.name,
+          pages: pages,
+          file: reader.result as string
+        })
+      }
+      reader.onerror = (error) => reject(error)
+      reader.readAsDataURL(file)
+    })
+  })
+
+  const serializedFileDraft = await Promise.all(serializedFiles)
+  return {
+    title: draft.title,
+    users: [...draft.users],
+    files: serializedFileDraft
+  }
+}
+
+const deserializeSigitDraft = async (
+  serializedDraft: SerializedSigitDraft
+): Promise<SigitDraft> => {
+  const files = await Promise.all(
+    serializedDraft.files.map(async (draft) => {
+      const response = await fetch(draft.file)
+      const arrayBuffer = await response.arrayBuffer()
+      const type = getMediaType(extractFileExtension(draft.name))
+      const file = toFile(arrayBuffer, draft.name, type)
+      const sigitFile = await getSigitFile(file)
+      if (draft.pages) {
+        for (let i = 0; i < draft.pages.length; i++) {
+          const drawnFields = draft.pages[i]
+          if (sigitFile.pages) sigitFile.pages[i].drawnFields = [...drawnFields]
+        }
+      }
+      return sigitFile
+    })
+  )
+
+  return {
+    ...serializedDraft,
+    files: files
+  }
+}
+
+export const saveSigitDraft = (draft: SigitDraft) => {
+  if (saveSigitDraftTimeout) {
+    clearTimeout(saveSigitDraftTimeout)
+  }
+
+  saveSigitDraftTimeout = window.setTimeout(() => {
+    serializeSigitDraft(draft)
+      .then((draftToSave) => {
+        localStorage.setItem('sigitDraft', JSON.stringify(draftToSave))
+      })
+      .catch((error) => {
+        console.log(`Error while saving sigit draft. Error: `, error)
+      })
+  }, 1000)
+}
+
+export const getSigitDraft = async () => {
+  const sigitDraft = localStorage.getItem('sigitDraft')
+  if (!sigitDraft) return null
+
+  try {
+    const serializedDraft = JSON.parse(sigitDraft) as SerializedSigitDraft
+    return await deserializeSigitDraft(serializedDraft)
+  } catch {
+    return null
+  }
+}
+
+export const clearSigitDraft = () => {
+  localStorage.removeItem('sigitDraft')
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 274ceab..9c4464a 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -12,3 +12,4 @@ export * from './string'
 export * from './url'
 export * from './utils'
 export * from './zip'
+export * from './draft'

From 9f4a891d5002532b72e9e068a85bd809491aeeef Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Tue, 18 Feb 2025 16:56:40 +0100
Subject: [PATCH 13/28] feat(create): add local draft, save progress to local
 storage

Closes #175
---
 package-lock.json                             |  17 +--
 package.json                                  |   3 +-
 .../DisplaySigit/LocalDraftSigit.tsx          | 117 ++++++++++++++++++
 src/components/DrawPDFFields/index.tsx        |  33 +++--
 src/pages/create/index.tsx                    |  79 ++++++++++--
 src/pages/home/index.tsx                      |  25 +++-
 src/types/draft.ts                            |   2 +
 src/utils/draft.ts                            |  34 +++--
 src/utils/meta.ts                             |   3 +-
 9 files changed, 254 insertions(+), 59 deletions(-)
 create mode 100644 src/components/DisplaySigit/LocalDraftSigit.tsx

diff --git a/package-lock.json b/package-lock.json
index 6235019..9e327f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "sigit",
-  "version": "0.0.0-beta",
+  "version": "1.0.3",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "sigit",
-      "version": "0.0.0-beta",
+      "version": "1.0.3",
       "hasInstallScript": true,
       "license": "AGPL-3.0-or-later ",
       "dependencies": {
@@ -51,8 +51,7 @@
         "react-toastify": "10.0.4",
         "redux": "5.0.1",
         "signature_pad": "^5.0.4",
-        "tseep": "1.2.1",
-        "use-immer": "^0.11.0"
+        "tseep": "1.2.1"
       },
       "devDependencies": {
         "@saithodev/semantic-release-gitea": "^2.1.0",
@@ -17265,16 +17264,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/use-immer": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz",
-      "integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==",
-      "license": "MIT",
-      "peerDependencies": {
-        "immer": ">=8.0.0",
-        "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
-      }
-    },
     "node_modules/use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
diff --git a/package.json b/package.json
index 3081a75..d650959 100644
--- a/package.json
+++ b/package.json
@@ -62,8 +62,7 @@
     "react-toastify": "10.0.4",
     "redux": "5.0.1",
     "signature_pad": "^5.0.4",
-    "tseep": "1.2.1",
-    "use-immer": "^0.11.0"
+    "tseep": "1.2.1"
   },
   "devDependencies": {
     "@saithodev/semantic-release-gitea": "^2.1.0",
diff --git a/src/components/DisplaySigit/LocalDraftSigit.tsx b/src/components/DisplaySigit/LocalDraftSigit.tsx
new file mode 100644
index 0000000..f59da16
--- /dev/null
+++ b/src/components/DisplaySigit/LocalDraftSigit.tsx
@@ -0,0 +1,117 @@
+import { useState } from 'react'
+import { Link } from 'react-router-dom'
+import { Tooltip, Button, Divider } from '@mui/material'
+import {
+  faCalendar,
+  faFile,
+  faFileCircleExclamation,
+  faPen,
+  faTrash
+} from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { SigitDraft, UserRole } from '../../types'
+import { appPrivateRoutes } from '../../routes'
+import {
+  formatTimestamp,
+  getSigitDraft,
+  npubToHex,
+  SigitStatus,
+  SignStatus
+} from '../../utils'
+import { DisplaySigner } from '../DisplaySigner'
+import { UserAvatarGroup } from '../UserAvatarGroup'
+import { getExtensionIconLabel } from '../getExtensionIconLabel'
+import { useAppSelector, useDidMount } from '../../hooks'
+import styles from './style.module.scss'
+
+interface LocalDraftSigitProps {
+  handleDraftDelete: () => void
+}
+export const LocalDraftSigit = ({
+  handleDraftDelete
+}: LocalDraftSigitProps) => {
+  const [draft, setDraft] = useState<SigitDraft>()
+  useDidMount(async () => {
+    // Check if draft exists and add link to direct
+    const draft = await getSigitDraft()
+    if (draft) {
+      setDraft(draft)
+    }
+  })
+  const submittedBy = useAppSelector((state) => state.auth.usersPubkey)
+
+  if (!draft) return null
+
+  const extensions = draft.files.map((f) => f.extension)
+  const isSame = extensions.every((e) => extensions[0] === e)
+
+  return (
+    <div className={styles.itemWrapper}>
+      <Link className={styles.insetLink} to={appPrivateRoutes.create}></Link>
+      <p className={`line-clamp-2 ${styles.title}`}>{draft.title}</p>
+      <div className={styles.users}>
+        {submittedBy && (
+          <DisplaySigner status={SignStatus.Pending} pubkey={submittedBy} />
+        )}
+        {submittedBy && draft.users.length ? (
+          <Divider orientation="vertical" flexItem />
+        ) : null}
+        <UserAvatarGroup max={7}>
+          {draft.users.map((user) => {
+            const pubkey = npubToHex(user.pubkey)!
+            return (
+              <DisplaySigner
+                key={pubkey}
+                status={
+                  user.role === UserRole.signer
+                    ? SignStatus.Pending
+                    : SignStatus.Viewer
+                }
+                pubkey={pubkey}
+              />
+            )
+          })}
+        </UserAvatarGroup>
+      </div>
+      <div className={`${styles.details} ${styles.iconLabel}`}>
+        <FontAwesomeIcon icon={faCalendar} />
+        {formatTimestamp(draft.lastUpdated)}
+      </div>
+      <div className={`${styles.details} ${styles.status}`}>
+        <span className={styles.iconLabel}>
+          <FontAwesomeIcon icon={faPen} /> {SigitStatus.LocalDraft}
+        </span>
+        {extensions.length > 0 ? (
+          <span className={styles.iconLabel}>
+            {!isSame ? (
+              <>
+                <FontAwesomeIcon icon={faFile} /> Multiple File Types
+              </>
+            ) : (
+              getExtensionIconLabel(extensions[0])
+            )}
+          </span>
+        ) : (
+          <>
+            <FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
+          </>
+        )}
+      </div>
+      <div className={styles.itemActions}>
+        <Tooltip title="Delete" arrow placement="top" disableInteractive>
+          <Button
+            onClick={handleDraftDelete}
+            sx={{
+              color: 'var(--primary-main)',
+              minWidth: '34px',
+              padding: '10px'
+            }}
+            variant={'text'}
+          >
+            <FontAwesomeIcon icon={faTrash} />
+          </Button>
+        </Tooltip>
+      </div>
+    </div>
+  )
+}
diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx
index 71fef1c..ee39939 100644
--- a/src/components/DrawPDFFields/index.tsx
+++ b/src/components/DrawPDFFields/index.tsx
@@ -18,7 +18,6 @@ import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
 import { useScale } from '../../hooks/useScale'
 import { AvatarIconButton } from '../UserAvatarIconButton'
 import { UserAvatar } from '../UserAvatar'
-import { Updater } from 'use-immer'
 import { FileItem } from './internal/FileItem'
 import { FileDivider } from '../FileDivider'
 import { Counterpart } from './internal/Counterpart'
@@ -28,6 +27,7 @@ const MINIMUM_RECT_SIZE = {
   height: 10
 } as const
 import { NDKUserProfile } from '@nostr-dev-kit/ndk'
+import _ from 'lodash'
 
 const DEFAULT_START_SIZE = {
   width: 140,
@@ -45,7 +45,7 @@ interface DrawPdfFieldsProps {
   users: User[]
   userProfiles: { [key: string]: NDKUserProfile }
   sigitFiles: SigitFile[]
-  updateSigitFiles: Updater<SigitFile[]>
+  setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
   selectedTool?: DrawTool
 }
 
@@ -53,11 +53,10 @@ export const DrawPDFFields = ({
   selectedTool,
   userProfiles,
   sigitFiles,
-  updateSigitFiles,
+  setSigitFiles,
   users
 }: DrawPdfFieldsProps) => {
   const { to, from } = useScale()
-
   const signers = users.filter((u) => u.role === UserRole.signer)
   const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
   const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
@@ -354,8 +353,10 @@ export const DrawPDFFields = ({
   ) => {
     event.stopPropagation()
 
-    updateSigitFiles((draft) => {
-      draft[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1)
+    setSigitFiles((prev) => {
+      const clone = _.cloneDeep(prev)
+      clone[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1)
+      return clone
     })
   }
 
@@ -416,22 +417,28 @@ export const DrawPDFFields = ({
 
         // Add new drawn field to the files
         if (mouseState.clicked) {
-          updateSigitFiles((draft) => {
-            draft[fileIndex].pages![pageIndex].drawnFields.push(field)
+          setSigitFiles((prev) => {
+            const clone = _.cloneDeep(prev)
+            clone[fileIndex].pages![pageIndex].drawnFields.push(field)
+            return clone
           })
         }
 
         // Move
         if (mouseState.dragging) {
-          updateSigitFiles((draft) => {
-            draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
+          setSigitFiles((prev) => {
+            const clone = _.cloneDeep(prev)
+            clone[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
+            return clone
           })
         }
 
         // Resize
         if (mouseState.resizing) {
-          updateSigitFiles((draft) => {
-            draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
+          setSigitFiles((prev) => {
+            const clone = _.cloneDeep(prev)
+            clone[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field
+            return clone
           })
         }
 
@@ -446,7 +453,7 @@ export const DrawPDFFields = ({
     mouseState.clicked,
     mouseState.dragging,
     mouseState.resizing,
-    updateSigitFiles
+    setSigitFiles
   ])
 
   /**
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index d5424a9..60011b7 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -45,7 +45,10 @@ import {
   uploadToFileStorage,
   DEFAULT_TOOLBOX,
   settleAllFullfilfedPromises,
-  uploadMetaToFileStorage
+  uploadMetaToFileStorage,
+  clearSigitDraft,
+  saveSigitDraft,
+  getSigitDraft
 } from '../../utils'
 import { Container } from '../../components/Container'
 import fileListStyles from '../../components/FileList/style.module.scss'
@@ -77,7 +80,6 @@ import { AvatarIconButton } from '../../components/UserAvatarIconButton'
 import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
 import { useNDKContext } from '../../hooks/useNDKContext.ts'
 import { useNDK } from '../../hooks/useNDK.ts'
-import { useImmer } from 'use-immer'
 import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
 
 type FoundUser = NostrEvent & { npub: string }
@@ -97,7 +99,9 @@ export const CreatePage = () => {
 
   const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
 
-  const [selectedFiles, setSelectedFiles] = useState<File[]>([...uploadedFiles])
+  const [selectedFiles, setSelectedFiles] = useState<File[]>([
+    ...(uploadedFiles || [])
+  ])
   const fileInputRef = useRef<HTMLInputElement>(null)
   const handleUploadButtonClick = () => {
     if (fileInputRef.current) {
@@ -123,7 +127,7 @@ export const CreatePage = () => {
     [key: string]: NDKUserProfile
   }>({})
 
-  const [drawnFiles, updateDrawnFiles] = useImmer<SigitFile[]>([])
+  const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
   const [parsingPdf, setIsParsing] = useState<boolean>(false)
 
   const searchFieldRef = useRef<HTMLInputElement>(null)
@@ -283,27 +287,29 @@ export const CreatePage = () => {
           selectedFiles,
           getSigitFile
         )
-        updateDrawnFiles((draft) => {
+        setDrawnFiles((prev) => {
+          const clone = _.cloneDeep(prev)
           // Existing files are untouched
 
           // Handle removed files
           // Remove in reverse to avoid index issues
-          for (let i = draft.length - 1; i >= 0; i--) {
+          for (let i = clone.length - 1; i >= 0; i--) {
             if (
               !files.some(
-                (f) => f.name === draft[i].name && f.size === draft[i].size
+                (f) => f.name === clone[i].name && f.size === clone[i].size
               )
             ) {
-              draft.splice(i, 1)
+              clone.splice(i, 1)
             }
           }
 
           // Add new files
           files.forEach((f) => {
-            if (!draft.some((d) => d.name === f.name && d.size === f.size)) {
-              draft.push(f)
+            if (!clone.some((d) => d.name === f.name && d.size === f.size)) {
+              clone.push(f)
             }
           })
+          return clone
         })
       }
 
@@ -313,7 +319,52 @@ export const CreatePage = () => {
         setIsParsing(false)
       })
     }
-  }, [selectedFiles, updateDrawnFiles])
+  }, [selectedFiles])
+
+  const [draftEnabled, setDraftEnabled] = useState(true)
+  useEffect(() => {
+    // Only proceed if we have no uploaded files
+    if (uploadedFiles?.length ?? 0) return
+
+    getSigitDraft().then((draft) => {
+      if (draft) {
+        setSelectedFiles(draft.files)
+        setDrawnFiles((prev) => {
+          const clone = _.cloneDeep(prev)
+          clone.splice(0, clone.length, ...draft.files)
+          return clone
+        })
+        setUsers(draft.users)
+        setTitle(draft.title)
+
+        // After loading draft clear it
+        clearSigitDraft()
+      }
+    })
+  }, [uploadedFiles])
+  useEffect(() => {
+    if (draftEnabled) {
+      saveSigitDraft({
+        title,
+        users,
+        lastUpdated: Date.now(),
+        files: drawnFiles
+      }).catch((error) => {
+        if (
+          error instanceof DOMException &&
+          error.name === 'QuotaExceededError'
+        ) {
+          // Disable draft if we hit size error
+          setDraftEnabled(false)
+          console.warn(
+            'Draft functionality disabled temporarily. File size exceeds local storage limit.'
+          )
+          clearSigitDraft()
+        }
+        // Ignore other errors
+      })
+    }
+  }, [draftEnabled, drawnFiles, title, users])
 
   /**
    * Changes the drawing tool
@@ -504,7 +555,7 @@ export const CreatePage = () => {
         })
       })
     })
-    updateDrawnFiles(drawnFilesCopy)
+    setDrawnFiles(drawnFilesCopy)
   }
 
   /**
@@ -940,6 +991,7 @@ export const CreatePage = () => {
       console.error(error)
     } finally {
       setIsLoading(false)
+      clearSigitDraft()
     }
   }
 
@@ -1017,6 +1069,7 @@ export const CreatePage = () => {
       console.error(error)
     } finally {
       setIsLoading(false)
+      clearSigitDraft()
     }
   }
 
@@ -1285,7 +1338,7 @@ export const CreatePage = () => {
             userProfiles={userProfiles}
             selectedTool={selectedTool}
             sigitFiles={drawnFiles}
-            updateSigitFiles={updateDrawnFiles}
+            setSigitFiles={setDrawnFiles}
           />
           {parsingPdf && <LoadingSpinner variant="small" />}
         </StickySideColumns>
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index abd3b4e..44779b4 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -2,7 +2,7 @@ import { Button, TextField } from '@mui/material'
 import { useCallback, useEffect, useState } from 'react'
 import { useNavigate, useSearchParams } from 'react-router-dom'
 import { toast } from 'react-toastify'
-import { useAppSelector } from '../../hooks'
+import { useAppSelector, useDidMount } from '../../hooks'
 import { appPrivateRoutes } from '../../routes'
 import { Meta } from '../../types'
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -13,12 +13,15 @@ import { useDropzone } from 'react-dropzone'
 import { Container } from '../../components/Container'
 import styles from './style.module.scss'
 import {
+  clearSigitDraft,
   extractSigitCardDisplayInfo,
+  hasSigitDraft,
   navigateFromZip,
   SigitCardDisplayInfo,
   SigitStatus
 } from '../../utils'
 import { Footer } from '../../components/Footer/Footer'
+import { LocalDraftSigit } from '../../components/DisplaySigit/LocalDraftSigit'
 
 // Unsupported Filter options are commented
 const FILTERS = [
@@ -44,6 +47,12 @@ export const HomePage = () => {
   const [searchParams, setSearchParams] = useSearchParams()
   const q = searchParams.get('q') ?? ''
 
+  const [showDraft, setShowDraft] = useState<boolean>(false)
+  useDidMount(async () => {
+    // Check if draft exists and add link to direct
+    setShowDraft(hasSigitDraft())
+  })
+
   useEffect(() => {
     const searchInput = document.getElementById('q') as HTMLInputElement | null
     if (searchInput) {
@@ -152,7 +161,7 @@ export const HomePage = () => {
           meta={sigits[key]}
         />
       ))
-    } else {
+    } else if (!showDraft) {
       return (
         <div className={styles.noResults}>
           <p>No results</p>
@@ -260,7 +269,17 @@ export const HomePage = () => {
           )}
         </button>
 
-        <div className={styles.submissions}>{renderSubmissions()}</div>
+        <div className={styles.submissions}>
+          {showDraft && (
+            <LocalDraftSigit
+              handleDraftDelete={() => {
+                clearSigitDraft()
+                setShowDraft(false)
+              }}
+            />
+          )}
+          {renderSubmissions()}
+        </div>
       </Container>
       <Footer />
     </div>
diff --git a/src/types/draft.ts b/src/types/draft.ts
index 900d87e..23709a7 100644
--- a/src/types/draft.ts
+++ b/src/types/draft.ts
@@ -11,9 +11,11 @@ export interface SigitDraft {
   title: string
   users: User[]
   files: SigitFile[]
+  lastUpdated: number
 }
 export interface SerializedSigitDraft {
   title: string
+  lastUpdated: number
   users: User[]
   files: SigitFileDraft[]
 }
diff --git a/src/utils/draft.ts b/src/utils/draft.ts
index 4e9ae56..e9ba239 100644
--- a/src/utils/draft.ts
+++ b/src/utils/draft.ts
@@ -10,7 +10,7 @@ import {
   toFile,
   getSigitFile
 } from './file'
-
+const DRAFT_KEY = 'sigitDraft'
 let saveSigitDraftTimeout: number | null = null
 const serializeSigitDraft = async (
   draft: SigitDraft
@@ -48,6 +48,7 @@ const serializeSigitDraft = async (
   const serializedFileDraft = await Promise.all(serializedFiles)
   return {
     title: draft.title,
+    lastUpdated: draft.lastUpdated,
     users: [...draft.users],
     files: serializedFileDraft
   }
@@ -79,24 +80,31 @@ const deserializeSigitDraft = async (
   }
 }
 
-export const saveSigitDraft = (draft: SigitDraft) => {
+export const saveSigitDraft = (draft: SigitDraft): Promise<void> => {
   if (saveSigitDraftTimeout) {
     clearTimeout(saveSigitDraftTimeout)
   }
 
-  saveSigitDraftTimeout = window.setTimeout(() => {
-    serializeSigitDraft(draft)
-      .then((draftToSave) => {
-        localStorage.setItem('sigitDraft', JSON.stringify(draftToSave))
-      })
-      .catch((error) => {
-        console.log(`Error while saving sigit draft. Error: `, error)
-      })
-  }, 1000)
+  return new Promise((resolve, reject) => {
+    saveSigitDraftTimeout = window.setTimeout(() => {
+      serializeSigitDraft(draft)
+        .then((draftToSave) => {
+          localStorage.setItem(DRAFT_KEY, JSON.stringify(draftToSave))
+          resolve()
+        })
+        .catch((error) => {
+          reject(error)
+        })
+    }, 1000)
+  })
+}
+
+export const hasSigitDraft = () => {
+  return DRAFT_KEY in localStorage
 }
 
 export const getSigitDraft = async () => {
-  const sigitDraft = localStorage.getItem('sigitDraft')
+  const sigitDraft = localStorage.getItem(DRAFT_KEY)
   if (!sigitDraft) return null
 
   try {
@@ -108,5 +116,5 @@ export const getSigitDraft = async () => {
 }
 
 export const clearSigitDraft = () => {
-  localStorage.removeItem('sigitDraft')
+  localStorage.removeItem(DRAFT_KEY)
 }
diff --git a/src/utils/meta.ts b/src/utils/meta.ts
index 75e1654..fd68541 100644
--- a/src/utils/meta.ts
+++ b/src/utils/meta.ts
@@ -31,7 +31,8 @@ export enum SignStatus {
 
 export enum SigitStatus {
   Partial = 'In-Progress',
-  Complete = 'Completed'
+  Complete = 'Completed',
+  LocalDraft = 'Draft'
 }
 
 export interface SigitCardDisplayInfo {

From f422ee338c96a926947396ed3febb3ecf6c9495b Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Tue, 18 Feb 2025 17:45:50 +0100
Subject: [PATCH 14/28] chore: remove extra comment whitespace

---
 src/utils/nostr.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index 3d8aa15..824a967 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -270,7 +270,7 @@ export const countLeadingZeroes = (hex: string) => {
 
 /**
  * Function to create a wrapped event with PoW
- * @param event  Original event to be wrapped (can be unsigned or verified)
+ * @param event Original event to be wrapped (can be unsigned or verified)
  * @param receiver Public key of the receiver
  * @returns
  */

From 08b13c291b3cae097e01416127e8674e53b59c53 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Tue, 18 Feb 2025 18:38:54 +0100
Subject: [PATCH 15/28] fix: hide DisplaySigit actions

Closes #246
---
 src/components/DisplaySigit/index.tsx | 57 +++++++++++++++------------
 1 file changed, 31 insertions(+), 26 deletions(-)

diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx
index 5147b45..20550b3 100644
--- a/src/components/DisplaySigit/index.tsx
+++ b/src/components/DisplaySigit/index.tsx
@@ -112,32 +112,37 @@ export const DisplaySigit = ({
           </>
         )}
       </div>
-      <div className={styles.itemActions}>
-        <Tooltip title="Duplicate" arrow placement="top" disableInteractive>
-          <Button
-            sx={{
-              color: 'var(--primary-main)',
-              minWidth: '34px',
-              padding: '10px'
-            }}
-            variant={'text'}
-          >
-            <FontAwesomeIcon icon={faCopy} />
-          </Button>
-        </Tooltip>
-        <Tooltip title="Archive" arrow placement="top" disableInteractive>
-          <Button
-            sx={{
-              color: 'var(--primary-main)',
-              minWidth: '34px',
-              padding: '10px'
-            }}
-            variant={'text'}
-          >
-            <FontAwesomeIcon icon={faArchive} />
-          </Button>
-        </Tooltip>
-      </div>
+      {
+        // TODO: enable buttons once feature is ready
+        false && (
+          <div className={styles.itemActions}>
+            <Tooltip title="Duplicate" arrow placement="top" disableInteractive>
+              <Button
+                sx={{
+                  color: 'var(--primary-main)',
+                  minWidth: '34px',
+                  padding: '10px'
+                }}
+                variant={'text'}
+              >
+                <FontAwesomeIcon icon={faCopy} />
+              </Button>
+            </Tooltip>
+            <Tooltip title="Archive" arrow placement="top" disableInteractive>
+              <Button
+                sx={{
+                  color: 'var(--primary-main)',
+                  minWidth: '34px',
+                  padding: '10px'
+                }}
+                variant={'text'}
+              >
+                <FontAwesomeIcon icon={faArchive} />
+              </Button>
+            </Tooltip>
+          </div>
+        )
+      }
     </div>
   )
 }

From 4b5625e5bd1354678bdade46ee511be187fe754b Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Wed, 19 Feb 2025 10:54:58 +0100
Subject: [PATCH 16/28] fix(search): intercept nsec1, delete, and show warning

---
 src/pages/create/index.tsx | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 2d476ae..2ee1985 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -1045,6 +1045,11 @@ export const CreatePage = () => {
     // Seems like it's npub format
     if (value.trim().startsWith('npub1')) {
       setPastedUserNpubOrNip05(value.trim())
+    } else if (value.trim().startsWith('nsec1')) {
+      toast.warn('Oops - never paste your nsec into a website! Key deleted.')
+      if (searchFieldRef.current) searchFieldRef.current.value = ''
+      setUserSearchInput('')
+      return
     } else {
       // Disarm the add user on enter hit, and trigger search after 1 second
       disarmAddOnEnter()

From cc65d85806b57ac271deef7e0fc31df44935c80a Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Mar 2025 11:42:09 +0000
Subject: [PATCH 17/28] refactor(styles): update css for other marks during
 sign

---
 src/components/PDFView/style.module.scss | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss
index 61983d7..92c044e 100644
--- a/src/components/PDFView/style.module.scss
+++ b/src/components/PDFView/style.module.scss
@@ -8,6 +8,4 @@
   position: absolute;
   z-index: 40;
   display: flex;
-  justify-content: center;
-  align-items: center;
 }

From 8e23a2d8a1c05a760f0233db49a3e0c1ccc2af38 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Mar 2025 11:43:01 +0000
Subject: [PATCH 18/28] refactor: remove custom sigit cache page and links

---
 src/pages/settings/Settings.tsx | 7 -------
 src/routes/index.tsx            | 1 -
 2 files changed, 8 deletions(-)

diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index 5acdd9c..1382bc1 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -1,6 +1,5 @@
 import AccountCircleIcon from '@mui/icons-material/AccountCircle'
 import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
-import CachedIcon from '@mui/icons-material/Cached'
 import RouterIcon from '@mui/icons-material/Router'
 import { ListItem, useTheme } from '@mui/material'
 import List from '@mui/material/List'
@@ -74,12 +73,6 @@ export const SettingsPage = () => {
             </ListItemIcon>
             {listItem('Relays')}
           </ListItem>
-          <ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
-            <ListItemIcon>
-              <CachedIcon />
-            </ListItemIcon>
-            {listItem('Local Cache')}
-          </ListItem>
           {loginMethod === LoginMethod.nostrLogin && (
             <ListItem component={Link} to={appPrivateRoutes.nostrLogin}>
               <ListItemIcon>
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index f3580f9..f514e78 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -6,7 +6,6 @@ export const appPrivateRoutes = {
   sign: '/sign',
   settings: '/settings',
   profileSettings: '/settings/profile/:npub',
-  cacheSettings: '/settings/cache',
   relays: '/settings/relays',
   nostrLogin: '/settings/nostrLogin'
 }

From cc681af11a34619ce6c41f6316b4fe9e831ff6c2 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Mar 2025 12:15:26 +0000
Subject: [PATCH 19/28] feat(marks): add full name

---
 .../MarkTypeStrategy/FullName/Input.tsx       | 20 ++++++
 .../MarkTypeStrategy/FullName/index.tsx       |  7 ++
 .../MarkTypeStrategy/MarkStrategy.tsx         |  4 +-
 src/hooks/useLocalStorage.ts                  | 64 +++++++++++++++++++
 src/utils/localStorage.ts                     | 44 +++++++++++++
 src/utils/mark.ts                             |  3 +-
 6 files changed, 139 insertions(+), 3 deletions(-)
 create mode 100644 src/components/MarkTypeStrategy/FullName/Input.tsx
 create mode 100644 src/components/MarkTypeStrategy/FullName/index.tsx
 create mode 100644 src/hooks/useLocalStorage.ts

diff --git a/src/components/MarkTypeStrategy/FullName/Input.tsx b/src/components/MarkTypeStrategy/FullName/Input.tsx
new file mode 100644
index 0000000..7b63ae6
--- /dev/null
+++ b/src/components/MarkTypeStrategy/FullName/Input.tsx
@@ -0,0 +1,20 @@
+import { useDidMount } from '../../../hooks'
+import { useLocalStorage } from '../../../hooks/useLocalStorage'
+import { MarkInputProps } from '../MarkStrategy'
+import { MarkInputText } from '../Text/Input'
+
+export const MarkInputFullName = (props: MarkInputProps) => {
+  const [fullName, setFullName] = useLocalStorage('mark-fullname', '')
+  useDidMount(() => {
+    props.handler(fullName)
+  })
+  return MarkInputText({
+    ...props,
+    placeholder: 'Full Name',
+    value: fullName,
+    handler: (value) => {
+      setFullName(value)
+      props.handler(value)
+    }
+  })
+}
diff --git a/src/components/MarkTypeStrategy/FullName/index.tsx b/src/components/MarkTypeStrategy/FullName/index.tsx
new file mode 100644
index 0000000..1574c42
--- /dev/null
+++ b/src/components/MarkTypeStrategy/FullName/index.tsx
@@ -0,0 +1,7 @@
+import { MarkStrategy } from '../MarkStrategy'
+import { MarkInputFullName } from './Input'
+
+export const FullNameStrategy: MarkStrategy = {
+  input: MarkInputFullName,
+  render: ({ value }) => <>{value}</>
+}
diff --git a/src/components/MarkTypeStrategy/MarkStrategy.tsx b/src/components/MarkTypeStrategy/MarkStrategy.tsx
index 562302e..f842220 100644
--- a/src/components/MarkTypeStrategy/MarkStrategy.tsx
+++ b/src/components/MarkTypeStrategy/MarkStrategy.tsx
@@ -2,6 +2,7 @@ import { MarkType } from '../../types/drawing'
 import { CurrentUserMark, Mark } from '../../types/mark'
 import { TextStrategy } from './Text'
 import { SignatureStrategy } from './Signature'
+import { FullNameStrategy } from './FullName'
 
 export interface MarkInputProps {
   value: string
@@ -28,5 +29,6 @@ export type MarkStrategies = {
 
 export const MARK_TYPE_CONFIG: MarkStrategies = {
   [MarkType.TEXT]: TextStrategy,
-  [MarkType.SIGNATURE]: SignatureStrategy
+  [MarkType.SIGNATURE]: SignatureStrategy,
+  [MarkType.FULLNAME]: FullNameStrategy
 }
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000..ec6c9cb
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,64 @@
+import React, { useMemo } from 'react'
+import {
+  getLocalStorageItem,
+  mergeWithInitialValue,
+  removeLocalStorageItem,
+  setLocalStorageItem
+} from '../utils'
+
+const useLocalStorageSubscribe = (callback: () => void) => {
+  window.addEventListener('storage', callback)
+  return () => window.removeEventListener('storage', callback)
+}
+
+export function useLocalStorage<T>(
+  key: string,
+  initialValue: T
+): [T, React.Dispatch<React.SetStateAction<T>>] {
+  const getSnapshot = () => {
+    // Get the stored value
+    const storedValue = getLocalStorageItem(key, initialValue)
+
+    // Parse the value
+    const parsedStoredValue = JSON.parse(storedValue)
+
+    // Merge the default and the stored in case some of the required fields are missing
+    return JSON.stringify(
+      mergeWithInitialValue(parsedStoredValue, initialValue)
+    )
+  }
+
+  const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
+
+  const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
+    (v: React.SetStateAction<T>) => {
+      try {
+        const nextState =
+          typeof v === 'function'
+            ? (v as (prevState: T) => T)(JSON.parse(data))
+            : v
+
+        if (nextState === undefined || nextState === null) {
+          removeLocalStorageItem(key)
+        } else {
+          setLocalStorageItem(key, JSON.stringify(nextState))
+        }
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    [data, key]
+  )
+
+  React.useEffect(() => {
+    // Set local storage only when it's empty
+    const data = window.localStorage.getItem(key)
+    if (data === null) {
+      setLocalStorageItem(key, JSON.stringify(initialValue))
+    }
+  }, [key, initialValue])
+
+  const memoized = useMemo(() => JSON.parse(data) as T, [data])
+
+  return [memoized, setState]
+}
diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts
index 8196e35..b0eb381 100644
--- a/src/utils/localStorage.ts
+++ b/src/utils/localStorage.ts
@@ -42,3 +42,47 @@ export const clear = () => {
   clearAuthToken()
   clearState()
 }
+
+export function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
+  if (
+    !Array.isArray(storedValue) &&
+    typeof storedValue === 'object' &&
+    storedValue !== null
+  ) {
+    return { ...initialValue, ...storedValue }
+  }
+  return storedValue
+}
+
+export function getLocalStorageItem<T>(key: string, defaultValue: T): string {
+  try {
+    const data = window.localStorage.getItem(key)
+    if (data === null) return JSON.stringify(defaultValue)
+    return data
+  } catch (err) {
+    console.error(`Error while fetching local storage value: `, err)
+    return JSON.stringify(defaultValue)
+  }
+}
+
+export function setLocalStorageItem(key: string, value: string) {
+  try {
+    window.localStorage.setItem(key, value)
+    dispatchLocalStorageEvent(key, value)
+  } catch (err) {
+    console.error(`Error while saving local storage value: `, err)
+  }
+}
+
+export function removeLocalStorageItem(key: string) {
+  try {
+    window.localStorage.removeItem(key)
+    dispatchLocalStorageEvent(key, null)
+  } catch (err) {
+    console.error(`Error while deleting local storage value: `, err)
+  }
+}
+
+function dispatchLocalStorageEvent(key: string, newValue: string | null) {
+  window.dispatchEvent(new StorageEvent('storage', { key, newValue }))
+}
diff --git a/src/utils/mark.ts b/src/utils/mark.ts
index 1868403..319371c 100644
--- a/src/utils/mark.ts
+++ b/src/utils/mark.ts
@@ -171,8 +171,7 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
   {
     identifier: MarkType.FULLNAME,
     icon: faIdCard,
-    label: 'Full Name',
-    isComingSoon: true
+    label: 'Full Name'
   },
   {
     identifier: MarkType.JOBTITLE,

From c8f0d135f13dfea86068f7efb6c4c7152b299085 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Fri, 7 Mar 2025 12:47:55 +0000
Subject: [PATCH 20/28] feat(marks): add job title and datetime

---
 .../MarkTypeStrategy/DateTime/Input.tsx       | 25 +++++++++++++++++++
 .../MarkTypeStrategy/DateTime/index.tsx       |  7 ++++++
 .../MarkTypeStrategy/JobTitle/Input.tsx       | 20 +++++++++++++++
 .../MarkTypeStrategy/JobTitle/index.tsx       |  7 ++++++
 .../MarkTypeStrategy/MarkStrategy.tsx         |  6 ++++-
 src/utils/mark.ts                             |  6 ++---
 6 files changed, 66 insertions(+), 5 deletions(-)
 create mode 100644 src/components/MarkTypeStrategy/DateTime/Input.tsx
 create mode 100644 src/components/MarkTypeStrategy/DateTime/index.tsx
 create mode 100644 src/components/MarkTypeStrategy/JobTitle/Input.tsx
 create mode 100644 src/components/MarkTypeStrategy/JobTitle/index.tsx

diff --git a/src/components/MarkTypeStrategy/DateTime/Input.tsx b/src/components/MarkTypeStrategy/DateTime/Input.tsx
new file mode 100644
index 0000000..fa68438
--- /dev/null
+++ b/src/components/MarkTypeStrategy/DateTime/Input.tsx
@@ -0,0 +1,25 @@
+import { MarkInputProps } from '../MarkStrategy'
+import styles from '../../MarkFormField/style.module.scss'
+import { useEffect, useRef } from 'react'
+
+export const MarkInputDateTime = ({ handler, placeholder }: MarkInputProps) => {
+  const ref = useRef<HTMLInputElement>(null)
+  useEffect(() => {
+    if (ref.current) {
+      ref.current.value = new Date().toISOString().slice(0, 16)
+      if (ref.current.valueAsDate) {
+        handler(ref.current.valueAsDate.toUTCString())
+      }
+    }
+  }, [handler])
+  return (
+    <input
+      type="datetime-local"
+      ref={ref}
+      className={styles.input}
+      placeholder={placeholder}
+      readOnly={true}
+      disabled={true}
+    />
+  )
+}
diff --git a/src/components/MarkTypeStrategy/DateTime/index.tsx b/src/components/MarkTypeStrategy/DateTime/index.tsx
new file mode 100644
index 0000000..1892d49
--- /dev/null
+++ b/src/components/MarkTypeStrategy/DateTime/index.tsx
@@ -0,0 +1,7 @@
+import { MarkStrategy } from '../MarkStrategy'
+import { MarkInputDateTime } from './Input'
+
+export const DateTimeStrategy: MarkStrategy = {
+  input: MarkInputDateTime,
+  render: ({ value }) => <>{value}</>
+}
diff --git a/src/components/MarkTypeStrategy/JobTitle/Input.tsx b/src/components/MarkTypeStrategy/JobTitle/Input.tsx
new file mode 100644
index 0000000..47d2969
--- /dev/null
+++ b/src/components/MarkTypeStrategy/JobTitle/Input.tsx
@@ -0,0 +1,20 @@
+import { useDidMount } from '../../../hooks'
+import { useLocalStorage } from '../../../hooks/useLocalStorage'
+import { MarkInputProps } from '../MarkStrategy'
+import { MarkInputText } from '../Text/Input'
+
+export const MarkInputJobTitle = (props: MarkInputProps) => {
+  const [jobTitle, setjobTitle] = useLocalStorage('mark-jobtitle', '')
+  useDidMount(() => {
+    props.handler(jobTitle)
+  })
+  return MarkInputText({
+    ...props,
+    placeholder: 'Job Title',
+    value: jobTitle,
+    handler: (value) => {
+      setjobTitle(value)
+      props.handler(value)
+    }
+  })
+}
diff --git a/src/components/MarkTypeStrategy/JobTitle/index.tsx b/src/components/MarkTypeStrategy/JobTitle/index.tsx
new file mode 100644
index 0000000..11f5d60
--- /dev/null
+++ b/src/components/MarkTypeStrategy/JobTitle/index.tsx
@@ -0,0 +1,7 @@
+import { MarkStrategy } from '../MarkStrategy'
+import { MarkInputJobTitle } from './Input'
+
+export const JobTitleStrategy: MarkStrategy = {
+  input: MarkInputJobTitle,
+  render: ({ value }) => <>{value}</>
+}
diff --git a/src/components/MarkTypeStrategy/MarkStrategy.tsx b/src/components/MarkTypeStrategy/MarkStrategy.tsx
index f842220..0ca0ebc 100644
--- a/src/components/MarkTypeStrategy/MarkStrategy.tsx
+++ b/src/components/MarkTypeStrategy/MarkStrategy.tsx
@@ -3,6 +3,8 @@ import { CurrentUserMark, Mark } from '../../types/mark'
 import { TextStrategy } from './Text'
 import { SignatureStrategy } from './Signature'
 import { FullNameStrategy } from './FullName'
+import { JobTitleStrategy } from './JobTitle'
+import { DateTimeStrategy } from './DateTime'
 
 export interface MarkInputProps {
   value: string
@@ -30,5 +32,7 @@ export type MarkStrategies = {
 export const MARK_TYPE_CONFIG: MarkStrategies = {
   [MarkType.TEXT]: TextStrategy,
   [MarkType.SIGNATURE]: SignatureStrategy,
-  [MarkType.FULLNAME]: FullNameStrategy
+  [MarkType.FULLNAME]: FullNameStrategy,
+  [MarkType.JOBTITLE]: JobTitleStrategy,
+  [MarkType.DATETIME]: DateTimeStrategy
 }
diff --git a/src/utils/mark.ts b/src/utils/mark.ts
index 319371c..37bda6b 100644
--- a/src/utils/mark.ts
+++ b/src/utils/mark.ts
@@ -176,14 +176,12 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
   {
     identifier: MarkType.JOBTITLE,
     icon: faBriefcase,
-    label: 'Job Title',
-    isComingSoon: true
+    label: 'Job Title'
   },
   {
     identifier: MarkType.DATETIME,
     icon: faClock,
-    label: 'Date Time',
-    isComingSoon: true
+    label: 'Date Time'
   },
   {
     identifier: MarkType.NUMBER,

From 8de86aac28c38fc4fb8d34eca04e0ce50b6ab13e Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Mon, 10 Mar 2025 09:44:09 +0000
Subject: [PATCH 21/28] fix(marks): date input

---
 src/components/MarkTypeStrategy/DateTime/Input.tsx | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/components/MarkTypeStrategy/DateTime/Input.tsx b/src/components/MarkTypeStrategy/DateTime/Input.tsx
index fa68438..b2e864c 100644
--- a/src/components/MarkTypeStrategy/DateTime/Input.tsx
+++ b/src/components/MarkTypeStrategy/DateTime/Input.tsx
@@ -6,10 +6,9 @@ export const MarkInputDateTime = ({ handler, placeholder }: MarkInputProps) => {
   const ref = useRef<HTMLInputElement>(null)
   useEffect(() => {
     if (ref.current) {
-      ref.current.value = new Date().toISOString().slice(0, 16)
-      if (ref.current.valueAsDate) {
-        handler(ref.current.valueAsDate.toUTCString())
-      }
+      const date = new Date()
+      ref.current.value = date.toISOString().slice(0, 16)
+      handler(date.toUTCString())
     }
   }, [handler])
   return (

From 745ba377d4d10d3df1ecb4ae2731bf8229385c79 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Mon, 10 Mar 2025 10:56:36 +0000
Subject: [PATCH 22/28] refactor(settings): remove cache links and page

---
 src/pages/settings/cache/index.tsx | 69 ------------------------------
 src/routes/util.tsx                |  9 ----
 2 files changed, 78 deletions(-)
 delete mode 100644 src/pages/settings/cache/index.tsx

diff --git a/src/pages/settings/cache/index.tsx b/src/pages/settings/cache/index.tsx
deleted file mode 100644
index 5dbf8af..0000000
--- a/src/pages/settings/cache/index.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import InputIcon from '@mui/icons-material/Input'
-import IosShareIcon from '@mui/icons-material/IosShare'
-import {
-  List,
-  ListItemButton,
-  ListItemIcon,
-  ListItemText,
-  ListSubheader,
-  useTheme
-} from '@mui/material'
-import { Container } from '../../../components/Container'
-import { Footer } from '../../../components/Footer/Footer'
-
-export const CacheSettingsPage = () => {
-  const theme = useTheme()
-
-  const listItem = (label: string) => {
-    return (
-      <ListItemText
-        primary={label}
-        sx={{
-          color: theme.palette.text.primary
-        }}
-      />
-    )
-  }
-
-  return (
-    <>
-      <Container>
-        <List
-          sx={{
-            width: '100%',
-            bgcolor: 'background.paper',
-            marginTop: 2
-          }}
-          subheader={
-            <ListSubheader
-              sx={{
-                fontSize: '1.5rem',
-                borderBottom: '0.5px solid',
-                paddingBottom: 2,
-                paddingTop: 2,
-                zIndex: 2
-              }}
-            >
-              Cache Setting
-            </ListSubheader>
-          }
-        >
-          <ListItemButton disabled>
-            <ListItemIcon>
-              <IosShareIcon />
-            </ListItemIcon>
-            {listItem('Export (coming soon)')}
-          </ListItemButton>
-
-          <ListItemButton disabled>
-            <ListItemIcon>
-              <InputIcon />
-            </ListItemIcon>
-            {listItem('Import (coming soon)')}
-          </ListItemButton>
-        </List>
-      </Container>
-      <Footer />
-    </>
-  )
-}
diff --git a/src/routes/util.tsx b/src/routes/util.tsx
index 2e1be26..e21dc13 100644
--- a/src/routes/util.tsx
+++ b/src/routes/util.tsx
@@ -4,7 +4,6 @@ import { CreatePage } from '../pages/create'
 import { HomePage } from '../pages/home'
 import { LandingPage } from '../pages/landing'
 import { ProfilePage } from '../pages/profile'
-import { CacheSettingsPage } from '../pages/settings/cache'
 import { NostrLoginPage } from '../pages/settings/nostrLogin'
 import { ProfileSettingsPage } from '../pages/settings/profile'
 import { RelaysPage } from '../pages/settings/relays'
@@ -109,14 +108,6 @@ export const privateRoutes = [
       </PrivateRoute>
     )
   },
-  {
-    path: appPrivateRoutes.cacheSettings,
-    element: (
-      <PrivateRoute>
-        <CacheSettingsPage />
-      </PrivateRoute>
-    )
-  },
   {
     path: appPrivateRoutes.relays,
     element: (

From c1a9475a89bb07ec522a95fede276d09eafd0813 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Mon, 10 Mar 2025 11:57:08 +0000
Subject: [PATCH 23/28] refactor(settings): update settings layout

---
 src/pages/settings/Settings.tsx              | 131 ++++++-----
 src/pages/settings/nostrLogin/index.tsx      |  62 ++----
 src/pages/settings/profile/index.tsx         | 217 +++++++++----------
 src/pages/settings/profile/style.module.scss |   6 -
 src/pages/settings/relays/index.tsx          |   7 +-
 src/pages/settings/relays/style.module.scss  | 170 +++++++--------
 src/pages/settings/style.module.scss         |  43 ++++
 src/routes/util.tsx                          |  44 ++--
 8 files changed, 325 insertions(+), 355 deletions(-)
 create mode 100644 src/pages/settings/style.module.scss

diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index 1382bc1..f00eab4 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -1,87 +1,82 @@
 import AccountCircleIcon from '@mui/icons-material/AccountCircle'
-import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
 import RouterIcon from '@mui/icons-material/Router'
-import { ListItem, useTheme } from '@mui/material'
-import List from '@mui/material/List'
-import ListItemIcon from '@mui/material/ListItemIcon'
-import ListItemText from '@mui/material/ListItemText'
-import ListSubheader from '@mui/material/ListSubheader'
+import { Button } from '@mui/material'
 import { useAppSelector } from '../../hooks/store'
-import { Link } from 'react-router-dom'
+import { NavLink, Outlet, To } from 'react-router-dom'
 import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
 import { Container } from '../../components/Container'
 import { Footer } from '../../components/Footer/Footer'
 import ExtensionIcon from '@mui/icons-material/Extension'
 import { LoginMethod } from '../../store/auth/types'
+import styles from './style.module.scss'
+import { ReactNode } from 'react'
 
-export const SettingsPage = () => {
-  const theme = useTheme()
-  const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
-  const listItem = (label: string, disabled = false) => {
-    return (
-      <>
-        <ListItemText
-          primary={label}
+const Item = (to: To, icon: ReactNode, label: string) => {
+  return (
+    <NavLink to={to}>
+      {({ isActive }) => (
+        <Button
+          fullWidth
           sx={{
-            color: theme.palette.text.primary
+            transition: 'ease 0.3s',
+            justifyContent: 'start',
+            gap: '10px',
+            background: 'rgba(76,130,163,0)',
+            color: '#434343',
+            fontWeight: 600,
+            opacity: 0.75,
+            textTransform: 'none',
+            ...(isActive
+              ? {
+                  background: '#447592',
+                  color: 'white'
+                }
+              : {}),
+            '&:hover': {
+              opacity: 0.85,
+              gap: '15px',
+              background: '#5e8eab',
+              color: 'white'
+            }
           }}
-        />
+          variant={'text'}
+        >
+          {icon}
+          {label}
+        </Button>
+      )}
+    </NavLink>
+  )
+}
 
-        {!disabled && (
-          <ArrowForwardIosIcon
-            style={{
-              color: theme.palette.action.active,
-              marginRight: -10
-            }}
-          />
-        )}
-      </>
-    )
-  }
+export const SettingsLayout = () => {
+  const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
 
   return (
     <>
       <Container>
-        <List
-          sx={{
-            width: '100%',
-            bgcolor: 'background.paper'
-          }}
-          subheader={
-            <ListSubheader
-              sx={{
-                fontSize: '1.5rem',
-                borderBottom: '0.5px solid',
-                paddingBottom: 2,
-                paddingTop: 2,
-                zIndex: 2
-              }}
-            >
-              Settings
-            </ListSubheader>
-          }
-        >
-          <ListItem component={Link} to={getProfileSettingsRoute(usersPubkey!)}>
-            <ListItemIcon>
-              <AccountCircleIcon />
-            </ListItemIcon>
-            {listItem('Profile')}
-          </ListItem>
-          <ListItem component={Link} to={appPrivateRoutes.relays}>
-            <ListItemIcon>
-              <RouterIcon />
-            </ListItemIcon>
-            {listItem('Relays')}
-          </ListItem>
-          {loginMethod === LoginMethod.nostrLogin && (
-            <ListItem component={Link} to={appPrivateRoutes.nostrLogin}>
-              <ListItemIcon>
-                <ExtensionIcon />
-              </ListItemIcon>
-              {listItem('Nostr Login')}
-            </ListItem>
-          )}
-        </List>
+        <h2 className={styles.title}>Settings</h2>
+        <div className={styles.main}>
+          <div>
+            <aside className={styles.aside}>
+              {Item(
+                getProfileSettingsRoute(usersPubkey!),
+                <AccountCircleIcon />,
+                'Profile'
+              )}
+              {Item(appPrivateRoutes.relays, <RouterIcon />, 'Relays')}
+              {loginMethod === LoginMethod.nostrLogin &&
+                Item(
+                  appPrivateRoutes.nostrLogin,
+                  <ExtensionIcon />,
+                  'Nostr Login'
+                )}
+            </aside>
+          </div>
+          <div className={styles.content}>
+            <Outlet />
+          </div>
+        </div>
       </Container>
       <Footer />
     </>
diff --git a/src/pages/settings/nostrLogin/index.tsx b/src/pages/settings/nostrLogin/index.tsx
index d2f0d29..31434ac 100644
--- a/src/pages/settings/nostrLogin/index.tsx
+++ b/src/pages/settings/nostrLogin/index.tsx
@@ -3,11 +3,9 @@ import {
   ListItemButton,
   ListItemIcon,
   ListItemText,
-  ListSubheader,
   useTheme
 } from '@mui/material'
 import { launch as launchNostrLoginDialog } from 'nostr-login'
-import { Container } from '../../../components/Container'
 import PeopleIcon from '@mui/icons-material/People'
 import ImportExportIcon from '@mui/icons-material/ImportExport'
 import { useAppSelector } from '../../../hooks/store'
@@ -20,59 +18,39 @@ export const NostrLoginPage = () => {
   )
 
   return (
-    <Container>
-      <List
-        sx={{
-          width: '100%',
-          bgcolor: 'background.paper'
+    <List>
+      <ListItemButton
+        onClick={() => {
+          launchNostrLoginDialog('switch-account')
         }}
-        subheader={
-          <ListSubheader
-            sx={{
-              fontSize: '1.5rem',
-              borderBottom: '0.5px solid',
-              paddingBottom: 2,
-              paddingTop: 2,
-              zIndex: 2
-            }}
-          >
-            Nostr Settings
-          </ListSubheader>
-        }
       >
+        <ListItemIcon>
+          <PeopleIcon />
+        </ListItemIcon>
+        <ListItemText
+          primary={'Nostr Login Accounts'}
+          sx={{
+            color: theme.palette.text.primary
+          }}
+        />
+      </ListItemButton>
+      {nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
         <ListItemButton
           onClick={() => {
-            launchNostrLoginDialog('switch-account')
+            launchNostrLoginDialog('import')
           }}
         >
           <ListItemIcon>
-            <PeopleIcon />
+            <ImportExportIcon />
           </ListItemIcon>
           <ListItemText
-            primary={'Nostr Login Accounts'}
+            primary={'Import / Export Keys'}
             sx={{
               color: theme.palette.text.primary
             }}
           />
         </ListItemButton>
-        {nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
-          <ListItemButton
-            onClick={() => {
-              launchNostrLoginDialog('import')
-            }}
-          >
-            <ListItemIcon>
-              <ImportExportIcon />
-            </ListItemIcon>
-            <ListItemText
-              primary={'Import / Export Keys'}
-              sx={{
-                color: theme.palette.text.primary
-              }}
-            />
-          </ListItemButton>
-        )}
-      </List>
-    </Container>
+      )}
+    </List>
   )
 }
diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx
index 57383a7..03db00f 100644
--- a/src/pages/settings/profile/index.tsx
+++ b/src/pages/settings/profile/index.tsx
@@ -12,7 +12,6 @@ import {
   InputProps,
   List,
   ListItem,
-  ListSubheader,
   TextField,
   Tooltip
 } from '@mui/material'
@@ -28,8 +27,6 @@ import { useAppDispatch, useAppSelector } from '../../../hooks/store'
 
 import { getRoboHashPicture, unixNow } from '../../../utils'
 
-import { Container } from '../../../components/Container'
-import { Footer } from '../../../components/Footer/Footer'
 import { LoadingSpinner } from '../../../components/LoadingSpinner'
 
 import { setUserProfile as updateUserProfile } from '../../../store/actions'
@@ -256,131 +253,111 @@ export const ProfileSettingsPage = () => {
   return (
     <>
       {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
-      <Container className={styles.container}>
-        <List
-          sx={{
-            bgcolor: 'background.paper',
-            marginTop: 2
-          }}
-          subheader={
-            <ListSubheader
+      <List>
+        {userProfile && (
+          <div>
+            <ListItem
               sx={{
-                paddingBottom: 1,
-                paddingTop: 1,
-                fontSize: '1.5rem',
-                zIndex: 2
+                marginTop: 1,
+                display: 'flex',
+                flexDirection: 'column'
               }}
-              className={styles.subHeader}
             >
-              Profile Settings
-            </ListSubheader>
-          }
-        >
-          {userProfile && (
-            <div>
-              <ListItem
-                sx={{
-                  marginTop: 1,
-                  display: 'flex',
-                  flexDirection: 'column'
-                }}
-              >
-                {userProfile.banner ? (
-                  <img
-                    className={styles.bannerImg}
-                    src={userProfile.banner}
-                    alt="Banner Image"
-                  />
-                ) : (
-                  <Box className={styles.noBanner}> No banner found </Box>
-                )}
-              </ListItem>
-
-              {editItem('banner', 'Banner URL', undefined, undefined)}
-
-              <ListItem
-                sx={{
-                  marginTop: 1,
-                  display: 'flex',
-                  flexDirection: 'column'
-                }}
-              >
+              {userProfile.banner ? (
                 <img
-                  onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
-                    event.currentTarget.src = getRoboHashPicture(npub!)
-                  }}
-                  className={styles.img}
-                  src={getProfileImage(userProfile)}
-                  alt="Profile Image"
+                  className={styles.bannerImg}
+                  src={userProfile.banner}
+                  alt="Banner Image"
                 />
-              </ListItem>
-
-              {editItem('image', 'Picture URL', undefined, undefined, {
-                endAdornment: isUsersOwnProfile ? robohashButton() : undefined
-              })}
-
-              {editItem('name', 'Username')}
-              {editItem('displayName', 'Display Name')}
-              {editItem('nip05', 'Nostr Address (nip05)')}
-              {editItem('lud16', 'Lightning Address (lud16)')}
-              {editItem('about', 'About', true, 4)}
-              {editItem('website', 'Website')}
-              {isUsersOwnProfile && (
-                <>
-                  {usersPubkey &&
-                    copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
-
-                  {loginMethod === LoginMethod.privateKey &&
-                    keys &&
-                    keys.private &&
-                    copyItem(
-                      '••••••••••••••••••••••••••••••••••••••••••••••••••',
-                      'Private Key',
-                      keys.private
-                    )}
-                </>
+              ) : (
+                <Box className={styles.noBanner}> No banner found </Box>
               )}
-              {isUsersOwnProfile && (
-                <>
-                  {loginMethod === LoginMethod.nostrLogin &&
-                    nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
-                      <ListItem
-                        sx={{ marginTop: 1 }}
-                        onClick={() => {
-                          launchNostrLoginDialog('import')
+            </ListItem>
+
+            {editItem('banner', 'Banner URL', undefined, undefined)}
+
+            <ListItem
+              sx={{
+                marginTop: 1,
+                display: 'flex',
+                flexDirection: 'column'
+              }}
+            >
+              <img
+                onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
+                  event.currentTarget.src = getRoboHashPicture(npub!)
+                }}
+                className={styles.img}
+                src={getProfileImage(userProfile)}
+                alt="Profile Image"
+              />
+            </ListItem>
+
+            {editItem('image', 'Picture URL', undefined, undefined, {
+              endAdornment: isUsersOwnProfile ? robohashButton() : undefined
+            })}
+
+            {editItem('name', 'Username')}
+            {editItem('displayName', 'Display Name')}
+            {editItem('nip05', 'Nostr Address (nip05)')}
+            {editItem('lud16', 'Lightning Address (lud16)')}
+            {editItem('about', 'About', true, 4)}
+            {editItem('website', 'Website')}
+            {isUsersOwnProfile && (
+              <>
+                {usersPubkey &&
+                  copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
+
+                {loginMethod === LoginMethod.privateKey &&
+                  keys &&
+                  keys.private &&
+                  copyItem(
+                    '••••••••••••••••••••••••••••••••••••••••••••••••••',
+                    'Private Key',
+                    keys.private
+                  )}
+              </>
+            )}
+            {isUsersOwnProfile && (
+              <>
+                {loginMethod === LoginMethod.nostrLogin &&
+                  nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
+                    <ListItem
+                      sx={{ marginTop: 1 }}
+                      onClick={() => {
+                        launchNostrLoginDialog('import')
+                      }}
+                    >
+                      <TextField
+                        label="Private Key (nostr-login)"
+                        defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
+                        size="small"
+                        className={styles.textField}
+                        disabled
+                        type={'password'}
+                        InputProps={{
+                          endAdornment: (
+                            <LaunchIcon className={styles.copyItem} />
+                          )
                         }}
-                      >
-                        <TextField
-                          label="Private Key (nostr-login)"
-                          defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
-                          size="small"
-                          className={styles.textField}
-                          disabled
-                          type={'password'}
-                          InputProps={{
-                            endAdornment: (
-                              <LaunchIcon className={styles.copyItem} />
-                            )
-                          }}
-                        />
-                      </ListItem>
-                    )}
-                </>
-              )}
-            </div>
-          )}
-        </List>
-        {isUsersOwnProfile && (
-          <LoadingButton
-            loading={savingProfileMetadata}
-            variant="contained"
-            onClick={handleSaveMetadata}
-          >
-            SAVE
-          </LoadingButton>
+                      />
+                    </ListItem>
+                  )}
+              </>
+            )}
+          </div>
         )}
-      </Container>
-      <Footer />
+      </List>
+      {isUsersOwnProfile && (
+        <LoadingButton
+          sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
+          loading={savingProfileMetadata}
+          variant="contained"
+          onClick={handleSaveMetadata}
+        >
+          PUBLISH CHANGES
+        </LoadingButton>
+      )}
     </>
   )
 }
diff --git a/src/pages/settings/profile/style.module.scss b/src/pages/settings/profile/style.module.scss
index 672e59c..6cdc029 100644
--- a/src/pages/settings/profile/style.module.scss
+++ b/src/pages/settings/profile/style.module.scss
@@ -1,9 +1,3 @@
-.container {
-  display: flex;
-  flex-direction: column;
-  gap: 25px;
-}
-
 .textField {
   width: 100%;
 }
diff --git a/src/pages/settings/relays/index.tsx b/src/pages/settings/relays/index.tsx
index c0542c5..9590695 100644
--- a/src/pages/settings/relays/index.tsx
+++ b/src/pages/settings/relays/index.tsx
@@ -12,7 +12,6 @@ import ListItemText from '@mui/material/ListItemText'
 import Switch from '@mui/material/Switch'
 import { useEffect, useState } from 'react'
 import { toast } from 'react-toastify'
-import { Container } from '../../../components/Container'
 import {
   useAppDispatch,
   useAppSelector,
@@ -32,7 +31,6 @@ import {
   timeout
 } from '../../../utils'
 import styles from './style.module.scss'
-import { Footer } from '../../../components/Footer/Footer'
 import {
   getRelayListForUser,
   NDKRelayList,
@@ -246,7 +244,7 @@ export const RelaysPage = () => {
   }
 
   return (
-    <Container className={styles.container}>
+    <>
       <Box className={styles.relayAddContainer}>
         <TextField
           label="Add new relay"
@@ -291,8 +289,7 @@ export const RelaysPage = () => {
           ))}
         </Box>
       )}
-      <Footer />
-    </Container>
+    </>
   )
 }
 
diff --git a/src/pages/settings/relays/style.module.scss b/src/pages/settings/relays/style.module.scss
index 3db7760..df7eb31 100644
--- a/src/pages/settings/relays/style.module.scss
+++ b/src/pages/settings/relays/style.module.scss
@@ -1,107 +1,103 @@
 @import '../../../styles/colors.scss';
 
-.container {
-  color: $text-color;
+.relayURItextfield {
+  width: 100%;
+}
 
-  .relayURItextfield {
-    width: 100%;
+.relayAddContainer {
+  display: flex;
+  flex-direction: row;
+  gap: 10px;
+  width: 100%;
+  align-items: start;
+}
+
+.sectionIcon {
+  font-size: 30px;
+}
+
+.sectionTitle {
+  margin-top: 35px;
+  margin-bottom: 10px;
+  display: flex;
+  flex-direction: row;
+  gap: 5px;
+  font-size: 1.5rem;
+  line-height: 2rem;
+  font-weight: 600;
+}
+
+.relaysContainer {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.relay {
+  border: 1px solid rgba(0, 0, 0, 0.12);
+  border-radius: 4px;
+
+  .relayDivider {
+    margin-left: 10px;
+    margin-right: 10px;
   }
 
-  .relayAddContainer {
+  .leaveRelayContainer {
     display: flex;
     flex-direction: row;
     gap: 10px;
-    width: 100%;
-    align-items: start;
+    cursor: pointer;
   }
 
-  .sectionIcon {
-    font-size: 30px;
+  .showInfo {
+    cursor: pointer;
   }
 
-  .sectionTitle {
-    margin-top: 35px;
-    margin-bottom: 10px;
+  .showInfoIcon {
+    margin-right: 3px;
+    margin-bottom: auto;
+    vertical-align: middle;
+  }
+
+  .relayInfoContainer {
     display: flex;
-    flex-direction: row;
+    flex-direction: column;
     gap: 5px;
-    font-size: 1.5rem;
-    line-height: 2rem;
+    text-wrap: wrap;
+  }
+
+  .relayInfoTitle {
     font-weight: 600;
   }
 
-  .relaysContainer {
-    display: flex;
-    flex-direction: column;
-    gap: 15px;
+  .relayInfoSubTitle {
+    font-weight: 500;
   }
 
-  .relay {
-    border: 1px solid rgba(0, 0, 0, 0.12);
-    border-radius: 4px;
-
-    .relayDivider {
-      margin-left: 10px;
-      margin-right: 10px;
-    }
-
-    .leaveRelayContainer {
-      display: flex;
-      flex-direction: row;
-      gap: 10px;
-      cursor: pointer;
-    }
-
-    .showInfo {
-      cursor: pointer;
-    }
-
-    .showInfoIcon {
-      margin-right: 3px;
-      margin-bottom: auto;
-      vertical-align: middle;
-    }
-
-    .relayInfoContainer {
-      display: flex;
-      flex-direction: column;
-      gap: 5px;
-      text-wrap: wrap;
-    }
-
-    .relayInfoTitle {
-      font-weight: 600;
-    }
-
-    .relayInfoSubTitle {
-      font-weight: 500;
-    }
-
-    .copyItem {
-      margin-left: 10px;
-      color: #34495e;
-      vertical-align: bottom;
-      cursor: pointer;
-    }
-
-    .connectionStatus {
-      border-radius: 9999px;
-      width: 10px;
-      height: 10px;
-      margin-right: 5px;
-      margin-top: 2px;
-    }
-
-    .connectionStatusConnected {
-      background-color: $relay-status-connected;
-    }
-
-    .connectionStatusNotConnected {
-      background-color: $relay-status-notconnected;
-    }
-
-    .connectionStatusUnknown {
-      background-color: $input-text-color;
-    }
+  .copyItem {
+    margin-left: 10px;
+    color: #34495e;
+    vertical-align: bottom;
+    cursor: pointer;
   }
-}
+
+  .connectionStatus {
+    border-radius: 9999px;
+    width: 10px;
+    height: 10px;
+    margin-right: 5px;
+    margin-top: 2px;
+  }
+
+  .connectionStatusConnected {
+    background-color: $relay-status-connected;
+  }
+
+  .connectionStatusNotConnected {
+    background-color: $relay-status-notconnected;
+  }
+
+  .connectionStatusUnknown {
+    background-color: $input-text-color;
+  }
+}
\ No newline at end of file
diff --git a/src/pages/settings/style.module.scss b/src/pages/settings/style.module.scss
new file mode 100644
index 0000000..f2d45ef
--- /dev/null
+++ b/src/pages/settings/style.module.scss
@@ -0,0 +1,43 @@
+.title {
+    margin: 0 0 15px 0;
+}
+
+.main {
+    width: 100%;
+    display: grid;
+    grid-template-columns: 0.4fr 1.6fr;
+    position: relative;
+    grid-gap: 25px;
+
+    >* {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        grid-gap: 25px;
+    }
+}
+
+.aside {
+    width: 100%;
+    background: white;
+    padding: 15px;
+    border-radius: 5px;
+    box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
+    display: flex;
+    flex-direction: column;
+    grid-gap: 15px;
+
+    position: sticky;
+    top: 15px;
+}
+
+.content {
+    width: 100%;
+    background: white;
+    padding: 15px;
+    border-radius: 5px;
+    box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
+    display: flex;
+    flex-direction: column;
+    grid-gap: 15px;
+}
\ No newline at end of file
diff --git a/src/routes/util.tsx b/src/routes/util.tsx
index e21dc13..99fe70f 100644
--- a/src/routes/util.tsx
+++ b/src/routes/util.tsx
@@ -7,7 +7,7 @@ import { ProfilePage } from '../pages/profile'
 import { NostrLoginPage } from '../pages/settings/nostrLogin'
 import { ProfileSettingsPage } from '../pages/settings/profile'
 import { RelaysPage } from '../pages/settings/relays'
-import { SettingsPage } from '../pages/settings/Settings'
+import { SettingsLayout } from '../pages/settings/Settings'
 import { SignPage } from '../pages/sign'
 import { VerifyPage } from '../pages/verify'
 import { PrivateRoute } from './PrivateRoute'
@@ -96,32 +96,22 @@ export const privateRoutes = [
     path: appPrivateRoutes.settings,
     element: (
       <PrivateRoute>
-        <SettingsPage />
+        <SettingsLayout />
       </PrivateRoute>
-    )
-  },
-  {
-    path: appPrivateRoutes.profileSettings,
-    element: (
-      <PrivateRoute>
-        <ProfileSettingsPage />
-      </PrivateRoute>
-    )
-  },
-  {
-    path: appPrivateRoutes.relays,
-    element: (
-      <PrivateRoute>
-        <RelaysPage />
-      </PrivateRoute>
-    )
-  },
-  {
-    path: appPrivateRoutes.nostrLogin,
-    element: (
-      <PrivateRoute>
-        <NostrLoginPage />
-      </PrivateRoute>
-    )
+    ),
+    children: [
+      {
+        path: appPrivateRoutes.profileSettings,
+        element: <ProfileSettingsPage />
+      },
+      {
+        path: appPrivateRoutes.relays,
+        element: <RelaysPage />
+      },
+      {
+        path: appPrivateRoutes.nostrLogin,
+        element: <NostrLoginPage />
+      }
+    ]
   }
 ]

From afdc9449b19b33b3a4daf2cd84a2f7c32ad2b91b Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Mon, 10 Mar 2025 12:36:47 +0000
Subject: [PATCH 24/28] refactor(settings): remove base settings page, go
 directly to profile

---
 src/components/AppBar/AppBar.tsx     |   2 +-
 src/pages/profile/index.tsx          |   4 +-
 src/pages/settings/Settings.tsx      |   6 +-
 src/pages/settings/profile/index.tsx | 131 ++++++++++-----------------
 src/routes/index.tsx                 |   6 +-
 src/routes/util.tsx                  |   5 +-
 6 files changed, 57 insertions(+), 97 deletions(-)

diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx
index 68b04dd..e1c2220 100644
--- a/src/components/AppBar/AppBar.tsx
+++ b/src/components/AppBar/AppBar.tsx
@@ -181,7 +181,7 @@ export const AppBar = () => {
                       onClick={() => {
                         setAnchorElUser(null)
 
-                        navigate(appPrivateRoutes.settings)
+                        navigate(appPrivateRoutes.profileSettings)
                       }}
                       sx={{
                         justifyContent: 'center'
diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx
index 8e1e8c0..e5b29f6 100644
--- a/src/pages/profile/index.tsx
+++ b/src/pages/profile/index.tsx
@@ -13,7 +13,7 @@ import { Footer } from '../../components/Footer/Footer'
 import { LoadingSpinner } from '../../components/LoadingSpinner'
 import { useAppSelector } from '../../hooks/store'
 
-import { getProfileSettingsRoute } from '../../routes'
+import { appPrivateRoutes } from '../../routes'
 
 import {
   getProfileUsername,
@@ -168,7 +168,7 @@ export const ProfilePage = () => {
               <Box className={styles.right}>
                 {isUsersOwnProfile && (
                   <IconButton
-                    onClick={() => navigate(getProfileSettingsRoute(pubkey))}
+                    onClick={() => navigate(appPrivateRoutes.profileSettings)}
                   >
                     <EditIcon />
                   </IconButton>
diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index f00eab4..4bd9735 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -3,7 +3,7 @@ import RouterIcon from '@mui/icons-material/Router'
 import { Button } from '@mui/material'
 import { useAppSelector } from '../../hooks/store'
 import { NavLink, Outlet, To } from 'react-router-dom'
-import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
+import { appPrivateRoutes } from '../../routes'
 import { Container } from '../../components/Container'
 import { Footer } from '../../components/Footer/Footer'
 import ExtensionIcon from '@mui/icons-material/Extension'
@@ -50,7 +50,7 @@ const Item = (to: To, icon: ReactNode, label: string) => {
 }
 
 export const SettingsLayout = () => {
-  const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
+  const { loginMethod } = useAppSelector((state) => state.auth)
 
   return (
     <>
@@ -60,7 +60,7 @@ export const SettingsLayout = () => {
           <div>
             <aside className={styles.aside}>
               {Item(
-                getProfileSettingsRoute(usersPubkey!),
+                appPrivateRoutes.profileSettings,
                 <AccountCircleIcon />,
                 'Profile'
               )}
diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx
index 03db00f..5ca9000 100644
--- a/src/pages/settings/profile/index.tsx
+++ b/src/pages/settings/profile/index.tsx
@@ -1,5 +1,4 @@
 import React, { useEffect, useRef, useState } from 'react'
-import { useParams } from 'react-router-dom'
 import { toast } from 'react-toastify'
 
 import { SmartToy } from '@mui/icons-material'
@@ -38,10 +37,8 @@ import styles from './style.module.scss'
 export const ProfileSettingsPage = () => {
   const dispatch: Dispatch = useAppDispatch()
 
-  const { npub } = useParams()
   const { ndk, findMetadata, publish } = useNDKContext()
 
-  const [pubkey, setPubkey] = useState<string>()
   const [userProfile, setUserProfile] = useState<NDKUserProfile | null>(null)
 
   const userRobotImage = useAppSelector((state) => state.user.robotImage)
@@ -52,27 +49,13 @@ export const ProfileSettingsPage = () => {
   )
 
   const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
-  const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
   const [isLoading, setIsLoading] = useState(true)
   const [loadingSpinnerDesc] = useState('Fetching metadata')
 
   const robotSet = useRef(1)
 
   useEffect(() => {
-    if (npub) {
-      try {
-        const hexPubkey = nip19.decode(npub).data as string
-        setPubkey(hexPubkey)
-
-        if (hexPubkey === usersPubkey) setIsUsersOwnProfile(true)
-      } catch (error) {
-        toast.error('Error occurred in decoding npub' + error)
-      }
-    }
-  }, [npub, usersPubkey])
-
-  useEffect(() => {
-    if (isUsersOwnProfile && currentUserProfile) {
+    if (usersPubkey && currentUserProfile) {
       setUserProfile(currentUserProfile)
 
       setIsLoading(false)
@@ -80,8 +63,8 @@ export const ProfileSettingsPage = () => {
       return
     }
 
-    if (pubkey) {
-      findMetadata(pubkey)
+    if (usersPubkey) {
+      findMetadata(usersPubkey)
         .then((profile) => {
           setUserProfile(profile)
         })
@@ -92,7 +75,7 @@ export const ProfileSettingsPage = () => {
           setIsLoading(false)
         })
     }
-  }, [ndk, isUsersOwnProfile, currentUserProfile, pubkey, findMetadata])
+  }, [ndk, currentUserProfile, findMetadata, usersPubkey])
 
   const editItem = (
     key: keyof NDKUserProfile,
@@ -110,7 +93,6 @@ export const ProfileSettingsPage = () => {
         multiline={multiline}
         rows={rows}
         className={styles.textField}
-        disabled={!isUsersOwnProfile}
         InputProps={inputProps}
         onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
           const { value } = event.target
@@ -167,7 +149,7 @@ export const ProfileSettingsPage = () => {
       content: serializedProfile,
       created_at: unixNow(),
       kind: kinds.Metadata,
-      pubkey: pubkey!,
+      pubkey: usersPubkey!,
       tags: []
     }
 
@@ -212,7 +194,7 @@ export const ProfileSettingsPage = () => {
     robotSet.current++
     if (robotSet.current > 5) robotSet.current = 1
 
-    const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current)
+    const robotAvatarLink = getRoboHashPicture(usersPubkey!, robotSet.current)
 
     setUserProfile((prev) => ({
       ...prev,
@@ -241,13 +223,9 @@ export const ProfileSettingsPage = () => {
    * @returns robohash image url
    */
   const getProfileImage = (profile: NDKUserProfile) => {
-    if (!isUsersOwnProfile) {
-      return profile.image || getRoboHashPicture(npub!)
-    }
-
     // userRobotImage is used only when visiting own profile
     // while kind 0 picture is not set
-    return profile.image || userRobotImage || getRoboHashPicture(npub!)
+    return profile.image || userRobotImage || getRoboHashPicture(usersPubkey!)
   }
 
   return (
@@ -285,7 +263,7 @@ export const ProfileSettingsPage = () => {
             >
               <img
                 onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
-                  event.currentTarget.src = getRoboHashPicture(npub!)
+                  event.currentTarget.src = getRoboHashPicture(usersPubkey!)
                 }}
                 className={styles.img}
                 src={getProfileImage(userProfile)}
@@ -294,7 +272,7 @@ export const ProfileSettingsPage = () => {
             </ListItem>
 
             {editItem('image', 'Picture URL', undefined, undefined, {
-              endAdornment: isUsersOwnProfile ? robohashButton() : undefined
+              endAdornment: robohashButton()
             })}
 
             {editItem('name', 'Username')}
@@ -303,61 +281,48 @@ export const ProfileSettingsPage = () => {
             {editItem('lud16', 'Lightning Address (lud16)')}
             {editItem('about', 'About', true, 4)}
             {editItem('website', 'Website')}
-            {isUsersOwnProfile && (
-              <>
-                {usersPubkey &&
-                  copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
-
-                {loginMethod === LoginMethod.privateKey &&
-                  keys &&
-                  keys.private &&
-                  copyItem(
-                    '••••••••••••••••••••••••••••••••••••••••••••••••••',
-                    'Private Key',
-                    keys.private
-                  )}
-              </>
-            )}
-            {isUsersOwnProfile && (
-              <>
-                {loginMethod === LoginMethod.nostrLogin &&
-                  nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
-                    <ListItem
-                      sx={{ marginTop: 1 }}
-                      onClick={() => {
-                        launchNostrLoginDialog('import')
-                      }}
-                    >
-                      <TextField
-                        label="Private Key (nostr-login)"
-                        defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
-                        size="small"
-                        className={styles.textField}
-                        disabled
-                        type={'password'}
-                        InputProps={{
-                          endAdornment: (
-                            <LaunchIcon className={styles.copyItem} />
-                          )
-                        }}
-                      />
-                    </ListItem>
-                  )}
-              </>
-            )}
+            {usersPubkey &&
+              copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
+            {loginMethod === LoginMethod.privateKey &&
+              keys &&
+              keys.private &&
+              copyItem(
+                '••••••••••••••••••••••••••••••••••••••••••••••••••',
+                'Private Key',
+                keys.private
+              )}
+            {loginMethod === LoginMethod.nostrLogin &&
+              nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
+                <ListItem
+                  sx={{ marginTop: 1 }}
+                  onClick={() => {
+                    launchNostrLoginDialog('import')
+                  }}
+                >
+                  <TextField
+                    label="Private Key (nostr-login)"
+                    defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
+                    size="small"
+                    className={styles.textField}
+                    disabled
+                    type={'password'}
+                    InputProps={{
+                      endAdornment: <LaunchIcon className={styles.copyItem} />
+                    }}
+                  />
+                </ListItem>
+              )}
           </div>
         )}
       </List>
-      {isUsersOwnProfile && (
-        <LoadingButton
-          sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
-          loading={savingProfileMetadata}
-          variant="contained"
-          onClick={handleSaveMetadata}
-        >
-          PUBLISH CHANGES
-        </LoadingButton>
-      )}
+      <LoadingButton
+        sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
+        loading={savingProfileMetadata}
+        variant="contained"
+        onClick={handleSaveMetadata}
+      >
+        PUBLISH CHANGES
+      </LoadingButton>
     </>
   )
 }
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index f514e78..f1bd004 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -4,8 +4,7 @@ export const appPrivateRoutes = {
   homePage: '/',
   create: '/create',
   sign: '/sign',
-  settings: '/settings',
-  profileSettings: '/settings/profile/:npub',
+  profileSettings: '/settings/profile',
   relays: '/settings/relays',
   nostrLogin: '/settings/nostrLogin'
 }
@@ -23,6 +22,3 @@ export const appPublicRoutes = {
 
 export const getProfileRoute = (hexKey: string) =>
   appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey))
-
-export const getProfileSettingsRoute = (hexKey: string) =>
-  appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
diff --git a/src/routes/util.tsx b/src/routes/util.tsx
index 99fe70f..e0356dd 100644
--- a/src/routes/util.tsx
+++ b/src/routes/util.tsx
@@ -37,7 +37,7 @@ export function recursiveRouteRenderer<T>(
   return routes.map((route, index) =>
     renderConditionCallbackFn(route) ? (
       <Route
-        key={`${route.path}${index}`}
+        key={route.path ? `${route.path}${index}` : index}
         path={route.path}
         element={route.element}
       >
@@ -67,7 +67,7 @@ export const publicRoutes: PublicRouteProps[] = [
   }
 ]
 
-export const privateRoutes = [
+export const privateRoutes: CustomRouteProps<unknown>[] = [
   {
     path: appPrivateRoutes.homePage,
     element: (
@@ -93,7 +93,6 @@ export const privateRoutes = [
     )
   },
   {
-    path: appPrivateRoutes.settings,
     element: (
       <PrivateRoute>
         <SettingsLayout />

From 4f5dcc03360d03b203261348b5809f0bf879ec03 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Tue, 11 Mar 2025 10:58:19 +0000
Subject: [PATCH 25/28] refactor(hooks): add comments to local storage hook

---
 src/hooks/useLocalStorage.ts | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
index ec6c9cb..a2a9532 100644
--- a/src/hooks/useLocalStorage.ts
+++ b/src/hooks/useLocalStorage.ts
@@ -6,6 +6,11 @@ import {
   setLocalStorageItem
 } from '../utils'
 
+/**
+ * Subscribe to the Browser's storage event. Get the new value if any of the tabs changes it.
+ * @param callback - function to be called when the storage event is triggered
+ * @returns clean up function
+ */
 const useLocalStorageSubscribe = (callback: () => void) => {
   window.addEventListener('storage', callback)
   return () => window.removeEventListener('storage', callback)
@@ -28,8 +33,11 @@ export function useLocalStorage<T>(
     )
   }
 
+  // https://react.dev/reference/react/useSyncExternalStore
+  // Returns the snapshot of the data and subscribes to the storage event
   const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
 
+  // Takes the value or a function that returns the value and updates the local storage
   const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
     (v: React.SetStateAction<T>) => {
       try {

From 493390bdc1152ea912a04485556ef5f9cb6a91b9 Mon Sep 17 00:00:00 2001
From: en <enes@nostrdev.com>
Date: Tue, 11 Mar 2025 11:05:53 +0000
Subject: [PATCH 26/28] chore(deps): bump axios from 1.7.4 to 1.8.2

---
 package-lock.json | 8 ++++----
 package.json      | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index b439f36..019491c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
         "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
         "@pdf-lib/fontkit": "^1.1.1",
         "@reduxjs/toolkit": "2.2.1",
-        "axios": "^1.7.4",
+        "axios": "^1.8.2",
         "crypto-hash": "3.0.0",
         "crypto-js": "^4.2.0",
         "dexie": "4.0.8",
@@ -4051,9 +4051,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.7.4",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
-      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
+      "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
       "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.6",
diff --git a/package.json b/package.json
index 7e372f1..d4de123 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
     "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
     "@pdf-lib/fontkit": "^1.1.1",
     "@reduxjs/toolkit": "2.2.1",
-    "axios": "^1.7.4",
+    "axios": "^1.8.2",
     "crypto-hash": "3.0.0",
     "crypto-js": "^4.2.0",
     "dexie": "4.0.8",

From dd2aa3dc40f8ef81798e1cfcd24f68ae5e0750e1 Mon Sep 17 00:00:00 2001
From: theborakompanioni <theborakompanioni+github@gmail.com>
Date: Wed, 2 Apr 2025 19:37:27 +0200
Subject: [PATCH 27/28] chore(deps): run audit fix

---
 package-lock.json | 167 ++++++++++++++++++++++------------------------
 1 file changed, 81 insertions(+), 86 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 019491c..4c6f81b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -108,12 +108,14 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -305,17 +307,19 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -330,37 +334,28 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
-      "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+      "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.23.9",
-        "@babel/traverse": "^7.23.9",
-        "@babel/types": "^7.23.9"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.22.20",
-        "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
-      "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+      "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.0"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -399,9 +394,10 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
-      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
+      "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
+      "license": "MIT",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -410,14 +406,15 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
-      "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+      "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.23.5",
-        "@babel/parser": "^7.23.9",
-        "@babel/types": "^7.23.9"
+        "@babel/code-frame": "^7.26.2",
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -445,13 +442,13 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
-      "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+      "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.23.4",
-        "@babel/helper-validator-identifier": "^7.22.20",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1939,9 +1936,9 @@
       }
     },
     "node_modules/@octokit/endpoint": {
-      "version": "10.1.2",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.2.tgz",
-      "integrity": "sha512-XybpFv9Ms4hX5OCHMZqyODYqGTZ3H6K6Vva+M9LR7ib/xr1y1ZnlChYv9H680y77Vd/i/k+thXApeRASBQkzhA==",
+      "version": "10.1.3",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
+      "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
       "dev": true,
       "license": "MIT",
       "peer": true,
@@ -1970,22 +1967,22 @@
       }
     },
     "node_modules/@octokit/openapi-types": {
-      "version": "23.0.1",
-      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
-      "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
+      "version": "24.2.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
+      "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
       "dev": true,
       "license": "MIT",
       "peer": true
     },
     "node_modules/@octokit/plugin-paginate-rest": {
-      "version": "11.4.0",
-      "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.0.tgz",
-      "integrity": "sha512-ttpGck5AYWkwMkMazNCZMqxKqIq1fJBNxBfsFwwfyYKTf914jKkLF0POMS3YkPBwp5g1c2Y4L79gDz01GhSr1g==",
+      "version": "11.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
+      "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
       "dev": true,
       "license": "MIT",
       "peer": true,
       "dependencies": {
-        "@octokit/types": "^13.7.0"
+        "@octokit/types": "^13.10.0"
       },
       "engines": {
         "node": ">= 18"
@@ -2032,15 +2029,15 @@
       }
     },
     "node_modules/@octokit/request": {
-      "version": "9.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.0.tgz",
-      "integrity": "sha512-kXLfcxhC4ozCnAXy2ff+cSxpcF0A1UqxjvYMqNuPIeOAzJbVWQ+dy5G2fTylofB/gTbObT8O6JORab+5XtA1Kw==",
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
+      "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
       "dev": true,
       "license": "MIT",
       "peer": true,
       "dependencies": {
-        "@octokit/endpoint": "^10.0.0",
-        "@octokit/request-error": "^6.0.1",
+        "@octokit/endpoint": "^10.1.3",
+        "@octokit/request-error": "^6.1.7",
         "@octokit/types": "^13.6.2",
         "fast-content-type-parse": "^2.0.0",
         "universal-user-agent": "^7.0.2"
@@ -2050,9 +2047,9 @@
       }
     },
     "node_modules/@octokit/request-error": {
-      "version": "6.1.6",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.6.tgz",
-      "integrity": "sha512-pqnVKYo/at0NuOjinrgcQYpEbv4snvP3bKMRqHaD9kIsk9u1LCpb2smHZi8/qJfgeNqLo5hNW4Z7FezNdEo0xg==",
+      "version": "6.1.7",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
+      "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
       "dev": true,
       "license": "MIT",
       "peer": true,
@@ -2064,14 +2061,14 @@
       }
     },
     "node_modules/@octokit/types": {
-      "version": "13.7.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.7.0.tgz",
-      "integrity": "sha512-BXfRP+3P3IN6fd4uF3SniaHKOO4UXWBfkdR3vA8mIvaoO/wLjGN5qivUtW0QRitBHHMcfC41SLhNVYIZZE+wkA==",
+      "version": "13.10.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
+      "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
       "dev": true,
       "license": "MIT",
       "peer": true,
       "dependencies": {
-        "@octokit/openapi-types": "^23.0.1"
+        "@octokit/openapi-types": "^24.2.0"
       }
     },
     "node_modules/@pdf-lib/fontkit": {
@@ -3862,6 +3859,7 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -4564,6 +4562,7 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -5021,6 +5020,7 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -5028,7 +5028,8 @@
     "node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "dev": true
     },
     "node_modules/color-support": {
       "version": "1.1.3",
@@ -6724,9 +6725,9 @@
       "dev": true
     },
     "node_modules/elliptic": {
-      "version": "6.6.0",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
-      "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -6939,6 +6940,7 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -8278,6 +8280,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -13879,7 +13882,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
       "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/picomatch": {
@@ -16582,6 +16584,7 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -16850,14 +16853,6 @@
         "node": ">=0.6.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-readable-stream": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz",
@@ -17310,9 +17305,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.8",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
-      "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
+      "version": "5.4.16",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
+      "integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {

From 13044d6b39e464b6e7de6a825b9a15858a652e4a Mon Sep 17 00:00:00 2001
From: tbk <theborakompanioni+nostrdev@gmail.com>
Date: Thu, 3 Apr 2025 11:40:04 +0000
Subject: [PATCH 28/28] feat: enable pwa (#324)

This PR addresses 2 of 3 tasks from #93.
- [x] It should be possible to download SIGit as a PWA on a device homescreen.
- [x] This app should self-update

Co-authored-by: theborakompanioni <theborakompanioni+github@gmail.com>
Co-authored-by: b <b@4j.cx>
Reviewed-on: https://git.nostrdev.com/sigit/sigit.io/pulls/324
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
Co-authored-by: tbk <theborakompanioni+nostrdev@gmail.com>
Co-committed-by: tbk <theborakompanioni+nostrdev@gmail.com>
---
 index.html                 |   1 +
 public/app.webmanifest     |  58 +++++++++++++++++++++++++++++++++++++
 public/favicon-128x128.png | Bin 0 -> 3371 bytes
 public/favicon-144x144.png | Bin 0 -> 3756 bytes
 public/favicon-192x192.png | Bin 0 -> 5352 bytes
 public/favicon-256x256.png | Bin 0 -> 8203 bytes
 public/favicon-384x384.png | Bin 0 -> 13215 bytes
 public/favicon-512x512.png | Bin 0 -> 15473 bytes
 public/favicon-64x64.png   | Bin 0 -> 1552 bytes
 public/favicon-72x72.png   | Bin 0 -> 1732 bytes
 public/favicon-96x96.png   | Bin 0 -> 2377 bytes
 public/favicon.svg         |  25 ++++++++++++++++
 12 files changed, 84 insertions(+)
 create mode 100644 public/app.webmanifest
 create mode 100644 public/favicon-128x128.png
 create mode 100644 public/favicon-144x144.png
 create mode 100644 public/favicon-192x192.png
 create mode 100644 public/favicon-256x256.png
 create mode 100644 public/favicon-384x384.png
 create mode 100644 public/favicon-512x512.png
 create mode 100644 public/favicon-64x64.png
 create mode 100644 public/favicon-72x72.png
 create mode 100644 public/favicon-96x96.png
 create mode 100644 public/favicon.svg

diff --git a/index.html b/index.html
index 501fda6..461cf18 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/png" href="/favicon.png" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="manifest" href="/app.webmanifest" />
     <title>SIGit</title>
   </head>
   <body>
diff --git a/public/app.webmanifest b/public/app.webmanifest
new file mode 100644
index 0000000..c0b073f
--- /dev/null
+++ b/public/app.webmanifest
@@ -0,0 +1,58 @@
+{
+  "short_name": "SIGit",
+  "name": "SIGit",
+  "description": "A decentralised document signing tool",
+  "icons": [
+    {
+      "src": "favicon-512x512.png",
+      "sizes": "512x512",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-384x384.png",
+      "sizes": "384x384",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-256x256.png",
+      "sizes": "256x256",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-192x192.png",
+      "sizes": "192x192",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-144x144.png",
+      "sizes": "144x144",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-128x128.png",
+      "sizes": "128x128",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-96x96.png",
+      "sizes": "96x96",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-72x72.png",
+      "sizes": "72x72",
+      "type": "image/png"
+    },
+    {
+      "src": "favicon-64x64.png",
+      "sizes": "64x64",
+      "type": "image/png"
+    }
+  ],
+  "start_url": "/",
+  "display_override": ["minimal-ui", "standalone"],
+  "display": "minimal-ui",
+  "orientation": "any",
+  "theme_color": "#7d54a3",
+  "background_color": "#ffffff"
+}
diff --git a/public/favicon-128x128.png b/public/favicon-128x128.png
new file mode 100644
index 0000000000000000000000000000000000000000..ada1e351e4d98617f75734c45b18e83e8540bed2
GIT binary patch
literal 3371
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
BP~rdp

literal 0
HcmV?d00001

diff --git a/public/favicon-144x144.png b/public/favicon-144x144.png
new file mode 100644
index 0000000000000000000000000000000000000000..d226bc3dca64b101a85c3487abcb4c588a8e872b
GIT binary patch
literal 3756
zcmV;d4pZ@oP)<h;3K|Lk000e1NJLTq0058x0058(1^@s6=SJeV00004XF*Lt006O%
z3;baP00001b5ch_0olnce*gdgAY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF*
zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L04^f{04^f|c%?sf000gH
zNkl<Zc-rlqdz4hwm50B5svkn@_&^5~F&Yqz2}YJN31p0B7B-LSXh};Pi4@>th>0t@
zlF3B7o35^=n#_P1&2o+7(h<4~CzwFfM7n@3W+X!(IA#(MP~!tJ^6<e!WB{SN>+bpE
z77im2y6IcpUH6`Et@@*C)vBty_pi>`=iIaR28+dFu~;k?i^XEGbRW`FUmT7n1q2aj
zYdb6jp<)#SC@7GMMb)||zycayGq=D<K|h4Sz)+CEz$L&%zy-iLz*)c<Kvp`m0!M)T
z!0zm02k2Iit*E}Luq_pf?z67*nSjQ{>kAE`s}a5?@HJorFfc0+FWCpY1H1^VL-jXk
zW=nI;G{?F#&jb{%Z8$@a(IDl(SfGCoOp6Y%1xPFU9I9(mu_*_wYkRSP!tvw)5xxsD
zB|9zkCf)^7s3ui)U8-hEyLEFf5l}eZaE^%iJK%fS7^b(g8Fa2H&!l3}UDn+_B%p9S
zDI#(c@E9;C53ln>AgRiuso3PVt=qd%K;fE(zGBS7!1U~DDWCZmn5T};PSr&JWZj=*
z0flRm{RLSJjPlua9{?V4RDZfG7X8S|AO`{p$CDR}$O_=|KED2URJEeDRTr$Rn_{;C
zyb@5jCfQGntOAAyzy_LKwSB)i7Tszk(M183)g+5BvJ@x{m@Ph3_yJBPnW~v;N23mM
zd{8V>5kT<(oGUO_j7f&$$#bnVPGbZL$CF<Yku^YZ!0qx1=<Utb(T!Fj9UOtmV)X?g
z@-r(Q0ImdCT~?bcx2EXm6c#c!0VAzRKH+ST1!eL1KO7f(+E{a(m{4}=G9oe;xY(NH
zQ}z*VG$vFs_^%?*zqxveJ)?Az5$G$xm#sNY8YliRRA?Rx#~TW*IZhxT5vKIy>BKw{
z(Y+$(@v@rwkTu6K0t&~H#USIYDLTYmn9xtd@p)E6pNv38uC%dFr$(atMM5*IDY60*
z(NT8#>a>ok%4+9NvvvRja<w(XX{}x#FN@Frh7}MDvwAo!&On$Sjwde(x_|~*Eu1z3
zMdX=qtiCwN0y^L7pi5jQq2LD92Iy-w&}DuUu1$VDr~<Mpu&z@i$dloC{h2`&(8t#Q
zo-j<rR0UB$d#&%iLhdh%C$9>cfOc5Nd%3iiIpgAUO^^iiruDn0j1Eb`H-jXgP1fh0
z@9xLO2ju97L3z>o+;c`4%!EJ)NMQqDvopQFZ(mUst1s}qfKsu^9|HenJ?=TfF`+R*
zInuhw`a1_>knljbCTV;xAXTRk_#f+Y&-rT+f1h?R0IArNy}+~9-#M_a$lZa+3scp{
zaI2(Ryydp>@rJ&>7f>n|-DG2-oG{Rcxh@dJ6%?IeW1*bztw0DURULf`m}UL#Ic4K&
z>WczV-o<e;j{s||&pqXWkeF)&A)sY7Q;(n;1>UoM$HP4#85@iOQ$TYpx>c1)z#;2<
z&-g}Q1mv#DCa+R-GH^Jc4$lDX*@qWZIj$TZPo6{I>O|KjBZABa&h$YsP0g+{{{(CY
zwt>D2@*c2T(LJK<2RVpntGh4-Af!UEDCYwG5iS84id%$q1a4x2ocago$W(Ro1wRSs
zly{3$)zKwo@nk6?3xLn$$#w0^#&E9#uK|Bl<#p6~%Mtk?gl3v!Q@T8=gkueYXkQUC
z3Xup<o^3dDvVZF}zzc!wl~EQ?4nX8_Aks@RIW#-H?7%$~_cE$4DQrgBenj?v^zX6z
zw40mgoh8EUAQdP1vpv~dJ$ZUyo2`tic}ha2cp}IwU~mtNDqDb!zy?*_gz&1ix4yru
z_HOM>Mh(|AoGr%0f%}eDfy=V4xq9*_+wQ5Rp+t;CK^_1u_jp9gWaE@S;x?6Auc{ja
z-J)%W52O;)^P<dLI5tnjgl<J-9#E2fd6%Q@{g=h2sckq1l*JkfHDs<AxDB}RG-4#p
z2D7gK>w%Y4b))07zqveqr!Q{ohHH~!v;9W;0IjN-3sbSFd#!+ukCBR0xJ*>8MYs~U
z6t_2ON%nh=;I>`b32X&kR@IFNyHeHBULlr>^7_6;%$Ed4f(!xvB6~t%r@}@-S7)?!
z`>e*!O_^o2$>|{f;x5E=M03nP^`~uGGf-LHaFG%705A!+&+Ex(1AeWlkF>V6t(g7X
zT^$lqxawa;ii#Jz3lJUMTpj%nD<D4zXF@|rjJZqTC%}1hu21VSRlPUS7~S5%5&CjO
zHh@lRu8#f^zwbo*or>~gkul~;fhRgCo}+dv6OnaQ<;f1+ByEmG-vAb%avnjrmEr4=
z=a%{c;}%xGvx}qf#Uir2syx}DGJ&f41mbxcbSt24zvxitM*`pW=;Qi{$im9<h7OD@
zx*pXnRzP{ojPm)zfohLGc8C#k%7b{R*p!Sas|or>V&29*6X!`t3l;&nNIPy-)vFVY
z(N}}raL(6x+@Z^WGICX=OyoPZV<Emi78n6?c{cf*RTCPDt$_T?^*R3h%Mcl41>|2o
zm-A6hWSA9@e>vYL-(dx0u>#6Rc9Z*alDn;d{7cUFXjJqyD<J>!>l}anRz<g20r?bF
zT|#b4*etDEH06jDkWXoCJ@PiNG*=(5s!!M;I!`HWswymPsw(+&@0q>y9#thyZnACX
z2JJqWzeXUT1mVl4dRwA#@+P3x<8OacRV(c>KEH7UxwRv2Y$r1Zc*dje-GeeQ(HPxp
z1(e_CQZfkROPi`LJk@)DvS@01M)fX*xm_%rupQO0GaDyw0K1Wx=amz*6>vo4?>qAT
zS&h-wc5S~$mFaZ8<&^_Gr|4H_Hb(y&f?W8>M~wN^jP<}!MMtG?PP{~i(|VE@8j<^f
zJLq6q&k^ISn6q%ILeMK``Aa})+zi>@tLhEuNa6sU$`7mWD`Kt@WF#;IIQwW~VYYC?
zZyY+GSeTP*ZK^OTrUV!!BEz%o+b_XQ<~%byyBwR$`G%^l2iDWBAEj@tQ!Ak31@t6v
zPxfgenz^GTGRqeQvc@!34nWKm2w%i46LS?X6gOqMGaSOLIj}@k7p5bLomN2qC!h+T
z?wFUBs4^`bnYk}tuAy{ERWV9GF>+N_C?m3ui@@`gphNB-O=q`65}U1nmQ;NUkzai3
z<;|-4opdCzwzvJ>(Tg4`GKHa`qH<MsWVsr+0=T%F7dWjzgR0I*M-uM`SwLTK(}(|G
zO&)LyQ9Y}w{}4KK_?6|89@g&NC9muQay}wgi0J1*h67*B#w~qL@OyiT_dvhfnmMp!
z)r3biZ~_|LSb0XM!0h@n^&H!9YheFURhI+X(vgJIW#KNa6pV;ES|Ua+1-TrzOV6<E
zh;lTzEy#=FqROL+^H4fc*A}?Eura@yu@3lZ2lsNe0)GHrQFt3=hoE~E*)M22f`ei}
zLW=egI1A)FpdUhiU_f@+KPdYckR46@VAn=fCz_7b9qLk%%{*jn2L)ts%k&NY<ewx*
z`@{i=131~IAqid~lX4N$XUyWtTU#d7eH@(Q9Lib`cj*gyQGC)9j3;ECE^VqT2#kOf
zeF69wi&xw(##96<H@*{bkfmd^p*prDl6XESClXg!pL<FnB2SezRh=CS0l8<N9qV^b
z`K%auC@2Ejlr0@>@s|5an`V60_X29UrOp9<Vg2nnC1Q9m7+D#rKPR}Ys&cw28`D%Z
z(Dwpri6q_vR#|_0&Y2h{2O=*_RsY%gI~T~L(x$4A{{@7$;I@LYc*_?Mx7L!c1++3!
z*N*Zt>+f9D(UNh2NNsl1S%BMT%!2n#gadJldCZckI}v%>`rK1?lF3{ah~f$k`ftEy
z>vK;zUybP>2m!5()Ez<fL3?|{Q;L8=fhq6e95}KJSZ@99X(T-`Falb6#~fAVe&8eP
zdc4{vIX6%RrqYp_TkyWLOOW%L`vk7cuWIHo;6JVRy)kFt1eA`<YE#uafj!p$UFJ|A
z3yr5EiMLes`*u~<Wp)R)Qhhp-__@8!(dC=f+XCI{Kpm|Bp0`ps?ZON@0xh8Qgv4Q#
zzo8o{*!z&3Ce!vo;G3<qL}u<nH3Gb6HPI=4yQ2Ib9owzPJ-zt4s)p^#tW%qaEnwR{
z9UD~tK~--C_E~G3l7pzT!V2hk5v^6#@!irED!*CUa!cJ_E1(mLXq~E#1>UvR;KXP2
zj_C<v1$1H&{T|ipfS0W`j$@6<w63-SI(1;ZMOAMAR$6O(LPk|9R+P_HE1-^zM7tF&
z1Lgo3YY`f7oZsU2@-F1&T=hgm9w%U3nU7&Bj$YexOJa}h+I)Ib+Co+5nt)De2T{KL
zY4KPAb$Uvxdk4n}quh(zLLdmVqdL7MGIJe4sZd7ubV&S+7?}f1z`u1A+ko%2+&uFs
zf>h$JyBNK&N<xKl1H!|&?Fjs2&x5LZcRG@ImLS!M?wLj?4;CPW6A>N)hWp@>-az$^
zmPq17f>vm}hdb&NVax>J2e^$=Odd{I^HlZ6=}2N<;94E@7H(YhV`B=6u1EM=;3l9i
zz5U)gtU{?eS+Hyc)N}hs{Slc2+?I`#ih9w{I}EHu^`8`3osP_O0^RN@4|;(O5s~r0
z7~tzbNe}p-75E*nSXCFNBZ;@M4d?PQUOG#~d_{zjz;(dqaGSLhcsw<I0K5kL4%Ib|
z>KYN*la3^8%S(UlD^(UFa)BVjfFTH%0+#}R3G@Tb!|g3r3>4y~B((ttfc>~<nBE7r
z<8~f;UD4On;r*2p5(lwOcY{9t%@9FR1d&hDYCs@;vp1uO#bU8oEEbE!VzF3yJO2v@
WVYJ<*qtn^|0000<MNUMnLSTY;YVNH7

literal 0
HcmV?d00001

diff --git a/public/favicon-192x192.png b/public/favicon-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..d5ce7918018ace53cedc6a38a32b3c6ef84b179e
GIT binary patch
literal 5352
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

literal 0
HcmV?d00001

diff --git a/public/favicon-256x256.png b/public/favicon-256x256.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6a340f82fc07c22b72fc5e0b67e64a2e2833df6
GIT binary patch
literal 8203
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
RSNIe>pr>uDRi%N5{6ENAEwlgt

literal 0
HcmV?d00001

diff --git a/public/favicon-384x384.png b/public/favicon-384x384.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc9872ee5958e9013a31c7be7d32b123ca3bd304
GIT binary patch
literal 13215
zcmbVTg;!Kvw7(49-Hjq24I-Tq5)wKvARsLuDM)vh(jhI~Ap+7cgGfk+z|bkp&|UBP
z-hc3B)~s1`X5X`aJI~(d+#B^uQ<)Ht4i5kTLRA$-C;*@Z{`=ryfg^>zKj7dOhV^re
z=K%0M4*%8+6a0<l0#%kroB7QX0RCXHP=RUyfcFys2n+>)Yj7xV3jo{%0AR-y03^}?
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

literal 0
HcmV?d00001

diff --git a/public/favicon-512x512.png b/public/favicon-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe48fe3c30090a704f6b7da417fdcf25f7579286
GIT binary patch
literal 15473
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
Z5%?CZs2j=l)e;92^-x2h<i2_E{{aToAhZAg

literal 0
HcmV?d00001

diff --git a/public/favicon-64x64.png b/public/favicon-64x64.png
new file mode 100644
index 0000000000000000000000000000000000000000..d59e0164438e87f3b237a36e092c3ec540088d63
GIT binary patch
literal 1552
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
C`mi7X

literal 0
HcmV?d00001

diff --git a/public/favicon-72x72.png b/public/favicon-72x72.png
new file mode 100644
index 0000000000000000000000000000000000000000..168bb20fe7434adec039ed7a2c1e890e8d042d18
GIT binary patch
literal 1732
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
agZ>1FjMM4_$`Sei0000<MNUMnLSTYji4S-H

literal 0
HcmV?d00001

diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png
new file mode 100644
index 0000000000000000000000000000000000000000..5267e602a23ecb0f31e18c28bc96e71c37d6f090
GIT binary patch
literal 2377
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

literal 0
HcmV?d00001

diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..144306d
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 282.61 282.61">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #47b17d;
+      }
+
+      .cls-2 {
+        fill: #4c82a3;
+      }
+
+      .cls-3 {
+        fill: #7d54a3;
+      }
+    </style>
+  </defs>
+  <g id="Layer_1-2" data-name="Layer 1" transform="translate(0, 13.775)">
+    <g>
+      <path class="cls-2" d="M181.53,115.06h0c-9.4-36.67-56.77-24.79-121.09-12.57C-3.54,114.64-25.35,19.85,37.72,3.62,46.91,1.26,56.55,0,66.47,0c63.55,0,115.06,51.51,115.06,115.06Z"/>
+      <path class="cls-1" d="M100,140h0c9.4,36.67,56.77,24.79,121.09,12.57,63.98-12.16,85.79,82.64,22.72,98.86-9.19,2.36-18.83,3.62-28.76,3.62-63.55,0-115.06-51.51-115.06-115.06Z"/>
+      <circle class="cls-3" cx="140.77" cy="127.53" r="24.88"/>
+    </g>
+  </g>
+</svg>
\ No newline at end of file