diff --git a/analysis_options.yaml b/analysis_options.yaml index 4c4650c..0cf006c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -32,6 +32,7 @@ linter: avoid_print: false prefer_const_literals_to_create_immutables: false no_wildcard_variable_uses: false + omit_local_variable_types: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/models/app.dart b/lib/models/app.dart index 50e69a2..47b0ab1 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -13,7 +13,7 @@ import 'package:install_plugin/install_plugin.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/file_metadata.dart'; import 'package:zapstore/models/local_app.dart'; @@ -29,7 +29,10 @@ part 'app.g.dart'; final mutex = Mutex(); @DataAdapter([NostrAdapter, AppAdapter]) -class App extends BaseApp with DataModelMixin { +class App extends base.App with DataModelMixin { + @override + Object? get id => event.id; + final HasMany releases; final BelongsTo signer; final BelongsTo developer; @@ -195,14 +198,14 @@ class App extends BaseApp with DataModelMixin { ); final i = await packageManager.getPackageInfo( - packageName: identifier!, flags: flags); + packageName: identifier, flags: flags); if (i == null) { return null; } final bytes = i.signingInfo!.signingCertificateHistory!.first; final installedApkSigHash = sha256.convert(bytes).toString().toLowerCase(); final metadataSigHashes = - latestMetadata?.tagMap['apk_signature_hash'] ?? {}; + latestMetadata?.event.getTagSet('apk_signature_hash') ?? {}; return metadataSigHashes .any((msh) => msh.toLowerCase() == installedApkSigHash); } @@ -287,14 +290,14 @@ mixin AppAdapter on Adapter { // Find most recent `created_at` from returned apps // and assign it to their queried at value for caching final m = apps.isNotEmpty - ? apps.fold(apps.first.createdAt!, (acc, e) { - return acc.isAfter(e.createdAt!) ? acc : e.createdAt!; + ? apps.fold(apps.first.event.createdAt, (acc, e) { + return acc.isAfter(e.event.createdAt) ? acc : e.event.createdAt; }) : null; if (m != null) { for (final app in apps) { - queriedAtMap[app.identifier!] = m; + queriedAtMap[app.identifier] = m; } } } else { @@ -304,8 +307,9 @@ mixin AppAdapter on Adapter { // Find all appid@version ($3) as we need to pick one tag to query on // (filters by kind ($1) and pubkey ($2) done locally) - final latestReleaseIdentifiers = - apps.map((app) => app.linkedReplaceableEvents.firstOrNull?.$3).nonNulls; + final latestReleaseIdentifiers = apps + .map((app) => app.event.linkedReplaceableEvents.firstOrNull?.$3) + .nonNulls; if (latestReleaseIdentifiers.isNotEmpty) { releases = await ref.releases.findAll( params: {'#d': latestReleaseIdentifiers}, @@ -369,10 +373,11 @@ mixin AppAdapter on Adapter { final list = data is Iterable ? data : [data as Map]; for (final Map map in list) { - final tagMap = tagsToMap(map['tags']); - map['developer'] = tagMap['zap']?.firstOrNull ?? - (map['pubkey'] != kZapstorePubkey ? map['pubkey'] : null); - map['localApp'] = tagMap['d']!.first; + final tags = map['tags']; + final pubkey = base.BaseUtil.getTag(tags, 'pubkey'); + map['developer'] = base.BaseUtil.getTag(tags, 'zap') ?? + (pubkey != kZapstorePubkey ? pubkey : null); + map['localApp'] = base.BaseUtil.getTag(tags, 'd')!; } return super.deserialize(data); @@ -408,7 +413,7 @@ Future _isHashMismatch(String path, String hash) async { extension AppExt on Iterable { List get sortedByLatest => - sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)); + sorted((a, b) => b.event.createdAt.compareTo(a.event.createdAt)); } // class diff --git a/lib/models/app_curation_set.dart b/lib/models/app_curation_set.dart index 11da409..ca96ac9 100644 --- a/lib/models/app_curation_set.dart +++ b/lib/models/app_curation_set.dart @@ -1,5 +1,5 @@ import 'package:flutter_data/flutter_data.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/nostr_adapter.dart'; @@ -9,8 +9,11 @@ import 'package:zapstore/utils/extensions.dart'; part 'app_curation_set.g.dart'; @DataAdapter([NostrAdapter, AppCurationSetAdapter]) -class AppCurationSet extends BaseAppCurationSet +class AppCurationSet extends base.AppCurationSet with DataModelMixin { + @override + Object get id => event.id; + final HasMany apps; final BelongsTo signer; @@ -21,7 +24,7 @@ class AppCurationSet extends BaseAppCurationSet Map toJson() => super.toMap(); - String get name => content.isNotEmpty ? content : identifier!; + String get name => event.content.isNotEmpty ? event.content : identifier; } mixin AppCurationSetAdapter on Adapter { @@ -45,8 +48,8 @@ mixin AppCurationSetAdapter on Adapter { DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; for (final Map map in list) { - final tagMap = tagsToMap(map['tags']); - map['apps'] = tagMap['a']; + final tags = map['tags']; + map['apps'] = base.BaseUtil.getTagSet(tags, 'a'); } return super.deserialize(data); } diff --git a/lib/models/feedback.dart b/lib/models/feedback.dart index eab3b66..012518f 100644 --- a/lib/models/feedback.dart +++ b/lib/models/feedback.dart @@ -1,10 +1,13 @@ import 'package:purplebase/purplebase.dart'; -class AppFeedback extends BaseEvent { - @override - int get kind => 1011; - - AppFeedback({ - required String content, - }) : super(content: content); +class AppFeedback extends RegularEvent { + AppFeedback.fromJson(super.map) : super.fromJson(); +} + +class PartialAppFeedback extends RegularPartialEvent { + PartialAppFeedback({ + required String content, + }) { + event.content = content; + } } diff --git a/lib/models/file_metadata.dart b/lib/models/file_metadata.dart index 7bcd66d..2879562 100644 --- a/lib/models/file_metadata.dart +++ b/lib/models/file_metadata.dart @@ -1,5 +1,5 @@ import 'package:flutter_data/flutter_data.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/user.dart'; @@ -8,18 +8,21 @@ import 'package:zapstore/utils/extensions.dart'; part 'file_metadata.g.dart'; @DataAdapter([NostrAdapter, FileMetadataAdapter]) -class FileMetadata extends BaseFileMetadata with DataModelMixin { +class FileMetadata extends base.FileMetadata with DataModelMixin { final BelongsTo author; final BelongsTo release; final BelongsTo signer; - FileMetadata( - {super.createdAt, - super.content, - super.tags, - required this.author, - required this.release, - required this.signer}); + @override + Object? get id => event.id; + + // FileMetadata( + // {super.createdAt, + // super.content, + // super.tags, + // required this.author, + // required this.release, + // required this.signer}); FileMetadata.fromJson(super.map) : author = belongsTo(map['author']), @@ -30,9 +33,8 @@ class FileMetadata extends BaseFileMetadata with DataModelMixin { Map toJson() => super.toMap(); // String? get version => tagMap['version']?.firstOrNull; - int? get versionCode => - int.tryParse(tagMap['version_code']?.firstOrNull ?? ''); - String? get apkSignatureHash => tagMap['apk_signature_hash']?.firstOrNull; + int? get versionCode => int.tryParse(event.getTag('version') ?? ''); + String? get apkSignatureHash => event.getTag('apk_signature_hash'); } mixin FileMetadataAdapter on Adapter { diff --git a/lib/models/local_app.dart b/lib/models/local_app.dart index 31f03df..7058dce 100644 --- a/lib/models/local_app.dart +++ b/lib/models/local_app.dart @@ -144,7 +144,7 @@ mixin LocalAppAdapter on Adapter { final installedApkSigHash = sha256.convert(signingCertificateBytes).toString().toLowerCase(); final metadataSigHashes = - app.latestMetadata?.tagMap['apk_signature_hash'] ?? {}; + app.latestMetadata?.event.getTagSet('apk_signature_hash') ?? {}; final matches = metadataSigHashes .any((msh) => msh.toLowerCase() == installedApkSigHash); if (!matches) { diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart index db25fe8..4f8cb9e 100644 --- a/lib/models/nostr_adapter.dart +++ b/lib/models/nostr_adapter.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' hide Release; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/release.dart'; @@ -13,7 +13,7 @@ import 'package:zapstore/widgets/latest_releases_container.dart'; // NOTE: Very important to use const in relay args to preserve equality in Riverpod families // const kAppRelays = {'ws://10.0.2.2:3000'}; const kAppRelays = {'wss://relay.zapstore.dev'}; -const kSocialRelays = {'wss://relay.damus.io', 'wss://relay.primal.net', 'ndk'}; +const kSocialRelays = {'wss://relay.damus.io', 'wss://relay.primal.net'}; mixin NostrAdapter> on Adapter { RelayMessageNotifier get relay => @@ -52,7 +52,10 @@ mixin NostrAdapter> on Adapter { } int get kind { - return BaseEvent.kindForType(internalType)!; + var pbType = internalType.singularize().capitalize(); + pbType = + pbType.replaceFirst('datum', 'data'); // handle FileMetadata edge case + return Event.types[pbType]!.kind; } @override @@ -66,13 +69,13 @@ mixin NostrAdapter> on Adapter { map['signer'] = map['pubkey']; // ID should be the replaceable link/reference so as to make it replaceable in local db too - final tagMap = tagsToMap(map['tags']); - final isReplaceable = tagMap.containsKey('d'); + final tags = map['tags']; + final isReplaceable = BaseUtil.containsTag(tags, 'd'); if (isReplaceable) { map['id'] = ( map['kind'] as int, map['pubkey'].toString(), - tagMap['d']?.firstOrNull + BaseUtil.getTag(tags, 'd')!, ).formatted; } @@ -80,7 +83,11 @@ mixin NostrAdapter> on Adapter { map.remove('sig'); // Collect models for current kind and included for others - final eventType = BaseEvent.typeForKind(kind)!; + final eventType = Event.types.entries + .firstWhere((e) => e.value.kind == kind) + .key + .pluralize() + .decapitalize(); if (eventType == internalType) { final newData = super.deserialize(map); models.addAll(newData.models as Iterable); @@ -144,7 +151,7 @@ mixin NostrAdapter> on Adapter { Future loadArtifactsAndUsers(Iterable releases) async { final metadataIds = - releases.map((r) => r.linkedEvents).nonNulls.expand((_) => _); + releases.map((r) => r.event.linkedEvents).nonNulls.expand((_) => _); final apps = releases.map((r) => r.app.value).nonNulls; final userIds = { diff --git a/lib/models/release.dart b/lib/models/release.dart index 80d0ced..8573044 100644 --- a/lib/models/release.dart +++ b/lib/models/release.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/file_metadata.dart'; import 'package:zapstore/models/nostr_adapter.dart'; @@ -10,18 +10,21 @@ import 'package:zapstore/utils/extensions.dart'; part 'release.g.dart'; @DataAdapter([NostrAdapter, ReleaseAdapter]) -class Release extends BaseRelease with DataModelMixin { +class Release extends base.Release with DataModelMixin { + @override + Object? get id => event.id; + final HasMany artifacts; final BelongsTo app; final BelongsTo signer; - Release( - {super.createdAt, - super.content, - super.tags, - required this.artifacts, - required this.app, - required this.signer}); + // Release( + // {super.createdAt, + // super.content, + // super.tags, + // required this.artifacts, + // required this.app, + // required this.signer}); Release.fromJson(super.map) : app = belongsTo(map['app']), @@ -38,9 +41,9 @@ mixin ReleaseAdapter on Adapter { final list = data is Iterable ? data : [data as Map]; for (final Map map in list) { - final tagMap = tagsToMap(map['tags']); - map['app'] = tagMap['a']?.firstOrNull; - map['artifacts'] = tagMap['e']!.toList(); + final tags = map['tags']; + map['app'] = base.BaseUtil.getTag(tags, 'a'); + map['artifacts'] = base.BaseUtil.getTagSet(tags, 'e').toList(); } return super.deserialize(data); } @@ -48,5 +51,5 @@ mixin ReleaseAdapter on Adapter { extension ReleaseExt on Iterable { List get sortedByLatest => - sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)); + sorted((a, b) => b.event.createdAt.compareTo(a.event.createdAt)); } diff --git a/lib/models/user.dart b/lib/models/user.dart index 223d44f..541709e 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -2,25 +2,100 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; +import 'package:http/http.dart' as http; +import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/utils/extensions.dart'; +import 'package:zapstore/utils/nwc.dart'; part 'user.g.dart'; @DataAdapter([NostrAdapter, UserAdapter]) -class User extends BaseUser with DataModelMixin { - User( - {super.name, - super.createdAt, - super.tags, - HasMany? followers, - HasMany? following, - BelongsTo? settings}) - : followers = followers ?? HasMany(), - following = following ?? HasMany(), - settings = settings ?? BelongsTo(); +class User extends base.User with DataModelMixin { + @override + Object? get id => event.id; + + Future zap(int amount, + {required base.Event event, String? comment}) async { + final adapter = DataModel.adapterFor(this) as NostrAdapter; + + final authorPubkey = event.event.pubkey; + final author = adapter.ref.users.findOneLocalById(authorPubkey); + + if (author?.lud16 == null) { + throw 'cant zap'; + } + + final partialZapRequest = base.PartialZapRequest(); + partialZapRequest + ..addLinkedEvent(event) + ..addLinkedUser(this) + ..amount = amount; + final zapRequest = await partialZapRequest + .signWith(base.Bip340PrivateKeySigner(kZapstorePubkey)); + print(zapRequest); + + // Now we fetch the invoice + final lnurlResponse = await author!.fetchLnUrl(); + + final recipientAllowsNostr = lnurlResponse['allowsNostr']; + final recipientPubkey = lnurlResponse['nostrPubkey']; + // All amounts are in millisats + final recipientMin = lnurlResponse['minSendable']; + final recipientMax = lnurlResponse['maxSendable']; + final amountInMillisats = amount * 1000; + + if (recipientAllowsNostr != true && recipientPubkey != null) { + throw 'cant zap, recipient does not allow nostr'; + } + if ((recipientMin != null && amountInMillisats < recipientMin) || + (recipientMax != null && amountInMillisats > recipientMax)) { + throw 'cant zap, amount not between min and max sendable'; + } + + final commentLength = lnurlResponse['commentAllowed']; + comment = + commentLength != null ? comment?.substringMax(commentLength) : comment; + + var callbackUri = Uri.parse(lnurlResponse['callback']!); + callbackUri = callbackUri.replace( + queryParameters: { + ...callbackUri.queryParameters, + 'amount': amountInMillisats.toString(), + if (comment != null) 'comment': comment, + 'nostr': jsonEncode(zapRequest.toMap()), + }, + ); + + print(callbackUri); + + final response = await http.get(callbackUri); + final invoiceMap = jsonDecode(response.body); + + print(invoiceMap); + final invoice = invoiceMap['pr']; + if (invoice == null) { + throw 'cant zap, no invoice'; + } + + final nwcConnection = adapter.ref.read(nwcConnectionProvider).asData?.value; + if (nwcConnection == null) { + throw 'cant zap, no nwc connection'; + } + + // Pay the invoice via NWC + adapter.socialRelays.ndk!.nwc.payInvoice(nwcConnection, invoice: invoice); + } + + Future> fetchLnUrl() async { + final [userName, domainName] = lud16!.split('@'); + final lnurl = 'https://$domainName/.well-known/lnurlp/$userName'; + final response = await http.get(Uri.parse(lnurl)); + final map = jsonDecode(response.body); + return map; + } User.fromJson(super.map) : followers = hasMany(map['followers']), @@ -58,7 +133,7 @@ mixin UserAdapter on NostrAdapter { return []; } - final request = RelayRequest( + final request = base.RelayRequest( kinds: {0}, // 3 authors: {...authors}, since: queriedAtMap[authors.join()], @@ -119,7 +194,7 @@ mixin UserAdapter on NostrAdapter { ); } - final result = await socialRelays.queryRaw(RelayRequest( + final result = await socialRelays.queryRaw(base.RelayRequest( kinds: {0}, // 3 tags: params ?? {}, authors: {publicKey}, diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index f3f37ef..cfe846c 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -35,11 +35,11 @@ class AppDetailScreen extends HookConsumerWidget { // TODO: Bug using remote=true in watchOne, use hook for now final snapshot = useFuture(useMemoized(() async { final storedApps = - ref.apps.appAdapter.findWhereIdentifierInLocal({model.identifier!}); + ref.apps.appAdapter.findWhereIdentifierInLocal({model.identifier}); if (storedApps.firstOrNull?.latestMetadata != null) { return storedApps.first; } - return ref.apps.findOne(model.identifier!, remote: true); + return ref.apps.findOne(model.identifier, remote: true); })); final state = ref.apps.watchOne(model.id!, alsoWatch: (_) => { @@ -59,7 +59,7 @@ class AppDetailScreen extends HookConsumerWidget { .nonNulls; return RefreshIndicator( - onRefresh: () => ref.apps.findOne(model.identifier!, remote: true), + onRefresh: () => ref.apps.findOne(model.identifier, remote: true), child: Column( children: [ Expanded( @@ -149,7 +149,7 @@ class AppDetailScreen extends HookConsumerWidget { ), ), selectable: false, - data: app.content.parseEmojis(), + data: app.event.content.parseEmojis(), ), Gap(10), Padding( @@ -224,7 +224,7 @@ class AppDetailScreen extends HookConsumerWidget { Gap(10), Flexible( child: AutoSizeText( - app.identifier!, + app.identifier, minFontSize: 12, maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4cc537c..5de3f60 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -78,7 +78,7 @@ class SettingsScreen extends HookConsumerWidget { onPressed: () async { if (feedbackController.text.trim().isNotEmpty) { final signedDirectMessage = await amberSigner.sign( - BaseDirectMessage( + PartialDirectMessage( content: feedbackController.text.trim(), receiver: kZapstorePubkey.npub), asUser: user.pubkey); diff --git a/lib/utils/signers.dart b/lib/utils/signers.dart index c598463..dd1afb6 100644 --- a/lib/utils/signers.dart +++ b/lib/utils/signers.dart @@ -26,35 +26,33 @@ class AmberSigner extends Signer { } @override - Future sign>(T model, {String? asUser}) async { + Future sign>(PartialEvent partialEvent, + {String? asUser}) async { if (!isAvailable) { throw Exception("Cannot sign, missing Amber"); } - if (model is BaseDirectMessage) { + if (partialEvent is DirectMessage) { final signedMessage = await _signerPlugin.nip04Encrypt( - model.content, + partialEvent.event.content, "", - (asUser ?? model.pubkey).npub, - (model as BaseDirectMessage).receiver.hexKey); + asUser!.npub, + (partialEvent as DirectMessage).receiver.hexKey); final encryptedContent = signedMessage['result']; - model = - (model as BaseDirectMessage).copyWith(content: encryptedContent) as T; + partialEvent.event.content = encryptedContent; } - final pubkey = asUser ?? model.pubkey; // Remove all null fields (Amber otherwise crashes) final map = { - for (final e in model.toMap().entries) + for (final e in partialEvent.toMap().entries) if (e.value != null) e.key: e.value }; final signedMessage = - await _signerPlugin.signEvent(jsonEncode(map), "", pubkey); + await _signerPlugin.signEvent(jsonEncode(map), "", asUser!); final signedEvent = jsonDecode(signedMessage['event']); - final fn = BaseEvent.constructorForKind(model.kind)!; - return fn.call(signedEvent); + return Event.getConstructor()!.call(signedEvent); } } -final pkSigner = PrivateKeySigner( +final pkSigner = Bip340PrivateKeySigner( 'e593c54f840b32054dcad0fac15d57e4ac6523e31fe26b3087de6b07a2e9af58'); diff --git a/lib/utils/zap.dart b/lib/utils/zap.dart index d5ba327..0745f8c 100644 --- a/lib/utils/zap.dart +++ b/lib/utils/zap.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter_data/flutter_data.dart'; import 'package:ndk/domain_layer/usecases/zaps/zap_receipt.dart'; import 'package:ndk/domain_layer/usecases/zaps/zaps.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' hide FileMetadata, User; import 'package:zapstore/models/file_metadata.dart'; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/utils/nip55_event_signer.dart'; @@ -25,7 +25,8 @@ class ZapReceiptsNotifier extends StateNotifier>?> { state = AsyncValue.loading(); final receiptsResponse = socialRelays.ndk!.zaps.fetchZappedReceipts( - pubKey: fileMetadata.pubkey, eventId: fileMetadata.id!.toString()); + pubKey: fileMetadata.event.pubkey, + eventId: fileMetadata.id!.toString()); sub = receiptsResponse.listen((receipt) { addZapReceipt(receipt); }); diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index ea62eca..c4c6d1a 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -74,7 +74,7 @@ class AppCard extends HookConsumerWidget { ), Gap(6), Text( - app.content.removeMarkdown().parseEmojis(), + app.description.removeMarkdown().parseEmojis(), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w300), overflow: TextOverflow.ellipsis, @@ -93,7 +93,8 @@ class AppCard extends HookConsumerWidget { size: 12, ), if (showDate) - Text(app.latestRelease!.createdAt!.toIso8601String()), + Text( + app.latestRelease!.event.createdAt.toIso8601String()), if (isUpdate) Padding( padding: const EdgeInsets.only(top: 10, bottom: 10), diff --git a/lib/widgets/app_curation_container.dart b/lib/widgets/app_curation_container.dart index c863c9f..6bd4b11 100644 --- a/lib/widgets/app_curation_container.dart +++ b/lib/widgets/app_curation_container.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app_curation_set.dart'; import 'package:zapstore/models/user.dart'; @@ -20,7 +20,7 @@ class AppCurationContainer extends HookConsumerWidget { final selectedAppCurationSet = ref.watch(_selectedIdProvider); final appCurationSets = ref.appCurationSets .findAllLocal() - .sortedBy((s) => s.createdAt!) + .sortedBy((s) => s.event.createdAt) .reversed .toList(); @@ -101,9 +101,9 @@ class AppCurationContainer extends HookConsumerWidget { // Providers class AppCurationSetNotifier - extends FamilyAsyncNotifier { + extends FamilyAsyncNotifier { @override - Future build(ReplaceableEventLink arg) async { + Future build(base.ReplaceableEventLink arg) async { final appCurationSet = ref.appCurationSets.findOneLocalById(arg.formatted)!; fetch(); return appCurationSet; @@ -133,9 +133,9 @@ class AppCurationSetNotifier final appCurationSetProvider = AsyncNotifierProvider.family< AppCurationSetNotifier, AppCurationSet, - ReplaceableEventLink>(AppCurationSetNotifier.new); + base.ReplaceableEventLink>(AppCurationSetNotifier.new); final _selectedIdProvider = - StateProvider((_) => kNostrCurationSetLink); + StateProvider((_) => kNostrCurationSetLink); const kNostrCurationSetLink = (30267, kFranzapPubkey, 'nostr'); diff --git a/lib/widgets/error_container.dart b/lib/widgets/error_container.dart index 4bdb7a4..cd13cc3 100644 --- a/lib/widgets/error_container.dart +++ b/lib/widgets/error_container.dart @@ -149,7 +149,8 @@ Future _sendErrorReport( 'info': systemInfo.androidInfo.toString() }; - final event = await pkSigner.sign(AppFeedback(content: jsonEncode(map))); + final event = + await pkSigner.sign(PartialAppFeedback(content: jsonEncode(map))); await http.post(Uri.parse('https://relay.zapstore.dev/'), body: jsonEncode(event.toMap()), headers: {'Content-Type': 'application/json'}); diff --git a/lib/widgets/install_alert_dialog.dart b/lib/widgets/install_alert_dialog.dart index b53ca43..d585b1c 100644 --- a/lib/widgets/install_alert_dialog.dart +++ b/lib/widgets/install_alert_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/settings.dart'; diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 2f81bdf..db3f2a7 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -42,7 +42,7 @@ class InstallButton extends HookConsumerWidget { AppInstallStatus.certificateMismatch => null, AppInstallStatus.updated => () { - packageManager.openApp(app.identifier!); + packageManager.openApp(app.identifier); }, _ => switch (progress) { IdleInstallProgress() => () { diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index 6cffdeb..4252cf5 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -125,7 +125,7 @@ class LatestReleasesAppNotifier extends PreloadingStateNotifier> { // Set timestamp of oldest, to prepare for next query if (apps.isNotEmpty) { - _oldestCreatedAt = apps.last.createdAt; + _oldestCreatedAt = apps.last.event.createdAt; } return AsyncData(apps); } diff --git a/lib/widgets/release_card.dart b/lib/widgets/release_card.dart index eb2a7f0..0340fba 100644 --- a/lib/widgets/release_card.dart +++ b/lib/widgets/release_card.dart @@ -28,16 +28,16 @@ class ReleaseCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(release.version!, + Text(release.version, style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold)), Gap(10), - release.content.length < 3000 - ? MarkdownBody(data: release.content) + release.releaseNotes.length < 3000 + ? MarkdownBody(data: release.releaseNotes) : Container( constraints: BoxConstraints( maxHeight: 300, ), - child: Markdown(data: release.content), + child: Markdown(data: release.releaseNotes), ), Gap(30), if (metadata != null) @@ -54,7 +54,7 @@ class ReleaseCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Date'), - Text(formatter.format(release.createdAt!)), + Text(formatter.format(release.event.createdAt)), ], ), ), diff --git a/lib/widgets/sign_in_container.dart b/lib/widgets/sign_in_container.dart index 382f458..c68a6bf 100644 --- a/lib/widgets/sign_in_container.dart +++ b/lib/widgets/sign_in_container.dart @@ -4,7 +4,7 @@ import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/models/user.dart'; @@ -137,6 +137,8 @@ class SignInContainer extends HookConsumerWidget { var user = await ref.users.findOne(signedInNpub); + // If user was not found on relays, we create a + // local user to represent this new npub user ??= User.fromJson({ 'id': signedInNpub.hexKey, 'pubkey': signedInNpub.hexKey, diff --git a/lib/widgets/wot_container.dart b/lib/widgets/wot_container.dart index 15fdf09..2e373ca 100644 --- a/lib/widgets/wot_container.dart +++ b/lib/widgets/wot_container.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:purplebase/purplebase.dart'; +import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/models/user.dart'; diff --git a/lib/widgets/zap_button.dart b/lib/widgets/zap_button.dart index 705387c..9eba80f 100644 --- a/lib/widgets/zap_button.dart +++ b/lib/widgets/zap_button.dart @@ -147,15 +147,17 @@ class ZapButton extends HookConsumerWidget { 0; if (amount > 0) { - final lnurl = developer.lud16!; - await ref.read(zapProvider.notifier).zap( - user: user, - lnurl: lnurl, - eventId: app.latestMetadata != null - ? app.latestMetadata!.id.toString() - : app.latestRelease!.id.toString(), - amount: amount, - pubKey: developer.pubkey); + // User should be able to sign any event + // Proposed API: + await user.zap(amount, event: app.latestMetadata!); + + // final lnurl = developer.lud16!; + // await ref.read(zapProvider.notifier).zap( + // user: user, + // lnurl: lnurl, + // eventId: app.latestMetadata!.id.toString(), + // amount: amount, + // pubKey: developer.pubkey); } }, builder: (context, child, callback, state) { diff --git a/lib/widgets/zaps.dart b/lib/widgets/zaps.dart index 4859e69..5f6d12d 100644 --- a/lib/widgets/zaps.dart +++ b/lib/widgets/zaps.dart @@ -30,7 +30,7 @@ class Zaps extends HookConsumerWidget { receipts .sort((a, b) => (b.amountSats ?? 0).compareTo(a.amountSats ?? 0)); - int eventSum = 0; + var eventSum = 0; for (var receipt in receipts) { eventSum += receipt.amountSats ?? 0; } @@ -40,7 +40,7 @@ class Zaps extends HookConsumerWidget { final senders = ref.watch(zappersProvider((zapperIds: senderIds.take(10)))); - Widget zapperAvatars = switch (senders) { + final zapperAvatars = switch (senders) { AsyncData>(value: final users) => Builder( builder: (context) { return RichText( @@ -98,7 +98,5 @@ class Zaps extends HookConsumerWidget { final zappersProvider = FutureProvider.autoDispose .family, ({Iterable zapperIds})>((ref, arg) { // TODO it should load from either local or remote if not cached locally still - List list = ref.users.findManyLocalByIds(arg.zapperIds); - return list; - // return await ref.users.findAll(params: {'authors': arg.zapperIds, 'ignoreReturn': true}); + return ref.users.findManyLocalByIds(arg.zapperIds); }); diff --git a/pubspec.lock b/pubspec.lock index 2304908..73a79b3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -310,14 +310,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" - decimal: - dependency: transitive - description: - name: decimal - sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" - url: "https://pub.dev" - source: hosted - version: "2.3.3" device_info_plus: dependency: "direct main" description: @@ -614,14 +606,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - humanizer: - dependency: transitive - description: - name: humanizer - sha256: "08728a4b6d62accd7d09e668bd54e81e6e09a82c8cfda30553224b3eb868d4f2" - url: "https://pub.dev" - source: hosted - version: "2.2.0" icons_launcher: dependency: "direct dev" description: @@ -1018,11 +1002,9 @@ packages: purplebase: dependency: "direct main" description: - path: "." - ref: a6a4dbf97faa23f827dbe8158cb110e961827ddc - resolved-ref: a6a4dbf97faa23f827dbe8158cb110e961827ddc - url: "https://github.com/purplebase/purplebase" - source: git + path: "../purplebase" + relative: true + source: path version: "0.2.0" quiver: dependency: transitive @@ -1032,14 +1014,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" - rational: - dependency: transitive - description: - name: rational - sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 - url: "https://pub.dev" - source: hosted - version: "2.2.3" remove_markdown: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f313527..90995b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,10 +62,10 @@ dependency_overrides: path: packages/ndk ref: b864cd2f842b6774dcce3decb9fe75c6c7887172 #lnurl-zaps purplebase: - # path: ../purplebase - git: - url: https://github.com/purplebase/purplebase - ref: a6a4dbf97faa23f827dbe8158cb110e961827ddc + path: ../purplebase + # git: + # url: https://github.com/purplebase/purplebase + # ref: a6a4dbf97faa23f827dbe8158cb110e961827ddc flutter_data: git: url: https://github.com/zapstore/flutter_data