send message to new recipient

This commit is contained in:
m5r
2021-08-01 15:40:18 +08:00
parent 7d7c4cb495
commit 56e8880715
15 changed files with 456 additions and 39 deletions

View File

@ -8,7 +8,8 @@ import NewMessageArea from "./new-message-area";
export default function Conversation() {
const router = useRouter();
const conversation = useConversation(router.params.recipient)[0];
const recipient = decodeURIComponent(router.params.recipient);
const conversation = useConversation(recipient)[0];
const messagesListRef = useRef<HTMLUListElement>(null);
useEffect(() => {
@ -75,7 +76,7 @@ export default function Conversation() {
</ul>
</div>
<Suspense fallback={null}>
<NewMessageArea />
<NewMessageArea recipient={recipient} />
</Suspense>
</>
);

View File

@ -8,14 +8,18 @@ import { Direction, Message, MessageStatus } from "../../../db";
import getConversationsQuery from "../queries/get-conversations";
import useCurrentCustomer from "../../core/hooks/use-current-customer";
import useCustomerPhoneNumber from "../../core/hooks/use-customer-phone-number";
import { FunctionComponent } from "react";
type Form = {
content: string;
};
export default function NewMessageArea() {
const router = useRouter();
const recipient = router.params.recipient;
type Props = {
recipient: string;
onSend?: () => void;
};
const NewMessageArea: FunctionComponent<Props> = ({ recipient, onSend }) => {
const { customer } = useCurrentCustomer();
const phoneNumber = useCustomerPhoneNumber();
const sendMessageMutation = useMutation(sendMessage)[0];
@ -30,6 +34,10 @@ export default function NewMessageArea() {
formState: { isSubmitting },
} = useForm<Form>();
const onSubmit = handleSubmit(async ({ content }) => {
if (!recipient) {
return;
}
if (isSubmitting) {
return;
}
@ -60,6 +68,7 @@ export default function NewMessageArea() {
{ refetch: false }
);
setValue("content", "");
onSend?.();
await sendMessageMutation({ to: recipient, content });
await refetchConversations({ cancelRefetch: true });
});
@ -83,7 +92,9 @@ export default function NewMessageArea() {
</button>
</form>
);
}
};
export default NewMessageArea;
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {

View File

@ -0,0 +1,53 @@
import { Suspense, useState } from "react";
import { BottomSheet } from "react-spring-bottom-sheet";
import { atom, useAtom } from "jotai";
import { useRouter, Routes } from "blitz";
import "react-spring-bottom-sheet/dist/style.css";
import NewMessageArea from "./new-message-area";
export const bottomSheetOpenAtom = atom(false);
export default function NewMessageBottomSheet() {
const router = useRouter();
const [isOpen, setIsOpen] = useAtom(bottomSheetOpenAtom);
const [recipient, setRecipient] = useState("");
return (
<BottomSheet
open={isOpen}
onDismiss={() => setIsOpen(false)}
snapPoints={({ maxHeight }) => maxHeight / 2}
header={
<div className="w-full flex items-center justify-center p-4 text-black relative">
<span className="font-semibold text-base">New Message</span>
<button onClick={() => setIsOpen(false)} className="absolute right-4">
Cancel
</button>
</div>
}
>
<main className="flex flex-col h-full overflow-hidden">
<div className="flex items-center p-4 border-t border-b">
<span className="mr-4 text-[#333]">To:</span>
<input
onChange={(event) => setRecipient(event.target.value)}
className="bg-none border-none outline-none flex-1 text-black"
/>
</div>
<Suspense fallback={null}>
<NewMessageArea
recipient={recipient}
onSend={() => {
router
.push(Routes.ConversationPage({ recipient }))
.then(() => setIsOpen(false));
}}
/>
</Suspense>
</main>
</BottomSheet>
);
}

View File

@ -0,0 +1,21 @@
import type { FunctionComponent, MouseEventHandler } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEdit } from "@fortawesome/pro-regular-svg-icons";
type Props = {
onClick: MouseEventHandler<HTMLButtonElement>;
};
const NewMessageButton: FunctionComponent<Props> = ({ onClick }) => {
return (
<button
onClick={onClick}
className="absolute bottom-20 right-6
w-14 h-14 bg-gray-800 rounded-full hover:bg-gray-900 active:shadow-lg shadow transition ease-in duration-200 focus:outline-none"
>
<FontAwesomeIcon size="lg" className="m-auto pl-1.5 text-white w-8 h-8" icon={faEdit} />
</button>
);
};
export default NewMessageButton;

View File

@ -14,6 +14,7 @@ export default function useConversation(recipient: string) {
return conversations[recipient]!;
},
keepPreviousData: true,
}
);
}

View File

@ -0,0 +1,14 @@
import { useQuery } from "blitz";
import getConversationsQuery from "../queries/get-conversations";
export default function useKnownRecipients() {
return useQuery(
getConversationsQuery,
{},
{
select(conversations) {
return Object.keys(conversations);
},
}
);
}

View File

@ -1,11 +1,15 @@
import { resolver } from "blitz";
import { z } from "zod";
import twilio from "twilio";
import db, { Direction, MessageStatus } from "../../../db";
import getCurrentCustomer from "../../customers/queries/get-current-customer";
import getCustomerPhoneNumber from "../../phone-numbers/queries/get-customer-phone-number";
import { encrypt } from "../../../db/_encryption";
import sendMessageQueue from "../../api/queue/send-message";
import appLogger from "../../../integrations/logger";
const logger = appLogger.child({ mutation: "send-message" });
const Body = z.object({
content: z.string(),
@ -17,6 +21,15 @@ export default resolver.pipe(
resolver.authorize(),
async ({ content, to }, context) => {
const customer = await getCurrentCustomer(null, context);
try {
await twilio(customer!.accountSid!, customer!.authToken!)
.lookups.v1.phoneNumbers(to)
.fetch();
} catch (error) {
logger.error(error);
return;
}
const customerId = customer!.id;
const customerPhoneNumber = await getCustomerPhoneNumber({ customerId }, context);

View File

@ -1,22 +1,28 @@
import { Suspense } from "react";
import { Suspense, useState } from "react";
import type { BlitzPage } from "blitz";
import { Routes } from "blitz";
import { useAtom } from "jotai";
import Layout from "../../core/layouts/layout";
import ConversationsList from "../components/conversations-list";
import NewMessageButton from "../components/new-message-button";
import NewMessageBottomSheet, { bottomSheetOpenAtom } from "../components/new-message-bottom-sheet";
import useRequireOnboarding from "../../core/hooks/use-require-onboarding";
const Messages: BlitzPage = () => {
useRequireOnboarding();
const setIsOpen = useAtom(bottomSheetOpenAtom)[1];
return (
<>
<div className="flex flex-col space-y-6 p-6">
<p>Messages page</p>
<h2 className="text-3xl font-bold">Messages</h2>
</div>
<Suspense fallback="Loading...">
<ConversationsList />
</Suspense>
<NewMessageButton onClick={() => setIsOpen(true)} />
<NewMessageBottomSheet />
</>
);
};

View File

@ -16,7 +16,7 @@ const ConversationPage: BlitzPage = () => {
useRequireOnboarding();
const router = useRouter();
const recipient = router.params.recipient;
const recipient = decodeURIComponent(router.params.recipient);
return (
<>
@ -39,7 +39,7 @@ const ConversationPage: BlitzPage = () => {
ConversationPage.getLayout = function ConversationLayout(page) {
const router = useRouter();
const recipient = router.params.recipient;
const recipient = decodeURIComponent(router.params.recipient);
const pageTitle = `Messages with ${recipient}`;
return (