From 7135d066733b2114fa85e1edf35e1d0f7530b5de Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Thu, 16 Mar 2023 11:00:29 +0600 Subject: [PATCH] notifications: notification settings page --- src/api/notifications/settings.ts | 18 ++-- src/api/notifications/types.ts | 2 + src/components/input/InputRow/index.tsx | 19 ++++ .../input/InputRow/styles.module.scss | 8 ++ .../input/Toggle/styles.module.scss | 4 + .../NotificationSettingsForm/index.tsx | 91 +++++++++++++++++++ .../styles.module.scss | 11 +++ .../ProfileSidebarNotifications/index.tsx | 6 +- .../NotificationSettings/index.tsx | 30 ++++++ .../profile/ProfileAccounts/index.tsx | 20 ++-- src/hooks/auth/useOAuth.ts | 12 +++ .../notifications/useNotificationSettings.ts | 14 +-- .../useNotificationSettingsForm.ts | 46 ++++++++++ .../useNotificationSettingsRequest.ts | 15 +-- src/hooks/useFormAutosubmit.ts | 20 ++++ src/types/notifications/index.ts | 10 ++ .../notificationSettingsFromRequest.ts | 28 ++++++ 17 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 src/components/input/InputRow/index.tsx create mode 100644 src/components/input/InputRow/styles.module.scss create mode 100644 src/components/notifications/NotificationSettingsForm/index.tsx create mode 100644 src/components/notifications/NotificationSettingsForm/styles.module.scss create mode 100644 src/containers/notifications/NotificationSettings/index.tsx create mode 100644 src/hooks/notifications/useNotificationSettingsForm.ts create mode 100644 src/hooks/useFormAutosubmit.ts create mode 100644 src/utils/notifications/notificationSettingsFromRequest.ts diff --git a/src/api/notifications/settings.ts b/src/api/notifications/settings.ts index 6947bd81..232bbb94 100644 --- a/src/api/notifications/settings.ts +++ b/src/api/notifications/settings.ts @@ -1,17 +1,22 @@ import { API } from '~/constants/api'; +import { NotificationSettings } from '~/types/notifications'; import { api, cleanResult } from '~/utils/api'; +import { + notificationSettingsFromRequest, + notificationSettingsToRequest, +} from '~/utils/notifications/notificationSettingsFromRequest'; import { ApiGetNotificationSettingsResponse, ApiGetNotificationsResponse, ApiUpdateNotificationSettingsResponse, - ApiUpdateNotificationSettingsRequest, } from './types'; -export const apiGetNotificationSettings = () => +export const apiGetNotificationSettings = (): Promise => api .get(API.NOTIFICATIONS.SETTINGS) - .then(cleanResult); + .then(cleanResult) + .then(notificationSettingsFromRequest); export const apiGetNotifications = () => api @@ -19,11 +24,12 @@ export const apiGetNotifications = () => .then(cleanResult); export const apiUpdateNotificationSettings = ( - settings: ApiUpdateNotificationSettingsRequest, + settings: Partial, ) => api .post( API.NOTIFICATIONS.SETTINGS, - settings, + notificationSettingsToRequest(settings), ) - .then(cleanResult); + .then(cleanResult) + .then(notificationSettingsFromRequest); diff --git a/src/api/notifications/types.ts b/src/api/notifications/types.ts index d998a1ec..c17c575d 100644 --- a/src/api/notifications/types.ts +++ b/src/api/notifications/types.ts @@ -4,6 +4,8 @@ export interface ApiGetNotificationSettingsResponse { enabled: boolean; flow: boolean; comments: boolean; + send_telegram: boolean; + show_indicator: boolean; last_seen?: string | null; last_date?: string | null; } diff --git a/src/components/input/InputRow/index.tsx b/src/components/input/InputRow/index.tsx new file mode 100644 index 00000000..9fb25231 --- /dev/null +++ b/src/components/input/InputRow/index.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import styles from './styles.module.scss'; + +interface InputRowProps { + className?: string; + input?: ReactNode; +} + +const InputRow: FC = ({ children, input, className }) => ( +
+
{children}
+ {!!input &&
{input}
} +
+); + +export { InputRow }; diff --git a/src/components/input/InputRow/styles.module.scss b/src/components/input/InputRow/styles.module.scss new file mode 100644 index 00000000..5de8b9d1 --- /dev/null +++ b/src/components/input/InputRow/styles.module.scss @@ -0,0 +1,8 @@ +@import 'src/styles/variables'; + +.row { + display: grid; + grid-template-columns: 1fr auto; + row-gap: $gap; + align-items: center; +} diff --git a/src/components/input/Toggle/styles.module.scss b/src/components/input/Toggle/styles.module.scss index 52db0036..253933a6 100644 --- a/src/components/input/Toggle/styles.module.scss +++ b/src/components/input/Toggle/styles.module.scss @@ -38,6 +38,10 @@ transition: transform 0.25s, color 0.25s, background-color; } + &:disabled { + opacity: 0.5; + } + &.active { &::after { transform: translate(24px, 0); diff --git a/src/components/notifications/NotificationSettingsForm/index.tsx b/src/components/notifications/NotificationSettingsForm/index.tsx new file mode 100644 index 00000000..3eb1d8b2 --- /dev/null +++ b/src/components/notifications/NotificationSettingsForm/index.tsx @@ -0,0 +1,91 @@ +import React, { FC, useCallback } from 'react'; + +import { Group } from '~/components/containers/Group'; +import { Zone } from '~/components/containers/Zone'; +import { Button } from '~/components/input/Button'; +import { InputRow } from '~/components/input/InputRow'; +import { Toggle } from '~/components/input/Toggle'; +import { useNotificationSettingsForm } from '~/hooks/notifications/useNotificationSettingsForm'; +import { NotificationSettings } from '~/types/notifications'; + +import styles from './styles.module.scss'; + +interface NotificationSettingsFormProps { + value: NotificationSettings; + onSubmit: (val: Partial) => Promise; + telegramConnected: boolean; + onConnectTelegram: () => void; +} + +const NotificationSettingsForm: FC = ({ + value, + onSubmit, + telegramConnected, + onConnectTelegram, +}) => { + const { setFieldValue, values } = useNotificationSettingsForm( + value, + onSubmit, + ); + + const toggle = useCallback( + (key: keyof NotificationSettings, disabled?: boolean) => ( + setFieldValue(key, val)} + value={values[key]} + disabled={disabled} + /> + ), + [setFieldValue, values], + ); + + const telegramInput = telegramConnected ? ( + toggle('sendTelegram', !values.enabled) + ) : ( + + ); + + return ( + + + + + Включены + + + Новые посты + + + + Комментарии + + + + + + + + На иконке профиля + + + + Телеграм + + + + + ); +}; + +export { NotificationSettingsForm }; diff --git a/src/components/notifications/NotificationSettingsForm/styles.module.scss b/src/components/notifications/NotificationSettingsForm/styles.module.scss new file mode 100644 index 00000000..c62e1dc5 --- /dev/null +++ b/src/components/notifications/NotificationSettingsForm/styles.module.scss @@ -0,0 +1,11 @@ +@import 'src/styles/variables'; + +.grid { + display: grid; + grid-auto-flow: row; + row-gap: $gap; +} + +.row { + font: $font_14_regular; +} diff --git a/src/components/profile/ProfileSidebarNotifications/index.tsx b/src/components/profile/ProfileSidebarNotifications/index.tsx index 07fc114c..ba095528 100644 --- a/src/components/profile/ProfileSidebarNotifications/index.tsx +++ b/src/components/profile/ProfileSidebarNotifications/index.tsx @@ -1,9 +1,9 @@ import { VFC } from 'react'; +import { Padder } from '~/components/containers/Padder'; import { useStackContext } from '~/components/sidebar/SidebarStack'; import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard'; - -import { NotificationList } from '../../../containers/notifications/NotificationList/index'; +import { NotificationSettings } from '~/containers/notifications/NotificationSettings'; interface ProfileSidebarNotificationsProps {} @@ -19,7 +19,7 @@ const ProfileSidebarNotifications: VFC< title="Уведомления" onBackPress={closeAllTabs} > - + ); }; diff --git a/src/containers/notifications/NotificationSettings/index.tsx b/src/containers/notifications/NotificationSettings/index.tsx new file mode 100644 index 00000000..24ac8ce5 --- /dev/null +++ b/src/containers/notifications/NotificationSettings/index.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; + +import { Padder } from '~/components/containers/Padder'; +import { NotificationSettingsForm } from '~/components/notifications/NotificationSettingsForm'; +import { useOAuth } from '~/hooks/auth/useOAuth'; +import { useNotificationSettings } from '~/hooks/notifications/useNotificationSettings'; + +interface NotificationSettingsProps {} + +const NotificationSettings: FC = () => { + const { settings, update } = useNotificationSettings(); + const { hasTelegram, showTelegramModal } = useOAuth(); + + if (!settings) { + return <>{null}; + } + + return ( + + + + ); +}; + +export { NotificationSettings }; diff --git a/src/containers/profile/ProfileAccounts/index.tsx b/src/containers/profile/ProfileAccounts/index.tsx index 6b953aa8..d57e7ad1 100644 --- a/src/containers/profile/ProfileAccounts/index.tsx +++ b/src/containers/profile/ProfileAccounts/index.tsx @@ -15,18 +15,14 @@ import styles from './styles.module.scss'; type ProfileAccountsProps = {}; const ProfileAccounts: FC = () => { - const { isLoading, accounts, dropAccount, openOauthWindow } = useOAuth(); - const { showModal } = useModal(); - - const hasTelegram = useMemo( - () => accounts.some((acc) => acc.provider === 'telegram'), - [accounts], - ); - - const showTelegramModal = useCallback( - () => showModal(Dialog.TelegramAttach, {}), - [], - ); + const { + isLoading, + accounts, + dropAccount, + openOauthWindow, + hasTelegram, + showTelegramModal, + } = useOAuth(); return ( diff --git a/src/hooks/auth/useOAuth.ts b/src/hooks/auth/useOAuth.ts index c097b4e7..8a1f9a82 100644 --- a/src/hooks/auth/useOAuth.ts +++ b/src/hooks/auth/useOAuth.ts @@ -97,7 +97,19 @@ export const useOAuth = () => { const accounts = useMemo(() => data || [], [data]); const refresh = useCallback(() => mutate(), []); + const hasTelegram = useMemo( + () => accounts.some((acc) => acc.provider === 'telegram'), + [accounts], + ); + + const showTelegramModal = useCallback( + () => showModal(Dialog.TelegramAttach, {}), + [], + ); + return { + hasTelegram, + showTelegramModal, openOauthWindow, loginWithSocial, createSocialAccount, diff --git a/src/hooks/notifications/useNotificationSettings.ts b/src/hooks/notifications/useNotificationSettings.ts index f49e1e38..e3ad2434 100644 --- a/src/hooks/notifications/useNotificationSettings.ts +++ b/src/hooks/notifications/useNotificationSettings.ts @@ -7,8 +7,7 @@ import { useAuth } from '../auth/useAuth'; import { useNotificationSettingsRequest } from './useNotificationSettingsRequest'; export const useNotificationSettings = () => { - // TODO: remove isTester - const { isUser, isTester } = useAuth(); + const { isUser } = useAuth(); const { error: settingsError, @@ -18,16 +17,15 @@ export const useNotificationSettings = () => { isLoading: isLoadingSettings, update, refresh, + settings, } = useNotificationSettingsRequest(); - const enabled = - !isLoadingSettings && !settingsError && settingsEnabled && isTester; + const enabled = !isLoadingSettings && !settingsError && settingsEnabled; const hasNew = enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen)); - // TODO: store `indicator` as option and include it here - const indicatorEnabled = enabled && true; + const indicatorEnabled = enabled && !!settings?.showIndicator; const markAsRead = useCallback(() => { if ( @@ -37,7 +35,7 @@ export const useNotificationSettings = () => { return; } - update({ last_seen: lastDate.toISOString() }); + update({ lastSeen: lastDate.toISOString() }); }, [update, lastDate, lastSeen]); return { @@ -45,7 +43,9 @@ export const useNotificationSettings = () => { hasNew, indicatorEnabled, available: isUser, + settings, markAsRead, refresh, + update, }; }; diff --git a/src/hooks/notifications/useNotificationSettingsForm.ts b/src/hooks/notifications/useNotificationSettingsForm.ts new file mode 100644 index 00000000..f01fabb7 --- /dev/null +++ b/src/hooks/notifications/useNotificationSettingsForm.ts @@ -0,0 +1,46 @@ +import { useCallback } from 'react'; + +import { FormikConfig, useFormik } from 'formik'; +import { Asserts, boolean, object } from 'yup'; + +import { showErrorToast } from '~/utils/errors/showToast'; + +import { useFormAutoSubmit } from '../useFormAutosubmit'; + +const validationSchema = object({ + enabled: boolean().default(false), + flow: boolean().default(false), + comments: boolean().default(false), + sendTelegram: boolean().default(false), + showIndicator: boolean().default(false), +}); + +type Values = Asserts; + +export const useNotificationSettingsForm = ( + initialValues: Values, + submit: (val: Values) => void, +) => { + const onSubmit = useCallback['onSubmit']>( + async (values, { setSubmitting }) => { + try { + await submit(values); + } catch (error) { + showErrorToast(error); + } finally { + setSubmitting(false); + } + }, + [submit], + ); + + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit, + }); + + useFormAutoSubmit(formik.values, formik.handleSubmit); + + return formik; +}; diff --git a/src/hooks/notifications/useNotificationSettingsRequest.ts b/src/hooks/notifications/useNotificationSettingsRequest.ts index 6cc5c2a9..531c33b5 100644 --- a/src/hooks/notifications/useNotificationSettingsRequest.ts +++ b/src/hooks/notifications/useNotificationSettingsRequest.ts @@ -7,8 +7,8 @@ import { apiGetNotificationSettings, apiUpdateNotificationSettings, } from '~/api/notifications/settings'; -import { ApiUpdateNotificationSettingsRequest } from '~/api/notifications/types'; import { API } from '~/constants/api'; +import { NotificationSettings } from '~/types/notifications'; import { getErrorMessage } from '~/utils/errors/getErrorMessage'; import { showErrorToast } from '~/utils/errors/showToast'; @@ -28,12 +28,12 @@ export const useNotificationSettingsRequest = () => { mutate, } = useSWR( isUser ? API.NOTIFICATIONS.SETTINGS : null, - async () => apiGetNotificationSettings(), + apiGetNotificationSettings, { refreshInterval }, ); const update = useCallback( - async (settings: ApiUpdateNotificationSettingsRequest) => { + async (settings: Partial) => { if (!data) { return; } @@ -69,14 +69,15 @@ export const useNotificationSettingsRequest = () => { isLoading, error, lastSeen: - data?.last_seen && isValid(parseISO(data.last_seen)) - ? parseISO(data?.last_seen) + data?.lastSeen && isValid(parseISO(data.lastSeen)) + ? parseISO(data?.lastSeen) : undefined, lastDate: - data?.last_date && isValid(parseISO(data.last_date)) - ? parseISO(data?.last_date) + data?.lastDate && isValid(parseISO(data.lastDate)) + ? parseISO(data?.lastDate) : undefined, enabled: !!data?.enabled && (data.flow || data.comments), + settings: data, refresh, update, updateError, diff --git a/src/hooks/useFormAutosubmit.ts b/src/hooks/useFormAutosubmit.ts new file mode 100644 index 00000000..6b966ad1 --- /dev/null +++ b/src/hooks/useFormAutosubmit.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef } from 'react'; + +export const useFormAutoSubmit = ( + values: T, + onSubmit: () => void, + delay = 1000, +) => { + const prevValue = useRef(); + + useEffect(() => { + if (!prevValue.current) { + prevValue.current = values; + return; + } + + const timeout = setTimeout(onSubmit, delay); + + return () => clearTimeout(timeout); + }, [values]); +}; diff --git a/src/types/notifications/index.ts b/src/types/notifications/index.ts index 0ceab48e..29f038b9 100644 --- a/src/types/notifications/index.ts +++ b/src/types/notifications/index.ts @@ -14,3 +14,13 @@ export enum NotificationType { Node = 'node', Comment = 'comment', } + +export interface NotificationSettings { + enabled: boolean; + flow: boolean; + comments: boolean; + sendTelegram: boolean; + showIndicator: boolean; + lastSeen: string | null; + lastDate: string | null; +} diff --git a/src/utils/notifications/notificationSettingsFromRequest.ts b/src/utils/notifications/notificationSettingsFromRequest.ts new file mode 100644 index 00000000..1f067a5d --- /dev/null +++ b/src/utils/notifications/notificationSettingsFromRequest.ts @@ -0,0 +1,28 @@ +import { ApiGetNotificationSettingsResponse } from '~/api/notifications/types'; +import { NotificationSettings } from '~/types/notifications'; + +import { ApiUpdateNotificationSettingsRequest } from '../../api/notifications/types'; + +export const notificationSettingsFromRequest = ( + req: ApiGetNotificationSettingsResponse, +): NotificationSettings => ({ + enabled: req.enabled, + flow: req.flow, + comments: req.comments, + sendTelegram: req.send_telegram, + showIndicator: req.show_indicator, + lastDate: req.last_date ?? null, + lastSeen: req.last_seen ?? null, +}); + +export const notificationSettingsToRequest = ( + req: Partial, +): ApiUpdateNotificationSettingsRequest => ({ + enabled: req.enabled, + flow: req.flow, + comments: req.comments, + send_telegram: req.sendTelegram, + show_indicator: req.showIndicator, + last_date: req.lastDate, + last_seen: req.lastSeen, +});