mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
New purplebase and zaps integration WIP
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -42,7 +42,7 @@ class InstallButton extends HookConsumerWidget {
|
||||
AppInstallStatus.certificateMismatch =>
|
||||
null,
|
||||
AppInstallStatus.updated => () {
|
||||
packageManager.openApp(app.identifier!);
|
||||
packageManager.openApp(app.identifier);
|
||||
},
|
||||
_ => switch (progress) {
|
||||
IdleInstallProgress() => () {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user