1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

notifications: notification settings page

This commit is contained in:
Fedor Katurov 2023-03-16 11:00:29 +06:00
parent d77a01d8bc
commit 7135d06673
17 changed files with 319 additions and 35 deletions

View file

@ -1,17 +1,22 @@
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { NotificationSettings } from '~/types/notifications';
import { api, cleanResult } from '~/utils/api'; import { api, cleanResult } from '~/utils/api';
import {
notificationSettingsFromRequest,
notificationSettingsToRequest,
} from '~/utils/notifications/notificationSettingsFromRequest';
import { import {
ApiGetNotificationSettingsResponse, ApiGetNotificationSettingsResponse,
ApiGetNotificationsResponse, ApiGetNotificationsResponse,
ApiUpdateNotificationSettingsResponse, ApiUpdateNotificationSettingsResponse,
ApiUpdateNotificationSettingsRequest,
} from './types'; } from './types';
export const apiGetNotificationSettings = () => export const apiGetNotificationSettings = (): Promise<NotificationSettings> =>
api api
.get<ApiGetNotificationSettingsResponse>(API.NOTIFICATIONS.SETTINGS) .get<ApiGetNotificationSettingsResponse>(API.NOTIFICATIONS.SETTINGS)
.then(cleanResult); .then(cleanResult)
.then(notificationSettingsFromRequest);
export const apiGetNotifications = () => export const apiGetNotifications = () =>
api api
@ -19,11 +24,12 @@ export const apiGetNotifications = () =>
.then(cleanResult); .then(cleanResult);
export const apiUpdateNotificationSettings = ( export const apiUpdateNotificationSettings = (
settings: ApiUpdateNotificationSettingsRequest, settings: Partial<NotificationSettings>,
) => ) =>
api api
.post<ApiUpdateNotificationSettingsResponse>( .post<ApiUpdateNotificationSettingsResponse>(
API.NOTIFICATIONS.SETTINGS, API.NOTIFICATIONS.SETTINGS,
settings, notificationSettingsToRequest(settings),
) )
.then(cleanResult); .then(cleanResult)
.then(notificationSettingsFromRequest);

View file

@ -4,6 +4,8 @@ export interface ApiGetNotificationSettingsResponse {
enabled: boolean; enabled: boolean;
flow: boolean; flow: boolean;
comments: boolean; comments: boolean;
send_telegram: boolean;
show_indicator: boolean;
last_seen?: string | null; last_seen?: string | null;
last_date?: string | null; last_date?: string | null;
} }

View file

@ -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<InputRowProps> = ({ children, input, className }) => (
<div className={classNames(styles.row, className)}>
<div>{children}</div>
{!!input && <div>{input}</div>}
</div>
);
export { InputRow };

View file

@ -0,0 +1,8 @@
@import 'src/styles/variables';
.row {
display: grid;
grid-template-columns: 1fr auto;
row-gap: $gap;
align-items: center;
}

View file

@ -38,6 +38,10 @@
transition: transform 0.25s, color 0.25s, background-color; transition: transform 0.25s, color 0.25s, background-color;
} }
&:disabled {
opacity: 0.5;
}
&.active { &.active {
&::after { &::after {
transform: translate(24px, 0); transform: translate(24px, 0);

View file

@ -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<NotificationSettings>) => Promise<unknown>;
telegramConnected: boolean;
onConnectTelegram: () => void;
}
const NotificationSettingsForm: FC<NotificationSettingsFormProps> = ({
value,
onSubmit,
telegramConnected,
onConnectTelegram,
}) => {
const { setFieldValue, values } = useNotificationSettingsForm(
value,
onSubmit,
);
const toggle = useCallback(
(key: keyof NotificationSettings, disabled?: boolean) => (
<Toggle
handler={(val) => setFieldValue(key, val)}
value={values[key]}
disabled={disabled}
/>
),
[setFieldValue, values],
);
const telegramInput = telegramConnected ? (
toggle('sendTelegram', !values.enabled)
) : (
<Button size="micro" onClick={onConnectTelegram}>
Подключить
</Button>
);
return (
<Group>
<Zone title="Уведомления">
<Group>
<InputRow className={styles.row} input={toggle('enabled')}>
Включены
</InputRow>
<InputRow
className={styles.row}
input={toggle('flow', !values.enabled)}
>
Новые посты
</InputRow>
<InputRow
className={styles.row}
input={toggle('comments', !values.enabled)}
>
Комментарии
</InputRow>
</Group>
</Zone>
<Zone title="Уведомления">
<Group>
<InputRow
className={styles.row}
input={toggle('showIndicator', !values.enabled)}
>
На иконке профиля
</InputRow>
<InputRow className={styles.row} input={telegramInput}>
Телеграм
</InputRow>
</Group>
</Zone>
</Group>
);
};
export { NotificationSettingsForm };

View file

@ -0,0 +1,11 @@
@import 'src/styles/variables';
.grid {
display: grid;
grid-auto-flow: row;
row-gap: $gap;
}
.row {
font: $font_14_regular;
}

View file

@ -1,9 +1,9 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { Padder } from '~/components/containers/Padder';
import { useStackContext } from '~/components/sidebar/SidebarStack'; import { useStackContext } from '~/components/sidebar/SidebarStack';
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard'; import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
import { NotificationSettings } from '~/containers/notifications/NotificationSettings';
import { NotificationList } from '../../../containers/notifications/NotificationList/index';
interface ProfileSidebarNotificationsProps {} interface ProfileSidebarNotificationsProps {}
@ -19,7 +19,7 @@ const ProfileSidebarNotifications: VFC<
title="Уведомления" title="Уведомления"
onBackPress={closeAllTabs} onBackPress={closeAllTabs}
> >
<NotificationList /> <NotificationSettings />
</SidebarStackCard> </SidebarStackCard>
); );
}; };

View file

@ -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<NotificationSettingsProps> = () => {
const { settings, update } = useNotificationSettings();
const { hasTelegram, showTelegramModal } = useOAuth();
if (!settings) {
return <>{null}</>;
}
return (
<Padder>
<NotificationSettingsForm
value={settings}
onSubmit={update}
telegramConnected={hasTelegram}
onConnectTelegram={showTelegramModal}
/>
</Padder>
);
};
export { NotificationSettings };

View file

@ -15,18 +15,14 @@ import styles from './styles.module.scss';
type ProfileAccountsProps = {}; type ProfileAccountsProps = {};
const ProfileAccounts: FC<ProfileAccountsProps> = () => { const ProfileAccounts: FC<ProfileAccountsProps> = () => {
const { isLoading, accounts, dropAccount, openOauthWindow } = useOAuth(); const {
const { showModal } = useModal(); isLoading,
accounts,
const hasTelegram = useMemo( dropAccount,
() => accounts.some((acc) => acc.provider === 'telegram'), openOauthWindow,
[accounts], hasTelegram,
); showTelegramModal,
} = useOAuth();
const showTelegramModal = useCallback(
() => showModal(Dialog.TelegramAttach, {}),
[],
);
return ( return (
<Group className={styles.wrap}> <Group className={styles.wrap}>

View file

@ -97,7 +97,19 @@ export const useOAuth = () => {
const accounts = useMemo(() => data || [], [data]); const accounts = useMemo(() => data || [], [data]);
const refresh = useCallback(() => mutate(), []); const refresh = useCallback(() => mutate(), []);
const hasTelegram = useMemo(
() => accounts.some((acc) => acc.provider === 'telegram'),
[accounts],
);
const showTelegramModal = useCallback(
() => showModal(Dialog.TelegramAttach, {}),
[],
);
return { return {
hasTelegram,
showTelegramModal,
openOauthWindow, openOauthWindow,
loginWithSocial, loginWithSocial,
createSocialAccount, createSocialAccount,

View file

@ -7,8 +7,7 @@ import { useAuth } from '../auth/useAuth';
import { useNotificationSettingsRequest } from './useNotificationSettingsRequest'; import { useNotificationSettingsRequest } from './useNotificationSettingsRequest';
export const useNotificationSettings = () => { export const useNotificationSettings = () => {
// TODO: remove isTester const { isUser } = useAuth();
const { isUser, isTester } = useAuth();
const { const {
error: settingsError, error: settingsError,
@ -18,16 +17,15 @@ export const useNotificationSettings = () => {
isLoading: isLoadingSettings, isLoading: isLoadingSettings,
update, update,
refresh, refresh,
settings,
} = useNotificationSettingsRequest(); } = useNotificationSettingsRequest();
const enabled = const enabled = !isLoadingSettings && !settingsError && settingsEnabled;
!isLoadingSettings && !settingsError && settingsEnabled && isTester;
const hasNew = const hasNew =
enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen)); enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen));
// TODO: store `indicator` as option and include it here const indicatorEnabled = enabled && !!settings?.showIndicator;
const indicatorEnabled = enabled && true;
const markAsRead = useCallback(() => { const markAsRead = useCallback(() => {
if ( if (
@ -37,7 +35,7 @@ export const useNotificationSettings = () => {
return; return;
} }
update({ last_seen: lastDate.toISOString() }); update({ lastSeen: lastDate.toISOString() });
}, [update, lastDate, lastSeen]); }, [update, lastDate, lastSeen]);
return { return {
@ -45,7 +43,9 @@ export const useNotificationSettings = () => {
hasNew, hasNew,
indicatorEnabled, indicatorEnabled,
available: isUser, available: isUser,
settings,
markAsRead, markAsRead,
refresh, refresh,
update,
}; };
}; };

View file

@ -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<typeof validationSchema>;
export const useNotificationSettingsForm = (
initialValues: Values,
submit: (val: Values) => void,
) => {
const onSubmit = useCallback<FormikConfig<Values>['onSubmit']>(
async (values, { setSubmitting }) => {
try {
await submit(values);
} catch (error) {
showErrorToast(error);
} finally {
setSubmitting(false);
}
},
[submit],
);
const formik = useFormik<Values>({
initialValues,
validationSchema,
onSubmit,
});
useFormAutoSubmit(formik.values, formik.handleSubmit);
return formik;
};

View file

@ -7,8 +7,8 @@ import {
apiGetNotificationSettings, apiGetNotificationSettings,
apiUpdateNotificationSettings, apiUpdateNotificationSettings,
} from '~/api/notifications/settings'; } from '~/api/notifications/settings';
import { ApiUpdateNotificationSettingsRequest } from '~/api/notifications/types';
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { NotificationSettings } from '~/types/notifications';
import { getErrorMessage } from '~/utils/errors/getErrorMessage'; import { getErrorMessage } from '~/utils/errors/getErrorMessage';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
@ -28,12 +28,12 @@ export const useNotificationSettingsRequest = () => {
mutate, mutate,
} = useSWR( } = useSWR(
isUser ? API.NOTIFICATIONS.SETTINGS : null, isUser ? API.NOTIFICATIONS.SETTINGS : null,
async () => apiGetNotificationSettings(), apiGetNotificationSettings,
{ refreshInterval }, { refreshInterval },
); );
const update = useCallback( const update = useCallback(
async (settings: ApiUpdateNotificationSettingsRequest) => { async (settings: Partial<NotificationSettings>) => {
if (!data) { if (!data) {
return; return;
} }
@ -69,14 +69,15 @@ export const useNotificationSettingsRequest = () => {
isLoading, isLoading,
error, error,
lastSeen: lastSeen:
data?.last_seen && isValid(parseISO(data.last_seen)) data?.lastSeen && isValid(parseISO(data.lastSeen))
? parseISO(data?.last_seen) ? parseISO(data?.lastSeen)
: undefined, : undefined,
lastDate: lastDate:
data?.last_date && isValid(parseISO(data.last_date)) data?.lastDate && isValid(parseISO(data.lastDate))
? parseISO(data?.last_date) ? parseISO(data?.lastDate)
: undefined, : undefined,
enabled: !!data?.enabled && (data.flow || data.comments), enabled: !!data?.enabled && (data.flow || data.comments),
settings: data,
refresh, refresh,
update, update,
updateError, updateError,

View file

@ -0,0 +1,20 @@
import { useEffect, useRef } from 'react';
export const useFormAutoSubmit = <T>(
values: T,
onSubmit: () => void,
delay = 1000,
) => {
const prevValue = useRef<T>();
useEffect(() => {
if (!prevValue.current) {
prevValue.current = values;
return;
}
const timeout = setTimeout(onSubmit, delay);
return () => clearTimeout(timeout);
}, [values]);
};

View file

@ -14,3 +14,13 @@ export enum NotificationType {
Node = 'node', Node = 'node',
Comment = 'comment', Comment = 'comment',
} }
export interface NotificationSettings {
enabled: boolean;
flow: boolean;
comments: boolean;
sendTelegram: boolean;
showIndicator: boolean;
lastSeen: string | null;
lastDate: string | null;
}

View file

@ -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<NotificationSettings>,
): 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,
});