mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
add user notifications (#148)
* added notification settings * notifications: added list to profile * notifications: changed appearance for comment notifications
This commit is contained in:
parent
23701a5261
commit
a39d000ff2
27 changed files with 552 additions and 218 deletions
17
src/api/notifications/settings.ts
Normal file
17
src/api/notifications/settings.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
import { api, cleanResult } from '~/utils/api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiGetNotificationSettingsResponse,
|
||||||
|
ApiGetNotificationsResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const apiGetNotificationSettings = () =>
|
||||||
|
api
|
||||||
|
.get<ApiGetNotificationSettingsResponse>(API.NOTIFICATIONS.SETTINGS)
|
||||||
|
.then(cleanResult);
|
||||||
|
|
||||||
|
export const apiGetNotifications = () =>
|
||||||
|
api
|
||||||
|
.get<ApiGetNotificationsResponse>(API.NOTIFICATIONS.LIST)
|
||||||
|
.then(cleanResult);
|
13
src/api/notifications/types.ts
Normal file
13
src/api/notifications/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { NotificationItem } from '~/types/notifications';
|
||||||
|
|
||||||
|
export interface ApiGetNotificationSettingsResponse {
|
||||||
|
enabled: boolean;
|
||||||
|
flow: boolean;
|
||||||
|
comments: boolean;
|
||||||
|
last_seen?: string | null;
|
||||||
|
last_date?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiGetNotificationsResponse {
|
||||||
|
items?: NotificationItem[];
|
||||||
|
}
|
20
src/components/common/InlineUsername/index.tsx
Normal file
20
src/components/common/InlineUsername/index.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { useColorFromString } from '~/hooks/color/useColorFromString';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface InlineUsernameProps {
|
||||||
|
children: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InlineUsername: FC<InlineUsernameProps> = ({ children }) => {
|
||||||
|
const backgroundColor = useColorFromString(children);
|
||||||
|
return (
|
||||||
|
<span style={{ backgroundColor }} className={styles.username}>
|
||||||
|
~{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { InlineUsername };
|
6
src/components/common/InlineUsername/styles.module.scss
Normal file
6
src/components/common/InlineUsername/styles.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.username {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0 2px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
77
src/components/notifications/NotificationBadge/index.tsx
Normal file
77
src/components/notifications/NotificationBadge/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { Anchor } from '~/components/common/Anchor';
|
||||||
|
import { InlineUsername } from '~/components/common/InlineUsername';
|
||||||
|
import { Square } from '~/components/common/Square';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
|
||||||
|
import { NotificationItem, NotificationType } from '~/types/notifications';
|
||||||
|
import { formatText, getPrettyDate, getURLFromString } from '~/utils/dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface NotificationBadgeProps {
|
||||||
|
item: NotificationItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTitle = (item: NotificationItem) => {
|
||||||
|
if (!item.user.username) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case NotificationType.Comment:
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<InlineUsername>{item.user.username}</InlineUsername> пишет:
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case NotificationType.Node:
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Новый пост от <InlineUsername>{item.user.username}</InlineUsername>:
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContent = (item: NotificationItem) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case NotificationType.Comment:
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: formatText(item.text),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case NotificationType.Node:
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: formatText(item.text),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = (item: NotificationItem) => {
|
||||||
|
return <Square image={getURLFromString(item.thumbnail, 'avatar')} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationBadge: FC<NotificationBadgeProps> = ({ item }) => (
|
||||||
|
<Anchor href={item.url} className={styles.link}>
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.icon}>{getIcon(item)}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b className={styles.title}>{getTitle(item)}</b>
|
||||||
|
<div className={styles.text}>{getContent(item)}</div>
|
||||||
|
<div className={styles.time}>{getPrettyDate(item.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NotificationBadge };
|
|
@ -0,0 +1,33 @@
|
||||||
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font: $font_14_regular;
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: $gap/2 $gap/2 $gap/4 $gap/2;
|
||||||
|
min-height: calc(1.3em * 3 + $gap);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px auto;
|
||||||
|
column-gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
@include clamp(2, 14px);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_14_semibold;
|
||||||
|
margin-bottom: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font: $font_10_regular;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: $gray_75;
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
import React, { createElement, FC } from 'react';
|
|
||||||
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import { useRandomPhrase } from '~/constants/phrases';
|
|
||||||
import { INotification, NOTIFICATION_TYPES } from '~/types';
|
|
||||||
|
|
||||||
import { NotificationMessage } from '../NotificationMessage';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
notifications: INotification[];
|
|
||||||
onClick: (notification: INotification) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NOTIFICATION_RENDERERS = {
|
|
||||||
[NOTIFICATION_TYPES.message]: NotificationMessage,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotificationBubble: FC<IProps> = ({ notifications, onClick }) => {
|
|
||||||
const placeholder = useRandomPhrase('NOTHING_HERE');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.wrap}>
|
|
||||||
<div className={styles.list}>
|
|
||||||
{notifications.length === 0 && (
|
|
||||||
<div className={styles.placeholder}>
|
|
||||||
<Icon icon="bell_ring" />
|
|
||||||
<div>{placeholder}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{notifications.length > 0 &&
|
|
||||||
notifications
|
|
||||||
.filter(notification => notification.type && NOTIFICATION_RENDERERS[notification.type])
|
|
||||||
.map(notification =>
|
|
||||||
createElement(NOTIFICATION_RENDERERS[notification.type], {
|
|
||||||
notification,
|
|
||||||
onClick,
|
|
||||||
key: notification.content.id,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { NotificationBubble };
|
|
|
@ -1,106 +0,0 @@
|
||||||
@import 'src/styles/variables';
|
|
||||||
|
|
||||||
$notification_color: $content_bg_dark;
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
position: absolute;
|
|
||||||
background: $notification_color;
|
|
||||||
top: 42px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
border-radius: $radius;
|
|
||||||
animation: appear 0.25s forwards;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: ' ';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 0 16px 16px;
|
|
||||||
border-color: transparent transparent $notification_color transparent;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: -16px;
|
|
||||||
transform: translate(-20px, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
width: 300px;
|
|
||||||
max-width: 100vw;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: stretch;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: $gap;
|
|
||||||
min-width: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: white;
|
|
||||||
margin-right: $gap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item_head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item_title {
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
font: $font_14_semibold;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
// text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item_text {
|
|
||||||
font: $font_14_regular;
|
|
||||||
max-height: 2.4em;
|
|
||||||
padding-left: 30px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
height: 200px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font: $font_16_semibold;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 80px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.6em;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
opacity: 0.05;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
}
|
|
43
src/components/notifications/NotificationComment/index.tsx
Normal file
43
src/components/notifications/NotificationComment/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { Anchor } from '~/components/common/Anchor';
|
||||||
|
import { InlineUsername } from '~/components/common/InlineUsername';
|
||||||
|
import { Square } from '~/components/common/Square';
|
||||||
|
import { NotificationItem } from '~/types/notifications';
|
||||||
|
import { formatText, getPrettyDate, getURLFromString } from '~/utils/dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface NotificationCommentProps {
|
||||||
|
item: NotificationItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationComment: FC<NotificationCommentProps> = ({ item }) => (
|
||||||
|
<Anchor href={item.url} className={styles.link}>
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<Square
|
||||||
|
image={getURLFromString(item.user.photo, 'avatar')}
|
||||||
|
className={styles.circle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<b className={styles.title}>
|
||||||
|
<span>
|
||||||
|
<InlineUsername>{item.user.username}</InlineUsername>:
|
||||||
|
</span>
|
||||||
|
</b>
|
||||||
|
<div className={styles.text}>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: formatText(item.text),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NotificationComment };
|
|
@ -0,0 +1,52 @@
|
||||||
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font: $font_14_regular;
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: $gap/2 $gap/2 $gap/4 $gap/2;
|
||||||
|
min-height: calc(1.3em * 3 + $gap);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px auto;
|
||||||
|
column-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background: $content_bg;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 0 4px 4px 4px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: $gap;
|
||||||
|
right: 100%;
|
||||||
|
@include arrow_left(10px, $content_bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
@include clamp(2, 14px);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_14_semibold;
|
||||||
|
margin-bottom: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font: $font_10_regular;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: $gray_75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
|
||||||
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import styles from '~/components/notifications/NotificationBubble/styles.module.scss';
|
|
||||||
import { IMessageNotification, INotification } from '~/types';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
notification: IMessageNotification;
|
|
||||||
onClick: (notification: INotification) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NotificationMessage: FC<IProps> = ({
|
|
||||||
notification,
|
|
||||||
notification: {
|
|
||||||
content: { text, from },
|
|
||||||
},
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const onMouseDown = useCallback(() => onClick(notification), [onClick, notification]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.item} onMouseDown={onMouseDown}>
|
|
||||||
<div className={styles.item_head}>
|
|
||||||
<Icon icon="message" />
|
|
||||||
<div className={styles.item_title}>Сообщение от ~{from?.username}:</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.item_text}>{text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { NotificationMessage };
|
|
|
@ -1,7 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.scroller {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
padding: $gap;
|
|
||||||
}
|
|
27
src/components/profile/ProfileSidebarNotifications/index.tsx
Normal file
27
src/components/profile/ProfileSidebarNotifications/index.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { VFC } from 'react';
|
||||||
|
|
||||||
|
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||||
|
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||||
|
|
||||||
|
import { NotificationList } from '../../../containers/notifications/NotificationList/index';
|
||||||
|
|
||||||
|
interface ProfileSidebarNotificationsProps {}
|
||||||
|
|
||||||
|
const ProfileSidebarNotifications: VFC<
|
||||||
|
ProfileSidebarNotificationsProps
|
||||||
|
> = () => {
|
||||||
|
const { closeAllTabs } = useStackContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarStackCard
|
||||||
|
width={400}
|
||||||
|
headerFeature="back"
|
||||||
|
title="Уведомления"
|
||||||
|
onBackPress={closeAllTabs}
|
||||||
|
>
|
||||||
|
<NotificationList />
|
||||||
|
</SidebarStackCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ProfileSidebarNotifications };
|
|
@ -62,4 +62,8 @@ export const API = {
|
||||||
STATS: '/nodes/lab/stats',
|
STATS: '/nodes/lab/stats',
|
||||||
UPDATES: '/nodes/lab/updates',
|
UPDATES: '/nodes/lab/updates',
|
||||||
},
|
},
|
||||||
|
NOTIFICATIONS: {
|
||||||
|
LIST: '/notifications/',
|
||||||
|
SETTINGS: '/notifications/settings',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
34
src/containers/notifications/NotificationList/index.tsx
Normal file
34
src/containers/notifications/NotificationList/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
|
import { NotificationComment } from '~/components/notifications/NotificationComment';
|
||||||
|
import { useNotificationsList } from '~/hooks/notifications/useNotificationsList';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface NotificationListProps {}
|
||||||
|
|
||||||
|
const NotificationList: FC<NotificationListProps> = () => {
|
||||||
|
const { isLoading, items } = useNotificationsList();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoaderCircle />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{/* <div className={styles.head}>HEAD</div> */}
|
||||||
|
<div className={styles.list}>
|
||||||
|
<div className={styles.items}>
|
||||||
|
{items?.map((item) => (
|
||||||
|
<div className={styles.item} key={item.created_at}>
|
||||||
|
<NotificationComment item={item} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationList };
|
|
@ -0,0 +1,41 @@
|
||||||
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 4;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
@include row_shadow;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
@include row_shadow;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
@include row_shadow;
|
||||||
|
padding-right: $gap;
|
||||||
|
transition: background-color 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $content_bg_lighter;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import React, { useCallback, VFC } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Zone } from '~/components/containers/Zone';
|
import { Zone } from '~/components/containers/Zone';
|
||||||
|
@ -44,7 +45,13 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
Настройки
|
Настройки
|
||||||
</VerticalMenu.Item>
|
</VerticalMenu.Item>
|
||||||
|
|
||||||
|
<Superpower>
|
||||||
<VerticalMenu.Item onClick={() => setActiveTab(1)}>
|
<VerticalMenu.Item onClick={() => setActiveTab(1)}>
|
||||||
|
Уведомления
|
||||||
|
</VerticalMenu.Item>
|
||||||
|
</Superpower>
|
||||||
|
|
||||||
|
<VerticalMenu.Item onClick={() => setActiveTab(2)}>
|
||||||
Заметки
|
Заметки
|
||||||
</VerticalMenu.Item>
|
</VerticalMenu.Item>
|
||||||
</VerticalMenu>
|
</VerticalMenu>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, VFC } from 'react';
|
||||||
|
|
||||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
|
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
|
||||||
|
import { ProfileSidebarNotifications } from '~/components/profile/ProfileSidebarNotifications';
|
||||||
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
|
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
|
||||||
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
||||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||||
|
@ -13,7 +14,7 @@ import { useUser } from '~/hooks/auth/useUser';
|
||||||
import type { SidebarComponentProps } from '~/types/sidebar';
|
import type { SidebarComponentProps } from '~/types/sidebar';
|
||||||
import { isNil } from '~/utils/ramda';
|
import { isNil } from '~/utils/ramda';
|
||||||
|
|
||||||
const tabs = ['profile', 'bookmarks'] as const;
|
const tabs = ['profile', 'notifications', 'bookmarks'] as const;
|
||||||
type TabName = typeof tabs[number];
|
type TabName = typeof tabs[number];
|
||||||
|
|
||||||
interface SettingsSidebarProps
|
interface SettingsSidebarProps
|
||||||
|
@ -71,6 +72,7 @@ const SettingsSidebar: VFC<SettingsSidebarProps> = ({
|
||||||
|
|
||||||
<SidebarStack.Cards>
|
<SidebarStack.Cards>
|
||||||
<ProfileSidebarSettings />
|
<ProfileSidebarSettings />
|
||||||
|
<ProfileSidebarNotifications />
|
||||||
<ProfileSidebarNotes />
|
<ProfileSidebarNotes />
|
||||||
</SidebarStack.Cards>
|
</SidebarStack.Cards>
|
||||||
</SidebarStack>
|
</SidebarStack>
|
||||||
|
|
28
src/hooks/notifications/useNotificationSettings.ts
Normal file
28
src/hooks/notifications/useNotificationSettings.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { isAfter, isValid, parse, parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
import { useAuth } from '../auth/useAuth';
|
||||||
|
|
||||||
|
import { useNotificationSettingsRequest } from './useNotificationSettingsRequest';
|
||||||
|
|
||||||
|
export const useNotificationSettings = () => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: settingsError,
|
||||||
|
enabled: settingsEnabled,
|
||||||
|
lastSeen,
|
||||||
|
lastDate,
|
||||||
|
isLoading: isLoadingSettings,
|
||||||
|
} = useNotificationSettingsRequest();
|
||||||
|
|
||||||
|
const enabled = !isLoadingSettings && !settingsError && settingsEnabled;
|
||||||
|
|
||||||
|
const hasNew =
|
||||||
|
enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen));
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
hasNew,
|
||||||
|
available: isUser,
|
||||||
|
};
|
||||||
|
};
|
36
src/hooks/notifications/useNotificationSettingsRequest.ts
Normal file
36
src/hooks/notifications/useNotificationSettingsRequest.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { isValid, parseISO } from 'date-fns';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { apiGetNotificationSettings } from '~/api/notifications/settings';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
|
||||||
|
import { useAuth } from '../auth/useAuth';
|
||||||
|
|
||||||
|
const refreshInterval = 60e3; // 1min
|
||||||
|
|
||||||
|
export const useNotificationSettingsRequest = () => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isValidating: isLoading,
|
||||||
|
error,
|
||||||
|
} = useSWR(
|
||||||
|
isUser ? API.NOTIFICATIONS.SETTINGS : null,
|
||||||
|
async () => apiGetNotificationSettings(),
|
||||||
|
{ refreshInterval },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
lastSeen:
|
||||||
|
data?.last_seen && isValid(parseISO(data.last_seen))
|
||||||
|
? parseISO(data?.last_seen)
|
||||||
|
: undefined,
|
||||||
|
lastDate:
|
||||||
|
data?.last_date && isValid(parseISO(data.last_date))
|
||||||
|
? parseISO(data?.last_date)
|
||||||
|
: undefined,
|
||||||
|
enabled: !!data?.enabled && (data.flow || data.comments),
|
||||||
|
};
|
||||||
|
};
|
20
src/hooks/notifications/useNotificationsList.ts
Normal file
20
src/hooks/notifications/useNotificationsList.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { apiGetNotifications } from '~/api/notifications/settings';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
|
||||||
|
import { useAuth } from '../auth/useAuth';
|
||||||
|
|
||||||
|
export const useNotificationsList = () => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isValidating: isLoading,
|
||||||
|
error,
|
||||||
|
} = useSWR(isUser ? API.NOTIFICATIONS.LIST : null, async () =>
|
||||||
|
apiGetNotifications(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isLoading, error, ...data };
|
||||||
|
};
|
|
@ -1,5 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import App from 'next/app';
|
import App from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
@ -16,14 +14,15 @@ import { UserContextProvider } from '~/utils/context/UserContextProvider';
|
||||||
import { AudioPlayerProvider } from '~/utils/providers/AudioPlayerProvider';
|
import { AudioPlayerProvider } from '~/utils/providers/AudioPlayerProvider';
|
||||||
import { AuthProvider } from '~/utils/providers/AuthProvider';
|
import { AuthProvider } from '~/utils/providers/AuthProvider';
|
||||||
import { MetadataProvider } from '~/utils/providers/MetadataProvider';
|
import { MetadataProvider } from '~/utils/providers/MetadataProvider';
|
||||||
|
import { NotificationProvider } from '~/utils/providers/NotificationProvider';
|
||||||
import { SWRConfigProvider } from '~/utils/providers/SWRConfigProvider';
|
import { SWRConfigProvider } from '~/utils/providers/SWRConfigProvider';
|
||||||
import { SearchProvider } from '~/utils/providers/SearchProvider';
|
import { SearchProvider } from '~/utils/providers/SearchProvider';
|
||||||
import { SidebarProvider } from '~/utils/providers/SidebarProvider';
|
import { SidebarProvider } from '~/utils/providers/SidebarProvider';
|
||||||
import { ThemeProvider } from '~/utils/providers/ThemeProvider';
|
import { ThemeProvider } from '~/utils/providers/ThemeProvider';
|
||||||
import { ToastProvider } from '~/utils/providers/ToastProvider';
|
import { ToastProvider } from '~/utils/providers/ToastProvider';
|
||||||
|
|
||||||
import '~/styles/main.scss';
|
|
||||||
import 'tippy.js/dist/tippy.css';
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
import '~/styles/main.scss';
|
||||||
|
|
||||||
const mobxStore = getMOBXStore();
|
const mobxStore = getMOBXStore();
|
||||||
|
|
||||||
|
@ -45,6 +44,7 @@ export default class MyApp extends App {
|
||||||
<AudioPlayerProvider>
|
<AudioPlayerProvider>
|
||||||
<MetadataProvider>
|
<MetadataProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<NotificationProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<Head>
|
<Head>
|
||||||
<meta
|
<meta
|
||||||
|
@ -65,6 +65,7 @@ export default class MyApp extends App {
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
<BottomContainer />
|
<BottomContainer />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</MetadataProvider>
|
</MetadataProvider>
|
||||||
</AudioPlayerProvider>
|
</AudioPlayerProvider>
|
||||||
|
|
|
@ -297,3 +297,11 @@
|
||||||
right: -10px;
|
right: -10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin arrow_left($size: 50, $color: $content_bg) {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: $size $size $size 0;
|
||||||
|
border-color: transparent $color transparent transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -28,3 +28,9 @@ export interface ISocialAccount {
|
||||||
name: string;
|
name: string;
|
||||||
photo: string;
|
photo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShallowUser {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
photo: string;
|
||||||
|
}
|
||||||
|
|
16
src/types/notifications/index.ts
Normal file
16
src/types/notifications/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ShallowUser } from '../auth';
|
||||||
|
|
||||||
|
export interface NotificationItem {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
type: NotificationType;
|
||||||
|
text: string;
|
||||||
|
user: ShallowUser;
|
||||||
|
thumbnail: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
Node = 'node',
|
||||||
|
Comment = 'comment',
|
||||||
|
}
|
|
@ -88,10 +88,8 @@ export const getURL = (
|
||||||
return file?.url ? getURLFromString(file.url, size) : '';
|
return file?.url ? getURLFromString(file.url, size) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatText = pipe(
|
export const formatTextWithoutImages = pipe(
|
||||||
formatTextSanitizeYoutube,
|
|
||||||
formatTextComments,
|
formatTextComments,
|
||||||
formatTextTodos,
|
|
||||||
formatExclamations,
|
formatExclamations,
|
||||||
formatTextDash,
|
formatTextDash,
|
||||||
formatTextMarkdown,
|
formatTextMarkdown,
|
||||||
|
@ -99,6 +97,12 @@ export const formatText = pipe(
|
||||||
formatTextClickableUsernames,
|
formatTextClickableUsernames,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const formatText = pipe(
|
||||||
|
formatTextSanitizeYoutube,
|
||||||
|
formatTextTodos,
|
||||||
|
formatTextWithoutImages,
|
||||||
|
);
|
||||||
|
|
||||||
export const formatTextParagraphs = (text: string): string =>
|
export const formatTextParagraphs = (text: string): string =>
|
||||||
(text && formatText(text)) || '';
|
(text && formatText(text)) || '';
|
||||||
|
|
||||||
|
|
31
src/utils/providers/NotificationProvider.tsx
Normal file
31
src/utils/providers/NotificationProvider.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { createContext, FC, useContext } from 'react';
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import { useNotificationSettings } from '~/hooks/notifications/useNotificationSettings';
|
||||||
|
|
||||||
|
interface NotificationProviderProps {}
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
available: false,
|
||||||
|
enabled: false,
|
||||||
|
hasNew: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationContext = createContext(defaultValue);
|
||||||
|
|
||||||
|
const NotificationProvider: FC<NotificationProviderProps> = observer(
|
||||||
|
({ children }) => {
|
||||||
|
const value = useNotificationSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useNotifications = () => useContext(NotificationContext);
|
||||||
|
|
||||||
|
export { NotificationProvider };
|
Loading…
Add table
Add a link
Reference in a new issue