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

message notifications working

This commit is contained in:
Fedor Katurov 2019-11-13 12:53:29 +07:00
parent 83c9900af1
commit 7583a57b04
11 changed files with 137 additions and 32 deletions

View file

@ -7,6 +7,7 @@ import pick from 'ramda/es/pick';
import classNames from 'classnames';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { NotificationBubble } from '../../notifications/NotificationBubble';
import { INotification, IMessageNotification } from '~/redux/types';
const mapStateToProps = state => ({
user: pick(['last_seen_messages'], selectAuthUser(state)),
@ -15,6 +16,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = {
authSetLastSeenMessages: AUTH_ACTIONS.authSetLastSeenMessages,
authOpenProfile: AUTH_ACTIONS.authOpenProfile,
};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
@ -23,8 +25,9 @@ const NotificationsUnconnected: FC<IProps> = ({
updates: { last, notifications },
user: { last_seen_messages },
authSetLastSeenMessages,
authOpenProfile,
}) => {
const [visible, setVisible] = useState(true);
const [visible, setVisible] = useState(false);
const has_new = useMemo(
() =>
notifications.length &&
@ -35,21 +38,42 @@ const NotificationsUnconnected: FC<IProps> = ({
[last, last_seen_messages, notifications]
);
const onNotificationClick = useCallback(
(notification: INotification) => {
switch (notification.type) {
case 'message':
return authOpenProfile(
(notification as IMessageNotification).content.from.username,
'messages'
);
default:
return;
}
},
[authOpenProfile]
);
const showList = useCallback(() => setVisible(true), [setVisible]);
const hideList = useCallback(() => setVisible(false), [setVisible]);
useEffect(() => {
if (!visible || !has_new) return;
authSetLastSeenMessages(new Date().toISOString());
}, [visible]);
const showList = useCallback(() => setVisible(true), [setVisible]);
const hideList = useCallback(() => setVisible(false), [setVisible]);
return (
<div className={classNames(styles.wrap, { [styles.is_new]: has_new })}>
<div
className={classNames(styles.wrap, {
[styles.is_new]: has_new,
[styles.active]: notifications.length > 0,
})}
>
<div className={styles.icon} onFocus={showList} onBlur={hideList} tabIndex={-1}>
{has_new ? <Icon icon="bell_ring" size={24} /> : <Icon icon="bell" size={24} />}
</div>
{visible && <NotificationBubble notifications={notifications} />}
{visible && (
<NotificationBubble notifications={notifications} onClick={onNotificationClick} />
)}
</div>
);
};

View file

@ -21,9 +21,16 @@
position: relative;
outline: none;
&.active {
.icon {
opacity: 1;
}
}
&.is_new {
.icon {
animation: ring 1s infinite alternate;
opacity: 1;
svg {
fill: $red;
@ -35,4 +42,5 @@
.icon {
outline: none;
cursor: pointer;
opacity: 0.5;
}

View file

@ -2,25 +2,34 @@ import React, { FC, createElement } from 'react';
import { INotification, NOTIFICATION_TYPES } from '~/redux/types';
import styles from './styles.scss';
import { NotificationMessage } from '../NotificationMessage';
import { Icon } from '~/components/input/Icon';
interface IProps {
notifications: INotification[];
onClick: (notification: INotification) => void;
}
const NOTIFICATION_RENDERERS = {
[NOTIFICATION_TYPES.message]: NotificationMessage,
};
const NotificationBubble: FC<IProps> = ({ notifications }) => {
const NotificationBubble: FC<IProps> = ({ notifications, onClick }) => {
return (
<div className={styles.wrap}>
<div className={styles.list}>
{notifications
{notifications.length === 0 && (
<div className={styles.placeholder}>
<Icon icon="bell_ring" />
<div>НЕТ УВЕДОМЛЕНИЙ</div>
</div>
)}
{notifications.length > 0 &&
notifications
.filter(notification => notification.type && NOTIFICATION_RENDERERS[notification.type])
.map(notification =>
createElement(NOTIFICATION_RENDERERS[notification.type], {
notification,
onClick: console.log,
onClick,
key: notification.content.id,
})
)}

View file

@ -1,3 +1,5 @@
$notification_color: darken($content_bg, 2%);
@keyframes appear {
0% {
opacity: 0;
@ -6,15 +8,16 @@
opacity: 1;
}
}
.wrap {
position: absolute;
position: absolute;
background: $content_bg;
top: 50px;
background: $notification_color;
top: 42px;
left: 50%;
transform: translate(-50%, 0);
border-radius: $radius;
animation: appear 0.5s forwards;
animation: appear 0.25s forwards;
z-index: 2;
&::before {
content: ' ';
@ -22,7 +25,7 @@
height: 0;
border-style: solid;
border-width: 0 0 16px 16px;
border-color: transparent transparent $content_bg transparent;
border-color: transparent transparent $notification_color transparent;
position: absolute;
left: 50%;
top: -16px;
@ -45,6 +48,7 @@
flex-direction: column;
padding: $gap;
min-width: 0;
cursor: pointer;
svg {
fill: white;
@ -74,3 +78,23 @@
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;
svg {
width: 120px;
height: 120px;
opacity: 0.05;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View file

@ -1,4 +1,4 @@
import React, { FC, useState } from 'react';
import React, { FC, useState, useCallback } from 'react';
import { BetterScrollDialog } from '../BetterScrollDialog';
import { ProfileInfo } from '~/containers/profile/ProfileInfo';
import { IDialogProps } from '~/redux/types';
@ -6,18 +6,30 @@ import { connect } from 'react-redux';
import { selectAuthProfile } from '~/redux/auth/selectors';
import { ProfileMessages } from '~/containers/profile/ProfileMessages';
import { ProfileDescription } from '~/components/profile/ProfileDescription';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { IAuthState } from '~/redux/auth/types';
const TAB_CONTENT = {
profile: <ProfileDescription />,
messages: <ProfileMessages />,
};
const mapStateToProps = selectAuthProfile;
const mapDispatchToProps = {};
const mapDispatchToProps = {
authSetProfile: AUTH_ACTIONS.authSetProfile,
};
type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & {};
type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const ProfileDialogUnconnected: FC<IProps> = ({ onRequestClose, is_loading, user }) => {
const [tab, setTab] = useState('profile');
const ProfileDialogUnconnected: FC<IProps> = ({
onRequestClose,
authSetProfile,
is_loading,
user,
tab,
}) => {
const setTab = useCallback((val: IAuthState['profile']['tab']) => authSetProfile({ tab: val }), [
authSetProfile,
]);
return (
<BetterScrollDialog

View file

@ -34,9 +34,14 @@ export const authLogout = () => ({
type: AUTH_USER_ACTIONS.LOGOUT,
});
export const authOpenProfile = (username: string) => ({
export const authLoggedIn = () => ({
type: AUTH_USER_ACTIONS.LOGGED_IN,
});
export const authOpenProfile = (username: string, tab?: IAuthState['profile']['tab']) => ({
type: AUTH_USER_ACTIONS.OPEN_PROFILE,
username,
tab,
});
export const authSetProfile = (profile: Partial<IAuthState['profile']>) => ({

View file

@ -7,6 +7,7 @@ export const AUTH_USER_ACTIONS = {
SET_TOKEN: 'SET_TOKEN',
LOGOUT: 'LOGOUT',
LOGGED_IN: 'LOGGED_IN',
GOT_AUTH_POST_MESSAGE: 'GOT_POST_MESSAGE',
OPEN_PROFILE: 'OPEN_PROFILE',

View file

@ -22,6 +22,7 @@ const INITIAL_STATE: IAuthState = {
},
profile: {
tab: 'profile',
is_loading: true,
is_loading_messages: true,
is_sending_messages: false,

View file

@ -11,6 +11,7 @@ import {
authGetMessages,
authSendMessage,
authSetUpdates,
authLoggedIn,
} from '~/redux/auth/actions';
import {
apiUserLogin,
@ -22,7 +23,7 @@ import {
} from '~/redux/auth/api';
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors';
import { IResultWithStatus, INotification } from '../types';
import { IResultWithStatus, INotification, IMessageNotification } from '../types';
import { IUser, IAuthState } from './types';
import { REHYDRATE, RehydrateAction } from 'redux-persist';
import { selectModal } from '../modal/selectors';
@ -60,6 +61,7 @@ function* sendLoginRequestSaga({ username, password }: ReturnType<typeof userSen
yield put(authSetToken(token));
yield put(authSetUser({ ...user, is_user: true }));
yield put(authLoggedIn());
yield put(modalSetShown(false));
}
@ -101,11 +103,17 @@ function* gotPostMessageSaga({ token }: ReturnType<typeof gotAuthPostMessage>) {
function* logoutSaga() {
yield put(authSetToken(null));
yield put(authSetUser({ ...EMPTY_USER }));
yield put(
authSetUpdates({
last: null,
notifications: [],
})
);
}
function* openProfile({ username }: ReturnType<typeof authOpenProfile>) {
function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenProfile>) {
yield put(modalShowDialog(DIALOGS.PROFILE));
yield put(authSetProfile({ is_loading: true }));
yield put(authSetProfile({ is_loading: true, tab }));
const {
error,
@ -151,6 +159,19 @@ function* getMessages({ username }: ReturnType<typeof authGetMessages>) {
}
yield put(authSetProfile({ is_loading_messages: false, messages: data.messages }));
const { notifications } = yield select(selectAuthUpdates);
// clear viewed message from notifcation list
const filtered = notifications.filter(
notification =>
notification.type !== 'message' ||
(notification as IMessageNotification).content.from.username !== username
);
if (filtered.length !== notifications.length) {
yield put(authSetUpdates({ notifications: filtered }));
}
}
function* sendMessage({ message, onSuccess }: ReturnType<typeof authSendMessage>) {
@ -235,7 +256,7 @@ function* authSaga() {
yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile);
yield takeLatest(AUTH_USER_ACTIONS.GET_MESSAGES, getMessages);
yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
yield takeLatest(REHYDRATE, startPollingSaga);
yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga);
}
export default authSaga;

View file

@ -38,6 +38,7 @@ export type IAuthState = Readonly<{
};
profile: {
tab: 'profile' | 'messages' | 'settings';
is_loading: boolean;
is_loading_messages: boolean;
is_sending_messages: boolean;

View file

@ -2,7 +2,6 @@ import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
import { DIALOGS } from '~/redux/modal/constants';
import { ERRORS } from '~/constants/errors';
import { IUser } from './auth/types';
import { string } from 'prop-types';
export interface ITag {
id: number;