You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
392 lines
12 KiB
392 lines
12 KiB
<template>
|
|
<QuickNav selected="Invite" />
|
|
<TopMessage />
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Back -->
|
|
<div class="text-lg text-center font-light relative px-7">
|
|
<h1
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
@click="$router.back()"
|
|
>
|
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Heading -->
|
|
<h1 class="text-4xl text-center font-light">Invitations</h1>
|
|
|
|
<ul class="ml-8 mt-4 list-outside list-disc w-5/6">
|
|
<li>
|
|
Note when sending
|
|
<span
|
|
v-if="!showAppleWarning"
|
|
class="text-blue-500 cursor-pointer"
|
|
@click="showAppleWarning = !showAppleWarning"
|
|
>
|
|
to Apple users...
|
|
</span>
|
|
<span v-else>
|
|
to Apple users: their links often fail because their device cuts off
|
|
part of the link. You might need to send it to them some other way,
|
|
like in an email.
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- New Project -->
|
|
<button
|
|
v-if="isRegistered"
|
|
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()"
|
|
>
|
|
<fa icon="plus" class="fa-fw"></fa>
|
|
</button>
|
|
|
|
<InviteDialog ref="inviteDialog" />
|
|
|
|
<!-- Invites Table -->
|
|
<div v-if="invites.length" class="mt-6">
|
|
<table class="min-w-full bg-white">
|
|
<thead>
|
|
<tr>
|
|
<th class="py-2">
|
|
ID
|
|
<br />
|
|
(click for link)
|
|
</th>
|
|
<th class="py-2">Notes</th>
|
|
<th class="py-2">Expires At</th>
|
|
<th class="py-2">Redeemed</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="invite in invites"
|
|
:key="invite.inviteIdentifier"
|
|
class="border-t py-2"
|
|
>
|
|
<td>
|
|
<span
|
|
v-if="
|
|
!invite.redeemedAt &&
|
|
invite.expiresAt > new Date().toISOString()
|
|
"
|
|
@click="
|
|
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
|
|
"
|
|
class="text-center text-blue-500 cursor-pointer"
|
|
:title="inviteLink(invite.jwt)"
|
|
>
|
|
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
|
</span>
|
|
<span
|
|
v-else
|
|
@click="
|
|
showInvite(
|
|
invite.inviteIdentifier,
|
|
!!invite.redeemedAt,
|
|
invite.expiresAt < new Date().toISOString(),
|
|
)
|
|
"
|
|
class="text-center text-slate-500 cursor-pointer"
|
|
:title="inviteLink(invite.jwt)"
|
|
>
|
|
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
|
</span>
|
|
</td>
|
|
<td class="text-left" :data-testId="inviteLink(invite.jwt)">
|
|
{{ invite.notes }}
|
|
</td>
|
|
<td class="text-center">
|
|
{{ invite.expiresAt.substring(0, 10) }}
|
|
</td>
|
|
<td class="text-center">
|
|
{{ invite.redeemedAt?.substring(0, 10) }}
|
|
<br />
|
|
{{ 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>
|
|
<fa
|
|
icon="trash-can"
|
|
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
|
|
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<ContactNameDialog ref="contactNameDialog" />
|
|
</div>
|
|
<p v-else class="mt-6 text-center">No invites found.</p>
|
|
</section>
|
|
</template>
|
|
<script lang="ts">
|
|
import axios from "axios";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { useClipboard } from "@vueuse/core";
|
|
|
|
import ContactNameDialog from "@/components/ContactNameDialog.vue";
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
import TopMessage from "@/components/TopMessage.vue";
|
|
import InviteDialog from "@/components/InviteDialog.vue";
|
|
import { APP_SERVER, NotificationIface } from "@/constants/app";
|
|
import { db, retrieveSettingsForActiveAccount } from "@/db";
|
|
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
|
|
|
|
interface Invite {
|
|
inviteIdentifier: string;
|
|
expiresAt: string;
|
|
jwt: string;
|
|
notes: string;
|
|
redeemedAt: string | null;
|
|
redeemedBy: string | null;
|
|
}
|
|
|
|
@Component({
|
|
components: { ContactNameDialog, QuickNav, TopMessage, InviteDialog },
|
|
})
|
|
export default class InviteOneView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
invites: Invite[] = [];
|
|
activeDid: string = "";
|
|
apiServer: string = "";
|
|
contactsRedeemed = {};
|
|
isRegistered: boolean = false;
|
|
showAppleWarning = false;
|
|
|
|
async mounted() {
|
|
try {
|
|
await db.open();
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
const headers = await getHeaders(this.activeDid);
|
|
const response = await axios.get(
|
|
this.apiServer + "/api/userUtil/invite",
|
|
{ headers },
|
|
);
|
|
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;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching invites:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Load Error",
|
|
text: "Got an error loading your invites.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
getTruncatedInviteId(inviteId: string): string {
|
|
if (inviteId.length <= 9) return inviteId;
|
|
return `${inviteId.slice(0, 6)}...`;
|
|
}
|
|
|
|
getTruncatedRedeemedBy(redeemedBy: string | null): string {
|
|
if (!redeemedBy) return "";
|
|
if (this.contactsRedeemed[redeemedBy]) {
|
|
return this.contactsRedeemed[redeemedBy].name;
|
|
}
|
|
if (redeemedBy.length <= 19) return redeemedBy;
|
|
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
|
|
}
|
|
|
|
inviteLink(jwt: string): string {
|
|
return APP_SERVER + "/contacts?inviteJwt=" + jwt;
|
|
}
|
|
|
|
copyInviteAndNotify(inviteId: string, jwt: string) {
|
|
useClipboard().copy(this.inviteLink(jwt));
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Copied",
|
|
text: "Your clipboard now contains the link for invite " + inviteId,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
|
let message = `Your clipboard now contains the invite ID ${inviteId}`;
|
|
if (redeemed) {
|
|
message += " (This invite has been used.)";
|
|
} else if (expired) {
|
|
message += " (This invite has expired.)";
|
|
}
|
|
useClipboard().copy(inviteId);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Copied",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
lookForErrorAndNotify(error, title: string, defaultMessage: string) {
|
|
console.error(title, "-", error);
|
|
let message = defaultMessage;
|
|
if (error.response && error.response.data && error.response.data.error) {
|
|
if (error.response.data.error.message) {
|
|
message = error.response.data.error.message;
|
|
} else {
|
|
message = error.response.data.error;
|
|
}
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: title,
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
async createInvite() {
|
|
const inviteIdentifier =
|
|
Math.random().toString(36).substring(2) +
|
|
Math.random().toString(36).substring(2) +
|
|
Math.random().toString(36).substring(2);
|
|
(this.$refs.inviteDialog as InviteDialog).open(
|
|
inviteIdentifier,
|
|
async (notes, expiresAt) => {
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
if (!expiresAt) {
|
|
throw {
|
|
response: {
|
|
data: { error: "You must select an expiration date." },
|
|
},
|
|
};
|
|
}
|
|
const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000;
|
|
const inviteJwt = await createInviteJwt(
|
|
this.activeDid,
|
|
undefined,
|
|
inviteIdentifier,
|
|
expiresIn,
|
|
);
|
|
await axios.post(
|
|
this.apiServer + "/api/userUtil/invite",
|
|
{ inviteJwt: inviteJwt, notes: notes },
|
|
{ headers },
|
|
);
|
|
this.invites.push({
|
|
inviteIdentifier: inviteIdentifier,
|
|
expiresAt: expiresAt,
|
|
jwt: inviteJwt,
|
|
notes: notes,
|
|
redeemedAt: null,
|
|
redeemedBy: null,
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
this.lookForErrorAndNotify(
|
|
error,
|
|
"Error Creating Invite",
|
|
"Got an error creating your invite.",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
addNewContact(did) {
|
|
(this.$refs.contactNameDialog as ContactNameDialog).open(
|
|
"Who Sent You The Invite?",
|
|
"Their name will be added to your contact list.",
|
|
(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) {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Delete Invite?",
|
|
text: `Are you sure you want to erase the invite for "${notes}"? (There is no undo.)`,
|
|
onYes: async () => {
|
|
const headers = await getHeaders(this.activeDid);
|
|
try {
|
|
const result = await axios.delete(
|
|
this.apiServer + "/api/userUtil/invite/" + inviteId,
|
|
{ headers },
|
|
);
|
|
if (result.status !== 204) {
|
|
throw result.data;
|
|
}
|
|
this.invites = this.invites.filter(
|
|
(invite) => invite.inviteIdentifier !== inviteId,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Deleted",
|
|
text: "Invite deleted.",
|
|
},
|
|
3000,
|
|
);
|
|
} catch (e) {
|
|
this.lookForErrorAndNotify(
|
|
e,
|
|
"Error Deleting Invite",
|
|
"Got an error deleting your invite.",
|
|
);
|
|
}
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
</script>
|
|
|