diff --git a/src/components/main/Header/index.tsx b/src/components/main/Header/index.tsx index 42d5170e..f3a7fc14 100644 --- a/src/components/main/Header/index.tsx +++ b/src/components/main/Header/index.tsx @@ -1,11 +1,11 @@ -import React, { FC, useCallback, memo, useState, useEffect } from 'react'; +import React, { FC, useCallback, memo, useState, useEffect, useMemo } from 'react'; import { connect } from 'react-redux'; import { push as historyPush } from 'connected-react-router'; import { Link } from 'react-router-dom'; import { Logo } from '~/components/main/Logo'; import { Filler } from '~/components/containers/Filler'; -import { selectUser } from '~/redux/auth/selectors'; +import { selectUser, selectAuthUpdates } from '~/redux/auth/selectors'; import { Group } from '~/components/containers/Group'; import { DIALOGS } from '~/redux/modal/constants'; import pick from 'ramda/es/pick'; @@ -19,9 +19,12 @@ import classNames from 'classnames'; import * as style from './style.scss'; import * as MODAL_ACTIONS from '~/redux/modal/actions'; import * as AUTH_ACTIONS from '~/redux/auth/actions'; +import { IState } from '~/redux/store'; +import isBefore from 'date-fns/isBefore'; -const mapStateToProps = state => ({ - user: pick(['username', 'is_user', 'photo'])(selectUser(state)), +const mapStateToProps = (state: IState) => ({ + user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)), + updates: pick(['boris_commented_at'])(selectAuthUpdates(state)), pathname: path(['router', 'location', 'pathname'], state), }); @@ -35,7 +38,15 @@ const mapDispatchToProps = { type IProps = ReturnType & typeof mapDispatchToProps & {}; const HeaderUnconnected: FC = memo( - ({ user, user: { is_user }, showDialog, pathname, authLogout, authOpenProfile }) => { + ({ + user, + user: { is_user, last_seen_boris }, + showDialog, + pathname, + updates: { boris_commented_at }, + authLogout, + authOpenProfile, + }) => { const [is_scrolled, setIsScrolled] = useState(false); const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]); @@ -55,6 +66,13 @@ const HeaderUnconnected: FC = memo( return () => window.removeEventListener('scroll', onScroll); }, [onScroll]); + const hasBorisUpdates = useMemo( + () => + boris_commented_at && + (!last_seen_boris || isBefore(new Date(last_seen_boris), new Date(boris_commented_at))), + [boris_commented_at, last_seen_boris] + ); + return createPortal(
@@ -71,7 +89,10 @@ const HeaderUnconnected: FC = memo( БОРИС diff --git a/src/components/main/Header/style.scss b/src/components/main/Header/style.scss index 4c0c8058..398b841c 100644 --- a/src/components/main/Header/style.scss +++ b/src/components/main/Header/style.scss @@ -90,11 +90,27 @@ transition: transform 0.5s, opacity 0.25s; } - @include tablet { - padding: $gap; + &::after { + content: ' '; + position: absolute; + width: 8px; + height: 8px; + border-radius: 4px; + background: $red; + left: 50%; + bottom: -2px; + transform: translate(-50%, 0); + transition: opacity 0.5s; + opacity: 0; + } + &.has_dot { &::after { - margin-left: $gap; + opacity: 1; } } + + @include tablet { + padding: $gap; + } } diff --git a/src/containers/node/BorisLayout/index.tsx b/src/containers/node/BorisLayout/index.tsx index 791605b9..b66a30ee 100644 --- a/src/containers/node/BorisLayout/index.tsx +++ b/src/containers/node/BorisLayout/index.tsx @@ -1,18 +1,20 @@ import React, { FC, useEffect } from 'react'; import { RouteComponentProps } from 'react-router'; -import * as NODE_ACTIONS from '~/redux/node/actions'; import { selectNode } from '~/redux/node/selectors'; import { selectUser } from '~/redux/auth/selectors'; import { connect } from 'react-redux'; import { NodeComments } from '~/components/node/NodeComments'; import styles from './styles.scss'; -import { CommentForm } from '~/components/node/CommentForm'; import { Group } from '~/components/containers/Group'; import boris from '~/sprites/boris_robot.svg'; import { NodeNoComments } from '~/components/node/NodeNoComments'; import { getRandomPhrase } from '~/constants/phrases'; import { NodeCommentForm } from '~/components/node/NodeCommentForm'; +import * as NODE_ACTIONS from '~/redux/node/actions'; +import * as AUTH_ACTIONS from '~/redux/auth/actions'; +import isBefore from 'date-fns/isBefore'; + const mapStateToProps = state => ({ node: selectNode(state), user: selectUser(state), @@ -23,6 +25,7 @@ const mapDispatchToProps = { nodeLockComment: NODE_ACTIONS.nodeLockComment, nodeEditComment: NODE_ACTIONS.nodeEditComment, nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments, + authSetUser: AUTH_ACTIONS.authSetUser, }; type IProps = ReturnType & @@ -34,14 +37,24 @@ const id = 696; const BorisLayoutUnconnected: FC = ({ node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count }, user, - user: { is_user }, + user: { is_user, last_seen_boris }, nodeLoadNode, nodeLockComment, nodeEditComment, nodeLoadMoreComments, + authSetUser, }) => { const title = getRandomPhrase('BORIS_TITLE'); + useEffect(() => { + const last_comment = comments[0]; + if (!last_comment) return; + if (last_seen_boris && !isBefore(new Date(last_seen_boris), new Date(last_comment.created_at))) + return; + + authSetUser({ last_seen_boris: last_comment.created_at }); + }, [comments, last_seen_boris]); + useEffect(() => { if (is_loading) return; nodeLoadNode(id, 'DESC'); diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index a4e76dbb..f24dd08e 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -1,6 +1,6 @@ import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api'; import { API } from '~/constants/api'; -import { IResultWithStatus, IMessage } from '~/redux/types'; +import { IResultWithStatus, IMessage, INotification } from '~/redux/types'; import { userLoginTransform } from '~/redux/auth/transforms'; import { IUser } from './types'; @@ -55,7 +55,9 @@ export const apiAuthGetUpdates = ({ access, exclude_dialogs, last, -}): Promise> => +}): Promise< + IResultWithStatus<{ notifications: INotification[]; boris: { commented_at: string } }> +> => api .get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } })) .then(resultMiddleware) diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index e1b0980f..5f763ede 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -62,6 +62,7 @@ export const EMPTY_USER: IUser = { last_seen: null, last_seen_messages: null, + last_seen_boris: null, }; export interface IApiUser { diff --git a/src/redux/auth/reducer.ts b/src/redux/auth/reducer.ts index 679042de..9b05e13c 100644 --- a/src/redux/auth/reducer.ts +++ b/src/redux/auth/reducer.ts @@ -14,6 +14,7 @@ const INITIAL_STATE: IAuthState = { updates: { last: null, notifications: [], + boris_commented_at: null, }, login: { diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index 8b9e3a79..2f75616c 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -40,7 +40,7 @@ import { selectAuthUpdates, selectAuthRestore, } from './selectors'; -import { IResultWithStatus, INotification, IMessageNotification } from '../types'; +import { IResultWithStatus, INotification, IMessageNotification, Unwrap } from '../types'; import { IUser, IAuthState } from './types'; import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { selectModal } from '~/redux/modal/selectors'; @@ -244,32 +244,42 @@ function* sendMessage({ message, onSuccess }: ReturnType } function* getUpdates() { - const user = yield select(selectAuthUser); + const user: ReturnType = yield select(selectAuthUser); if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return; const modal: IModalState = yield select(selectModal); const profile: IAuthState['profile'] = yield select(selectAuthProfile); - const { last }: IAuthState['updates'] = yield select(selectAuthUpdates); + const { last, boris_commented_at }: IAuthState['updates'] = yield select(selectAuthUpdates); const exclude_dialogs = modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null; - const { error, data }: IResultWithStatus<{ notifications: INotification[] }> = yield call( + const { error, data }: Unwrap> = yield call( reqWrapper, apiAuthGetUpdates, { exclude_dialogs, last: last || user.last_seen_messages } ); - if (error || !data || !data.notifications || !data.notifications.length) return; + if (error || !data) { + return; + } - const { notifications } = data; + if (data.notifications && data.notifications.length) { + yield put( + authSetUpdates({ + last: data.notifications[0].created_at, + notifications: data.notifications, + }) + ); + } - yield put( - authSetUpdates({ - last: notifications[0].created_at, - notifications, - }) - ); + if (data.boris && data.boris.commented_at && boris_commented_at !== data.boris.commented_at) { + yield put( + authSetUpdates({ + boris_commented_at: data.boris.commented_at, + }) + ); + } } function* startPollingSaga() { diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts index c3d049be..98f312f4 100644 --- a/src/redux/auth/types.ts +++ b/src/redux/auth/types.ts @@ -18,6 +18,7 @@ export interface IUser { last_seen: string; last_seen_messages: string; + last_seen_boris: string; is_activated: boolean; is_user: boolean; @@ -30,6 +31,7 @@ export type IAuthState = Readonly<{ updates: { last: string; notifications: INotification[]; + boris_commented_at: string; }; login: {