diff --git a/app/core/styles/index.css b/app/core/styles/index.css index 0bdeb6c..238f4b4 100644 --- a/app/core/styles/index.css +++ b/app/core/styles/index.css @@ -52,6 +52,14 @@ font-display: optional; } +.divide-y > :first-child { + @apply border-t; +} + +.divide-y > :last-child:not([hidden]) { + @apply border-b; +} + .h1 { @apply text-4xl font-extrabold tracking-tighter; } diff --git a/app/messages/components/conversations-list.tsx b/app/messages/components/conversations-list.tsx index 0529d27..f5340a6 100644 --- a/app/messages/components/conversations-list.tsx +++ b/app/messages/components/conversations-list.tsx @@ -17,7 +17,7 @@ export default function ConversationsList() { {Object.values(conversations).map(({ recipient, formattedPhoneNumber, messages }) => { const lastMessage = messages[messages.length - 1]!; return ( -
  • +
  • diff --git a/app/messages/pages/messages.tsx b/app/messages/pages/messages.tsx index 5cd9f27..4a94e12 100644 --- a/app/messages/pages/messages.tsx +++ b/app/messages/pages/messages.tsx @@ -23,7 +23,7 @@ const Messages: BlitzPage = () => { return ( <> -
    +

    Messages

    diff --git a/app/phone-calls/api/queue/fetch-calls.ts b/app/phone-calls/api/queue/fetch-calls.ts index c06a18c..0abb8c9 100644 --- a/app/phone-calls/api/queue/fetch-calls.ts +++ b/app/phone-calls/api/queue/fetch-calls.ts @@ -3,6 +3,9 @@ import { Queue } from "quirrel/blitz"; import db from "../../../../db"; import insertCallsQueue from "./insert-calls"; import getTwilioClient from "../../../../integrations/twilio"; +import appLogger from "../../../../integrations/logger"; + +const logger = appLogger.child({ queue: "fetch-calls" }); type Payload = { organizationId: string; @@ -15,7 +18,7 @@ const fetchCallsQueue = Queue("api/queue/fetch-calls", async ({ organiz include: { organization: true }, }); if (!phoneNumber) { - console.log("no phone number found"); + logger.warn(`No phone number found with id=${phoneNumberId}, organizationId=${organizationId}`); return; } diff --git a/app/phone-calls/api/webhook/call.ts b/app/phone-calls/api/webhook/call.ts index 3b6865c..5e6beaa 100644 --- a/app/phone-calls/api/webhook/call.ts +++ b/app/phone-calls/api/webhook/call.ts @@ -116,10 +116,9 @@ export default async function incomingCallHandler(req: BlitzApiRequest, res: Bli // TODO dial.client("unique id of device user is picking up with"); // TODO send notification // TODO db.phoneCall.create(...); + // TODO subscribe to status updates to update duration when call ends } - // TODO queue job to update duration when call ends - res.status(500).end(); } @@ -135,3 +134,32 @@ const outgoingBody = { From: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4", To: "+33613370787", }; + +const incomingBody = { + AccountSid: "ACa886d066be0832990d1cf43fb1d53362", + ApiVersion: "2010-04-01", + ApplicationSid: "APa43d85150ad6f6cf9869fbe1c1e36a66", + CallSid: "CA09a5d9a4cfacf2b56d66f8f743d2881a", + CallStatus: "ringing", + Called: "+33757592025", + CalledCity: "", + CalledCountry: "FR", + CalledState: "", + CalledZip: "", + Caller: "+33613370787", + CallerCity: "", + CallerCountry: "FR", + CallerState: "", + CallerZip: "", + Direction: "inbound", + From: "+33613370787", + FromCity: "", + FromCountry: "FR", + FromState: "", + FromZip: "", + To: "+33757592025", + ToCity: "", + ToCountry: "FR", + ToState: "", + ToZip: "", +}; diff --git a/app/phone-calls/components/phone-calls-list.tsx b/app/phone-calls/components/phone-calls-list.tsx index fe0bc21..a74356b 100644 --- a/app/phone-calls/components/phone-calls-list.tsx +++ b/app/phone-calls/components/phone-calls-list.tsx @@ -1,5 +1,9 @@ +import { PhoneMissedCallIcon, PhoneOutgoingIcon } from "@heroicons/react/solid"; + import { Direction } from "../../../db"; import usePhoneCalls from "../hooks/use-phone-calls"; +import { formatRelativeDate } from "../../core/helpers/date-formatter"; +import clsx from "clsx"; export default function PhoneCallsList() { const phoneCalls = usePhoneCalls()[0]; @@ -11,11 +15,27 @@ export default function PhoneCallsList() { return (
      {phoneCalls.map((phoneCall) => { - const recipient = Direction.Outbound ? phoneCall.to : phoneCall.from; + const isOutboundCall = phoneCall.direction === Direction.Outbound; + const isMissedCall = !isOutboundCall && phoneCall.duration === "0"; // TODO + const recipient = isOutboundCall + ? phoneCall.toMeta.formattedPhoneNumber + : phoneCall.fromMeta.formattedPhoneNumber; return ( -
    • -
      {recipient}
      -
      {new Date(phoneCall.createdAt).toLocaleString("en-US")}
      +
    • +
      + {isOutboundCall ? : null} +
      + +
      + {recipient} + + {isOutboundCall ? phoneCall.toMeta.country : phoneCall.fromMeta.country} + +
      + + + {formatRelativeDate(new Date(phoneCall.createdAt))} +
    • ); })} diff --git a/app/phone-calls/pages/calls.tsx b/app/phone-calls/pages/calls.tsx index a40161f..6abd83c 100644 --- a/app/phone-calls/pages/calls.tsx +++ b/app/phone-calls/pages/calls.tsx @@ -11,7 +11,7 @@ const PhoneCalls: BlitzPage = () => { return ( <> -
      +

      Calls

      diff --git a/app/phone-calls/pages/keypad.tsx b/app/phone-calls/pages/keypad.tsx index ea8ad38..9b0feb7 100644 --- a/app/phone-calls/pages/keypad.tsx +++ b/app/phone-calls/pages/keypad.tsx @@ -25,7 +25,6 @@ const KeypadPage: BlitzPage = () => { const longPressDigit = useAtom(longPressDigitAtom)[1]; const onZeroPressProps = { onPressStart() { - console.log("0"); pressDigit("0"); timeoutRef.current = setTimeout(() => { longPressDigit("+"); diff --git a/app/phone-calls/queries/get-phone-calls.ts b/app/phone-calls/queries/get-phone-calls.ts index 2ebff75..22b303d 100644 --- a/app/phone-calls/queries/get-phone-calls.ts +++ b/app/phone-calls/queries/get-phone-calls.ts @@ -1,5 +1,7 @@ import { resolver } from "blitz"; import { z } from "zod"; +import PhoneNumber from "awesome-phonenumber"; + import db, { Prisma } from "db"; const Body = z.object({ @@ -8,9 +10,272 @@ const Body = z.object({ export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ phoneNumberId }, context) => { const organizationId = context.session.orgId; - - return db.phoneCall.findMany({ + const phoneCalls = await db.phoneCall.findMany({ where: { organizationId, phoneNumberId }, orderBy: { createdAt: Prisma.SortOrder.desc }, }); + + return phoneCalls.map((phoneCall) => ({ + ...phoneCall, + fromMeta: getPhoneNumberMeta(phoneCall.from), + toMeta: getPhoneNumberMeta(phoneCall.to), + })); }); + +function getPhoneNumberMeta(rawPhoneNumber: string) { + const phoneNumber = new PhoneNumber(rawPhoneNumber); + const formattedPhoneNumber = + phoneNumber.getNumber("international") ?? phoneNumber.getNumber("national") ?? rawPhoneNumber; + + return { + formattedPhoneNumber, + country: countries[phoneNumber.getRegionCode()] ?? "unknown", + }; +} + +const countries: Record = { + AF: "Afghanistan", + AL: "Albania", + DZ: "Algeria", + AS: "American Samoa", + AD: "Andorra", + AO: "Angola", + AI: "Anguilla", + AQ: "Antarctica", + AG: "Antigua and Barbuda", + AR: "Argentina", + AM: "Armenia", + AW: "Aruba", + AU: "Australia", + AT: "Austria", + AZ: "Azerbaijan", + BS: "Bahamas", + BH: "Bahrain", + BD: "Bangladesh", + BB: "Barbados", + BY: "Belarus", + BE: "Belgium", + BZ: "Belize", + BJ: "Benin", + BM: "Bermuda", + BT: "Bhutan", + BO: "Bolivia", + BA: "Bosnia and Herzegovina", + BW: "Botswana", + BV: "Bouvet Island", + BR: "Brazil", + IO: "British Indian Ocean Territory", + BN: "Brunei", + BG: "Bulgaria", + BF: "Burkina Faso", + BI: "Burundi", + KH: "Cambodia", + CM: "Cameroon", + CA: "Canada", + CV: "Cape Verde", + KY: "Cayman Islands", + CF: "Central African Republic", + TD: "Chad", + CL: "Chile", + CN: "China", + CX: "Christmas Island", + CC: "Cocos (Keeling) Islands", + CO: "Colombia", + KM: "Comoros", + CG: "Congo", + CD: "Congo, the Democratic Republic of the", + CK: "Cook Islands", + CR: "Costa Rica", + CI: "Ivory Coast", + HR: "Croatia", + CU: "Cuba", + CY: "Cyprus", + CZ: "Czech Republic", + DK: "Denmark", + DJ: "Djibouti", + DM: "Dominica", + DO: "Dominican Republic", + EC: "Ecuador", + EG: "Egypt", + SV: "El Salvador", + GQ: "Equatorial Guinea", + ER: "Eritrea", + EE: "Estonia", + ET: "Ethiopia", + FK: "Falkland Islands (Malvinas)", + FO: "Faroe Islands", + FJ: "Fiji", + FI: "Finland", + FR: "France", + GF: "French Guiana", + PF: "French Polynesia", + TF: "French Southern Territories", + GA: "Gabon", + GM: "Gambia", + GE: "Georgia", + DE: "Germany", + GH: "Ghana", + GI: "Gibraltar", + GR: "Greece", + GL: "Greenland", + GD: "Grenada", + GP: "Guadeloupe", + GU: "Guam", + GT: "Guatemala", + GG: "Guernsey", + GN: "Guinea", + GW: "Guinea-Bissau", + GY: "Guyana", + HT: "Haiti", + HM: "Heard Island and McDonald Islands", + VA: "Holy See (Vatican City State)", + HN: "Honduras", + HK: "Hong Kong", + HU: "Hungary", + IS: "Iceland", + IN: "India", + ID: "Indonesia", + IR: "Iran, Islamic Republic of", + IQ: "Iraq", + IE: "Ireland", + IM: "Isle of Man", + IL: "Israel", + IT: "Italy", + JM: "Jamaica", + JP: "Japan", + JE: "Jersey", + JO: "Jordan", + KZ: "Kazakhstan", + KE: "Kenya", + KI: "Kiribati", + KP: "Korea, Democratic People's Republic of", + KR: "South Korea", + KW: "Kuwait", + KG: "Kyrgyzstan", + LA: "Lao People's Democratic Republic", + LV: "Latvia", + LB: "Lebanon", + LS: "Lesotho", + LR: "Liberia", + LY: "Libya", + LI: "Liechtenstein", + LT: "Lithuania", + LU: "Luxembourg", + MO: "Macao", + MK: "Macedonia, the former Yugoslav Republic of", + MG: "Madagascar", + MW: "Malawi", + MY: "Malaysia", + MV: "Maldives", + ML: "Mali", + MT: "Malta", + MH: "Marshall Islands", + MQ: "Martinique", + MR: "Mauritania", + MU: "Mauritius", + YT: "Mayotte", + MX: "Mexico", + FM: "Micronesia, Federated States of", + MD: "Moldova, Republic of", + MC: "Monaco", + MN: "Mongolia", + ME: "Montenegro", + MS: "Montserrat", + MA: "Morocco", + MZ: "Mozambique", + MM: "Burma", + NA: "Namibia", + NR: "Nauru", + NP: "Nepal", + NL: "Netherlands", + AN: "Netherlands Antilles", + NC: "New Caledonia", + NZ: "New Zealand", + NI: "Nicaragua", + NE: "Niger", + NG: "Nigeria", + NU: "Niue", + NF: "Norfolk Island", + MP: "Northern Mariana Islands", + NO: "Norway", + OM: "Oman", + PK: "Pakistan", + PW: "Palau", + PS: "Palestinian Territory, Occupied", + PA: "Panama", + PG: "Papua New Guinea", + PY: "Paraguay", + PE: "Peru", + PH: "Philippines", + PN: "Pitcairn", + PL: "Poland", + PT: "Portugal", + PR: "Puerto Rico", + QA: "Qatar", + RE: "RĂ©union", + RO: "Romania", + RU: "Russia", + RW: "Rwanda", + SH: "Saint Helena, Ascension and Tristan da Cunha", + KN: "Saint Kitts and Nevis", + LC: "Saint Lucia", + PM: "Saint Pierre and Miquelon", + VC: "St. Vincent and the Grenadines", + WS: "Samoa", + SM: "San Marino", + ST: "Sao Tome and Principe", + SA: "Saudi Arabia", + SN: "Senegal", + RS: "Serbia", + SC: "Seychelles", + SL: "Sierra Leone", + SG: "Singapore", + SK: "Slovakia", + SI: "Slovenia", + SB: "Solomon Islands", + SO: "Somalia", + ZA: "South Africa", + GS: "South Georgia and the South Sandwich Islands", + SS: "South Sudan", + ES: "Spain", + LK: "Sri Lanka", + SD: "Sudan", + SR: "Suriname", + SJ: "Svalbard and Jan Mayen", + SZ: "Swaziland", + SE: "Sweden", + CH: "Switzerland", + SY: "Syrian Arab Republic", + TW: "Taiwan", + TJ: "Tajikistan", + TZ: "Tanzania, United Republic of", + TH: "Thailand", + TL: "Timor-Leste", + TG: "Togo", + TK: "Tokelau", + TO: "Tonga", + TT: "Trinidad and Tobago", + TN: "Tunisia", + TR: "Turkey", + TM: "Turkmenistan", + TC: "Turks and Caicos Islands", + TV: "Tuvalu", + UG: "Uganda", + UA: "Ukraine", + AE: "United Arab Emirates", + GB: "United Kingdom", + US: "United States", + UM: "United States Minor Outlying Islands", + UY: "Uruguay", + UZ: "Uzbekistan", + VU: "Vanuatu", + VE: "Venezuela", + VN: "Vietnam", + VG: "Virgin Islands, British", + VI: "Virgin Islands, U.S.", + WF: "Wallis and Futuna", + EH: "Western Sahara", + YE: "Yemen", + ZM: "Zambia", + ZW: "Zimbabwe", +};