From b74e54f40a8ca3b4ff807a85d3b37ee56746a97c Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 7 Feb 2024 21:55:39 +0000 Subject: [PATCH] Demo WIP --- .fvm/flutter_sdk | 2 +- .fvm/fvm_config.json | 2 +- README.md | 21 +- lib/adapters/nostr_adapter.dart | 78 ------ lib/main.dart | 13 +- lib/main.data.dart | 16 +- lib/models/base_event.dart | 43 ---- lib/models/metadata.dart | 44 ---- lib/models/metadata.g.dart | 82 ------- lib/models/note.dart | 19 -- lib/models/note.g.dart | 75 ------ lib/models/user.dart | 39 ++- lib/models/user.g.dart | 55 ++++- lib/screens/profile_screen.dart | 175 ++++++++++++-- lib/screens/search_screen.dart | 237 +++++++++++++++---- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 120 ++++------ pubspec.yaml | 12 +- 19 files changed, 507 insertions(+), 531 deletions(-) delete mode 100644 lib/adapters/nostr_adapter.dart delete mode 100644 lib/models/base_event.dart delete mode 100644 lib/models/metadata.dart delete mode 100644 lib/models/metadata.g.dart delete mode 100644 lib/models/note.dart delete mode 100644 lib/models/note.g.dart diff --git a/.fvm/flutter_sdk b/.fvm/flutter_sdk index 9b1f2b2..d0e2938 120000 --- a/.fvm/flutter_sdk +++ b/.fvm/flutter_sdk @@ -1 +1 @@ -/home/admin/fvm/versions/3.16.0 \ No newline at end of file +/home/admin/fvm/versions/3.16.9 \ No newline at end of file diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index f1f9cee..3e71957 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.16.0", + "flutterSdkVersion": "3.16.9", "flavors": {} } \ No newline at end of file diff --git a/README.md b/README.md index 7b40353..981e296 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,9 @@ # zapstore -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. + - test on macos + - drop file + - compute sha256 and query nostr by `x` + - install button + - downloads file (to whwatever the downloads provider is) + - checks sha256 with crypto and updates statenotifier bool state + - display if matches (green) or not (red) \ No newline at end of file diff --git a/lib/adapters/nostr_adapter.dart b/lib/adapters/nostr_adapter.dart deleted file mode 100644 index 033810c..0000000 --- a/lib/adapters/nostr_adapter.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; - -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter_data/flutter_data.dart'; -import 'package:zapstore/models/base_event.dart'; - -mixin NostrAdapter> on RemoteAdapter { - late final Nostr nostr; - - @override - Future onInitialized() async { - nostr = await ref.read(nostrProvider.future); - super.onInitialized(); - } - - void update(List filters) async { - final req = NostrRequest(filters: filters); - await Nostr.instance.relaysService.startEventsSubscriptionAsync( - request: req, - timeout: Duration(seconds: 10), - ); - } - - @override - DataStateNotifier> watchAllNotifier( - {bool? remote, - Map? params, - Map? headers, - bool? syncLocal, - String? finder, - DataRequestLabel? label}) { - late final StreamSubscription _sub; - _sub = Nostr.instance.relaysService.streamsController.events - .listen((NostrEvent event) { - final baseEvent = localAdapter.deserialize({ - 'id': event.id, - 'kind': event.kind, - 'content': event.content, - 'sig': event.sig, - 'pubkey': event.pubkey, - 'created_at': event.createdAt.toIso8601String(), - 'tags': event.tags, - }); - baseEvent.saveLocal(); - }); - - ref.onDispose(() { - print('disposing nostr sub'); - _sub.cancel(); - }); - - return super - .watchAllNotifier( - remote: remote, - params: params, - headers: headers, - syncLocal: syncLocal, - finder: finder, - label: label) - .where((e) { - final search = params?['search']; - if (search != null && search.isNotEmpty) { - return e.content.contains(search); - } - return true; - }); - } -} - -final nostrProvider = FutureProvider((Ref ref) async { - final relays = ['wss://relay.nostr.band']; - print('***** initing nostr ********'); - await Nostr.instance.relaysService.init( - relaysUrl: relays, - ); - Nostr.instance.utilsService.utils.disableLogs(); - return Nostr.instance; -}); diff --git a/lib/main.dart b/lib/main.dart index 41afbc0..2de90e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ndk/ndk.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/screens/profile_screen.dart'; import 'package:zapstore/screens/search_screen.dart'; @@ -12,7 +13,8 @@ void main() { runApp( ProviderScope( overrides: [ - configureRepositoryLocalStorage(clear: LocalStorageClearStrategy.never), + configureRepositoryLocalStorage( + clear: LocalStorageClearStrategy.always), ], child: const ZapStoreApp(), ), @@ -31,12 +33,19 @@ void main() { const borderColor = Color(0xFF805306); +final newInitializer = FutureProvider((ref) async { + await ref.read(repositoryInitializerProvider.future); + await ref + .read(frameProvider.notifier) + .initialize({'wss://relay.nostr.band', 'wss://relay.damus.io'}); +}); + class ZapStoreApp extends HookConsumerWidget { const ZapStoreApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final initializer = ref.watch(repositoryInitializerProvider); + final initializer = ref.watch(newInitializer); return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), diff --git a/lib/main.data.dart b/lib/main.data.dart index 2a6ed0c..96fa480 100644 --- a/lib/main.data.dart +++ b/lib/main.data.dart @@ -8,8 +8,6 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:path_provider/path_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:zapstore/models/metadata.dart'; -import 'package:zapstore/models/note.dart'; import 'package:zapstore/models/user.dart'; // ignore: prefer_function_declarations_over_variables @@ -31,18 +29,14 @@ ConfigureRepositoryLocalStorage configureRepositoryLocalStorage = ({FutureFn>>{ - 'fileMetadata': fileMetadataRepositoryProvider, -'notes': notesRepositoryProvider, -'users': usersRepositoryProvider + 'users': usersRepositoryProvider }; final repositoryInitializerProvider = FutureProvider((ref) async { - DataHelpers.setInternalType('fileMetadata'); - DataHelpers.setInternalType('notes'); DataHelpers.setInternalType('users'); - final adapters = {'fileMetadata': ref.watch(internalFileMetadataRemoteAdapterProvider), 'notes': ref.watch(internalNotesRemoteAdapterProvider), 'users': ref.watch(internalUsersRemoteAdapterProvider)}; - final remotes = {'fileMetadata': true, 'notes': true, 'users': true}; + final adapters = {'users': ref.watch(internalUsersRemoteAdapterProvider)}; + final remotes = {'users': true}; await ref.watch(graphNotifierProvider).initialize(); @@ -60,14 +54,10 @@ final repositoryInitializerProvider = return RepositoryInitializer(); }); extension RepositoryWidgetRefX on WidgetRef { - Repository get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch; - Repository get notes => watch(notesRepositoryProvider)..remoteAdapter.internalWatch = watch; Repository get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch; } extension RepositoryRefX on Ref { - Repository get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; - Repository get notes => watch(notesRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; Repository get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; } \ No newline at end of file diff --git a/lib/models/base_event.dart b/lib/models/base_event.dart deleted file mode 100644 index 9fa0d4a..0000000 --- a/lib/models/base_event.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter_data/flutter_data.dart'; -import 'package:json_annotation/json_annotation.dart'; - -abstract class BaseEvent> extends DataModel { - @override - final String id; - - final NostrEvent _event; - String get pubkey => _event.pubkey; - int get kind => _event.kind; - String get content => _event.content; - String get sig => _event.sig; - @JsonKey(name: 'created_at') - DateTime get createdAt => _event.createdAt; - List> get tags => _event.tags; - - Map> get tagMap { - final f2 = _event.tags.groupFoldBy>( - (e) => e.first, (acc, e) => [...?acc, e[1]]); - return f2; - } - - BaseEvent( - {required this.id, - required String pubkey, - required DateTime createdAt, - required int kind, - List> tags = const [], - required String content, - required String sig}) - : _event = NostrEvent( - id: id, - pubkey: pubkey, - createdAt: createdAt, - kind: kind, - tags: tags, - content: content, - sig: sig, - ots: null, - ); -} diff --git a/lib/models/metadata.dart b/lib/models/metadata.dart deleted file mode 100644 index 6e3bab1..0000000 --- a/lib/models/metadata.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter_data/flutter_data.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:zapstore/adapters/nostr_adapter.dart'; -import 'package:zapstore/models/base_event.dart'; - -part 'metadata.g.dart'; - -@JsonSerializable() -@DataRepository([NostrAdapter]) -class FileMetadata extends BaseEvent { - String get url => tagMap['url']!.first; - String get mimeType => tagMap['m']!.first; - String get sha256 => tagMap['x']!.first; - - FileMetadata( - {required super.id, - required super.pubkey, - required super.createdAt, - required super.content, - required super.sig, - super.tags}) - : super(kind: 1063); -} - -class FileMetadataFilter extends NostrFilter { - final List? m; - FileMetadataFilter( - {super.ids, - super.authors, - super.e, - super.p, - this.m, - super.since, - super.until, - super.limit, - super.search}) - : super(kinds: [1063]); - - @override - Map toMap() { - return {'#m': m, ...super.toMap()}; - } -} diff --git a/lib/models/metadata.g.dart b/lib/models/metadata.g.dart deleted file mode 100644 index 4f1b295..0000000 --- a/lib/models/metadata.g.dart +++ /dev/null @@ -1,82 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'metadata.dart'; - -// ************************************************************************** -// RepositoryGenerator -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, duplicate_ignore - -mixin $FileMetadataLocalAdapter on LocalAdapter { - static final Map _kFileMetadataRelationshipMetas = - {}; - - @override - Map get relationshipMetas => - _kFileMetadataRelationshipMetas; - - @override - FileMetadata deserialize(map) { - map = transformDeserialize(map); - return _$FileMetadataFromJson(map); - } - - @override - Map serialize(model, {bool withRelationships = true}) { - final map = _$FileMetadataToJson(model); - return transformSerialize(map, withRelationships: withRelationships); - } -} - -final _fileMetadataFinders = {}; - -// ignore: must_be_immutable -class $FileMetadataHiveLocalAdapter = HiveLocalAdapter - with $FileMetadataLocalAdapter; - -class $FileMetadataRemoteAdapter = RemoteAdapter - with NostrAdapter; - -final internalFileMetadataRemoteAdapterProvider = - Provider>((ref) => $FileMetadataRemoteAdapter( - $FileMetadataHiveLocalAdapter(ref), - InternalHolder(_fileMetadataFinders))); - -final fileMetadataRepositoryProvider = - Provider>((ref) => Repository(ref)); - -extension FileMetadataDataRepositoryX on Repository { - NostrAdapter get nostrAdapter => - remoteAdapter as NostrAdapter; -} - -extension FileMetadataRelationshipGraphNodeX - on RelationshipGraphNode {} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -FileMetadata _$FileMetadataFromJson(Map json) => FileMetadata( - id: json['id'] as String, - pubkey: json['pubkey'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - content: json['content'] as String, - sig: json['sig'] as String, - tags: (json['tags'] as List?) - ?.map( - (e) => (e as List).map((e) => e as String).toList()) - .toList() ?? - const [], - ); - -Map _$FileMetadataToJson(FileMetadata instance) => - { - 'id': instance.id, - 'pubkey': instance.pubkey, - 'content': instance.content, - 'sig': instance.sig, - 'created_at': instance.createdAt.toIso8601String(), - 'tags': instance.tags, - }; diff --git a/lib/models/note.dart b/lib/models/note.dart deleted file mode 100644 index e00b98b..0000000 --- a/lib/models/note.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_data/flutter_data.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:zapstore/adapters/nostr_adapter.dart'; -import 'package:zapstore/models/base_event.dart'; - -part 'note.g.dart'; - -@JsonSerializable() -@DataRepository([NostrAdapter]) -class Note extends BaseEvent { - Note( - {required super.id, - required super.pubkey, - required super.createdAt, - required super.content, - required super.sig, - super.tags}) - : super(kind: 1); -} diff --git a/lib/models/note.g.dart b/lib/models/note.g.dart deleted file mode 100644 index d29983d..0000000 --- a/lib/models/note.g.dart +++ /dev/null @@ -1,75 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'note.dart'; - -// ************************************************************************** -// RepositoryGenerator -// ************************************************************************** - -// ignore_for_file: non_constant_identifier_names, duplicate_ignore - -mixin $NoteLocalAdapter on LocalAdapter { - static final Map _kNoteRelationshipMetas = {}; - - @override - Map get relationshipMetas => - _kNoteRelationshipMetas; - - @override - Note deserialize(map) { - map = transformDeserialize(map); - return _$NoteFromJson(map); - } - - @override - Map serialize(model, {bool withRelationships = true}) { - final map = _$NoteToJson(model); - return transformSerialize(map, withRelationships: withRelationships); - } -} - -final _notesFinders = {}; - -// ignore: must_be_immutable -class $NoteHiveLocalAdapter = HiveLocalAdapter with $NoteLocalAdapter; - -class $NoteRemoteAdapter = RemoteAdapter with NostrAdapter; - -final internalNotesRemoteAdapterProvider = Provider>( - (ref) => $NoteRemoteAdapter( - $NoteHiveLocalAdapter(ref), InternalHolder(_notesFinders))); - -final notesRepositoryProvider = - Provider>((ref) => Repository(ref)); - -extension NoteDataRepositoryX on Repository { - NostrAdapter get nostrAdapter => remoteAdapter as NostrAdapter; -} - -extension NoteRelationshipGraphNodeX on RelationshipGraphNode {} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Note _$NoteFromJson(Map json) => Note( - id: json['id'] as String, - pubkey: json['pubkey'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - content: json['content'] as String, - sig: json['sig'] as String, - tags: (json['tags'] as List?) - ?.map( - (e) => (e as List).map((e) => e as String).toList()) - .toList() ?? - const [], - ); - -Map _$NoteToJson(Note instance) => { - 'id': instance.id, - 'pubkey': instance.pubkey, - 'content': instance.content, - 'sig': instance.sig, - 'created_at': instance.createdAt.toIso8601String(), - 'tags': instance.tags, - }; diff --git a/lib/models/user.dart b/lib/models/user.dart index f81cd31..e093dcf 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,13 +1,44 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @JsonSerializable() -@DataRepository([]) -class User extends DataModel { +@DataRepository([UserAdapter]) +class User extends DataModel with EquatableMixin { final String id; - final String name; + String? name; + String? pictureUrl; + String? nip05; + @DataRelationship(inverse: 'followers') + final HasMany following; + @DataRelationship(inverse: 'following') + final HasMany followers; - User({required this.id, required this.name}); + User({required this.id, HasMany? following, HasMany? followers}) + : this.following = following ?? HasMany(), + this.followers = followers ?? HasMany(); + + @override + List get props => [id]; +} + +mixin UserAdapter on RemoteAdapter { + @override + DataStateNotifier> watchAllNotifier( + {bool? remote, + Map? params, + Map? headers, + bool? syncLocal, + String? finder, + DataRequestLabel? label}) { + return super.watchAllNotifier( + remote: remote, + params: params, + headers: headers, + syncLocal: syncLocal, + finder: finder, + label: label); + } } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index beecfe9..ec4f338 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -9,7 +9,22 @@ part of 'user.dart'; // ignore_for_file: non_constant_identifier_names, duplicate_ignore mixin $UserLocalAdapter on LocalAdapter { - static final Map _kUserRelationshipMetas = {}; + static final Map _kUserRelationshipMetas = { + 'following': RelationshipMeta( + name: 'following', + inverseName: 'followers', + type: 'users', + kind: 'HasMany', + instance: (_) => (_ as User).following, + ), + 'followers': RelationshipMeta( + name: 'followers', + inverseName: 'following', + type: 'users', + kind: 'HasMany', + instance: (_) => (_ as User).followers, + ) + }; @override Map get relationshipMetas => @@ -33,7 +48,7 @@ final _usersFinders = {}; // ignore: must_be_immutable class $UserHiveLocalAdapter = HiveLocalAdapter with $UserLocalAdapter; -class $UserRemoteAdapter = RemoteAdapter with NothingMixin; +class $UserRemoteAdapter = RemoteAdapter with UserAdapter; final internalUsersRemoteAdapterProvider = Provider>( (ref) => $UserRemoteAdapter( @@ -42,9 +57,25 @@ final internalUsersRemoteAdapterProvider = Provider>( final usersRepositoryProvider = Provider>((ref) => Repository(ref)); -extension UserDataRepositoryX on Repository {} +extension UserDataRepositoryX on Repository { + UserAdapter get userAdapter => remoteAdapter as UserAdapter; +} -extension UserRelationshipGraphNodeX on RelationshipGraphNode {} +extension UserRelationshipGraphNodeX on RelationshipGraphNode { + RelationshipGraphNode get following { + final meta = $UserLocalAdapter._kUserRelationshipMetas['following'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } + + RelationshipGraphNode get followers { + final meta = $UserLocalAdapter._kUserRelationshipMetas['followers'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } +} // ************************************************************************** // JsonSerializableGenerator @@ -52,10 +83,22 @@ extension UserRelationshipGraphNodeX on RelationshipGraphNode {} User _$UserFromJson(Map json) => User( id: json['id'] as String, - name: json['name'] as String, - ); + following: json['following'] == null + ? null + : HasMany.fromJson(json['following'] as Map), + followers: json['followers'] == null + ? null + : HasMany.fromJson(json['followers'] as Map), + ) + ..name = json['name'] as String? + ..pictureUrl = json['pictureUrl'] as String? + ..nip05 = json['nip05'] as String?; Map _$UserToJson(User instance) => { 'id': instance.id, 'name': instance.name, + 'pictureUrl': instance.pictureUrl, + 'nip05': instance.nip05, + 'following': instance.following, + 'followers': instance.followers, }; diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 109d536..4d0098e 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -1,34 +1,167 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:bech32/bech32.dart'; import 'package:flutter/material.dart'; import 'package:flutter_data/flutter_data.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ndk/ndk.dart'; import 'package:zapstore/main.data.dart'; +import 'package:zapstore/models/user.dart'; +import 'package:convert/convert.dart'; + +final profileProvider = + FutureProvider.family((ref, pubkey) async { + final completer = Completer(); + final notifier = ref.read(frameProvider.notifier); + notifier.send(jsonEncode([ + "REQ", + pubkey, + { + 'authors': [pubkey], + 'kinds': [0, 3], + } + ])); + + late Function _sub; + + User? u; + List? contacts; + _sub = ref.watch(frameProvider.notifier).addListener((frame) async { + // print('listener: ${frame.event}'); + final event = frame.event; + + if (event is Metadata) { + u = (await ref.users.findOne(event.pubkey)) ?? User(id: event.pubkey); + final map = jsonDecode(event.content); + u!.name = map['displayName'] ?? map['display_name'] ?? map['name']; + u!.nip05 = map['nip05']; + } + + if (event is ContactList) { + contacts = [...?contacts, ...event.tagMap['p']!]; + } + + if (u != null && contacts != null) { + if (completer.isCompleted == false) { + print('completing with $u'); + contacts!.forEach((c) { + final uc = User(id: c); + u!.following.add(uc); + uc.saveLocal(); + }); + u!.saveLocal(); + completer.complete(u); + } + } + }); + ref.onDispose(() { + print('disposing'); + _sub(); + }); + return completer.future; +}); + +final loggedInUser = StateProvider((ref) => null); +final downloadsPath = StateProvider((ref) => '/tmp'); class ProfileScreen extends HookConsumerWidget { - const ProfileScreen({super.key}); + ProfileScreen({super.key}); + final codec = Bech32Codec(); @override Widget build(BuildContext context, WidgetRef ref) { - final watcher = ref.notes.watchAll(remote: false); + final u2 = ref.watch(loggedInUser); + final controller = useTextEditingController(); + final controller2 = useTextEditingController(text: ref.read(downloadsPath)); + return Expanded( - child: Column( - children: [ - TextButton( - onPressed: () => {}, - child: Text('press me'), - ), - switch (watcher) { - DataState(isLoading: false, exception: null, model: final value) => - Text(value.length.toString()), - _ => Text('loading') - } - ], + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + TextField(controller: controller), + SizedBox(height: 10), + ElevatedButton( + onPressed: () async { + final pubkey = bech32Decode(controller.text); + final uid = await ref.read(profileProvider(pubkey).future); + ref.read(loggedInUser.notifier).state = + await ref.users.findOne(uid, remote: false); + }, + child: Text('Log in with npub')), + if (u2 != null) + Expanded( + child: Column( + children: [ + Text('Logged in as ${u2.name}'), + Text('(following: ${u2.following.length})'), + ], + ), + ), + SizedBox(height: 20), + // ListView.builder( + // shrinkWrap: true, + // itemCount: contacts.length, + // itemBuilder: (context, i) { + // return Text(' - ${contacts[i].id}'); + // }), + TextField(controller: controller2), + SizedBox(height: 10), + ElevatedButton( + onPressed: () async { + ref.read(downloadsPath.notifier).state = controller2.text; + }, + child: Text('Save downloads path'), + ), + ], + ), ), ); - // return Column( - // children: [ - // Text( - // 'profile, ${Nip19.decodePubkey('npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9')}'), - // Text(value.isLoading ? 'loading' : value.value!.join('\n')), - // ], - // ); } } + +String bech32Encode(String prefix, String hexData) { + final data = hex.decode(hexData); + final convertedData = convertBits(data, 8, 5, true); + final bech32Data = Bech32(prefix, convertedData); + return bech32.encode(bech32Data); +} + +String bech32Decode(String bech32Data) { + final decodedData = bech32.decode(bech32Data); + final convertedData = convertBits(decodedData.data, 5, 8, false); + final hexData = hex.encode(convertedData); + + return hexData; +} + +List convertBits(List data, int fromBits, int toBits, bool pad) { + var acc = 0; + var bits = 0; + final maxv = (1 << toBits) - 1; + final result = []; + + for (final value in data) { + if (value < 0 || value >> fromBits != 0) { + throw Exception('Invalid value: $value'); + } + acc = (acc << fromBits) | value; + bits += fromBits; + + while (bits >= toBits) { + bits -= toBits; + result.add((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + result.add((acc << (toBits - bits)) & maxv); + } + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { + throw Exception('Invalid data'); + } + + return result; +} diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index cf21cd7..f4b57a4 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,74 +1,207 @@ -import 'package:dart_nostr/dart_nostr.dart'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:crypto/crypto.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:expansion_tile_card/expansion_tile_card.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ndk/ndk.dart'; import 'package:zapstore/main.data.dart'; -import 'package:zapstore/models/note.dart'; +import 'package:zapstore/models/user.dart'; +import 'package:zapstore/screens/profile_screen.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; class SearchScreen extends HookConsumerWidget { - const SearchScreen({super.key}); + SearchScreen({super.key}); + final r = Random(); @override Widget build(BuildContext context, WidgetRef ref) { final searchState = useState(null); - final watcher = ref.notes - .watchAll(remote: false, params: {'search': searchState.value}); + final subscriptionIdState = useState(null); + final value = + ref.watch(subscriptionFrameProvider(subscriptionIdState.value)); + return Expanded( - child: switch (watcher) { - DataState(isLoading: false, exception: null, model: final value) => - Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - children: [ - SearchBar( - elevation: MaterialStatePropertyAll(2.2), - onSubmitted: (query) { - searchState.value = query; - if (query.isNotEmpty) { - ref.notes.nostrAdapter.update( - [ - NostrFilter( - search: query, - since: DateTime.now().subtract(Duration(days: 1)), - limit: 10, - ) - ], - ); + child: DropTarget( + onDragDone: (detail) async { + if (detail.files.isNotEmpty) { + final bytes = await detail.files.first.readAsBytes(); + final digest = sha256.convert(bytes).toString(); + + subscriptionIdState.value = 'sub-${digest}'; + final notifier = ref.read(frameProvider.notifier); + notifier.send(jsonEncode([ + "REQ", + subscriptionIdState.value!, + { + 'kinds': [1063], + '#x': [digest], + } + ])); + } + // print(detail.files.map((e) => e.path)); + }, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + SearchBar( + elevation: MaterialStatePropertyAll(2.2), + onSubmitted: (query) { + searchState.value = query; + subscriptionIdState.value = 'sub-${r.nextInt(9999999)}'; + final notifier = ref.read(frameProvider.notifier); + notifier.send(jsonEncode([ + "REQ", + subscriptionIdState.value!, + { + 'kinds': [1063], + 'limit': 20, + 'search': query, + // '#m': ['video/mp4'], + // 'since': DateTime.now() + // .subtract(Duration(days: 1)) + // .millisecondsSinceEpoch ~/ + // 1000, } + ])); + }, + ), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text('Total results: ${value.length}'), + ), + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: value.length, + itemBuilder: (context, index) { + final event = value[index]; + return CardWidget(note: event as FileMetadata); }, ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - 'Total results: ${value.length}, search term: ${searchState.value}'), - ), - Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: value.length, - itemBuilder: (context, index) { - final note = value[index]; - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Text(note.pubkey), - Text(note.content), - // Image.network(note.), - ], - ), - ); - }, + ), + ], + ), + ), + ), + ); + } +} + +class CardWidget extends HookConsumerWidget { + final FileMetadata note; + + const CardWidget({super.key, required this.note}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authorUser = useState(null); + final currentUser = ref.read(loggedInUser); + + final isWebOfTrust = + currentUser?.following.contains(authorUser.value) ?? false; + final isSha256Ok = useState(null); + + return ExpansionTileCard( + // key: cardA, + leading: ClipRRect( + borderRadius: BorderRadius.circular(15.0), + child: Image.network( + note.tagMap['url']!.first, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, err, _) => Text(err.toString()), + ), + ), + title: Text( + note.content, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Type: ${note.tagMap['m']?.first}'), + ], + ), + onExpansionChanged: (isOpen) async { + if (isOpen && currentUser != null) { + final existingUser = + await ref.users.findOne(note.pubkey, remote: false); + if (existingUser?.name != null) { + print('fd has ${note.pubkey}! (${existingUser!.name}) '); + authorUser.value = existingUser; + } else { + authorUser.value = + await ref.read(profileProvider(note.pubkey).future); + } + } + }, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (authorUser.value != null) + Text( + 'Author: ${authorUser.value!.name} (${authorUser.value!.nip05})'), + if (isWebOfTrust) + Text( + 'Author is in your web of trust', + style: TextStyle(color: Colors.green), ), + if (!isWebOfTrust) + Text('Author is not in your web of trust', + style: TextStyle(color: Colors.red)), + Text( + 'SHA-256: ${note.tagMap['x']?.first}', + style: TextStyle( + color: (isSha256Ok.value != null) + ? (isSha256Ok.value! ? Colors.green : Colors.red) + : Colors.white), + ), + TextButton( + child: const Column( + children: [ + Text('Install'), + Icon(Icons.arrow_downward), + ], + ), + onPressed: () async { + final uri = Uri.parse(note.tagMap['url']!.first); + final response = await http.get(uri); + final dir = ref.read(downloadsPath); + File file = File(path.join(dir, uri.pathSegments.last)); + await file.writeAsBytes(response.bodyBytes); + final digest = sha256.convert(response.bodyBytes); + isSha256Ok.value = + digest.toString() == note.tagMap['x']!.first; + if (isSha256Ok.value == false) { + print('actual digest $digest'); + } else { + print('digest ok $digest'); + } + }, ), ], ), ), - DataState(isLoading: true, exception: null) => - CircularProgressIndicator(), - DataState(:final exception) => Text(exception.toString()), - }, + ), + ], ); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 5f2f8ff..66ce9d5 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) handy_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); handy_window_plugin_register_with_registrar(handy_window_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e9b7d46..5b15847 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux + desktop_drop handy_window screen_retriever window_manager diff --git a/pubspec.lock b/pubspec.lock index c493ae6..8b198ca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,46 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - base58check: - dependency: transitive - description: - name: base58check - sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" - url: "https://pub.dev" - source: hosted - version: "2.0.0" bech32: - dependency: transitive + dependency: "direct main" description: name: bech32 sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" url: "https://pub.dev" source: hosted version: "0.2.2" - bip32_bip44: - dependency: transitive - description: - name: bip32_bip44 - sha256: "8e28e6bde00da1ed207f2c0a5361792375799196176742c0d36c71e89a5485d3" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - bip340: - dependency: transitive - description: - name: bip340 - sha256: "9a2f454d3f5e2d67a1b21494203132786d95bde8784e7ada74adac00df3ddc11" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - bip39: - dependency: transitive - description: - name: bip39 - sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc - url: "https://pub.dev" - source: hosted - version: "1.0.6" bitsdojo_window: dependency: "direct main" description: @@ -233,8 +201,16 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - crypto: + cross_file: dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.dev" + source: hosted + version: "0.3.3+8" + crypto: + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -249,14 +225,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - dart_nostr: - dependency: "direct main" - description: - name: dart_nostr - sha256: "9d75157b4fe9ade7a186765efe3200a40f8b6673f88d617e96de2bc69671cb8c" - url: "https://pub.dev" - source: hosted - version: "8.0.0" dart_style: dependency: transitive description: @@ -265,14 +233,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + url: "https://pub.dev" + source: hosted + version: "0.4.4" equatable: - dependency: transitive + dependency: "direct main" description: name: equatable sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 url: "https://pub.dev" source: hosted version: "2.0.5" + expansion_tile_card: + dependency: "direct main" + description: + name: expansion_tile_card + sha256: "27ce4cb518f00e21d0f2309aaa6462b26b148e93cee2029a73088cecf42b1eb0" + url: "https://pub.dev" + source: hosted + version: "3.0.0" fake_async: dependency: transitive description: @@ -338,15 +322,20 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 + sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -387,14 +376,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" - hex: - dependency: transitive - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" hive: dependency: transitive description: @@ -407,10 +388,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae + sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" http: dependency: transitive description: @@ -523,6 +504,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + ndk: + dependency: "direct main" + description: + path: "../ndk" + relative: true + source: path + version: "0.1.0" package_config: dependency: transitive description: @@ -603,14 +591,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -655,10 +635,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" + sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.5.0" screen_retriever: dependency: transitive description: @@ -824,14 +804,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" - web_socket_client: - dependency: "direct main" - description: - name: web_socket_client - sha256: de52c9b3acf1f9fafb2951aa2764dd67c7d99d70be70c7c1fd791fae8f632f98 - url: "https://pub.dev" - source: hosted - version: "0.1.0" win32: dependency: transitive description: @@ -865,5 +837,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <=3.9.9" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 31e4314..a5b75e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,8 +40,16 @@ dependencies: build_runner: ^2.4.8 collection: ^1.18.0 path_provider: ^2.1.2 - web_socket_client: ^0.1.0 - dart_nostr: ^8.0.0 + ndk: ^0.0.1 + bech32: ^0.2.2 + expansion_tile_card: ^3.0.0 + desktop_drop: ^0.4.4 + equatable: ^2.0.5 + crypto: ^3.0.3 + +dependency_overrides: + ndk: + path: ../ndk dev_dependencies: flutter_test: