Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/messages #106

Merged
merged 11 commits into from
Mar 11, 2025
Merged

Conversation

josefinalliende
Copy link
Contributor

@josefinalliende josefinalliende commented Mar 11, 2025

Context

While working on issue #70, I realized that the current deletion logic couldn’t be reused for reactions. The existing approach checks all events for each message to find reactions and then repeats the process to look for deletions. Adding deletion handling on top of that would have led to a lot of unnecessary checks.

To improve this, I adjusted how events are processed, making it easier to handle reaction deletions more efficiently. I also moved this logic out of the chat page, which was already quite complex. This keeps the chat component focused on handling messages, reactions, and payments without needing to deal with Nostr events directly for messages, while also making the logic easier to test.

What has been done:

Introduced a new store, chatStore, to structure and store processed Nostr events. To support this, new types were added to make handling events easier for the chat page. When Nostr events are processed, they are mapped into these types, allowing the page to work with structured data rather than raw events.

Here are the key new types:

  • Message: Represents processed kind 9 events. Depending on their tags, they may include aLightningInvoiceor a LightningPayment. Messages can also have an array of Reactions.
  • Reaction: Represents kind 7 events.
  • Deletion: Represents kind 5 events, which can currently target messages and reactions.

Additionally, the way deletions are handled was improved. Instead of checking all events to determine if a message or reaction has been deleted, deletions are now mapped by their target event ID. This allows for direct lookups, making the process more efficient.

Commit by commit:

  • Fixes type error for qrcode package
  • Adds new types for the chat, the ChatState for the chat store interface, Message, Reaction, Deletion, LightningInvoice and LightningPayment.
  • Adds a tags util to extract information from a nostr event
  • Adds lightning util to extract the lightning invoice info from tags and also moves the qrcode generation to the same file
  • Adds reaction util to process nostr event into Reaction type
  • Adds deletion util to process nostr event into Deletion type
  • Adds message util to process nostr event into message. In this processing the content is shortened when it has a lightning invoice. Is single emoji logic was moved to this util too.
  • Adds a derived store hasLightningWallet to the account store. I had a lot if trouble making tests for this one and I gave up, so help with tests here is welcome.
  • Creates a new store called chatStore. This store process the nostr events into the new types to make it easier to the page to render
  • Changes chat page and othe related components to use the new types
  • Removes and unused util method.

What to check:

  • Whether the new types and structural changes make sense.
  • That I haven’t unintentionally broken any expected behavior. I tested it locally on my Mac, and from what I can see, sending messages, deletions, reactions, and lightning payments are all working as expected. However, I’d really appreciate if someone else could double-check to make sure everything is in order.

What's next

  • I'll get back to working on reaction deletions—that was the original plan before this refactor pulled me in.

Some ideas for the future...

  • We could remove the original nostr events from the new types. I left added them to the types because it was easier to make the refactor without breaking too many things
  • Sending messages logic could also be moved to the chat store, as other methods like sending reactions. Now this is still handled inside of a component

PS: Sorry about he length of the PR, next ones will be shorter!

Copy link
Member

@erskingardner erskingardner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I love the direction @josefinalliende! And the tests! 🔥

One major question and a small nitpick.

import { eventToReaction } from '$lib/utils/reaction';
import { eventToDeletion } from '$lib/utils/deletion';

export function createChatStore() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about why you chose to use a store here instead of a Class. No judgement, I'm not super clear myself on why one would want to use one or the other... I think I'm using both in other parts of the app (toasts are a class, accounts are a store) and I feel like we should pick one pattern and use that unless we have a good reason otherwise. In any case, maybe you understand this better than I do so would love to hear your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, coming from a Ruby background, I love classes. However, from what I’ve learned, Svelte takes a more functional approach rather than an object-oriented one, with store functions and runes for reactivity. In this particular case, the advantage of using a Svelte store is that they are are inherently reactive. If we used a class, we would need to manually implement the subscription logic. (In the page we have suscriptions to the chat sotre when it is used with a $ , for example $chatStore.messages will be automatically updated when any change happens in the internal messageMap of the chat store).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could get something similar using $state fields inside a class but it's more to implement ourselves. This looks great as is.


// Add messages to the chat store
chatStore.clear();
events.forEach(event => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick but I'm starting to slowly try to switch out all the forEach loops for for..of

  • better performance
  • more predictable execution with async ops
for (const event of events) {
    chatStore.handleEvent(event);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thanks! I had no idea that it was better. I'll change it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick overview courtesy of Claude. :)

Here are some TypeScript examples comparing forEach with recommended alternatives:

1. Basic iteration with side effects

// Using forEach (not recommended for simple iteration)
const numbers: number[] = [1, 2, 3, 4, 5];
numbers.forEach(num => {
  console.log(num * 2);
});

// Better: Using for...of (cleaner and more flexible)
for (const num of numbers) {
  console.log(num * 2);
  if (num > 3) break; // Can exit early if needed
}

2. Transforming data

// Using forEach with side effects (not recommended)
const numbers: number[] = [1, 2, 3, 4, 5];
const doubled: number[] = [];
numbers.forEach(num => {
  doubled.push(num * 2);
});

// Better: Using map (functional, clearer intent)
const doubledBetter: number[] = numbers.map(num => num * 2);

3. Filtering data

// Using forEach with side effects (not recommended)
const numbers: number[] = [1, 2, 3, 4, 5];
const evens: number[] = [];
numbers.forEach(num => {
  if (num % 2 === 0) {
    evens.push(num);
  }
});

// Better: Using filter (functional, clearer intent)
const evensBetter: number[] = numbers.filter(num => num % 2 === 0);

4. Async operations

// Using forEach with async (PROBLEMATIC - doesn't wait for promises)
const userIds: number[] = [1, 2, 3];
userIds.forEach(async (id) => {
  const userData = await fetchUser(id);
  console.log(userData); // These will execute in unpredictable order
});

// Better: Using for...of with async/await
async function processUsers() {
  for (const id of userIds) {
    const userData = await fetchUser(id);
    console.log(userData); // These will execute in sequence
  }
}

5. Performance-critical code

// Using forEach (slower for large arrays)
function sumArray(numbers: number[]): number {
  let sum = 0;
  numbers.forEach(num => {
    sum += num;
  });
  return sum;
}

// Better: Using traditional for loop (faster)
function sumArrayFaster(numbers: number[]): number {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  return sum;
}

// Alternative: Using reduce (functional but still faster than forEach)
function sumArrayReduce(numbers: number[]): number {
  return numbers.reduce((sum, num) => sum + num, 0);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation! Now I've replaced them

@josefinalliende josefinalliende marked this pull request as ready for review March 11, 2025 14:16
@erskingardner erskingardner merged commit 69467d2 into parres-hq:master Mar 11, 2025
8 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants