add import messages/calls ui feedback
This commit is contained in:
parent
8f0a6f7060
commit
2f45e1d9a8
23
app/core/components/phone-init-loader.tsx
Normal file
23
app/core/components/phone-init-loader.tsx
Normal 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're finalizing your cloud phone initialization.</p>
|
||||||
|
<p>
|
||||||
|
You don't have to refresh this page, we will do it automatically for you when your phone is ready.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
|
<section className="flex flex-grow flex-col">
|
||||||
<Suspense fallback="Loading...">
|
<Suspense fallback="Loading...">
|
||||||
<ConversationsList />
|
<ConversationsList />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</section>
|
||||||
<NewMessageButton onClick={() => setIsOpen(true)} />
|
<NewMessageButton onClick={() => setIsOpen(true)} />
|
||||||
<NewMessageBottomSheet />
|
<NewMessageBottomSheet />
|
||||||
</>
|
</>
|
||||||
|
@ -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 },
|
||||||
|
@ -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}` },
|
||||||
|
@ -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;
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
|
<section className="flex flex-grow flex-col">
|
||||||
<Suspense fallback="Loading...">
|
<Suspense fallback="Loading...">
|
||||||
<PhoneCallsList />
|
<PhoneCallsList />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 },
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
@ -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[]
|
||||||
|
|
||||||
|
processingPhoneNumber ProcessingPhoneNumber?
|
||||||
organization Organization @relation(fields: [organizationId], references: [id])
|
organization Organization @relation(fields: [organizationId], references: [id])
|
||||||
organizationId String
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user