From ef8a5c5c5cb8fe2095aced873bfbd289691f7b3b Mon Sep 17 00:00:00 2001
From: Trent Larson <trent@trentlarson.com>
Date: Sat, 4 Jan 2025 16:35:05 -0700
Subject: [PATCH] add a contact-edit page and allow saving of notes

---
 src/db/tables/contacts.ts     |   1 +
 src/router/index.ts           |   5 ++
 src/views/ClaimView.vue       |   2 +-
 src/views/ConfirmGiftView.vue |   2 +-
 src/views/ContactEditView.vue | 130 ++++++++++++++++++++++++++++++++++
 src/views/ContactsView.vue    |  41 +++++++----
 src/views/DIDView.vue         |  63 +---------------
 7 files changed, 167 insertions(+), 77 deletions(-)
 create mode 100644 src/views/ContactEditView.vue

diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts
index e6369b228..9a438e51e 100644
--- a/src/db/tables/contacts.ts
+++ b/src/db/tables/contacts.ts
@@ -2,6 +2,7 @@ export interface Contact {
   did: string;
   name?: string;
   nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
+  notes?: string;
   profileImageUrl?: string;
   publicKeyBase64?: string;
   seesMe?: boolean; // cached value of the server setting
diff --git a/src/router/index.ts b/src/router/index.ts
index ba4b3a244..cf4300a1d 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -64,6 +64,11 @@ const routes: Array<RouteRecordRaw> = [
     name: "contact-amounts",
     component: () => import("../views/ContactAmountsView.vue"),
   },
+  {
+    path: "/contact-edit/:did",
+    name: "contact-edit",
+    component: () => import("../views/ContactEditView.vue"),
+  },
   {
     path: "/contact-gift",
     name: "contact-gift",
diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue
index 95325810b..9e4243789 100644
--- a/src/views/ClaimView.vue
+++ b/src/views/ClaimView.vue
@@ -351,7 +351,7 @@
       >
         Details
         <fa v-if="showVeriClaimDump" icon="chevron-up" />
-        <fa v-else icon="chevron-down" />
+        <fa v-else icon="chevron-right" />
     </h2>
     <div v-if="showVeriClaimDump">
       <div
diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue
index e9df0524e..fb7059b3d 100644
--- a/src/views/ConfirmGiftView.vue
+++ b/src/views/ConfirmGiftView.vue
@@ -261,7 +261,7 @@
       >
         Details
         <fa v-if="showVeriClaimDump" icon="chevron-up" />
-        <fa v-else icon="chevron-down" />
+        <fa v-else icon="chevron-right" />
       </h2>
       <div v-if="showVeriClaimDump">
         <div
diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue
new file mode 100644
index 000000000..04e63f2cf
--- /dev/null
+++ b/src/views/ContactEditView.vue
@@ -0,0 +1,130 @@
+<template>
+  <QuickNav selected="Contacts" />
+  <TopMessage />
+
+  <section id="ContactEdit" class="p-6 max-w-3xl mx-auto">
+    <div id="ViewBreadcrumb" class="mb-8">
+      <h1 class="text-4xl text-center font-light relative px-7">
+        <!-- Back -->
+        <button
+          @click="$router.go(-1)"
+          class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
+        >
+          <fa icon="chevron-left" class="fa-fw" />
+        </button>
+        {{ contact.name || AppString.NO_CONTACT_NAME }}
+      </h1>
+    </div>
+
+    <!-- Contact Name -->
+    <div class="mt-4 flex">
+      <label
+        for="contactName"
+        class="block text-sm font-medium text-gray-700 mt-2"
+      >
+        Name
+      </label>
+      <input
+        type="text"
+        class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
+        v-model="contactName"
+      />
+    </div>
+
+    <!-- Contact Notes -->
+    <div class="mt-4">
+      <label for="contactNotes" class="block text-sm font-medium text-gray-700">
+        Notes
+      </label>
+      <textarea
+        id="contactNotes"
+        rows="4"
+        class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
+        v-model="contactNotes"
+      ></textarea>
+    </div>
+
+    <!-- Save Button -->
+    <div class="mt-4 flex justify-between">
+      <button
+        class="px-4 py-2 bg-blue-500 text-white rounded-md"
+        @click="saveEdit"
+      >
+        Save
+      </button>
+      <button
+        class="ml-4 px-4 py-2 bg-slate-500 text-white rounded-md"
+        @click="$router.go(-1)"
+      >
+        Cancel
+      </button>
+    </div>
+  </section>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from "vue-facing-decorator";
+import { RouteLocation, Router } from "vue-router";
+
+import QuickNav from "@/components/QuickNav.vue";
+import TopMessage from "@/components/TopMessage.vue";
+import { AppString, NotificationIface } from "@/constants/app";
+import { db } from "@/db/index";
+import { Contact } from "@/db/tables/contacts";
+
+@Component({
+  components: {
+    QuickNav,
+    TopMessage,
+  },
+})
+export default class ContactEditView extends Vue {
+  $notify!: (notification: NotificationIface, timeout?: number) => void;
+
+  contact: Contact = {
+    did: "",
+    name: "",
+    notes: "",
+  };
+  contactName = "";
+  contactNotes = "";
+
+  AppString = AppString;
+
+  async created() {
+    const contactDid = (this.$route as RouteLocation).params.did;
+    if (!contactDid) {
+      this.$notify({
+        group: "alert",
+        type: "error",
+        title: "Contact Not Found",
+        text: "There is no contact with that DID.",
+      });
+      (this.$router as Router).push({ path: "/contacts" });
+      return;
+    }
+    const contact = await db.contacts.get(contactDid);
+    if (contact) {
+      this.contact = contact;
+      this.contactName = contact.name || "";
+      this.contactNotes = contact.notes || "";
+    }
+  }
+
+  async saveEdit() {
+    await db.contacts.update(this.contact.did, {
+      name: this.contactName,
+      notes: this.contactNotes,
+    });
+    this.$notify({
+      group: "alert",
+      type: "success",
+      title: "Notes Saved",
+      text: "The contact notes have been updated successfully.",
+    });
+    (this.$router as Router).push({
+      path: "/did/" + encodeURIComponent(this.contact.did),
+    });
+  }
+}
+</script>
diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue
index 6f9181585..014f32a6b 100644
--- a/src/views/ContactsView.vue
+++ b/src/views/ContactsView.vue
@@ -170,27 +170,34 @@
                     )
                   : contactsSelected.push(contact.did)
               "
-              class="ml-2 h-6 w-6"
+              class="ml-2 h-6 w-6 flex-shrink-0"
               data-testId="contactCheckOne"
             />
 
-            <h2 class="text-base font-semibold ml-2">
-              {{ contact.name || AppString.NO_CONTACT_NAME }}
+            <h2 class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0">
+              {{ contactNameNonBreakingSpace(contact.name) }}
             </h2>
 
-            <router-link
-              :to="{
-                path: '/did/' + encodeURIComponent(contact.did),
-              }"
-              title="See more about this person"
-            >
-              <fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
-            </router-link>
+            <span>
+              <div class="flex items-center">
+                <router-link
+                  :to="{
+                    path: '/did/' + encodeURIComponent(contact.did),
+                  }"
+                  title="See more about this person"
+                >
+                  <fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
+                </router-link>
 
-            <span class="ml-4 text-sm overflow-hidden">{{
-              shortDid(contact.did)
-            }}</span
-            ><!-- The first 18 characters of did:peer are the same. -->
+                <span class="ml-4 text-sm overflow-hidden">{{
+                  shortDid(contact.did)
+                }}</span
+                >
+              </div>
+              <div class="ml-4 text-sm">
+                {{ contact.notes }}
+              </div>
+            </span>
           </div>
           <div id="ContactActions" class="flex gap-1.5 mt-2">
             <div
@@ -542,6 +549,10 @@ export default class ContactsView extends Vue {
     }
   }
 
+  private contactNameNonBreakingSpace(contactName?: string) {
+    return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
+  }
+
   private danger(message: string, title: string = "Error", timeout = 5000) {
     this.$notify(
       {
diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue
index 1540daaf4..fde24311b 100644
--- a/src/views/DIDView.vue
+++ b/src/views/DIDView.vue
@@ -26,15 +26,11 @@
       <div>
         <h2 class="text-xl font-semibold">
           {{ contactFromDid?.name || "(no name)" }}
-          <button
-            @click="
-              contactEdit = true;
-              contactNewName = (contactFromDid?.name as string) || '';
-            "
-            title="Edit"
+          <router-link
+            :to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
           >
             <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
-          </button>
+          </router-link>
         </h2>
         <button
           @click="showDidDetails = !showDidDetails"
@@ -163,34 +159,6 @@
       </div>
     </div>
 
-    <!-- Edit Name Dialog, maybe should be replaced by ContactNameDialog -->
-    <div v-if="contactEdit" class="dialog-overlay">
-      <div class="dialog">
-        <h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
-        <input
-          type="text"
-          class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
-          placeholder="Name"
-          v-model="contactNewName"
-        />
-        <div class="flex justify-between">
-          <button
-            class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
-            @click="onClickSaveName(contactNewName)"
-          >
-            <fa icon="save" />
-          </button>
-          <span class="inline-block w-2" />
-          <button
-            class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
-            @click="onClickCancelName()"
-          >
-            <fa icon="ban" />
-          </button>
-        </div>
-      </div>
-    </div>
-
     <!-- Loading Animation -->
     <div
       class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
@@ -290,8 +258,6 @@ export default class DIDView extends Vue {
   apiServer = "";
   claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
   contactFromDid?: Contact;
-  contactEdit = false;
-  contactNewName: string = "";
   contactYaml = "";
   hitEnd = false;
   isLoading = false;
@@ -559,29 +525,6 @@ export default class DIDView extends Vue {
     return claim.claim.name || claim.claim.description || "";
   }
 
-  private async onClickCancelName() {
-    this.contactEdit = false;
-  }
-
-  private async onClickSaveName(newName: string) {
-    if (!this.contactFromDid) {
-      this.$notify(
-        {
-          group: "alert",
-          type: "danger",
-          title: "Not A Contact",
-          text: "First add this on the contact page, then you can edit here.",
-        },
-        5000,
-      );
-      return;
-    }
-    this.contactFromDid.name = newName;
-    return db.contacts
-      .update(this.contactFromDid.did, { name: newName })
-      .then(() => (this.contactEdit = false));
-  }
-
   // note that this is also in ContactView.vue
   async confirmSetVisibility(contact: Contact, visibility: boolean) {
     const visibilityPrompt = visibility