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