diff --git a/.gitignore b/.gitignore index 3cf932a..e1cd6b5 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,6 @@ app.*.map.json /tmp /flutter_data -.fvm/ \ No newline at end of file +.fvm/ + +TODO.txt \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 63fbc06..9a78568 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,7 @@ class ZapstoreApp extends StatelessWidget { useMaterial3: true, scaffoldBackgroundColor: kBackgroundColor, visualDensity: VisualDensity.adaptivePlatformDensity, + // textTheme: context.theme.textTheme.copyWith( // bodyLarge: // TextStyle(color: Colors.white, fontWeight: FontWeight.w300)) @@ -225,13 +226,7 @@ class ScaffoldWithNavigationBar extends HookConsumerWidget { onDestinationSelected: onDestinationSelected, ), drawer: Drawer( - child: Column( - children: [ - DrawerHeader( - child: AppDrawer(), - ), - ], - ), + child: AppDrawer(), ), ); } diff --git a/lib/models/user.dart b/lib/models/user.dart index 642a59b..e0934a6 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:purplebase/purplebase.dart'; @@ -17,46 +20,47 @@ class User extends ZapstoreEvent with BaseUser { mixin UserAdapter on NostrAdapter { @override DeserializedData deserialize(Object? data, {String? key}) { + final Iterable> list = + (data is Iterable ? data : [data as Map]).cast(); + + final k0s = list + .where((e) => e['kind'] == 0 && jsonDecode(e['content']).isNotEmpty) + .toList() + .groupSetsBy((e) => e['pubkey'] as String); + final k3 = (list.where((e) => e['kind'] == 3).toList() + ..sortBy((e) => e['created_at'])) + .firstOrNull; + + // collect contacts and then assign them to user + final included = []; + if (k3 != null) { + final contactMaps = []; + for (final [_, id, ..._] in k3['tags'] as Iterable) { + contactMaps.add({ + 'id': id, + 'content': '', + 'pubkey': id, + 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'kind': 0, + 'tags': [], + }); + } + final data = super.deserialize(contactMaps); + included.addAll(data.models); + } + final users = []; - final list = data is Iterable ? data : [data as Map]; - - // first process kind 3 (if any) - for (final e in list) { - final map = e as Map; - if (map['kind'] == 3) { - final contactMaps = []; - for (final [_, id, ..._] in map['tags'] as Iterable) { - contactMaps.add({ - 'id': id, - 'content': '', - 'pubkey': id, - 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'kind': 0, - 'tags': [], - }); - } - final data = super.deserialize(contactMaps); - users.addAll(data.models); + for (final _ in k0s.entries) { + if (_.value.length > 1) { + print('more than one for ${_.key}'); } + final k0 = _.value.first; + k0['id'] = k0['pubkey']; + k0['following'] = included.map((e) => e.id).toList(); + users.addAll(super.deserialize(k0).models); } - for (final e in list) { - final map = e as Map; - if (map['kind'] == 0) { - map['id'] = map['pubkey']; - // TODO workaround - should not assume empty is null - if (users.isNotEmpty) { - map['following'] = users.map((e) => e.id).toList(); - } - final data0 = super.deserialize(map); - final user = data0.model; - if (user != null) { - users.add(user); - } - } - } - - return DeserializedData(users); + return DeserializedData(users, included: included); } @override @@ -90,6 +94,8 @@ mixin UserAdapter on NostrAdapter { OnSuccessOne? onSuccess, OnErrorOne? onError, DataRequestLabel? label}) async { + if (id.toString().isEmpty) return null; + var publicKey = id.toString(); if (publicKey.startsWith('npub')) { @@ -113,6 +119,8 @@ mixin UserAdapter on NostrAdapter { final result = await notifier.query(req, relayUrls: ['wss://relay.nostr.band']); final data = await deserializeAsync(result, save: true); - return data.models.firstWhere((e) => e.id == publicKey); + return data.models.firstWhere((e) { + return e.id == publicKey; + }); } } diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 92af992..0ea2e0c 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -76,14 +76,17 @@ class AppDetailScreen extends HookConsumerWidget { child: SingleChildScrollView( controller: scrollController, scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (final i in app.tagMap['image']!) - Padding( - padding: const EdgeInsets.only(right: 12), - child: CachedNetworkImage(imageUrl: i), - ), - ], + child: SizedBox( + height: 320, + child: Row( + children: [ + for (final i in app.tagMap['image']!) + Padding( + padding: const EdgeInsets.only(right: 12), + child: CachedNetworkImage(imageUrl: i), + ), + ], + ), ), ), ), @@ -92,7 +95,7 @@ class AppDetailScreen extends HookConsumerWidget { styleSheet: MarkdownStyleSheet( h1: TextStyle(fontWeight: FontWeight.bold), h2: TextStyle(fontWeight: FontWeight.bold), - p: TextStyle(fontSize: 16, fontWeight: FontWeight.w300), + p: TextStyle(fontSize: 18, fontWeight: FontWeight.w300), ), selectable: false, data: app.content, diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index d219f68..7d36d83 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -5,6 +5,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; +import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/widgets/card.dart'; import 'package:zapstore/widgets/user_avatar.dart'; @@ -74,12 +75,17 @@ class SearchScreen extends HookConsumerWidget { ), Gap(10), if (state.hasMessage) - Text( - state.message!, - textAlign: TextAlign.center, + Expanded( + child: Center( + child: Text( + state.message!, + textAlign: TextAlign.center, + style: context.theme.textTheme.bodyLarge, + ), + ), ), if (state.hasException) Text(state.exception!.toString()), - if (state.hasModel) + if (state.hasModel && state.model.isNotEmpty) Expanded( child: ListView.builder( shrinkWrap: true, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d7cbe8a..e497d24 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -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'; diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index d7acd2d..629329f 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -11,51 +11,62 @@ class AppDrawer extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final u2 = ref.watch(loggedInUser); + final user = ref.watch(loggedInUser); final controller = useTextEditingController(); return Padding( - padding: const EdgeInsets.all(12.0), - child: Column( + padding: const EdgeInsets.only(top: 24, left: 16), + child: ListView( children: [ - // Text('npub or NIP-05'), - if (u2 == null) TextField(autocorrect: false, controller: controller), - Gap(5), - if (u2 == null) - ElevatedButton( - onPressed: () async { - ref.read(loggedInUser.notifier).state = await ref.users - .findOne(controller.text, params: {'contacts': true}); - }, - child: Text('Log in'), - ), - if (u2 != null) - Expanded( - child: Column( - children: [ - Card( - child: ListTile( - dense: true, - leading: CircularImage( - size: 20, - url: u2.avatarUrl, - ), - title: Text( - u2.name ?? u2.id?.toString() ?? '', - ), - subtitle: Text('following ${u2.following.length}'), - ), - ), - ElevatedButton( - onPressed: () async { - ref.read(loggedInUser.notifier).state = null; - controller.clear(); - }, - child: Text('Log out'), - ), - ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 4, bottom: 4), + child: Row( + children: [ + CircularImage(url: user?.avatarUrl, size: 46), + Gap(10), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (user != null) + Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (user != null) + Text('${user.following.length} contacts'), + ], + ) + ], + ), ), - ), + if (user == null) + TextField( + autocorrect: false, + controller: controller, + ), + Gap(5), + if (user == null) + ElevatedButton( + onPressed: () async { + ref.read(loggedInUser.notifier).state = await ref.users + .findOne(controller.text, params: {'contacts': true}); + }, + child: Text('Log in'), + ), + if (user != null) + ElevatedButton( + onPressed: () async { + ref.read(loggedInUser.notifier).state = null; + controller.clear(); + }, + child: Text('Log out'), + ), + ], + ), ], ), ); diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 3186064..89f51f9 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -110,8 +110,6 @@ class AuthorContainer extends StatelessWidget { this.oneLine = true, }); - String get name => user.name ?? user.npub; - @override Widget build(BuildContext context) { return Padding( @@ -120,7 +118,14 @@ class AuthorContainer extends StatelessWidget { children: [ CircularImage(url: user.avatarUrl, size: oneLine ? 22 : 46), Gap(10), - if (oneLine) Text('$text $name'), + if (oneLine) + Expanded( + child: Text( + '$text ${user.name}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), if (!oneLine) Column( mainAxisAlignment: MainAxisAlignment.center, @@ -129,8 +134,12 @@ class AuthorContainer extends StatelessWidget { Text(text), Padding( padding: const EdgeInsets.only(left: 1), - child: - Text(name, style: TextStyle(fontWeight: FontWeight.bold)), + child: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ) @@ -158,6 +167,7 @@ class CircularImage extends StatelessWidget { borderRadius: BorderRadius.circular(radius.toDouble()), child: CachedNetworkImage( imageUrl: url ?? '', + errorWidget: (_, __, ___) => Container(color: Colors.grey[800]), fit: BoxFit.cover, width: size.toDouble(), height: size.toDouble(), diff --git a/lib/widgets/user_avatar.dart b/lib/widgets/user_avatar.dart index e5252a2..39d2c95 100644 --- a/lib/widgets/user_avatar.dart +++ b/lib/widgets/user_avatar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/services/session_service.dart'; +import 'package:zapstore/widgets/card.dart'; class UserAvatar extends HookConsumerWidget { const UserAvatar({super.key}); @@ -8,11 +9,6 @@ class UserAvatar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(loggedInUser); - return CircleAvatar( - radius: 20, - // backgroundImage: AssetImage('assets/images/logo.png'), - backgroundColor: Colors.grey[850], - foregroundImage: user != null ? NetworkImage(user.avatarUrl!) : null, - ); + return CircularImage(url: user?.avatarUrl, size: 46); } }