From 68570ff3d4985a9087ce4c53614384911f805019 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 1 Jun 2022 22:45:49 +0200 Subject: [PATCH] clean up notification stuff --- .../actions/notifications-subscription.ts | 13 +- .../messages/actions/messages.$recipient.tsx | 5 - .../settings/notifications-settings.tsx | 84 ++++++++++ .../settings/components/settings/toggle.tsx | 77 +++++++++ app/routes/__app/settings/notifications.tsx | 155 ++++-------------- app/routes/cache[.]manifest.ts | 20 --- app/utils/web-push.server.ts | 43 +++-- 7 files changed, 219 insertions(+), 178 deletions(-) create mode 100644 app/features/settings/components/settings/notifications-settings.tsx create mode 100644 app/features/settings/components/settings/toggle.tsx delete mode 100644 app/routes/cache[.]manifest.ts diff --git a/app/features/core/actions/notifications-subscription.ts b/app/features/core/actions/notifications-subscription.ts index 1dce1b0..4cd8431 100644 --- a/app/features/core/actions/notifications-subscription.ts +++ b/app/features/core/actions/notifications-subscription.ts @@ -61,9 +61,10 @@ async function subscribe(request: Request) { }); } catch (error: any) { if (error.code !== "P2002") { - logger.error(error); throw error; } + + logger.warn(`Duplicate insertion of subscription with endpoint=${subscription.endpoint}`); } return null; @@ -80,7 +81,15 @@ async function unsubscribe(request: Request) { } const endpoint = validation.data.subscriptionEndpoint; - await db.notificationSubscription.delete({ where: { endpoint } }); + try { + await db.notificationSubscription.delete({ where: { endpoint } }); + } catch (error: any) { + if (error.code !== "P2025") { + throw error; + } + + logger.warn(`Could not delete subscription with endpoint=${endpoint} because it has already been deleted`); + } return null; } diff --git a/app/features/messages/actions/messages.$recipient.tsx b/app/features/messages/actions/messages.$recipient.tsx index 13b9333..2bfb0fe 100644 --- a/app/features/messages/actions/messages.$recipient.tsx +++ b/app/features/messages/actions/messages.$recipient.tsx @@ -16,11 +16,6 @@ const action: ActionFunction = async ({ params, request }) => { const formData = Object.fromEntries(await request.formData()); const twilioClient = getTwilioClient(twilioAccount); try { - console.log({ - body: formData.content.toString(), - to: recipient, - from: phoneNumber!.number, - }); const message = await twilioClient.messages.create({ body: formData.content.toString(), to: recipient, diff --git a/app/features/settings/components/settings/notifications-settings.tsx b/app/features/settings/components/settings/notifications-settings.tsx new file mode 100644 index 0000000..c6925a3 --- /dev/null +++ b/app/features/settings/components/settings/notifications-settings.tsx @@ -0,0 +1,84 @@ +import Toggle from "~/features/settings/components/settings/toggle"; +import useNotifications from "~/features/core/hooks/use-notifications.client"; +import { useEffect, useState } from "react"; + +export default function NotificationsSettings() { + const { subscription, subscribe, unsubscribe } = useNotifications(); + const [notificationsEnabled, setNotificationsEnabled] = useState(!!subscription); + const [errorMessage, setErrorMessage] = useState(""); + const [isChanging, setIsChanging] = useState(false); + const onChange = async (checked: boolean) => { + if (isChanging) { + return; + } + + setIsChanging(true); + setNotificationsEnabled(checked); + setErrorMessage(""); + try { + if (checked) { + await subscribe(); + } else { + await unsubscribe(); + } + } catch (error: any) { + console.error(error); + setNotificationsEnabled(!checked); + if (!checked) { + unsubscribe().catch((error) => console.error(error)); + } + + switch (error.name) { + case "NotAllowedError": + setErrorMessage( + "Your browser is not allowing Shellphone to register push notifications for you. Please allow Shellphone's notifications in your browser's settings if you wish to receive them.", + ); + break; + case "TypeError": + setErrorMessage("Your browser does not support push notifications yet."); + break; + } + } finally { + setIsChanging(false); + } + }; + useEffect(() => { + setNotificationsEnabled(!!subscription); + }, [subscription]); + + return ( + + ); +} + +export function FallbackNotificationsSettings() { + return ( + + ); +} diff --git a/app/features/settings/components/settings/toggle.tsx b/app/features/settings/components/settings/toggle.tsx new file mode 100644 index 0000000..d691c4b --- /dev/null +++ b/app/features/settings/components/settings/toggle.tsx @@ -0,0 +1,77 @@ +import type { ElementType } from "react"; +import { Switch } from "@headlessui/react"; +import clsx from "clsx"; +import Alert from "~/features/core/components/alert"; + +type Props = { + as?: ElementType; + checked: boolean; + description?: string; + onChange(checked: boolean): void; + title: string; + error: null | { + title: string; + message: string; + }; + isLoading?: true; +}; + +export default function Toggle({ as, checked, description, onChange, title, error, isLoading }: Props) { + return ( + +
+
+ + {title} + + {description && ( + + {description} + + )} +
+ {isLoading ? ( +
+ +
+ ) : ( + + + )} +
+ {error !== null && } +
+ ); +} + +function Loader() { + return ( + + + + + ); +} diff --git a/app/routes/__app/settings/notifications.tsx b/app/routes/__app/settings/notifications.tsx index 4063b99..cfbb6f2 100644 --- a/app/routes/__app/settings/notifications.tsx +++ b/app/routes/__app/settings/notifications.tsx @@ -1,17 +1,31 @@ -import { type ElementType, useEffect, useState } from "react"; import type { ActionFunction } from "@remix-run/node"; import { ClientOnly } from "remix-utils"; -import { Switch } from "@headlessui/react"; -import clsx from "clsx"; - -import useNotifications from "~/features/core/hooks/use-notifications.client"; -import Alert from "~/features/core/components/alert"; import { Form } from "@remix-run/react"; + import { notify } from "~/utils/web-push.server"; import Button from "~/features/settings/components/button"; +import NotificationsSettings, { + FallbackNotificationsSettings, +} from "~/features/settings/components/settings/notifications-settings"; +import db from "~/utils/db.server"; export const action: ActionFunction = async () => { - await notify("PN4f11f0c4155dfb5d5ac8bbab2cc23cbc", { + const phoneNumber = await db.phoneNumber.findUnique({ + where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" }, + select: { + organization: { + select: { + memberships: { + select: { notificationSubscription: true }, + }, + }, + }, + }, + }); + const subscriptions = phoneNumber!.organization.memberships.flatMap( + (membership) => membership.notificationSubscription, + ); + await notify(subscriptions, { title: "+33 6 13 37 07 87", body: "wesh le zin, wesh la zine, copain copine mais si y'a moyen on pine", actions: [ @@ -25,134 +39,21 @@ export const action: ActionFunction = async () => { return null; }; -export default function NotificationsPage() { - return }>{() => }; -} - -function Notifications() { - const { subscription, subscribe, unsubscribe } = useNotifications(); - const [notificationsEnabled, setNotificationsEnabled] = useState(!!subscription); - const [errorMessage, setErrorMessage] = useState(""); - const [isChanging, setIsChanging] = useState(false); - const onChange = async (checked: boolean) => { - if (isChanging) { - return; - } - - setIsChanging(true); - setNotificationsEnabled(checked); - setErrorMessage(""); - try { - if (checked) { - await subscribe(); - } else { - await unsubscribe(); - } - } catch (error: any) { - console.error(error); - setNotificationsEnabled(!checked); - - switch (error.name) { - case "NotAllowedError": - setErrorMessage( - "Your browser is not allowing Shellphone to register push notifications for you. Please allow Shellphone's notifications in your browser's settings if you wish to receive them.", - ); - break; - case "TypeError": - setErrorMessage("Your browser does not support push notifications yet."); - break; - } - } finally { - setIsChanging(false); - } - }; - useEffect(() => { - setNotificationsEnabled(!!subscription); - }, [subscription]); - +export default function Notifications() { return (
+
+

Notifications

+ }>{() => } +
+
-
+
-
-
-

Notifications

-
-
    - -
- {errorMessage !== "" && } -
); } - -type ToggleProps = { - as?: ElementType; - checked: boolean; - description?: string; - onChange(checked: boolean): void; - title: string; -}; - -function Toggle({ as, checked, description, onChange, title }: ToggleProps) { - return ( - -
- - {title} - - {description && ( - - {description} - - )} -
- - -
- ); -} - -function Loader() { - return ( - - - - - ); -} diff --git a/app/routes/cache[.]manifest.ts b/app/routes/cache[.]manifest.ts deleted file mode 100644 index 15fcb80..0000000 --- a/app/routes/cache[.]manifest.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { LoaderFunction } from "@remix-run/node"; - -const manifest = `CACHE MANIFEST - -# Version 1.0000 - -NETWORK: -* -`; - -export const loader: LoaderFunction = async () => { - const headers = new Headers({ - "Content-Type": "text/cache-manifest", - "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", - Pragma: "no-cache", - Expires: "Thu, 01 Jan 1970 00:00:01 GMT", - }); - - return new Response(manifest, { headers }); -}; diff --git a/app/utils/web-push.server.ts b/app/utils/web-push.server.ts index c0012bb..98cffd7 100644 --- a/app/utils/web-push.server.ts +++ b/app/utils/web-push.server.ts @@ -1,37 +1,24 @@ import webpush, { type PushSubscription, WebPushError } from "web-push"; +import type { NotificationSubscription } from "@prisma/client"; import serverConfig from "~/config/config.server"; import db from "~/utils/db.server"; import logger from "~/utils/logger.server"; export type NotificationPayload = NotificationOptions & { - title: string; - body: string; + title: string; // max 50 characters + body: string; // max 150 characters }; -export async function notify(phoneNumberId: string, payload: NotificationPayload) { +export async function notify(subscriptions: NotificationSubscription[], payload: NotificationPayload) { webpush.setVapidDetails("mailto:mokht@rmi.al", serverConfig.webPush.publicKey, serverConfig.webPush.privateKey); - - const phoneNumber = await db.phoneNumber.findUnique({ - where: { id: phoneNumberId }, - select: { - organization: { - select: { - memberships: { - select: { notificationSubscription: true }, - }, - }, - }, - }, + const title = truncate(payload.title, 50); + const body = truncate(payload.body, 150); + const _payload = JSON.stringify({ + ...payload, + title, + body, }); - if (!phoneNumber) { - // TODO - return; - } - - const subscriptions = phoneNumber.organization.memberships.flatMap( - (membership) => membership.notificationSubscription, - ); await Promise.all( subscriptions.map(async (subscription) => { @@ -44,7 +31,7 @@ export async function notify(phoneNumberId: string, payload: NotificationPayload }; try { - await webpush.sendNotification(webPushSubscription, JSON.stringify(payload)); + await webpush.sendNotification(webPushSubscription, _payload); } catch (error: any) { logger.error(error); if (error instanceof WebPushError) { @@ -55,3 +42,11 @@ export async function notify(phoneNumberId: string, payload: NotificationPayload }), ); } + +function truncate(str: string, maxLength: number) { + if (str.length <= maxLength) { + return str; + } + + return str.substring(0, maxLength - 1) + "\u2026"; +}