Skip to content

Commit

Permalink
repost state on posts
Browse files Browse the repository at this point in the history
  • Loading branch information
leo-lox committed Nov 20, 2024
1 parent e0a9b99 commit fe88518
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 12 deletions.
1 change: 1 addition & 0 deletions lib/config/palette.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ class Palette {
static const Color black = Color(0xFF000000);
static const Color error = Color.fromARGB(255, 254, 29, 29);
static const Color likeActive = Color.fromARGB(255, 230, 40, 85);
static const Color repostActive = Color.fromARGB(255, 22, 163, 74);
}
6 changes: 5 additions & 1 deletion lib/data_layer/models/nostr_note_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class NostrNoteModel extends NostrNote {
required super.sig,
required super.tags,
super.sig_valid,
super.sources,
});

factory NostrNoteModel.fromJson(Map<String, dynamic> json) {
Expand All @@ -30,6 +31,7 @@ class NostrNoteModel extends NostrNote {
kind: json['kind'],
content: json['content'],
sig: json['sig'],
sources: json['sources'],
tags: tags.map((tag) => NostrTagModel.fromJson(tag)).toList(),
);
}
Expand All @@ -56,6 +58,7 @@ class NostrNoteModel extends NostrNote {
sig: nip01event.sig,
tags: myTags,
sig_valid: nip01event.validSig,
sources: nip01event.sources,
);
}

Expand All @@ -79,6 +82,7 @@ class NostrNoteModel extends NostrNote {
sig: nostrNote.sig,
tags: nostrNote.tags,
sig_valid: nostrNote.sig_valid,
sources: nostrNote.sources,
);
}

Expand All @@ -89,6 +93,6 @@ class NostrNoteModel extends NostrNote {
'kind': kind,
'content': content,
'sig': sig,
'tags': tags
'tags': tags,
};
}
28 changes: 28 additions & 0 deletions lib/data_layer/repositories/note_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,32 @@ class NoteRepositoryImpl implements NoteRepository {
dartNdkSource.dartNdk.broadcast.broadcastDeletion(eventId: eventId);
// await res.publishDone;
}

@override
Future<List<NostrNote>> getReposts({
String? postId,
required List<String> authors,
}) async {
ndk.Filter filter = ndk.Filter(
eTags: postId != null ? [postId] : null,
authors: authors,
kinds: [6],
);

final response = dartNdkSource.dartNdk.requests.query(
timeout: 3,
filters: [filter],
name: 'getReposts',
cacheRead: false,
cacheWrite: false,
);

final events = await response.future;

return events
.map(
(event) => NostrNoteModel.fromNDKEvent(event),
)
.toList();
}
}
4 changes: 4 additions & 0 deletions lib/domain_layer/entities/nostr_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class NostrNote {
final bool? sig_valid;
final List<NostrTag> tags;

/// Relay that an event was received from
List<String> sources = [];

NostrNote({
required this.id,
required this.pubkey,
Expand All @@ -22,6 +25,7 @@ class NostrNote {
required this.sig,
required this.tags,
this.sig_valid,
this.sources = const [],
});

List<String> get relayHints => _extractRelayHints();
Expand Down
5 changes: 5 additions & 0 deletions lib/domain_layer/repositories/note_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ abstract class NoteRepository {
required String postId,
required List<String> authors,
});

Future<List<NostrNote>> getReposts({
String? postId,
required List<String> authors,
});
}
117 changes: 117 additions & 0 deletions lib/domain_layer/usecases/user_reposts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'dart:convert';

import 'package:camelus/config/default_relays.dart';
import 'package:camelus/data_layer/models/nostr_note_model.dart';

import '../entities/nostr_note.dart';
import '../entities/nostr_tag.dart';
import '../repositories/note_repository.dart';

class UserReposts {
final NoteRepository _noteRepository;
final String? selfPubkey;

UserReposts({
required NoteRepository noteRepository,
this.selfPubkey,
}) : _noteRepository = noteRepository;

Future<bool> isPostSelfReposted({required String postId}) async {
if (selfPubkey == null) {
return Future.value(false);
}
final res = await isPostRepostedBy(
repostedByPubkey: selfPubkey!,
postId: postId,
);
return res != null;
}

/// Check if a post is liked by a specific user
/// [returns] the reaction if the post is liked, null otherwise
Future<NostrNote?> isPostRepostedBy({
required String repostedByPubkey,
required String postId,
}) async {
final reposts = await _noteRepository.getReposts(
postId: postId,
authors: [repostedByPubkey],
);
if (reposts.isEmpty) {
return null;
}

final res = reposts.where((reaction) {
return reaction.tags.any((tag) => tag.type == "e" && tag.value == postId);
}).first;

return res;
}

/// repost a post \
/// [postToRepost] the post to repost \
///
Future<void> repostPost({
required NostrNote postToRepost,
}) {
if (selfPubkey == null) {
throw Exception("selfPubkey is null");
}

final recivedOnRelays = postToRepost.sources;

final selectedSource = recivedOnRelays.isNotEmpty
? recivedOnRelays.first
: DEFAULT_ACCOUNT_CREATION_RELAYS.keys.last;

final postToRepostModel = NostrNoteModel(
id: postToRepost.id,
pubkey: postToRepost.pubkey,
created_at: postToRepost.created_at,
kind: postToRepost.kind,
content: postToRepost.content,
sig: postToRepost.sig,
tags: postToRepost.tags,
);

final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final myRepost = NostrNote(
content: jsonEncode(postToRepostModel.toJson()),
pubkey: selfPubkey!,
created_at: now,
kind: 6,
id: "",
sig: "",
tags: [
NostrTag(
type: "e",
value: postToRepost.id,
recommended_relay: selectedSource,
),
NostrTag(type: "p", value: postToRepost.pubkey),
],
);
return _noteRepository.broadcastNote(myRepost);
}

/// delete a repost \
/// [postId] the id of the post to delete the repost from \
/// [throws] an exception if the repost is not found \
/// [returns] a future that completes when the repost is deleted
Future<void> deleteRepost({required String postId}) async {
if (selfPubkey == null) {
throw Exception("selfPubkey is null");
}

// get the repost to delete
final myRepostEvent =
await isPostRepostedBy(repostedByPubkey: selfPubkey!, postId: postId);
if (myRepostEvent == null) {
throw Exception("Repost event not found");
}

return _noteRepository.deleteNote(
myRepostEvent.id,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class _BottomActionRowState extends State<BottomActionRow>
onTap: widget.onRetweet,
svgIcon: 'assets/icons/retweet.svg',
count: widget.retweetCount,
color: widget.isRetweeted ? Palette.repostActive : null,
),
_buildLikeButton(),
_buildActionButton(
Expand Down Expand Up @@ -153,6 +154,7 @@ class _BottomActionRowState extends State<BottomActionRow>
Icon? icon,
String? svgIcon,
int? count,
Color? color,
}) {
return SizedBox(
height: 35,
Expand All @@ -170,8 +172,8 @@ class _BottomActionRowState extends State<BottomActionRow>
SvgPicture.asset(
svgIcon,
height: 35,
colorFilter: const ColorFilter.mode(
BottomActionRow.defaultColor,
colorFilter: ColorFilter.mode(
color ?? BottomActionRow.defaultColor,
BlendMode.srcATop,
),
),
Expand Down
11 changes: 3 additions & 8 deletions lib/presentation_layer/components/note_card/note_card.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:ui';

import 'package:camelus/presentation_layer/providers/reactions_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Expand All @@ -10,6 +9,7 @@ import '../../../domain_layer/entities/nostr_note.dart';
import '../../../domain_layer/entities/user_metadata.dart';
import '../../atoms/my_profile_picture.dart';
import '../../providers/reactions_state_provider.dart';
import '../../providers/reposts_state_provider.dart';
import '../bottom_sheet_share.dart';
import '../write_post.dart';
import 'bottom_action_row.dart';
Expand Down Expand Up @@ -85,16 +85,11 @@ class NoteCard extends ConsumerWidget {
_writeReply(context, note);
},
onLike: () {
print("onLikeInNoteCard");
ref.read(postLikeProvider(note).notifier).toggleLike();
},
isRetweeted: ref.watch(postRepostProvider(note)).isReposted,
onRetweet: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Not implemented yet',
style: TextStyle(color: Palette.black)),
),
);
ref.read(postRepostProvider(note).notifier).toggleRepost();
},
onShare: () => openBottomSheetShare(context, note),
onMore: () => openBottomSheetMore(context, note),
Expand Down
30 changes: 30 additions & 0 deletions lib/presentation_layer/providers/reposts_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:riverpod/riverpod.dart';

import '../../data_layer/data_sources/dart_ndk_source.dart';
import '../../data_layer/repositories/note_repository_impl.dart';
import '../../domain_layer/repositories/note_repository.dart';
import '../../domain_layer/usecases/user_reposts.dart';
import 'event_signer_provider.dart';
import 'event_verifier.dart';
import 'ndk_provider.dart';

final repostsProvider = Provider<UserReposts>((ref) {
final ndk = ref.watch(ndkProvider);

final eventVerifier = ref.watch(eventVerifierProvider);
final eventSigner = ref.watch(eventSignerProvider);

final DartNdkSource dartNdkSource = DartNdkSource(ndk);

final NoteRepository noteRepository = NoteRepositoryImpl(
dartNdkSource: dartNdkSource,
eventVerifier: eventVerifier,
);

final UserReposts userReposts = UserReposts(
noteRepository: noteRepository,
selfPubkey: eventSigner?.getPublicKey(),
);

return userReposts;
});
69 changes: 69 additions & 0 deletions lib/presentation_layer/providers/reposts_state_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:riverpod/riverpod.dart';

import '../../domain_layer/entities/nostr_note.dart';
import '../../domain_layer/usecases/user_reposts.dart';
import 'reposts_provider.dart';

/// class for to save state for each post if it is reposted or not
class PostRepostState {
final bool isReposted;
final bool isLoading;

PostRepostState({
required this.isReposted,
required this.isLoading,
});

PostRepostState copyWith({bool? isReposted, bool? isLoading}) {
return PostRepostState(
isReposted: isReposted ?? this.isReposted,
isLoading: isLoading ?? this.isLoading,
);
}
}

final postRepostProvider = StateNotifierProvider.family<PostRepostNotifier,
PostRepostState, NostrNote>(
(ref, arg) {
final userReactions = ref.watch(repostsProvider);
return PostRepostNotifier(userReactions, arg);
},
);

class PostRepostNotifier extends StateNotifier<PostRepostState> {
final UserReposts _userReposts;
final NostrNote _displayNote;

PostRepostNotifier(
this._userReposts,
this._displayNote,
) : super(PostRepostState(isReposted: false, isLoading: true)) {
_initializeRepostState();
}

Future<void> _initializeRepostState() async {
final isReposted =
await _userReposts.isPostSelfReposted(postId: _displayNote.id);

state = state.copyWith(isReposted: isReposted, isLoading: false);
}

Future<void> toggleRepost() async {
if (state.isLoading) return;

state = state.copyWith(isLoading: true);

try {
if (state.isReposted) {
await _userReposts.deleteRepost(postId: _displayNote.id);
} else {
await _userReposts.repostPost(postToRepost: _displayNote);
}
state = state.copyWith(isReposted: !state.isReposted, isLoading: false);
} catch (e) {
print(e);
// Handle error
state = state.copyWith(isLoading: false);
}
}
}
2 changes: 1 addition & 1 deletion lib/presentation_layer/routes/search_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
name: metadata['name'] ?? '',
pictureUrl: metadata['picture'] ?? '',
about: metadata['about'] ?? '',
nip05: metadata['nip05'] ?? '',
nip05: metadata['nip05'],
isFollowing: currentFollowing.contacts
.any((element) => element == profile.pubkey),
onTap: () {
Expand Down

0 comments on commit fe88518

Please sign in to comment.