-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
class TreeNode<T> { | ||
T value; | ||
List<TreeNode<T>> children; | ||
|
||
TreeNode(this.value) : children = <TreeNode<T>>[]; | ||
|
||
void addChild(TreeNode<T> child) { | ||
children.add(child); | ||
} | ||
|
||
void printTree([String prefix = '']) { | ||
print('$prefix${value.toString()}'); | ||
for (var i = 0; i < children.length; i++) { | ||
var child = children[i]; | ||
var isLastChild = i == children.length - 1; | ||
child.printTree('$prefix${isLastChild ? '└── ' : '├── '}'); | ||
} | ||
} | ||
|
||
@override | ||
String toString() { | ||
return value.toString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import 'dart:async'; | ||
import 'dart:developer'; | ||
|
||
import '../entities/nostr_note.dart'; | ||
import '../entities/tree_node.dart'; | ||
import '../repositories/note_repository.dart'; | ||
import 'follow.dart'; | ||
|
||
/// | ||
/// idea is to combine multiple streams here into the feed stream | ||
/// the feed stream gets then sorted on the ui in an intervall to prevent huge layout shifts | ||
/// | ||
/// there could be one update stream and one for scrolling | ||
/// | ||
/// | ||
class EventFeed { | ||
final NoteRepository _noteRepository; | ||
final Follow _follow; | ||
|
||
final String rootNoteFetchId = "event-root"; | ||
final String repliesFetchId = "event-replies"; | ||
|
||
// root streams | ||
final StreamController<NostrNote> _rootNoteController = | ||
StreamController<NostrNote>(); | ||
Stream<NostrNote> get rootNoteStream => _rootNoteController.stream; | ||
|
||
final StreamController<NostrNote> _replyNotesController = | ||
StreamController<NostrNote>(); | ||
Stream<NostrNote> get replyNotesStream => _replyNotesController.stream; | ||
|
||
EventFeed(this._noteRepository, this._follow); | ||
|
||
Future<void> subscribeToReplyNotes({ | ||
required String rootNoteId, | ||
required int since, | ||
}) async { | ||
final replyNotes = _noteRepository.subscribeReplyNotes( | ||
requestId: repliesFetchId, | ||
rootNoteId: rootNoteId, | ||
); | ||
|
||
replyNotes.listen((event) { | ||
_replyNotesController.add(event); | ||
}); | ||
} | ||
|
||
Future<void> subscribeToRootNote({ | ||
required String noteId, | ||
}) async { | ||
final rootNote = _noteRepository.getTextNote( | ||
noteId, | ||
); | ||
|
||
rootNote.listen((event) { | ||
_rootNoteController.add(event); | ||
}); | ||
} | ||
|
||
/// build a tree from the replies \ | ||
/// [returns] a list of first level replies \ | ||
/// the cildren are replies of replies | ||
static List<TreeNode<NostrNote>> buildRepliesTree({ | ||
required String rootNoteId, | ||
required List<NostrNote> replies, | ||
}) { | ||
final List<NostrNote> workingList = List.from(replies, growable: true); | ||
workingList.sort((a, b) => a.created_at.compareTo(b.created_at)); | ||
final List<TreeNode<NostrNote>> tree = []; | ||
|
||
// find top level replies | ||
for (var i = 0; i < workingList.length; i++) { | ||
final reply = workingList[i]; | ||
|
||
if (reply.getDirectReply?.value == rootNoteId) { | ||
tree.add(TreeNode<NostrNote>(reply)); | ||
workingList.remove(reply); | ||
i--; // Adjust index after removal | ||
} | ||
} | ||
|
||
// build the tree | ||
for (final node in tree) { | ||
_buildSubtree(workingList: workingList, parent: node); | ||
} | ||
|
||
return tree; | ||
} | ||
|
||
/// recursive function to build the tree | ||
/// | ||
static _buildSubtree({ | ||
required List<NostrNote> workingList, | ||
required TreeNode<NostrNote> parent, | ||
}) { | ||
for (var i = 0; i < workingList.length; i++) { | ||
final reply = workingList[i]; | ||
|
||
if (reply.getDirectReply?.value == parent.value.id) { | ||
final child = TreeNode<NostrNote>(reply); | ||
parent.addChild(child); | ||
workingList.remove(reply); | ||
i--; // Adjust index after removal | ||
_buildSubtree(workingList: workingList, parent: child); | ||
} | ||
} | ||
} | ||
|
||
/// clean up everything including closing subscriptions | ||
Future<void> dispose() async { | ||
final List<Future> futures = []; | ||
futures.add(_noteRepository.closeSubscription(repliesFetchId)); | ||
futures.add(_rootNoteController.close()); | ||
futures.add(_replyNotesController.close()); | ||
|
||
await Future.wait(futures); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import 'package:camelus/domain_layer/entities/nostr_note.dart'; | ||
import 'package:camelus/domain_layer/entities/nostr_tag.dart'; | ||
import 'package:camelus/domain_layer/usecases/event_feed.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
group('Tree', () { | ||
// Create a root note | ||
final rootNote = NostrNote( | ||
id: 'root123', | ||
pubkey: 'pubkey1', | ||
created_at: 1635000000, | ||
kind: 1, | ||
content: 'This is the root note.', | ||
sig: 'sig1', | ||
tags: [], | ||
); | ||
|
||
// Create replies to the root note | ||
final reply1 = NostrNote( | ||
id: 'reply1', | ||
pubkey: 'pubkey2', | ||
created_at: 1635000100, | ||
kind: 1, | ||
content: 'This is a reply to the root note.', | ||
sig: 'sig2', | ||
tags: [NostrTag(type: 'e', value: 'root123', marker: 'root')], | ||
); | ||
|
||
final reply2 = NostrNote( | ||
id: 'reply2', | ||
pubkey: 'pubkey3', | ||
created_at: 1635000200, | ||
kind: 1, | ||
content: 'Another reply to the root note.', | ||
sig: 'sig3', | ||
tags: [NostrTag(type: 'e', value: 'root123', marker: 'root')], | ||
); | ||
|
||
// Create replies to the replies | ||
final nestedReply1 = NostrNote( | ||
id: 'nestedReply1', | ||
pubkey: 'pubkey4', | ||
created_at: 1635000300, | ||
kind: 1, | ||
content: 'A reply to the first reply.', | ||
sig: 'sig4', | ||
tags: [ | ||
NostrTag(type: 'e', value: 'root123', marker: 'root'), | ||
NostrTag(type: 'e', value: 'reply1', marker: 'reply'), | ||
], | ||
); | ||
|
||
final nestedReply2 = NostrNote( | ||
id: 'nestedReply2', | ||
pubkey: 'pubkey5', | ||
created_at: 1635000400, | ||
kind: 1, | ||
content: 'A reply to the second reply.', | ||
sig: 'sig5', | ||
tags: [ | ||
NostrTag(type: 'e', value: 'root123', marker: 'root'), | ||
NostrTag(type: 'e', value: 'reply2', marker: 'reply'), | ||
], | ||
); | ||
|
||
// Create replies to the replies to the replies | ||
final nestedNestedReply1 = NostrNote( | ||
id: 'nestedNestedReply1', | ||
pubkey: 'pubkey6', | ||
created_at: 1635000500, | ||
kind: 1, | ||
content: 'A reply to the first nested reply.', | ||
sig: 'sig6', | ||
tags: [ | ||
NostrTag(type: 'e', value: 'root123', marker: 'root'), | ||
NostrTag(type: 'e', value: 'nestedReply1', marker: 'reply'), | ||
], | ||
); | ||
|
||
final nestedNestedReply2 = NostrNote( | ||
id: 'nestedNestedReply2', | ||
pubkey: 'pubkey7', | ||
created_at: 1635000600, | ||
kind: 1, | ||
content: 'A reply to the second nested reply.', | ||
sig: 'sig7', | ||
tags: [ | ||
NostrTag(type: 'e', value: 'root123', marker: 'root'), | ||
NostrTag(type: 'e', value: 'nestedReply1', marker: 'reply'), | ||
], | ||
); | ||
|
||
final notFoundReply = NostrNote( | ||
id: 'notFoundReply', | ||
pubkey: 'pubkey8', | ||
created_at: 1635000700, | ||
kind: 1, | ||
content: 'A reply to a note that does not exist.', | ||
sig: 'sig8', | ||
tags: [ | ||
NostrTag(type: 'e', value: 'notFound', marker: 'root'), | ||
], | ||
); | ||
|
||
// Create a list of all notes | ||
final List<NostrNote> allValidReplies = [ | ||
reply1, | ||
reply2, | ||
nestedReply1, | ||
nestedReply2, | ||
nestedNestedReply1, | ||
nestedNestedReply2, | ||
notFoundReply, | ||
]; | ||
|
||
test('test building tree', () { | ||
final tree = EventFeed.buildRepliesTree( | ||
rootNoteId: rootNote.id, | ||
replies: allValidReplies, | ||
); | ||
for (final node in tree) { | ||
node.printTree(); | ||
} | ||
|
||
// first level replies | ||
expect(tree.length, 2); | ||
expect(tree[0].value.id, equals(reply1.id)); | ||
expect(tree[1].value.id, equals(reply2.id)); | ||
|
||
// second level replies | ||
expect(tree[0].children.length, 1); | ||
expect(tree[0].children[0].value.id, equals(nestedReply1.id)); | ||
|
||
expect(tree[1].children.length, 1); | ||
expect(tree[1].children[0].value.id, equals(nestedReply2.id)); | ||
|
||
// third level replies | ||
|
||
expect(tree[0].children[0].children.length, 2); | ||
expect(tree[0].children[0].children[0].value.id, | ||
equals(nestedNestedReply1.id)); | ||
expect(tree[0].children[0].children[1].value.id, | ||
equals(nestedNestedReply2.id)); | ||
|
||
expect(tree[1].children[0].children.length, 0); | ||
}); | ||
}); | ||
} |