list phone calls

This commit is contained in:
m5r 2021-07-22 00:48:49 +08:00
parent f55f1c5359
commit a262f61823
18 changed files with 268 additions and 105 deletions

View File

@ -1,16 +0,0 @@
export enum SmsType {
SENT = "sent",
RECEIVED = "received",
}
export type Sms = {
id: string;
customerId: string;
content: string;
from: string;
to: string;
type: SmsType;
twilioSid?: string;
// status: sent/delivered/received
sentAt: string; // timestampz
};

View File

@ -1,31 +1,44 @@
import { MessageStatus } from "twilio/lib/rest/api/v2010/account/message";
import appLogger from "../../lib/logger"; import appLogger from "../../lib/logger";
import supabase from "../supabase/server"; import supabase from "../supabase/server";
import type { Sms } from "./_types";
import { findCustomer } from "./customer"; import { findCustomer } from "./customer";
import { decrypt } from "./_encryption"; import { decrypt } from "./_encryption";
const logger = appLogger.child({ module: "sms" }); const logger = appLogger.child({ module: "message" });
export async function insertSms(messages: Omit<Sms, "id" | "twilioSid">): Promise<Sms> { export type Message = {
id: string;
customerId: string;
content: string;
from: string;
to: string;
direction: "inbound" | "outbound";
status: MessageStatus;
twilioSid?: string;
sentAt: string; // timestampz
};
export async function insertMessage(message: Omit<Message, "id" | "twilioSid">): Promise<Message> {
const { error, data } = await supabase const { error, data } = await supabase
.from<Sms>("sms") .from<Message>("message")
.insert(messages); .insert(message);
if (error) throw error; if (error) throw error;
return data![0]; return data![0];
} }
export async function insertManySms(messages: Omit<Sms, "id">[]) { export async function insertManyMessage(messages: Omit<Message, "id">[]) {
await supabase await supabase
.from<Sms>("sms") .from<Message>("message")
.insert(messages) .insert(messages)
.throwOnError(); .throwOnError();
} }
export async function findCustomerMessages(customerId: Sms["customerId"]): Promise<Sms[]> { export async function findCustomerMessages(customerId: Message["customerId"]): Promise<Message[]> {
const { error, data } = await supabase const { error, data } = await supabase
.from<Sms>("sms") .from<Message>("message")
.select("*") .select("*")
.eq("customerId", customerId); .eq("customerId", customerId);
@ -34,9 +47,9 @@ export async function findCustomerMessages(customerId: Sms["customerId"]): Promi
return data!; return data!;
} }
export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<Sms, "customerId" | "twilioSid">): Promise<Sms> { export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<Message, "customerId" | "twilioSid">): Promise<Message> {
const { error, data } = await supabase const { error, data } = await supabase
.from<Sms>("sms") .from<Message>("message")
.select("*") .select("*")
.eq("customerId", customerId) .eq("customerId", customerId)
.eq("twilioSid", twilioSid) .eq("twilioSid", twilioSid)
@ -47,17 +60,17 @@ export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<S
return data!; return data!;
} }
export async function setTwilioSid({ id, twilioSid }: Pick<Sms, "id" | "twilioSid">) { export async function setTwilioSid({ id, twilioSid }: Pick<Message, "id" | "twilioSid">) {
await supabase.from<Sms>("sms") await supabase.from<Message>("message")
.update({ twilioSid }) .update({ twilioSid })
.eq("id", id) .eq("id", id)
.throwOnError(); .throwOnError();
} }
export async function findConversation(customerId: Sms["customerId"], recipient: Sms["to"]): Promise<Sms[]> { export async function findConversation(customerId: Message["customerId"], recipient: Message["to"]): Promise<Message[]> {
const customer = await findCustomer(customerId); const customer = await findCustomer(customerId);
const { error, data } = await supabase const { error, data } = await supabase
.from<Sms>("sms") .from<Message>("message")
.select("*") .select("*")
.eq("customerId", customerId) .eq("customerId", customerId)
.or(`to.eq.${recipient},from.eq.${recipient}`); .or(`to.eq.${recipient},from.eq.${recipient}`);

View File

@ -0,0 +1,53 @@
import type { CallStatus } from "twilio/lib/rest/api/v2010/account/call";
import appLogger from "../../lib/logger";
import supabase from "../supabase/server";
const logger = appLogger.child({ module: "phone-call" });
export type PhoneCall = {
id: string;
customerId: string;
twilioSid: string;
from: string;
to: string;
status: CallStatus;
direction: "inbound" | "outbound";
duration: string;
createdAt: string; // timestampz
}
export async function insertPhoneCall(phoneCall: Omit<PhoneCall, "id" | "twilioSid">): Promise<PhoneCall> {
const { error, data } = await supabase
.from<PhoneCall>("phone-call")
.insert(phoneCall);
if (error) throw error;
return data![0];
}
export async function insertManyPhoneCalls(phoneCalls: Omit<PhoneCall, "id">[]) {
await supabase
.from<PhoneCall>("phone-call")
.insert(phoneCalls)
.throwOnError();
}
export async function findCustomerPhoneCalls(customerId: PhoneCall["customerId"]): Promise<PhoneCall[]> {
const { error, data } = await supabase
.from<PhoneCall>("phone-call")
.select("*")
.eq("customerId", customerId);
if (error) throw error;
return data!;
}
export async function setTwilioSid({ id, twilioSid }: Pick<PhoneCall, "id" | "twilioSid">) {
await supabase.from<PhoneCall>("phone-call")
.update({ twilioSid })
.eq("id", id)
.throwOnError();
}

View File

@ -1,11 +1,10 @@
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "react-query";
import axios from "axios"; import axios from "axios";
import type { Sms } from "../database/_types"; import type { Message } from "../database/message";
import { SmsType } from "../database/_types";
import useUser from "./use-user"; import useUser from "./use-user";
type UseConversationParams = { type UseConversationParams = {
initialData?: Sms[]; initialData?: Message[];
recipient: string; recipient: string;
} }
@ -16,11 +15,11 @@ export default function useConversation({
const user = useUser(); const user = useUser();
const getConversationUrl = `/api/conversation/${encodeURIComponent(recipient)}`; const getConversationUrl = `/api/conversation/${encodeURIComponent(recipient)}`;
const fetcher = async () => { const fetcher = async () => {
const { data } = await axios.get<Sms[]>(getConversationUrl); const { data } = await axios.get<Message[]>(getConversationUrl);
return data; return data;
}; };
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const getConversationQuery = useQuery<Sms[]>( const getConversationQuery = useQuery<Message[]>(
getConversationUrl, getConversationUrl,
fetcher, fetcher,
{ {
@ -31,21 +30,22 @@ export default function useConversation({
); );
const sendMessage = useMutation( const sendMessage = useMutation(
(sms: Pick<Sms, "to" | "content">) => axios.post(`/api/conversation/${sms.to}/send-message`, sms, { withCredentials: true }), (sms: Pick<Message, "to" | "content">) => axios.post(`/api/conversation/${sms.to}/send-message`, sms, { withCredentials: true }),
{ {
onMutate: async (sms: Pick<Sms, "to" | "content">) => { onMutate: async (sms: Pick<Message, "to" | "content">) => {
await queryClient.cancelQueries(getConversationUrl); await queryClient.cancelQueries(getConversationUrl);
const previousMessages = queryClient.getQueryData<Sms[]>(getConversationUrl); const previousMessages = queryClient.getQueryData<Message[]>(getConversationUrl);
if (previousMessages) { if (previousMessages) {
queryClient.setQueryData<Sms[]>(getConversationUrl, [ queryClient.setQueryData<Message[]>(getConversationUrl, [
...previousMessages, ...previousMessages,
{ {
id: "", // TODO: somehow generate an id id: "", // TODO: somehow generate an id
from: "", // TODO: get user's phone number from: "", // TODO: get user's phone number
customerId: user.userProfile!.id, customerId: user.userProfile!.id,
sentAt: new Date().toISOString(), sentAt: new Date().toISOString(),
type: SmsType.SENT, direction: "outbound",
status: "queued",
content: sms.content, content: sms.content,
to: sms.to, to: sms.to,
}, },
@ -56,7 +56,7 @@ export default function useConversation({
}, },
onError: (error, variables, context) => { onError: (error, variables, context) => {
if (context?.previousMessages) { if (context?.previousMessages) {
queryClient.setQueryData<Sms[]>(getConversationUrl, context.previousMessages); queryClient.setQueryData<Message[]>(getConversationUrl, context.previousMessages);
} }
}, },
onSettled: () => queryClient.invalidateQueries(getConversationUrl), onSettled: () => queryClient.invalidateQueries(getConversationUrl),

View File

@ -1,7 +1,7 @@
import Joi from "joi"; import Joi from "joi";
import { withApiAuthRequired } from "../../../../../lib/session-helpers"; import { withApiAuthRequired } from "../../../../../lib/session-helpers";
import { findConversation } from "../../../../database/sms"; import { findConversation } from "../../../../database/message";
import type { ApiError } from "../../_types"; import type { ApiError } from "../../_types";
import appLogger from "../../../../../lib/logger"; import appLogger from "../../../../../lib/logger";

View File

@ -1,14 +1,12 @@
import Joi from "joi"; import Joi from "joi";
import { SmsType } from "../../../../database/_types";
import { withApiAuthRequired } from "../../../../../lib/session-helpers"; import { withApiAuthRequired } from "../../../../../lib/session-helpers";
import { findConversation, insertSms } from "../../../../database/sms"; import { insertMessage } from "../../../../database/message";
import type { ApiError } from "../../_types"; import type { ApiError } from "../../_types";
import appLogger from "../../../../../lib/logger"; import appLogger from "../../../../../lib/logger";
import { findCustomerPhoneNumber } from "../../../../database/phone-number"; import { findCustomerPhoneNumber } from "../../../../database/phone-number";
import { encrypt } from "../../../../database/_encryption"; import { encrypt } from "../../../../database/_encryption";
import { findCustomer } from "../../../../database/customer"; import { findCustomer } from "../../../../database/customer";
import twilio from "twilio";
import sendMessageQueue from "../../queue/send-message"; import sendMessageQueue from "../../queue/send-message";
const logger = appLogger.child({ route: "/api/conversation" }); const logger = appLogger.child({ route: "/api/conversation" });
@ -60,11 +58,12 @@ export default withApiAuthRequired(async function sendMessageHandler(
const { phoneNumber } = await findCustomerPhoneNumber(customerId); const { phoneNumber } = await findCustomerPhoneNumber(customerId);
const body: Body = validationResult.value; const body: Body = validationResult.value;
const sms = await insertSms({ const sms = await insertMessage({
from: phoneNumber, from: phoneNumber,
customerId: customerId, customerId: customerId,
sentAt: new Date().toISOString(), sentAt: new Date().toISOString(),
type: SmsType.SENT, direction: "outbound",
status: "queued",
content: encrypt(body.content, customer.encryptionKey), content: encrypt(body.content, customer.encryptionKey),
to: body.to, to: body.to,
}); });

View File

@ -1,19 +1,19 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { insertSms } from "../../database/sms"; import { insertMessage } from "../../database/message";
import { SmsType } from "../../database/_types";
import { encrypt } from "../../database/_encryption"; import { encrypt } from "../../database/_encryption";
import twilio from "twilio"; import twilio from "twilio";
import fetchCallsQueue from "./queue/fetch-calls";
export default async function ddd(req: NextApiRequest, res: NextApiResponse) { export default async function ddd(req: NextApiRequest, res: NextApiResponse) {
const accountSid = "ACa886d066be0832990d1cf43fb1d53362"; const accountSid = "ACa886d066be0832990d1cf43fb1d53362";
const authToken = "8696a59a64b94bb4eba3548ed815953b"; const authToken = "8696a59a64b94bb4eba3548ed815953b";
// const ddd = await twilio(accountSid, authToken).incomingPhoneNumbers.list(); // const ddd = await twilio(accountSid, authToken).incomingPhoneNumbers.list();
const phoneNumber = "+33757592025"; const phoneNumber = "+33757592025";
const ddd = await twilio(accountSid, authToken) /*const ddd = await twilio(accountSid, authToken)
.messages .messages
.list({ .list({
to: phoneNumber, to: phoneNumber,
}); });*/
/*const ddd = await insertSms({ /*const ddd = await insertSms({
to: "+213", to: "+213",
@ -43,6 +43,9 @@ export default async function ddd(req: NextApiRequest, res: NextApiResponse) {
voiceApplicationSid: appSid, voiceApplicationSid: appSid,
});*/ });*/
const customerId = "bcb723bc-9706-4811-a964-cc03018bd2ac";
const ddd = fetchCallsQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` })
console.log("ddd", ddd); console.log("ddd", ddd);
return res.status(200).send(ddd); return res.status(200).send(ddd);

View File

@ -0,0 +1,40 @@
import { Queue } from "quirrel/next";
import twilio from "twilio";
import { findCustomerPhoneNumber } from "../../../database/phone-number";
import { findCustomer } from "../../../database/customer";
import insertCallsQueue from "./insert-calls";
type Payload = {
customerId: string;
}
const fetchCallsQueue = Queue<Payload>(
"api/queue/fetch-calls",
async ({ customerId }) => {
const customer = await findCustomer(customerId);
const phoneNumber = await findCustomerPhoneNumber(customerId);
const [callsSent, callsReceived] = await Promise.all([
twilio(customer.accountSid, customer.authToken)
.calls
.list({ from: phoneNumber.phoneNumber }),
twilio(customer.accountSid, customer.authToken)
.calls
.list({ to: phoneNumber.phoneNumber })
]);
const calls = [
...callsSent,
...callsReceived,
].sort((a, b) => a.dateCreated.getTime() - b.dateCreated.getTime());
await insertCallsQueue.enqueue({
customerId,
calls,
}, {
id: `insert-calls-${customerId}`,
});
},
);
export default fetchCallsQueue;

View File

@ -15,12 +15,14 @@ const fetchMessagesQueue = Queue<Payload>(
const customer = await findCustomer(customerId); const customer = await findCustomer(customerId);
const phoneNumber = await findCustomerPhoneNumber(customerId); const phoneNumber = await findCustomerPhoneNumber(customerId);
const messagesSent = await twilio(customer.accountSid, customer.authToken) const [messagesSent, messagesReceived] = await Promise.all([
.messages twilio(customer.accountSid, customer.authToken)
.list({ from: phoneNumber.phoneNumber }); .messages
const messagesReceived = await twilio(customer.accountSid, customer.authToken) .list({ from: phoneNumber.phoneNumber }),
.messages twilio(customer.accountSid, customer.authToken)
.list({ to: phoneNumber.phoneNumber }); .messages
.list({ to: phoneNumber.phoneNumber }),
]);
const messages = [ const messages = [
...messagesSent, ...messagesSent,
...messagesReceived, ...messagesReceived,

View File

@ -0,0 +1,32 @@
import { Queue } from "quirrel/next";
import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import type { PhoneCall } from "../../../database/phone-call";
import { insertManyPhoneCalls } from "../../../database/phone-call";
type Payload = {
customerId: string;
calls: CallInstance[];
}
const insertCallsQueue = Queue<Payload>(
"api/queue/insert-calls",
async ({ calls, customerId }) => {
const phoneCalls = calls
.map<Omit<PhoneCall, "id">>(call => ({
customerId,
twilioSid: call.sid,
from: call.from,
to: call.to,
direction: call.direction === "inbound" ? "inbound" : "outbound",
status: call.status,
duration: call.duration,
createdAt: new Date(call.dateCreated).toISOString(),
}))
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
await insertManyPhoneCalls(phoneCalls);
},
);
export default insertCallsQueue;

View File

@ -2,9 +2,8 @@ import { Queue } from "quirrel/next";
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import { findCustomer } from "../../../database/customer"; import { findCustomer } from "../../../database/customer";
import type { Sms } from "../../../database/_types"; import type { Message } from "../../../database/message";
import { SmsType } from "../../../database/_types"; import { insertManyMessage } from "../../../database/message";
import { insertManySms } from "../../../database/sms";
import { encrypt } from "../../../database/_encryption"; import { encrypt } from "../../../database/_encryption";
type Payload = { type Payload = {
@ -18,16 +17,20 @@ const insertMessagesQueue = Queue<Payload>(
const customer = await findCustomer(customerId); const customer = await findCustomer(customerId);
const encryptionKey = customer.encryptionKey; const encryptionKey = customer.encryptionKey;
const sms = messages.map<Omit<Sms, "id">>(message => ({ const sms = messages
customerId, .map<Omit<Message, "id">>(message => ({
content: encrypt(message.body, encryptionKey), customerId,
from: message.from, content: encrypt(message.body, encryptionKey),
to: message.to, from: message.from,
type: ["received", "receiving"].includes(message.status) ? SmsType.RECEIVED : SmsType.SENT, to: message.to,
messageSid: message.sid, status: message.status,
sentAt: message.dateSent.toISOString(), direction: message.direction === "inbound" ? "inbound" : "outbound",
})); twilioSid: message.sid,
await insertManySms(sms); sentAt: new Date(message.dateSent).toISOString(),
}))
.sort((a, b) => a.sentAt.localeCompare(b.sentAt));
await insertManyMessage(sms);
}, },
); );

View File

@ -3,7 +3,7 @@ import twilio from "twilio";
import { findCustomer } from "../../../database/customer"; import { findCustomer } from "../../../database/customer";
import { findCustomerPhoneNumber } from "../../../database/phone-number"; import { findCustomerPhoneNumber } from "../../../database/phone-number";
import { setTwilioSid } from "../../../database/sms"; import { setTwilioSid } from "../../../database/message";
type Payload = { type Payload = {
id: string; id: string;

View File

@ -9,14 +9,14 @@ type Payload = {
} }
const setTwilioWebhooks = Queue<Payload>( const setTwilioWebhooks = Queue<Payload>(
"api/queue/send-message", "api/queue/set-twilio-webhooks",
async ({ customerId }) => { async ({ customerId }) => {
const customer = await findCustomer(customerId); const customer = await findCustomer(customerId);
const twimlApp = await twilio(customer.accountSid, customer.authToken) const twimlApp = await twilio(customer.accountSid, customer.authToken)
.applications .applications
.create({ .create({
friendlyName: "Virtual Phone", friendlyName: "Virtual Phone",
smsUrl: "https://phone.mokhtar.dev/api/webhook/incoming-sms", smsUrl: "https://phone.mokhtar.dev/api/webhook/incoming-message",
smsMethod: "POST", smsMethod: "POST",
voiceUrl: "https://phone.mokhtar.dev/api/webhook/incoming-call", voiceUrl: "https://phone.mokhtar.dev/api/webhook/incoming-call",
voiceMethod: "POST", voiceMethod: "POST",

View File

@ -7,6 +7,7 @@ import appLogger from "../../../../lib/logger";
import { createPhoneNumber } from "../../../database/phone-number"; import { createPhoneNumber } from "../../../database/phone-number";
import { findCustomer } from "../../../database/customer"; import { findCustomer } from "../../../database/customer";
import fetchMessagesQueue from "../queue/fetch-messages"; import fetchMessagesQueue from "../queue/fetch-messages";
import fetchCallsQueue from "../queue/fetch-calls";
import setTwilioWebhooks from "../queue/set-twilio-webhooks"; import setTwilioWebhooks from "../queue/set-twilio-webhooks";
const logger = appLogger.child({ route: "/api/user/add-phone-number" }); const logger = appLogger.child({ route: "/api/user/add-phone-number" });
@ -49,6 +50,7 @@ export default withApiAuthRequired(async function addPhoneNumberHandler(req, res
await Promise.all([ await Promise.all([
fetchMessagesQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }), fetchMessagesQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }),
fetchCallsQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }),
setTwilioWebhooks.enqueue({ customerId }, { id: `set-twilio-webhooks-${customerId}` }), setTwilioWebhooks.enqueue({ customerId }, { id: `set-twilio-webhooks-${customerId}` }),
]); ]);

View File

@ -3,14 +3,13 @@ import twilio from "twilio";
import type { ApiError } from "../_types"; import type { ApiError } from "../_types";
import appLogger from "../../../../lib/logger"; import appLogger from "../../../../lib/logger";
import { Customer, findCustomerByPhoneNumber } from "../../../database/customer"; import { findCustomerByPhoneNumber } from "../../../database/customer";
import { insertSms } from "../../../database/sms"; import { insertMessage } from "../../../database/message";
import { SmsType } from "../../../database/_types";
import { encrypt } from "../../../database/_encryption"; import { encrypt } from "../../../database/_encryption";
const logger = appLogger.child({ route: "/api/webhook/incoming-sms" }); const logger = appLogger.child({ route: "/api/webhook/incoming-message" });
export default async function incomingSmsHandler(req: NextApiRequest, res: NextApiResponse) { export default async function incomingMessageHandler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") { if (req.method !== "POST") {
const statusCode = 405; const statusCode = 405;
const apiError: ApiError = { const apiError: ApiError = {
@ -41,7 +40,7 @@ export default async function incomingSmsHandler(req: NextApiRequest, res: NextA
try { try {
const phoneNumber = req.body.To; const phoneNumber = req.body.To;
const customer = await findCustomerByPhoneNumber(phoneNumber); const customer = await findCustomerByPhoneNumber(phoneNumber);
const url = "https://phone.mokhtar.dev/api/webhook/incoming-sms"; const url = "https://phone.mokhtar.dev/api/webhook/incoming-message";
const isRequestValid = twilio.validateRequest(customer.authToken!, twilioSignature, url, req.body); const isRequestValid = twilio.validateRequest(customer.authToken!, twilioSignature, url, req.body);
if (!isRequestValid) { if (!isRequestValid) {
const statusCode = 400; const statusCode = 400;
@ -55,11 +54,12 @@ export default async function incomingSmsHandler(req: NextApiRequest, res: NextA
return; return;
} }
await insertSms({ await insertMessage({
customerId: customer.id, customerId: customer.id,
to: req.body.To, to: req.body.To,
from: req.body.From, from: req.body.From,
type: SmsType.RECEIVED, status: "received",
direction: "inbound",
sentAt: req.body.DateSent, sentAt: req.body.DateSent,
content: encrypt(req.body.Body, customer.encryptionKey), content: encrypt(req.body.Body, customer.encryptionKey),
}); });

View File

@ -1,14 +1,15 @@
import type { InferGetServerSidePropsType, NextPage } from "next"; import type { InferGetServerSidePropsType, NextPage } from "next";
import { withPageOnboardingRequired } from "../../lib/session-helpers"; import { withPageOnboardingRequired } from "../../lib/session-helpers";
import Layout from "../components/layout"; import { findCustomerPhoneCalls } from "../database/phone-call";
import useUser from "../hooks/use-user"; import useUser from "../hooks/use-user";
import Layout from "../components/layout";
type Props = InferGetServerSidePropsType<typeof getServerSideProps>; type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const pageTitle = "Calls"; const pageTitle = "Calls";
const Calls: NextPage<Props> = (props) => { const Calls: NextPage<Props> = ({ phoneCalls }) => {
const { userProfile } = useUser(); const { userProfile } = useUser();
console.log("userProfile", userProfile); console.log("userProfile", userProfile);
@ -21,19 +22,34 @@ const Calls: NextPage<Props> = (props) => {
<Layout title={pageTitle}> <Layout title={pageTitle}>
<div className="flex flex-col space-y-6 p-6"> <div className="flex flex-col space-y-6 p-6">
<p>Calls page</p> <p>Calls page</p>
<ul className="divide-y">
{phoneCalls.map((phoneCall) => {
const recipient = phoneCall.direction === "outbound" ? phoneCall.to : phoneCall.from;
return (
<li key={phoneCall.twilioSid} className="flex flex-row justify-between py-2">
<div>{recipient}</div>
<div>{new Date(phoneCall.createdAt).toLocaleString("fr-FR")}</div>
</li>
)
})}
</ul>
</div> </div>
</Layout> </Layout>
); );
}; };
export const getServerSideProps = withPageOnboardingRequired( export const getServerSideProps = withPageOnboardingRequired(
async ({ res }) => { async ({ res }, user) => {
res.setHeader( res.setHeader(
"Cache-Control", "Cache-Control",
"private, s-maxage=15, stale-while-revalidate=59", "private, s-maxage=15, stale-while-revalidate=59",
); );
return { props: {} }; const phoneCalls = await findCustomerPhoneCalls(user.id);
return {
props: { phoneCalls },
};
}, },
); );

View File

@ -7,9 +7,8 @@ import clsx from "clsx";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { withPageOnboardingRequired } from "../../../lib/session-helpers"; import { withPageOnboardingRequired } from "../../../lib/session-helpers";
import { findConversation } from "../../database/sms"; import type { Message } from "../../database/message";
import type { Sms } from "../../database/_types"; import { findConversation } from "../../database/message";
import { SmsType } from "../../database/_types";
import supabase from "../../supabase/client"; import supabase from "../../supabase/client";
import useUser from "../../hooks/use-user"; import useUser from "../../hooks/use-user";
import useConversation from "../../hooks/use-conversation"; import useConversation from "../../hooks/use-conversation";
@ -17,7 +16,7 @@ import Layout from "../../components/layout";
type Props = { type Props = {
recipient: string; recipient: string;
conversation: Sms[]; conversation: Message[];
} }
type Form = { type Form = {
@ -60,7 +59,7 @@ const Messages: NextPage<Props> = (props) => {
} }
const subscription = supabase const subscription = supabase
.from<Sms>(`sms:customerId=eq.${userProfile.id}`) .from<Message>(`sms:customerId=eq.${userProfile.id}`)
.on("INSERT", (payload) => { .on("INSERT", (payload) => {
const message = payload.new; const message = payload.new;
if ([message.from, message.to].includes(recipient)) { if ([message.from, message.to].includes(recipient)) {
@ -98,12 +97,28 @@ const Messages: NextPage<Props> = (props) => {
</header> </header>
<div className="flex flex-col space-y-6 p-6"> <div className="flex flex-col space-y-6 p-6">
<ul> <ul>
{conversation!.map(message => { {conversation!.map((message, index) => {
const isOutbound = message.direction === "outbound";
const isSameSender = message.from === conversation![index + 1]?.from;
const isLast = index === conversation!.length;
return ( return (
<li key={message.id} className={clsx(message.type === SmsType.SENT ? "text-right" : "text-left")}> <li
{message.content} key={message.id}
className={clsx(
isSameSender || isLast ? "pb-3" : "pb-4",
isOutbound ? "text-right" : "text-left",
)}
>
<span
className={clsx(
"p-2 rounded-lg text-white",
isOutbound ? "bg-[#3194ff] rounded-br-none" : "bg-black rounded-bl-none",
)}
>
{message.content}
</span>
</li> </li>
) );
})} })}
</ul> </ul>
</div> </div>

View File

@ -2,9 +2,8 @@ import type { InferGetServerSidePropsType, NextPage } from "next";
import Link from "next/link"; import Link from "next/link";
import { withPageOnboardingRequired } from "../../../lib/session-helpers"; import { withPageOnboardingRequired } from "../../../lib/session-helpers";
import type { Sms } from "../../database/_types"; import type { Message } from "../../database/message";
import { SmsType } from "../../database/_types"; import { findCustomerMessages } from "../../database/message";
import { findCustomerMessages } from "../../database/sms";
import { findCustomer } from "../../database/customer"; import { findCustomer } from "../../database/customer";
import { decrypt } from "../../database/_encryption"; import { decrypt } from "../../database/_encryption";
import useUser from "../../hooks/use-user"; import useUser from "../../hooks/use-user";
@ -25,15 +24,17 @@ const Messages: NextPage<Props> = ({ conversations }) => {
<Layout title={pageTitle}> <Layout title={pageTitle}>
<div className="flex flex-col space-y-6 p-6"> <div className="flex flex-col space-y-6 p-6">
<p>Messages page</p> <p>Messages page</p>
<ul> <ul className="divide-y">
{Object.entries(conversations).map(([recipient, message]) => { {Object.entries(conversations).map(([recipient, message]) => {
return ( return (
<li key={recipient}> <li key={recipient} className="py-2">
<Link href={`/messages/${recipient}`}> <Link href={`/messages/${recipient}`}>
<a> <a className="flex flex-col">
<div>{recipient}</div> <div className="flex flex-row justify-between">
<strong>{recipient}</strong>
<div>{new Date(message.sentAt).toLocaleString("fr-FR")}</div>
</div>
<div>{message.content}</div> <div>{message.content}</div>
<div>{new Date(message.sentAt).toLocaleDateString()}</div>
</a> </a>
</Link> </Link>
</li> </li>
@ -59,10 +60,10 @@ export const getServerSideProps = withPageOnboardingRequired(
findCustomerMessages(user.id), findCustomerMessages(user.id),
]); ]);
let conversations: Record<Recipient, Sms> = {}; let conversations: Record<Recipient, Message> = {};
for (const message of messages) { for (const message of messages) {
let recipient: string; let recipient: string;
if (message.type === SmsType.SENT) { if (message.direction === "outbound") {
recipient = message.to; recipient = message.to;
} else { } else {
recipient = message.from; recipient = message.from;