From 8d14778f9ce751f3b5c6d2c60a0b6f77c9e20417 Mon Sep 17 00:00:00 2001 From: vjrj <vjrj@comunes.org> Date: Sun, 5 Jan 2025 18:07:47 +0100 Subject: [PATCH] Improve contact page and certifications page --- lib/data/models/contact_wot_info.dart | 6 +++ lib/data/models/contact_wot_info.g.dart | 67 +++++++++++++++++++++++++ lib/g1/duniter_indexer_helper.dart | 14 +++++- lib/g1/g1_helper.dart | 8 +++ lib/ui/widgets/certifications_page.dart | 42 ++++++++++------ lib/ui/widgets/contact_page.dart | 54 +++++++++++--------- 6 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 lib/data/models/contact_wot_info.g.dart diff --git a/lib/data/models/contact_wot_info.dart b/lib/data/models/contact_wot_info.dart index 2b1f84d3..0acdb936 100644 --- a/lib/data/models/contact_wot_info.dart +++ b/lib/data/models/contact_wot_info.dart @@ -1,6 +1,11 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; + import '../../ui/ui_helpers.dart'; import 'contact.dart'; +part 'contact_wot_info.g.dart'; + +@CopyWith() class ContactWotInfo { ContactWotInfo({ required this.me, @@ -21,6 +26,7 @@ class ContactWotInfo { bool? distRuleOk; double? distRuleRatio; int? currentBlockHeight; + bool loaded = false; bool get isme => isMe(you, me.pubKey); diff --git a/lib/data/models/contact_wot_info.g.dart b/lib/data/models/contact_wot_info.g.dart new file mode 100644 index 00000000..8cc6c05f --- /dev/null +++ b/lib/data/models/contact_wot_info.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contact_wot_info.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ContactWotInfoCWProxy { + ContactWotInfo me(Contact me); + + ContactWotInfo you(Contact you); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContactWotInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContactWotInfo(...).copyWith(id: 12, name: "My name") + /// ```` + ContactWotInfo call({ + Contact? me, + Contact? you, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfContactWotInfo.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfContactWotInfo.copyWith.fieldName(...)` +class _$ContactWotInfoCWProxyImpl implements _$ContactWotInfoCWProxy { + const _$ContactWotInfoCWProxyImpl(this._value); + + final ContactWotInfo _value; + + @override + ContactWotInfo me(Contact me) => this(me: me); + + @override + ContactWotInfo you(Contact you) => this(you: you); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContactWotInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContactWotInfo(...).copyWith(id: 12, name: "My name") + /// ```` + ContactWotInfo call({ + Object? me = const $CopyWithPlaceholder(), + Object? you = const $CopyWithPlaceholder(), + }) { + return ContactWotInfo( + me: me == const $CopyWithPlaceholder() || me == null + ? _value.me + // ignore: cast_nullable_to_non_nullable + : me as Contact, + you: you == const $CopyWithPlaceholder() || you == null + ? _value.you + // ignore: cast_nullable_to_non_nullable + : you as Contact, + ); + } +} + +extension $ContactWotInfoCopyWith on ContactWotInfo { + /// Returns a callable class that can be used as follows: `instanceOfContactWotInfo.copyWith(...)` or like so:`instanceOfContactWotInfo.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ContactWotInfoCWProxy get copyWith => _$ContactWotInfoCWProxyImpl(this); +} diff --git a/lib/g1/duniter_indexer_helper.dart b/lib/g1/duniter_indexer_helper.dart index 7717c0cd..ee789ab4 100644 --- a/lib/g1/duniter_indexer_helper.dart +++ b/lib/g1/duniter_indexer_helper.dart @@ -183,11 +183,21 @@ Cert _buildCert(dynamic cert) { issuerId: Contact.withAddress( name: (issuer as dynamic).name as String, createdOn: ((issuer as dynamic).account as dynamic).createdOn as int, - address: (issuer as dynamic).accountId as String), + address: (issuer as dynamic).accountId as String, + status: parseIdentityStatus( + ((issuer as dynamic)?.status as dynamic)?.name as String?), + isMember: (issuer as dynamic)?.isMember as bool?, + expireOn: (issuer as dynamic).expireOn as int?, + index: (issuer as dynamic).index as int?), receiverId: Contact.withAddress( name: (receiver as dynamic).name as String, createdOn: ((receiver as dynamic).account as dynamic).createdOn as int, - address: (receiver as dynamic).accountId as String), + address: (receiver as dynamic).accountId as String, + status: parseIdentityStatus( + ((issuer as dynamic)?.status as dynamic)?.name as String?), + isMember: (receiver as dynamic)?.isMember as bool?, + expireOn: (receiver as dynamic).expireOn as int?, + index: (receiver as dynamic).index as int?), createdOn: (cert as dynamic).createdOn as int, expireOn: (cert as dynamic).expireOn as int, isActive: (cert as dynamic).isActive as bool, diff --git a/lib/g1/g1_helper.dart b/lib/g1/g1_helper.dart index 176f68f9..dbcf6d52 100644 --- a/lib/g1/g1_helper.dart +++ b/lib/g1/g1_helper.dart @@ -421,3 +421,11 @@ Uint8List decryptAes(Uint8List encryptedData, Uint8List key) { } const Duration defPolkadotTimeout = Duration(seconds: 20); + +// Based on duniter-vue +DateTime estimateDateFromBlock( + {required int futureBlock, required int currentBlockHeight}) { + const int millisPerBlock = 6000; + final int diff = futureBlock - currentBlockHeight; + return DateTime.now().add(Duration(milliseconds: diff * millisPerBlock)); +} diff --git a/lib/ui/widgets/certifications_page.dart b/lib/ui/widgets/certifications_page.dart index fa47b6fd..a73e30f7 100644 --- a/lib/ui/widgets/certifications_page.dart +++ b/lib/ui/widgets/certifications_page.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import '../../data/models/cert.dart'; import '../../data/models/contact.dart'; import '../contact_list_item.dart'; -import '../ui_helpers.dart'; import 'contacts_actions.dart'; class CertificationsPage extends StatelessWidget { @@ -21,6 +20,7 @@ class CertificationsPage extends StatelessWidget { final bool issued; final int currentBlockHeight; final List<Cert> certifications; + static const int limit = 201600; @override Widget build(BuildContext context) { @@ -51,27 +51,39 @@ class CertificationsPage extends StatelessWidget { final Cert cert = certifications[index]; final Contact contact = issued ? cert.receiverId : cert.issuerId; - // FIXME Use block estimation to calculate the cert date - String? certDate = inDevelopment - ? humanizeTimeFull( - utcDateTime: DateTime.now() - .add(Duration(seconds: cert.updatedOn)), - locale: currentLocale(context)) - : null; - certDate = null; + final bool isExpired = cert.expireOn <= currentBlockHeight; + final bool isExpiringSoon = cert.isActive && + (cert.expireOn - currentBlockHeight < limit); + final bool isMember = contact.isMember ?? false; + /* final DateTime updateOn = estimateDateFromBlock( + futureBlock: cert.updatedOn, + currentBlockHeight: currentBlockHeight); */ + /* final String certDate = humanizeTimeFull( + locale: currentLocale(context), utcDateTime: updateOn); */ + final String statusMsg = + tr('idty_status_${contact.status!.name}'); return ContactListItem( contact: contact, - subtitleExtra: certDate, + // subtitleExtra: statusMsg, index: index, isV2: true, onTap: () { showContactPage(context, contact); }, - trailing: cert.expireOn > 0 - ? Text(humanizeTimeFuture( - currentLocale(context), cert.expireOn) ?? - '') - : Text(tr('cert_expired'))); + trailing: Tooltip( + message: statusMsg, + child: Icon( + isMember + ? isExpiringSoon + ? Icons.timelapse + : Icons.check_circle_outline + : Icons.warning_amber_outlined, + color: isMember + ? isExpiringSoon + ? Colors.orange.shade300 + : Theme.of(context).colorScheme.primary + : Colors.red.shade300, + ))); }, ), ) diff --git a/lib/ui/widgets/contact_page.dart b/lib/ui/widgets/contact_page.dart index 6f22a0ee..f8617008 100644 --- a/lib/ui/widgets/contact_page.dart +++ b/lib/ui/widgets/contact_page.dart @@ -20,6 +20,7 @@ import '../../g1/api.dart'; import '../../g1/distance_precompute.dart'; import '../../g1/distance_precompute_provider.dart'; import '../../g1/duniter_indexer_helper.dart'; +import '../../g1/g1_helper.dart'; import '../../g1/g1_v2_helper.dart'; import '../../g1/sing_and_send.dart'; import '../../g1/wot_actions.dart'; @@ -70,26 +71,25 @@ class _ContactPageState extends State<ContactPage> { @override Widget build(BuildContext context) { - final Contact contact = widget.contact; - - return FutureBuilder<ContactWotInfo>( - future: _getWotInfo(), + final Contact me = Contact(pubKey: SharedPreferencesHelper().getPubKey()); + return StreamBuilder<ContactWotInfo>( + stream: _getWotInfo(), + initialData: ContactWotInfo( + me: me, + you: widget.contact, + ), builder: (BuildContext context, AsyncSnapshot<ContactWotInfo> snapshot) { if (snapshot.hasData) { - return _buildContactWidget(snapshot.data!, context, true); + return _buildContactWidget(snapshot.requireData, context); } return _buildContactWidget( - ContactWotInfo( - me: Contact(pubKey: SharedPreferencesHelper().getPubKey()), - you: contact), - context, - false); + ContactWotInfo(me: me, you: widget.contact), context); }, ); } DefaultTabController _buildContactWidget( - ContactWotInfo contactWotInfo, BuildContext context, bool loaded) { + ContactWotInfo contactWotInfo, BuildContext context) { final Contact contact = contactWotInfo.you; final bool isContact = context.read<ContactsCubit>().isContact(contact.pubKey); @@ -191,7 +191,7 @@ class _ContactPageState extends State<ContactPage> { Expanded( child: TabBarView( children: <Widget>[ - _buildInfoTab(contact, contactWotInfo, loaded), + _buildInfoTab(contact, contactWotInfo, contactWotInfo.loaded), _buildTransactionsTab(contact), ], ), @@ -512,7 +512,7 @@ class _ContactPageState extends State<ContactPage> { ); } - Future<ContactWotInfo> _getWotInfo() async { + Stream<ContactWotInfo> _getWotInfo() async* { final Contact you = await getProfile(widget.contact.pubKey, resize: false, complete: true); final Contact me = await getProfile(SharedPreferencesHelper().getPubKey(), @@ -539,6 +539,8 @@ class _ContactPageState extends State<ContactPage> { (await getIdentity(address: you.address)) != null; wotInfo.canCreateIdty = iAmMember && enoughBalance && !identityUsed; } + yield wotInfo; + final int currentBlock = await polkadotCurrentBlock(); wotInfo.currentBlockHeight = currentBlock; final bool youAMember = you.isMember ?? false; @@ -546,11 +548,13 @@ class _ContactPageState extends State<ContactPage> { final IdtyValue? youIdty = await polkadotIdentity(you); final IdtyCertMeta? youCertMeta = await polkadotIdtyCertMeta(you); if (youIdty != null && youCertMeta != null) { - wotInfo.canCertOn = estimateDate( + wotInfo.canCertOn = estimateDateFromBlock( futureBlock: youCertMeta.nextIssuableOn, currentBlockHeight: currentBlock); } } + yield wotInfo; + // Can Certificate final IdtyValue? myIdty = await polkadotIdentity(me); final IdtyCertMeta? idtyCertMeta = await polkadotIdtyCertMeta(me); @@ -563,7 +567,10 @@ class _ContactPageState extends State<ContactPage> { idtyCertMeta.nextIssuableOn < currentBlock && idtyCertMeta.issuedCount < Constants().maxByIssuer; wotInfo.canCert = canCert; + } else { + wotInfo.canCert = false; } + yield wotInfo; // Waiting for Certifications if (you.certsReceived != null && @@ -574,9 +581,10 @@ class _ContactPageState extends State<ContactPage> { } else { wotInfo.waitingForCerts = false; } + yield wotInfo; final int membershipExpireOn = you.expireOn!; - wotInfo.expireOn = estimateDate( + wotInfo.expireOn = estimateDateFromBlock( futureBlock: membershipExpireOn, currentBlockHeight: currentBlock); // Can call distance @@ -592,6 +600,7 @@ class _ContactPageState extends State<ContactPage> { } else { wotInfo.canCalcDistance = false; } + yield wotInfo; // Can call distanceFor if (!wotInfo.isme && @@ -601,14 +610,16 @@ class _ContactPageState extends State<ContactPage> { } else { wotInfo.canCalcDistanceFor = false; } + yield wotInfo; final bool alreadyCert = you.certsReceived != null && you.certsReceived!.isNotEmpty && you.certsReceived! .any((Cert cert) => cert.issuerId.pubKey == me.pubKey); wotInfo.alreadyCert = alreadyCert; + yield wotInfo; if (!mounted) { - return wotInfo; + yield wotInfo; } final AppCubit appCubit = context.read<AppCubit>(); DistancePrecompute? distancePrecompute = appCubit.distancePrecompute; @@ -632,7 +643,8 @@ class _ContactPageState extends State<ContactPage> { wotInfo.distRuleRatio = distRuleRatio; } } - return wotInfo; + wotInfo.loaded = true; + yield wotInfo; } } @@ -653,14 +665,6 @@ Widget _buildBadge(BuildContext context, int count) { ); } -// Based on duniter-vue -DateTime estimateDate( - {required int futureBlock, required int currentBlockHeight}) { - const int millisPerBlock = 6000; - final int diff = futureBlock - currentBlockHeight; - return DateTime.now().add(Duration(milliseconds: diff * millisPerBlock)); -} - String _getIdentityStatusDescription(IdentityStatus status, bool waitingCerts) { switch (status) { case IdentityStatus.UNCONFIRMED: -- GitLab