New purplebase and zaps integration WIP

This commit is contained in:
franzap
2024-12-31 14:10:54 -03:00
parent 4f129e1f52
commit 5cba51c846
26 changed files with 237 additions and 161 deletions
+1
View File
@@ -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
+19 -14
View File
@@ -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<App> {
class App extends base.App with DataModelMixin<App> {
@override
Object? get id => event.id;
final HasMany<Release> releases;
final BelongsTo<User> signer;
final BelongsTo<User> developer;
@@ -195,14 +198,14 @@ class App extends BaseApp with DataModelMixin<App> {
);
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<App> {
// 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<App> {
// 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<App> {
final list = data is Iterable ? data : [data as Map];
for (final Map<String, dynamic> 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<bool> _isHashMismatch(String path, String hash) async {
extension AppExt on Iterable<App> {
List<App> get sortedByLatest =>
sorted((a, b) => b.createdAt!.compareTo(a.createdAt!));
sorted((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
}
// class
+8 -5
View File
@@ -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<AppCurationSet> {
@override
Object get id => event.id;
final HasMany<App> apps;
final BelongsTo<User> signer;
@@ -21,7 +24,7 @@ class AppCurationSet extends BaseAppCurationSet
Map<String, dynamic> toJson() => super.toMap();
String get name => content.isNotEmpty ? content : identifier!;
String get name => event.content.isNotEmpty ? event.content : identifier;
}
mixin AppCurationSetAdapter on Adapter<AppCurationSet> {
@@ -45,8 +48,8 @@ mixin AppCurationSetAdapter on Adapter<AppCurationSet> {
DeserializedData<AppCurationSet> deserialize(Object? data, {String? key}) {
final list = data is Iterable ? data : [data as Map];
for (final Map<String, dynamic> 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);
}
+10 -7
View File
@@ -1,10 +1,13 @@
import 'package:purplebase/purplebase.dart';
class AppFeedback extends BaseEvent<AppFeedback> {
@override
int get kind => 1011;
AppFeedback({
required String content,
}) : super(content: content);
class AppFeedback extends RegularEvent<AppFeedback> {
AppFeedback.fromJson(super.map) : super.fromJson();
}
class PartialAppFeedback extends RegularPartialEvent<AppFeedback> {
PartialAppFeedback({
required String content,
}) {
event.content = content;
}
}
+14 -12
View File
@@ -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<FileMetadata> {
class FileMetadata extends base.FileMetadata with DataModelMixin<FileMetadata> {
final BelongsTo<User> author;
final BelongsTo<Release> release;
final BelongsTo<User> 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<FileMetadata> {
Map<String, dynamic> 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<FileMetadata> {
+1 -1
View File
@@ -144,7 +144,7 @@ mixin LocalAppAdapter on Adapter<LocalApp> {
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) {
+15 -8
View File
@@ -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<T extends DataModelMixin<T>> on Adapter<T> {
RelayMessageNotifier get relay =>
@@ -52,7 +52,10 @@ mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
}
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<T extends DataModelMixin<T>> on Adapter<T> {
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<T extends DataModelMixin<T>> on Adapter<T> {
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<T>);
@@ -144,7 +151,7 @@ mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
Future<void> loadArtifactsAndUsers(Iterable<Release> 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 = {
+16 -13
View File
@@ -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<Release> {
class Release extends base.Release with DataModelMixin<Release> {
@override
Object? get id => event.id;
final HasMany<FileMetadata> artifacts;
final BelongsTo<App> app;
final BelongsTo<User> 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<Release> {
final list = data is Iterable ? data : [data as Map];
for (final Map<String, dynamic> 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<Release> {
extension ReleaseExt on Iterable<Release> {
List<Release> get sortedByLatest =>
sorted((a, b) => b.createdAt!.compareTo(a.createdAt!));
sorted((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
}
+89 -14
View File
@@ -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> {
User(
{super.name,
super.createdAt,
super.tags,
HasMany<User>? followers,
HasMany<User>? following,
BelongsTo<Settings>? settings})
: followers = followers ?? HasMany(),
following = following ?? HasMany(),
settings = settings ?? BelongsTo();
class User extends base.User with DataModelMixin<User> {
@override
Object? get id => event.id;
Future<void> 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<Map<String, dynamic>> 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<User> {
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<User> {
);
}
final result = await socialRelays.queryRaw(RelayRequest(
final result = await socialRelays.queryRaw(base.RelayRequest(
kinds: {0}, // 3
tags: params ?? {},
authors: {publicKey},
+5 -5
View File
@@ -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,
+1 -1
View File
@@ -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);
+11 -13
View File
@@ -26,35 +26,33 @@ class AmberSigner extends Signer {
}
@override
Future<T> sign<T extends BaseEvent<T>>(T model, {String? asUser}) async {
Future<E> sign<E extends Event<E>>(PartialEvent<E> 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<T>(model.kind)!;
return fn.call(signedEvent);
return Event.getConstructor<E>()!.call(signedEvent);
}
}
final pkSigner = PrivateKeySigner(
final pkSigner = Bip340PrivateKeySigner(
'e593c54f840b32054dcad0fac15d57e4ac6523e31fe26b3087de6b07a2e9af58');
+3 -2
View File
@@ -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<AsyncValue<List<ZapReceipt>>?> {
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);
});
+3 -2
View File
@@ -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),
+6 -6
View File
@@ -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<AppCurationSet, ReplaceableEventLink> {
extends FamilyAsyncNotifier<AppCurationSet, base.ReplaceableEventLink> {
@override
Future<AppCurationSet> build(ReplaceableEventLink arg) async {
Future<AppCurationSet> 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<ReplaceableEventLink>((_) => kNostrCurationSetLink);
StateProvider<base.ReplaceableEventLink>((_) => kNostrCurationSetLink);
const kNostrCurationSetLink = (30267, kFranzapPubkey, 'nostr');
+2 -1
View File
@@ -149,7 +149,8 @@ Future<void> _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'});
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -42,7 +42,7 @@ class InstallButton extends HookConsumerWidget {
AppInstallStatus.certificateMismatch =>
null,
AppInstallStatus.updated => () {
packageManager.openApp(app.identifier!);
packageManager.openApp(app.identifier);
},
_ => switch (progress) {
IdleInstallProgress() => () {
+1 -1
View File
@@ -125,7 +125,7 @@ class LatestReleasesAppNotifier extends PreloadingStateNotifier<List<App>> {
// Set timestamp of oldest, to prepare for next query
if (apps.isNotEmpty) {
_oldestCreatedAt = apps.last.createdAt;
_oldestCreatedAt = apps.last.event.createdAt;
}
return AsyncData(apps);
}
+5 -5
View File
@@ -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)),
],
),
),
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -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';
+11 -9
View File
@@ -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) {
+3 -5
View File
@@ -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<List<User>>(value: final users) => Builder(
builder: (context) {
return RichText(
@@ -98,7 +98,5 @@ class Zaps extends HookConsumerWidget {
final zappersProvider = FutureProvider.autoDispose
.family<List<User>, ({Iterable<String> zapperIds})>((ref, arg) {
// TODO it should load from either local or remote if not cached locally still
List<User> 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);
});
+3 -29
View File
@@ -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:
+4 -4
View File
@@ -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