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:
parent
d77a01d8bc
commit
7135d06673
17 changed files with 319 additions and 35 deletions
|
@ -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<NotificationSettings> =>
|
||||
api
|
||||
.get<ApiGetNotificationSettingsResponse>(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<NotificationSettings>,
|
||||
) =>
|
||||
api
|
||||
.post<ApiUpdateNotificationSettingsResponse>(
|
||||
API.NOTIFICATIONS.SETTINGS,
|
||||
settings,
|
||||
notificationSettingsToRequest(settings),
|
||||
)
|
||||
.then(cleanResult);
|
||||
.then(cleanResult)
|
||||
.then(notificationSettingsFromRequest);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
19
src/components/input/InputRow/index.tsx
Normal file
19
src/components/input/InputRow/index.tsx
Normal 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 };
|
8
src/components/input/InputRow/styles.module.scss
Normal file
8
src/components/input/InputRow/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
row-gap: $gap;
|
||||
align-items: center;
|
||||
}
|
|
@ -38,6 +38,10 @@
|
|||
transition: transform 0.25s, color 0.25s, background-color;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::after {
|
||||
transform: translate(24px, 0);
|
||||
|
|
|
@ -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 };
|
|
@ -0,0 +1,11 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
row-gap: $gap;
|
||||
}
|
||||
|
||||
.row {
|
||||
font: $font_14_regular;
|
||||
}
|
|
@ -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}
|
||||
>
|
||||
<NotificationList />
|
||||
<NotificationSettings />
|
||||
</SidebarStackCard>
|
||||
);
|
||||
};
|
||||
|
|
30
src/containers/notifications/NotificationSettings/index.tsx
Normal file
30
src/containers/notifications/NotificationSettings/index.tsx
Normal 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 };
|
|
@ -15,18 +15,14 @@ import styles from './styles.module.scss';
|
|||
type ProfileAccountsProps = {};
|
||||
|
||||
const ProfileAccounts: FC<ProfileAccountsProps> = () => {
|
||||
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 (
|
||||
<Group className={styles.wrap}>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
46
src/hooks/notifications/useNotificationSettingsForm.ts
Normal file
46
src/hooks/notifications/useNotificationSettingsForm.ts
Normal 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;
|
||||
};
|
|
@ -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<NotificationSettings>) => {
|
||||
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,
|
||||
|
|
20
src/hooks/useFormAutosubmit.ts
Normal file
20
src/hooks/useFormAutosubmit.ts
Normal 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]);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
28
src/utils/notifications/notificationSettingsFromRequest.ts
Normal file
28
src/utils/notifications/notificationSettingsFromRequest.ts
Normal 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,
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue