add import messages/calls ui feedback

This commit is contained in:
m5r 2021-09-25 07:07:40 +08:00
parent 8f0a6f7060
commit 2f45e1d9a8
14 changed files with 153 additions and 16 deletions

View File

@ -0,0 +1,23 @@
export default function PhoneInitLoader() {
return (
<div className="px-4 my-auto text-center space-y-2">
<svg
className="animate-spin mx-auto h-5 w-5 text-primary-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p>We&#39;re finalizing your cloud phone initialization.</p>
<p>
You don&#39;t have to refresh this page, we will do it automatically for you when your phone is ready.
</p>
</div>
);
}

View File

@ -35,7 +35,7 @@ const Layout: FunctionComponent<Props> = ({ children, title, pageTitle = title,
<div className="h-full w-full overflow-hidden fixed bg-gray-50"> <div className="h-full w-full overflow-hidden fixed bg-gray-50">
<div className="flex flex-col w-full h-full"> <div className="flex flex-col w-full h-full">
<div className="flex flex-col flex-1 w-full overflow-y-auto"> <div className="flex flex-col flex-1 w-full overflow-y-auto">
<main className="flex-1 my-0 h-full"> <main className="flex flex-col flex-1 my-0 h-full">
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</main> </main>
</div> </div>

View File

@ -37,6 +37,22 @@ const insertMessagesQueue = Queue<Payload>(
.sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime()); .sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime());
await db.message.createMany({ data: sms, skipDuplicates: true }); await db.message.createMany({ data: sms, skipDuplicates: true });
const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } });
if (!processingState) {
return;
}
if (processingState.hasFetchedCalls) {
await db.processingPhoneNumber.delete({
where: { organizationId_phoneNumberId: { organizationId, phoneNumberId } },
});
} else {
await db.processingPhoneNumber.update({
where: { organizationId_phoneNumberId: { organizationId, phoneNumberId } },
data: { hasFetchedMessages: true },
});
}
}, },
); );

View File

@ -3,9 +3,22 @@ import { IoChevronForward } from "react-icons/io5";
import getConversationsQuery from "../queries/get-conversations"; import getConversationsQuery from "../queries/get-conversations";
import { formatRelativeDate } from "../../core/helpers/date-formatter"; import { formatRelativeDate } from "../../core/helpers/date-formatter";
import { useEffect } from "react";
import PhoneInitLoader from "../../core/components/phone-init-loader";
export default function ConversationsList() { export default function ConversationsList() {
const conversations = useQuery(getConversationsQuery, {})[0]; const [conversations, query] = useQuery(getConversationsQuery, {});
useEffect(() => {
if (!conversations) {
const pollInterval = setInterval(() => query.refetch(), 1500);
return () => clearInterval(pollInterval);
}
}, [conversations, query]);
if (!conversations) {
return <PhoneInitLoader />;
}
if (Object.keys(conversations).length === 0) { if (Object.keys(conversations).length === 0) {
return <div>empty state</div>; return <div>empty state</div>;

View File

@ -26,9 +26,11 @@ const Messages: BlitzPage = () => {
<div className="flex flex-col space-y-6 p-3"> <div className="flex flex-col space-y-6 p-3">
<h2 className="text-3xl font-bold">Messages</h2> <h2 className="text-3xl font-bold">Messages</h2>
</div> </div>
<Suspense fallback="Loading..."> <section className="flex flex-grow flex-col">
<ConversationsList /> <Suspense fallback="Loading...">
</Suspense> <ConversationsList />
</Suspense>
</section>
<NewMessageButton onClick={() => setIsOpen(true)} /> <NewMessageButton onClick={() => setIsOpen(true)} />
<NewMessageBottomSheet /> <NewMessageBottomSheet />
</> </>

View File

@ -27,6 +27,12 @@ export default resolver.pipe(
} }
const phoneNumberId = organization.phoneNumbers[0]!.id; const phoneNumberId = organization.phoneNumbers[0]!.id;
const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } });
if (processingState && !processingState.hasFetchedMessages) {
return;
}
const messages = await db.message.findMany({ const messages = await db.message.findMany({
where: { organizationId, phoneNumberId }, where: { organizationId, phoneNumberId },
orderBy: { sentAt: Prisma.SortOrder.desc }, orderBy: { sentAt: Prisma.SortOrder.desc },

View File

@ -46,6 +46,14 @@ export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({
const phoneNumberId = phoneNumberSid; const phoneNumberId = phoneNumberSid;
await Promise.all([ await Promise.all([
db.processingPhoneNumber.create({
data: {
organizationId,
phoneNumberId,
hasFetchedMessages: false,
hasFetchedCalls: false,
},
}),
fetchMessagesQueue.enqueue( fetchMessagesQueue.enqueue(
{ organizationId, phoneNumberId }, { organizationId, phoneNumberId },
{ id: `fetch-messages-${organizationId}-${phoneNumberId}` }, { id: `fetch-messages-${organizationId}-${phoneNumberId}` },

View File

@ -34,6 +34,22 @@ const insertCallsQueue = Queue<Payload>("api/queue/insert-calls", async ({ calls
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true }); await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } });
if (!processingState) {
return;
}
if (processingState.hasFetchedMessages) {
await db.processingPhoneNumber.delete({
where: { organizationId_phoneNumberId: { organizationId, phoneNumberId } },
});
} else {
await db.processingPhoneNumber.update({
where: { organizationId_phoneNumberId: { organizationId, phoneNumberId } },
data: { hasFetchedCalls: true },
});
}
}); });
export default insertCallsQueue; export default insertCallsQueue;

View File

@ -1,12 +1,25 @@
import { useEffect } from "react";
import { HiPhoneMissedCall, HiPhoneOutgoing } from "react-icons/hi"; import { HiPhoneMissedCall, HiPhoneOutgoing } from "react-icons/hi";
import { Direction } from "../../../db";
import usePhoneCalls from "../hooks/use-phone-calls";
import { formatRelativeDate } from "../../core/helpers/date-formatter";
import clsx from "clsx"; import clsx from "clsx";
import { Direction } from "../../../db";
import PhoneInitLoader from "../../core/components/phone-init-loader";
import usePhoneCalls from "../hooks/use-phone-calls";
import { formatRelativeDate } from "../../core/helpers/date-formatter";
export default function PhoneCallsList() { export default function PhoneCallsList() {
const phoneCalls = usePhoneCalls()[0]; const [phoneCalls, query] = usePhoneCalls();
useEffect(() => {
if (!phoneCalls) {
const pollInterval = setInterval(() => query.refetch(), 1500);
return () => clearInterval(pollInterval);
}
}, [phoneCalls, query]);
if (!phoneCalls) {
return <PhoneInitLoader />;
}
if (phoneCalls.length === 0) { if (phoneCalls.length === 0) {
return <div>empty state</div>; return <div>empty state</div>;

View File

@ -14,9 +14,11 @@ const PhoneCalls: BlitzPage = () => {
<div className="flex flex-col space-y-6 py-3 pl-12"> <div className="flex flex-col space-y-6 py-3 pl-12">
<h2 className="text-3xl font-bold">Calls</h2> <h2 className="text-3xl font-bold">Calls</h2>
</div> </div>
<Suspense fallback="Loading..."> <section className="flex flex-grow flex-col">
<PhoneCallsList /> <Suspense fallback="Loading...">
</Suspense> <PhoneCallsList />
</Suspense>
</section>
</> </>
); );
}; };

View File

@ -10,6 +10,11 @@ const Body = z.object({
export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ phoneNumberId }, context) => { export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ phoneNumberId }, context) => {
const organizationId = context.session.orgId; const organizationId = context.session.orgId;
const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } });
if (processingState && !processingState.hasFetchedCalls) {
return;
}
const phoneCalls = await db.phoneCall.findMany({ const phoneCalls = await db.phoneCall.findMany({
where: { organizationId, phoneNumberId }, where: { organizationId, phoneNumberId },
orderBy: { createdAt: Prisma.SortOrder.desc }, orderBy: { createdAt: Prisma.SortOrder.desc },

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect } from "react";
import { useMutation, useRouter } from "blitz"; import { useMutation, useRouter } from "blitz";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import * as Panelbear from "@panelbear/panelbear-js"; import * as Panelbear from "@panelbear/panelbear-js";

View File

@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "ProcessingPhoneNumber" (
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"hasFetchedMessages" BOOLEAN NOT NULL,
"hasFetchedCalls" BOOLEAN NOT NULL,
"phoneNumberId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
PRIMARY KEY ("organizationId","phoneNumberId")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProcessingPhoneNumber_phoneNumberId_unique" ON "ProcessingPhoneNumber"("phoneNumberId");
-- AddForeignKey
ALTER TABLE "ProcessingPhoneNumber" ADD FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProcessingPhoneNumber" ADD FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -31,6 +31,7 @@ model Organization {
notificationSubscriptions NotificationSubscription[] notificationSubscriptions NotificationSubscription[]
messages Message[] messages Message[]
phoneCalls PhoneCall[] phoneCalls PhoneCall[]
processingPhoneNumbers ProcessingPhoneNumber[]
@@unique([id, twilioAccountSid]) @@unique([id, twilioAccountSid])
} }
@ -194,12 +195,25 @@ model PhoneNumber {
phoneCalls PhoneCall[] phoneCalls PhoneCall[]
notificationSubscriptions NotificationSubscription[] notificationSubscriptions NotificationSubscription[]
organization Organization @relation(fields: [organizationId], references: [id]) processingPhoneNumber ProcessingPhoneNumber?
organizationId String organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
@@unique([organizationId, id]) @@unique([organizationId, id])
} }
model ProcessingPhoneNumber {
createdAt DateTime @default(now()) @db.Timestamptz
hasFetchedMessages Boolean
hasFetchedCalls Boolean
phoneNumber PhoneNumber @relation(fields: [phoneNumberId], references: [id])
phoneNumberId String
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
@@id([organizationId, phoneNumberId])
}
model NotificationSubscription { model NotificationSubscription {
id String @id @default(uuid()) id String @id @default(uuid())
createdAt DateTime @default(now()) @db.Timestamptz createdAt DateTime @default(now()) @db.Timestamptz