mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46: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
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 };
|
Loading…
Add table
Add a link
Reference in a new issue