diff --git a/src/components/main/Notifications/index.tsx b/src/components/main/Notifications/index.tsx index d14b6045..30fa2abf 100644 --- a/src/components/main/Notifications/index.tsx +++ b/src/components/main/Notifications/index.tsx @@ -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 mapDispatchToProps & {}; @@ -23,8 +25,9 @@ const NotificationsUnconnected: FC = ({ 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 = ({ [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 ( -
+
0, + })} + >
{has_new ? : }
- {visible && } + {visible && ( + + )}
); }; diff --git a/src/components/main/Notifications/styles.scss b/src/components/main/Notifications/styles.scss index e258c92e..a1315f34 100644 --- a/src/components/main/Notifications/styles.scss +++ b/src/components/main/Notifications/styles.scss @@ -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; } diff --git a/src/components/notifications/NotificationBubble/index.tsx b/src/components/notifications/NotificationBubble/index.tsx index fb955536..043ae34f 100644 --- a/src/components/notifications/NotificationBubble/index.tsx +++ b/src/components/notifications/NotificationBubble/index.tsx @@ -2,28 +2,37 @@ 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 = ({ notifications }) => { +const NotificationBubble: FC = ({ notifications, onClick }) => { return (
- {notifications - .filter(notification => notification.type && NOTIFICATION_RENDERERS[notification.type]) - .map(notification => - createElement(NOTIFICATION_RENDERERS[notification.type], { - notification, - onClick: console.log, - key: notification.content.id, - }) - )} + {notifications.length === 0 && ( +
+ +
НЕТ УВЕДОМЛЕНИЙ
+
+ )} + {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, + }) + )}
); diff --git a/src/components/notifications/NotificationBubble/styles.scss b/src/components/notifications/NotificationBubble/styles.scss index 77ec52f9..47154066 100644 --- a/src/components/notifications/NotificationBubble/styles.scss +++ b/src/components/notifications/NotificationBubble/styles.scss @@ -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%); + } +} diff --git a/src/containers/dialogs/ProfileDialog/index.tsx b/src/containers/dialogs/ProfileDialog/index.tsx index dec0625f..b09f89d7 100644 --- a/src/containers/dialogs/ProfileDialog/index.tsx +++ b/src/containers/dialogs/ProfileDialog/index.tsx @@ -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: , messages: , }; const mapStateToProps = selectAuthProfile; -const mapDispatchToProps = {}; +const mapDispatchToProps = { + authSetProfile: AUTH_ACTIONS.authSetProfile, +}; -type IProps = IDialogProps & ReturnType & {}; +type IProps = IDialogProps & ReturnType & typeof mapDispatchToProps & {}; -const ProfileDialogUnconnected: FC = ({ onRequestClose, is_loading, user }) => { - const [tab, setTab] = useState('profile'); +const ProfileDialogUnconnected: FC = ({ + onRequestClose, + authSetProfile, + is_loading, + user, + tab, +}) => { + const setTab = useCallback((val: IAuthState['profile']['tab']) => authSetProfile({ tab: val }), [ + authSetProfile, + ]); return ( ({ 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) => ({ diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index 38e5a3c6..fae2c39c 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -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', diff --git a/src/redux/auth/reducer.ts b/src/redux/auth/reducer.ts index 1255f788..433dd4bd 100644 --- a/src/redux/auth/reducer.ts +++ b/src/redux/auth/reducer.ts @@ -22,6 +22,7 @@ const INITIAL_STATE: IAuthState = { }, profile: { + tab: 'profile', is_loading: true, is_loading_messages: true, is_sending_messages: false, diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index 5b211fee..8d329ec3 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -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) { function* logoutSaga() { yield put(authSetToken(null)); yield put(authSetUser({ ...EMPTY_USER })); + yield put( + authSetUpdates({ + last: null, + notifications: [], + }) + ); } -function* openProfile({ username }: ReturnType) { +function* openProfile({ username, tab = 'profile' }: ReturnType) { 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) { } 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) { @@ -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; diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts index 391221bb..e5a9636e 100644 --- a/src/redux/auth/types.ts +++ b/src/redux/auth/types.ts @@ -38,6 +38,7 @@ export type IAuthState = Readonly<{ }; profile: { + tab: 'profile' | 'messages' | 'settings'; is_loading: boolean; is_loading_messages: boolean; is_sending_messages: boolean; diff --git a/src/redux/types.ts b/src/redux/types.ts index 9b6d438a..d3b92066 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -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;