Browse Source

show more redeemed info & action on the invites, refactor onboarding instructions

master
Trent Larson 1 month ago
parent
commit
b9115357c3
  1. 6
      src/components/ContactNameDialog.vue
  2. 7
      src/components/InviteDialog.vue
  3. 4
      src/views/ContactsView.vue
  4. 37
      src/views/HelpOnboardingView.vue
  5. 60
      src/views/InviteOneView.vue
  6. 2
      src/views/ProjectsView.vue

6
src/components/ContactNameDialog.vue

@ -1,8 +1,8 @@
<!-- similar to ContactNameDialog --> <!-- similar to UserNameDialog -->
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Inviter's Name</h1> <h1 class="text-xl font-bold text-center mb-4">Contact Name</h1>
Note that their name is only stored on this device. Note that their name is only stored on this device.
<input <input
type="text" type="text"
@ -37,7 +37,7 @@
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
@Component @Component
export default class UserNameDialog extends Vue { export default class ContactNameDialog extends Vue {
callback: (name?: string) => void = () => {}; callback: (name?: string) => void = () => {};
newText = ""; newText = "";
visible = false; visible = false;

7
src/components/InviteDialog.vue

@ -3,9 +3,10 @@
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Invitation & Notes</h1> <h1 class="text-xl font-bold text-center mb-4">Invitation & Notes</h1>
These are optional notes for your use, to make comments for to recall These are optional notes for your use; they are comments to help you
later when redeemed by someone. These notes are sent to the server. If you recall who it is when they accept it. These notes are sent to the server.
want to store your own way, the invitation ID is: {{ inviteIdentifier }} If you want to store your own way, the invitation ID is:
{{ inviteIdentifier }}
<input <input
type="text" type="text"
placeholder="Notes" placeholder="Notes"

4
src/views/ContactsView.vue

@ -39,12 +39,12 @@
<textarea <textarea
type="text" type="text"
placeholder="URL or DID, Name, Public Key, Next Public Key Hash" placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
v-model="contactInput" v-model="contactInput"
/> />
<button <button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
@click="onClickNewContact()" @click="onClickNewContact()"
> >
<fa icon="plus" class="fa-fw" /> <fa icon="plus" class="fa-fw" />

37
src/views/HelpOnboardingView.vue

@ -12,6 +12,24 @@
</h1> </h1>
</div> </div>
<p>
To invite someone, generate a link to send to them from this page:
<router-link
:to="{ name: 'invite-one' }"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
<fa icon="envelope-open-text" class="fa-fw text-xl"
/></router-link>
</p>
<p>That's all it takes.</p>
<p>Watch that page to see when people accept their invite.</p>
<p>
That page is also reachable from the Contacts <fa icon="users" /> page
though the invitation <fa icon="envelope-open-text" /> icon.
</p>
<h1 class="font-bold text-xl">Advanced</h1>
The following are optional steps for even more functionality:
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->
<div class="ml-4"> <div class="ml-4">
<h1 class="font-bold text-xl">Install</h1> <h1 class="font-bold text-xl">Install</h1>
@ -25,25 +43,36 @@
</div> </div>
<h1 class="font-bold text-xl">Add Contact & Register</h1> <h1 class="font-bold text-xl">Add Contact & Register</h1>
<p>
You share even more information such as your picture and name when
you share with your QR code at these links: <fa icon="qrcode" /> Scanning
those with your cameras will automatically register people and add them
to each other's contact lists.
</p>
<p>
The following are more detailed manual steps:
</p>
<div> <div>
<p> <p>
3) Have them follow their yellow prompts. 3) Have them follow their yellow prompts.
</p> </p>
<p> <p>
4) Add them to your contacts <fa icon="users" /> 4) Scan their QR, or have them tap on it to copy their info and send it to you.
Then you can add them to your Contacts <fa icon="users" />
</p> </p>
<p> <p>
5) Register them <fa icon="person-circle-question" /> 5) You can register them at their info page <fa icon="circle-info" />
and click on the register button <fa icon="person-circle-question" />
</p> </p>
<p> <p>
6) Add yourself to their contacts <fa icon="users" /> 6) Add yourself to their Contacts <fa icon="users" />
</p> </p>
</div> </div>
<h1 class="font-bold text-xl">Enable Notifications</h1> <h1 class="font-bold text-xl">Enable Notifications</h1>
<div> <div>
<p> <p>
7) Enable notifications from <fa icon="circle-user" /> 7) Enable notifications from the Account page <fa icon="circle-user" />
</p> </p>
</div> </div>

60
src/views/InviteOneView.vue

@ -19,7 +19,7 @@
<!-- New Project --> <!-- New Project -->
<button <button
v-if="isRegistered" v-if="isRegistered"
class="fixed right-6 top-12 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full" class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="createInvite()" @click="createInvite()"
> >
<fa icon="plus" class="fa-fw"></fa> <fa icon="plus" class="fa-fw"></fa>
@ -35,7 +35,7 @@
<th class="py-2">ID</th> <th class="py-2">ID</th>
<th class="py-2">Notes</th> <th class="py-2">Notes</th>
<th class="py-2">Expires At</th> <th class="py-2">Expires At</th>
<th class="py-2">Redeemed By</th> <th class="py-2">Redeemed</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -45,7 +45,7 @@
class="border-t" class="border-t"
> >
<td <td
class="py-2 text-center text-blue-500" class="py-2 text-center text-blue-500 cursor-pointer"
@click="copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)" @click="copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)"
title="{{ inviteLink(invite.jwt) }}" title="{{ inviteLink(invite.jwt) }}"
> >
@ -58,7 +58,16 @@
{{ invite.expiresAt.substring(0, 10) }} {{ invite.expiresAt.substring(0, 10) }}
</td> </td>
<td class="py-2 text-center"> <td class="py-2 text-center">
{{ invite.redeemedAt?.substring(0, 10) }}
<br />
{{ getTruncatedRedeemedBy(invite.redeemedBy) }} {{ getTruncatedRedeemedBy(invite.redeemedBy) }}
<br />
<fa
v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
icon="plus"
class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
@click="addNewContact(invite.redeemedBy)"
/>
</td> </td>
<td> <td>
<fa <fa
@ -70,6 +79,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ContactNameDialog ref="contactNameDialog" />
</div> </div>
<p v-else class="mt-6 text-center">No invites found.</p> <p v-else class="mt-6 text-center">No invites found.</p>
</section> </section>
@ -79,11 +89,12 @@ import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { db, retrieveSettingsForActiveAccount } from "../db"; import ContactNameDialog from "@/components/ContactNameDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import InviteDialog from "@/components/InviteDialog.vue"; import InviteDialog from "@/components/InviteDialog.vue";
import { APP_SERVER, NotificationIface } from "@/constants/app"; import { APP_SERVER, NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db";
import { createInviteJwt, getHeaders } from "@/libs/endorserServer"; import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
interface Invite { interface Invite {
@ -91,11 +102,12 @@ interface Invite {
expiresAt: string; expiresAt: string;
jwt: string; jwt: string;
notes: string; notes: string;
redeemedAt: string | null;
redeemedBy: string | null; redeemedBy: string | null;
} }
@Component({ @Component({
components: { QuickNav, TopMessage, InviteDialog }, components: { ContactNameDialog, QuickNav, TopMessage, InviteDialog },
}) })
export default class InviteOneView extends Vue { export default class InviteOneView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@ -103,6 +115,7 @@ export default class InviteOneView extends Vue {
invites: Invite[] = []; invites: Invite[] = [];
activeDid: string = ""; activeDid: string = "";
apiServer: string = ""; apiServer: string = "";
contactsRedeemed = {};
isRegistered: boolean = false; isRegistered: boolean = false;
async mounted() { async mounted() {
@ -119,6 +132,17 @@ export default class InviteOneView extends Vue {
{ headers }, { headers },
); );
this.invites = response.data.data; this.invites = response.data.data;
const baseContacts = await db.contacts.toArray();
for (const invite of this.invites) {
const contact = baseContacts.find(
(contact) => contact.did === invite.redeemedBy,
);
if (contact) {
this.contactsRedeemed[invite.redeemedBy] = contact;
}
}
console.log("contactsRedeemed", this.contactsRedeemed);
} catch (error) { } catch (error) {
console.error("Error fetching invites:", error); console.error("Error fetching invites:", error);
this.$notify( this.$notify(
@ -140,6 +164,9 @@ export default class InviteOneView extends Vue {
getTruncatedRedeemedBy(redeemedBy: string | null): string { getTruncatedRedeemedBy(redeemedBy: string | null): string {
if (!redeemedBy) return ""; if (!redeemedBy) return "";
if (this.contactsRedeemed[redeemedBy]) {
return this.contactsRedeemed[redeemedBy].name;
}
if (redeemedBy.length <= 19) return redeemedBy; if (redeemedBy.length <= 19) return redeemedBy;
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`; return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
} }
@ -215,6 +242,7 @@ export default class InviteOneView extends Vue {
expiresAt: expiresAt, expiresAt: expiresAt,
jwt: inviteJwt, jwt: inviteJwt,
notes: notes, notes: notes,
redeemedAt: null,
redeemedBy: null, redeemedBy: null,
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -229,6 +257,28 @@ export default class InviteOneView extends Vue {
); );
} }
addNewContact(did) {
(this.$refs.contactNameDialog as ContactNameDialog).open((name) => {
// the person obviously registered themselves and this user already granted visibility, so we just add them
const contact = {
did: did,
name: name,
registered: true,
};
db.contacts.add(contact);
this.contactsRedeemed[did] = contact;
this.$notify(
{
group: "alert",
type: "success",
title: "Contact Added",
text: `${name} has been added to your contacts.`,
},
3000,
);
});
}
deleteInvite(inviteId: string, notes: string) { deleteInvite(inviteId: string, notes: string) {
this.$notify( this.$notify(
{ {

2
src/views/ProjectsView.vue

@ -61,7 +61,7 @@
<!-- New Project --> <!-- New Project -->
<button <button
v-if="isRegistered && showProjects" v-if="isRegistered && showProjects"
class="fixed right-6 top-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full" class="fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()" @click="onClickNewProject()"
> >
<fa icon="plus" class="fa-fw"></fa> <fa icon="plus" class="fa-fw"></fa>

Loading…
Cancel
Save