Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .cursor/rules/flutter.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ Generate code, corrections, and refactorings that comply with the basic principl

- Use English for all code and documentation.
- Always declare the type of each variable and function (parameters and return value).
- Avoid using any.
- Avoid using dynamic and Object without justification.
- Create necessary types.
- Don't leave blank lines within a function.
- One export per file.

### Nomenclature
Expand All @@ -35,7 +34,6 @@ Generate code, corrections, and refactorings that comply with the basic principl
- i, j for loops
- err for errors
- ctx for contexts
- req, res, next for middleware function parameters

### Functions

Expand All @@ -62,8 +60,8 @@ Generate code, corrections, and refactorings that comply with the basic principl
- Don't abuse primitive types and encapsulate data in composite types.
- Avoid data validations in functions and use classes with internal validation.
- Prefer immutability for data.
- Use readonly for data that doesn't change.
- Use as const for literals that don't change.
- Use final for runtime constants and const for compile-time constants.
- Use const constructors and const literals where possible.

### Classes

Expand Down Expand Up @@ -105,10 +103,6 @@ Generate code, corrections, and refactorings that comply with the basic principl
- see keepAlive if you need to keep the state alive
- Use freezed to manage UI states
- Controller always takes methods as input and updates the UI state that effects the UI
- Use getIt to manage dependencies
- Use singleton for services and repositories
- Use factory for use cases
- Use lazy singleton for controllers
- Use AutoRoute to manage routes
- Use extras to pass data between pages
- Use extensions to manage reusable code
Expand Down
128 changes: 24 additions & 104 deletions lib/config/providers/chat_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import 'package:whitenoise/config/providers/group_provider.dart';
import 'package:whitenoise/config/states/chat_state.dart';
import 'package:whitenoise/domain/models/message_model.dart';
import 'package:whitenoise/domain/services/message_merger_service.dart';
import 'package:whitenoise/domain/services/message_sender_service.dart';
import 'package:whitenoise/domain/services/reaction_comparison_service.dart';
import 'package:whitenoise/src/rust/api/error.dart' show ApiError;
import 'package:whitenoise/src/rust/api/messages.dart';
import 'package:whitenoise/src/rust/api/utils.dart';
import 'package:whitenoise/utils/message_converter.dart';
import 'package:whitenoise/utils/pubkey_formatter.dart';

class ChatNotifier extends Notifier<ChatState> {
final _logger = Logger('ChatNotifier');
final _messageSenderService = MessageSenderService();

@override
ChatState build() {
Expand Down Expand Up @@ -104,7 +106,6 @@ class ChatNotifier extends Notifier<ChatState> {
Future<MessageWithTokens?> sendMessage({
required String groupId,
required String message,
int kind = 9, // Default to text message
List<Tag>? tags,
bool isEditing = false,
void Function()? onMessageSent,
Expand All @@ -124,7 +125,6 @@ class ChatNotifier extends Notifier<ChatState> {
content: message,
currentUserPublicKey: activePubkey,
groupId: groupId,
kind: kind,
);
final optimisticId = optimisticMessageModel.id;

Expand All @@ -148,11 +148,10 @@ class ChatNotifier extends Notifier<ChatState> {
try {
_logger.info('ChatProvider: Sending message to group $groupId');

final sentMessage = await sendMessageToGroup(
final sentMessage = await _messageSenderService.sendMessage(
pubkey: activePubkey,
groupId: groupId,
message: message,
kind: kind,
content: message,
tags: tags,
);

Expand Down Expand Up @@ -282,8 +281,7 @@ class ChatNotifier extends Notifier<ChatState> {

// Compare message content and reactions
if (newMsg.content != currentMsg.content ||
newMsg.reactions.length != currentMsg.reactions.length ||
!_areReactionsEqual(newMsg.reactions, currentMsg.reactions)) {
ReactionComparisonService.areDifferent(newMsg.reactions, currentMsg.reactions)) {
hasChanges = true;
break;
}
Expand Down Expand Up @@ -389,9 +387,9 @@ class ChatNotifier extends Notifier<ChatState> {
}

bool isSameSender(int index, {String? groupId}) {
final gId = groupId ??= state.selectedGroupId;
if (gId == null) return false;
final groupMessages = state.groupMessages[gId] ?? [];
final selectedGroupId = groupId ?? state.selectedGroupId;
if (selectedGroupId == null) return false;
final groupMessages = state.groupMessages[selectedGroupId] ?? [];
if (index <= 0 || index >= groupMessages.length) return false;
final currentSenderPubkey = groupMessages[index].sender.publicKey;
final currentSenderHexPubkey = PubkeyFormatter(pubkey: currentSenderPubkey).toHex() ?? '';
Expand All @@ -402,9 +400,9 @@ class ChatNotifier extends Notifier<ChatState> {
}

bool isNextSameSender(int index, {String? groupId}) {
final gId = groupId ??= state.selectedGroupId;
if (gId == null) return false;
final groupMessages = state.groupMessages[gId] ?? [];
final selectedGroupId = groupId ?? state.selectedGroupId;
if (selectedGroupId == null) return false;
final groupMessages = state.groupMessages[selectedGroupId] ?? [];
if (index < 0 || index >= groupMessages.length - 1) return false;
final currentSenderPubkey = groupMessages[index].sender.publicKey;
final currentSenderHexPubkey = PubkeyFormatter(pubkey: currentSenderPubkey).toHex() ?? '';
Expand Down Expand Up @@ -487,33 +485,15 @@ class ChatNotifier extends Notifier<ChatState> {

_logger.info('ChatProvider: Adding reaction "$reaction" to message ${message.id}');

// Create reaction content (emoji) - NIP-25 compliant
final reactionContent = reaction; // This should be an emoji like 👍, ❤️, etc.

// Use the message's actual kind (now stored in MessageModel)
final originalMessageKind = messageKind ?? message.kind;

// NIP-25 compliant reaction tags
// According to NIP-25:
// - MUST have e tag with event id being reacted to
// - SHOULD have p tag with pubkey of event being reacted to
// - SHOULD have k tag with stringified kind number of reacted event
final reactionTags = [
// e tag: ["e", <event-id>]
await tagFromVec(vec: ['e', message.id]),
// p tag: ["p", <pubkey>, <relay-hint>]
await tagFromVec(vec: ['p', message.sender.publicKey, '']),
// k tag: ["k", <kind-number>]
await tagFromVec(vec: ['k', originalMessageKind.toString()]),
];

// Send reaction message (kind 7 for reactions in Nostr)
await sendMessageToGroup(
await _messageSenderService.sendReaction(
pubkey: activePubkey,
groupId: message.groupId ?? '',
message: reactionContent,
kind: 7, // Nostr kind 7 = reaction
tags: reactionTags,
messageId: message.id,
messagePubkey: message.sender.publicKey,
messageKind: originalMessageKind,
emoji: reaction,
);

// Refresh messages to get updated reactions
Expand Down Expand Up @@ -560,7 +540,6 @@ class ChatNotifier extends Notifier<ChatState> {
content: message,
currentUserPublicKey: activePubkey,
groupId: groupId,
kind: 9,
replyToMessage: replyToMessage,
);
final optimisticId = optimisticMessageModel.id;
Expand All @@ -581,17 +560,11 @@ class ChatNotifier extends Notifier<ChatState> {
try {
_logger.info('ChatProvider: Sending reply to message $replyToMessageId');

// Create tags for reply
final replyTags = [
await tagFromVec(vec: ['e', replyToMessageId]),
];

final sentMessage = await sendMessageToGroup(
final sentMessage = await _messageSenderService.sendReply(
pubkey: activePubkey,
groupId: groupId,
message: message,
kind: 9,
tags: replyTags,
replyToMessageId: replyToMessageId,
content: message,
);

final stateMessages = state.groupMessages[groupId] ?? [];
Expand Down Expand Up @@ -675,20 +648,12 @@ class ChatNotifier extends Notifier<ChatState> {

_logger.info('ChatProvider: Deleting message $messageId');

// Create tags for deletion (NIP-09)
final deleteTags = [
await tagFromVec(vec: ['e', messageId]),
await tagFromVec(vec: ['p', messagePubkey]), // Author of the message being deleted
await tagFromVec(vec: ['k', messageKind.toString()]), // Kind of the message being deleted
];

// Send deletion message using rust API
await sendMessageToGroup(
await _messageSenderService.sendDeletion(
pubkey: activePubkey,
groupId: groupId,
message: '', // Empty content for deletion
kind: 5, // Nostr kind 5 = deletion
tags: deleteTags,
messageId: messageId,
messagePubkey: messagePubkey,
messageKind: messageKind,
);

// Refresh messages to get updated state
Expand Down Expand Up @@ -772,51 +737,6 @@ class ChatNotifier extends Notifier<ChatState> {

await ref.read(groupsProvider.notifier).updateGroupActivityTime(groupId, now);
}

/// Helper method to compare if two reaction lists are equal
bool _areReactionsEqual(List<Reaction> reactions1, List<Reaction> reactions2) {
if (reactions1.length != reactions2.length) {
return false;
}

// Create maps of emoji -> list of user public keys for comparison
final map1 = <String, List<String>>{};
final map2 = <String, List<String>>{};

for (final reaction in reactions1) {
map1.putIfAbsent(reaction.emoji, () => []).add(reaction.user.publicKey);
}

for (final reaction in reactions2) {
map2.putIfAbsent(reaction.emoji, () => []).add(reaction.user.publicKey);
}

// Compare the maps
if (map1.keys.length != map2.keys.length) {
return false;
}

for (final emoji in map1.keys) {
if (!map2.containsKey(emoji)) {
return false;
}

final users1 = map1[emoji]!..sort();
final users2 = map2[emoji]!..sort();

if (users1.length != users2.length) {
return false;
}

for (int i = 0; i < users1.length; i++) {
if (users1[i] != users2[i]) {
return false;
}
}
}

return true;
}
}

final chatProvider = NotifierProvider<ChatNotifier, ChatState>(
Expand Down
110 changes: 110 additions & 0 deletions lib/domain/services/message_sender_service.dart
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, let's keep some of those important comments from the original file (chat_provider) especially the ones mentioning kinds or how to handle specific events. Maybe just copy them over. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'm not a huge fan of comments in code but maybe those ones are in fact useful, I willl add them back 👍🏻

I thought that by adding the the description in the test cases of those specifications (should have, must have, etc) would be enough

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added comments back but in the nostr tags builder service cause the more relevant comments were related to the tags.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Related to the comments about kinds, I think the kinds are already pretty clear by the variable names...

const int _messageKind = 9;
const int _reactionKind = 7;
const int _deletionKind = 5; 

If I add the comments back they would say basically the same but in comment form, this are the comments related to kinds that I've found in master:

 int kind = 9, // Default to text message
 // Send reaction message (kind 7 for reactions in Nostr)
// Nostr kind 7 = reaction
// Nostr kind 5 = deletion

If you really think is useful I can add them, but I think its just repetitive 😅

const int _messageKind = 9; // Nostr kind 9 = text message
const int _reactionKind = 7; // Nostr kind 7 = reaction
const int _deletionKind = 5;  // Nostr kind 5 = deletion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please let me know it adding this comments back c8df2b1 is ok or if you are missing more of them

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm okay with c8df2b1. Thanks.

Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:whitenoise/domain/services/nostr_tag_builder_service.dart';
import 'package:whitenoise/src/rust/api/messages.dart';

const int _messageKind = 9;
const int _reactionKind = 7;
const int _deletionKind = 5;

class MessageSenderService {
final NostrTagBuilderService _tagBuilder;
final Future<MessageWithTokens> Function({
required String pubkey,
required String groupId,
required String message,
required int kind,
List<Tag>? tags,
})
_sendMessageToGroupFn;

MessageSenderService({
NostrTagBuilderService? tagBuilder,
Future<MessageWithTokens> Function({
required String pubkey,
required String groupId,
required String message,
required int kind,
List<Tag>? tags,
})?
sendMessageToGroupFn,
}) : _tagBuilder = tagBuilder ?? NostrTagBuilderService(),
_sendMessageToGroupFn = sendMessageToGroupFn ?? sendMessageToGroup;

Future<MessageWithTokens> sendMessage({
required String pubkey,
required String groupId,
required String content,
List<Tag>? tags,
}) async {
return _sendMessageToGroupFn(
pubkey: pubkey,
groupId: groupId,
message: content,
kind: _messageKind,
tags: tags,
);
}

Future<MessageWithTokens> sendReaction({
required String pubkey,
required String groupId,
required String messageId,
required String messagePubkey,
required int messageKind,
required String emoji,
}) async {
final tags = await _tagBuilder.buildReactionTags(
messageId: messageId,
messagePubkey: messagePubkey,
messageKind: messageKind,
);

return _sendMessageToGroupFn(
pubkey: pubkey,
groupId: groupId,
message: emoji,
kind: _reactionKind,
tags: tags,
);
}

Future<MessageWithTokens> sendReply({
required String pubkey,
required String groupId,
required String replyToMessageId,
required String content,
}) async {
final tags = await _tagBuilder.buildReplyTags(
replyToMessageId: replyToMessageId,
);

return _sendMessageToGroupFn(
pubkey: pubkey,
groupId: groupId,
message: content,
kind: _messageKind,
tags: tags,
);
}

Future<MessageWithTokens> sendDeletion({
required String pubkey,
required String groupId,
required String messageId,
required String messagePubkey,
required int messageKind,
}) async {
final tags = await _tagBuilder.buildDeletionTags(
messageId: messageId,
messagePubkey: messagePubkey,
messageKind: messageKind,
);

return _sendMessageToGroupFn(
pubkey: pubkey,
groupId: groupId,
message: '',
kind: _deletionKind,
tags: tags,
);
}
}
Loading