From 5d74dae63e3f38b406c996012bc55f4c9ce1b9f2 Mon Sep 17 00:00:00 2001 From: k2xl Date: Mon, 10 Jul 2023 19:10:08 -0400 Subject: [PATCH] Message email (#927) * wip. email notifications * Notification and push notif customization in settings * don't send if it is not enabled by user * on signup add all notification types that exist to the list * merge from main (#931) * fixes #928 * Revert "npm update" This reverts commit fe61f21c2ebca6df3c164acb61bc7f663acebf5c. * achievement unlock improvements * handle long user input * truncate in multiplayer --------- Co-authored-by: Spencer Spenst * updated package lock * small cleanup * test for notification preferences * fix test in signup * reset password test fix * make sure new users have notifications on * rename notification preferences test * add test for guest account * Update initializeLocalDb.ts * follow button tweak; package-lock revert * cleanup package-lock again * move to sep file * level of the day * fix a longstanding bug where we sent email digests to guest users * tweak css * fix test * level of day email * labels * move level of the day to notifications tab * email body css * set emailLog.type notification type * update deafult emailNotificationsList * fix tests --------- Co-authored-by: Spencer Spenst --- components/followButton.tsx | 40 +-- components/settings/settingsAccount.tsx | 56 +--- components/settings/settingsNotifications.tsx | 247 ++++++++++++++++++ constants/emailDigest.ts | 3 +- constants/testId.ts | 1 + helpers/emails/getEmailBody.tsx | 77 +++--- lib/initializeLocalDb.ts | 17 ++ models/db/emailLog.d.ts | 3 +- models/db/userConfig.d.ts | 3 + models/schemas/emailLogSchema.ts | 3 +- models/schemas/queueMessageSchema.ts | 3 +- models/schemas/userConfigSchema.ts | 11 + pages/api/follow/index.ts | 12 +- pages/api/internal-jobs/email-digest/index.ts | 27 +- pages/api/internal-jobs/worker/index.ts | 111 ++++---- .../worker/sendEmailNotification.ts | 39 +++ .../worker/sendPushNotification.ts | 70 +++++ pages/api/signup/index.ts | 5 + pages/api/user-config/index.ts | 17 +- pages/settings/[[...tab]].tsx | 11 +- tests/pages/api/follow/follow.test.ts | 18 +- .../email-single-email-a-day.test.ts | 22 +- .../api/reset-password/reset-password.test.ts | 8 +- tests/pages/api/review/review.test.ts | 6 +- tests/pages/api/signup/signup.test.ts | 20 +- .../notification-preferences.test.ts | 151 +++++++++++ tests/pages/users/users.page.test.ts | 2 +- 27 files changed, 755 insertions(+), 228 deletions(-) create mode 100644 components/settings/settingsNotifications.tsx create mode 100644 pages/api/internal-jobs/worker/sendEmailNotification.ts create mode 100644 pages/api/internal-jobs/worker/sendPushNotification.ts create mode 100644 tests/pages/api/user-config/notification-preferences.test.ts diff --git a/components/followButton.tsx b/components/followButton.tsx index 0b59b4447..250f4b276 100644 --- a/components/followButton.tsx +++ b/components/followButton.tsx @@ -12,32 +12,26 @@ interface FollowButtonProps { } export default function FollowButton({ isFollowing, onResponse, user }: FollowButtonProps) { - const [_isFollowing, setIsFollowing] = useState(isFollowing); + const [_isFollowing, setIsFollowing] = useState(isFollowing); + const [disabled, setDisabled] = useState(false); - const onFollowButtonPress = async (ele: React.MouseEvent) => { - // disable button and make it opacity 0.5 - const targ = ele.currentTarget; + const onFollowButtonPress = async () => { + setDisabled(true); - targ.disabled = true; - targ.style.opacity = '0.5'; + const queryParams = new URLSearchParams({ + action: GraphType.FOLLOW, + id: user._id.toString(), + targetModel: 'User', + }); - const res = await fetch('/api/follow', { + const res = await fetch(`/api/follow?${queryParams}`, { method: !_isFollowing ? 'PUT' : 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', - - body: JSON.stringify({ - action: GraphType.FOLLOW, - id: user._id, - targetModel: 'User', - }), }); - targ.disabled = false; - targ.style.opacity = '1'; - if (res.status === 200) { const resp: FollowData = await res.json(); @@ -50,13 +44,19 @@ export default function FollowButton({ isFollowing, onResponse, user }: FollowBu toast.dismiss(); toast.error('Something went wrong following this user'); } + + setDisabled(false); }; return ( - ); diff --git a/components/settings/settingsAccount.tsx b/components/settings/settingsAccount.tsx index 2921bb449..66b65c00a 100644 --- a/components/settings/settingsAccount.tsx +++ b/components/settings/settingsAccount.tsx @@ -1,9 +1,7 @@ import User from '@root/models/db/user'; import UserConfig from '@root/models/db/userConfig'; -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import toast from 'react-hot-toast'; -import Select from 'react-select'; -import { EmailDigestSettingTypes } from '../../constants/emailDigest'; interface SettingsAccountProps { user: User; @@ -13,8 +11,6 @@ interface SettingsAccountProps { export default function SettingsAccount({ user, userConfig }: SettingsAccountProps) { const [currentPassword, setCurrentPassword] = useState(''); const [email, setEmail] = useState(user.email); - const [emailDigest, setEmailDigest] = useState(userConfig?.emailDigest ?? EmailDigestSettingTypes.ONLY_NOTIFICATIONS); - const [isUserConfigLoading, setIsUserConfigLoading] = useState(false); const [password, setPassword] = useState(''); const [password2, setPassword2] = useState(''); const [showPlayStats, setShowPlayStats] = useState(userConfig?.showPlayStats ?? false); @@ -84,7 +80,6 @@ export default function SettingsAccount({ user, userConfig }: SettingsAccountPro ) { toast.dismiss(); toast.loading(`Updating ${property}...`); - setIsUserConfigLoading(true); fetch('/api/user-config', { method: 'PUT', @@ -107,8 +102,6 @@ export default function SettingsAccount({ user, userConfig }: SettingsAccountPro console.error(err); toast.dismiss(); toast.error(`Error updating ${property}`); - }).finally(() => { - setIsUserConfigLoading(false); }); } @@ -186,14 +179,6 @@ export default function SettingsAccount({ user, userConfig }: SettingsAccountPro const inputClass = 'shadow appearance-none border mb-2 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'; - const emailDigestLabels = useCallback(() => { - return { - [EmailDigestSettingTypes.DAILY]: 'Daily digest', - [EmailDigestSettingTypes.ONLY_NOTIFICATIONS]: 'Only for unread notifications', - [EmailDigestSettingTypes.NONE]: 'None', - }; - }, []); - async function clearTours() { const res = await fetch('/api/user-config', { method: 'PUT', @@ -249,45 +234,6 @@ export default function SettingsAccount({ user, userConfig }: SettingsAccountPro -
-
- Email Notifications -
-
- { + if (emailNotifs.length === allNotifs.length) { + updateUserConfig( + JSON.stringify({ + emailNotificationsList: [], + }), + 'notification settings', + ); + } else { + updateUserConfig( + JSON.stringify({ + emailNotificationsList: allNotifs, + }), + 'notification settings', + ); + } + }} + type='checkbox' + /> +
+ + +
+ + { + if (pushNotifs.length === allNotifs.length) { + updateUserConfig( + JSON.stringify({ + pushNotificationsList: [], + }), + 'notification settings', + ); + } else { + updateUserConfig( + JSON.stringify({ + pushNotificationsList: allNotifs, + }), + 'notification settings', + ); + } + }} + type='checkbox' + /> +
+ + + + + {allNotifs.map((notif) => { + const label = notifLabels[notif]; + + return ( + + + + + + updateNotifs(notif, 'email')} + type='checkbox' + /> + + + updateNotifs(notif, 'push')} + type='checkbox' + /> + + + ); + })} + + +
+ ); + + return ( +
+
+
+ Level of the day +
+
+