mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
Merge branch 'develop'
This commit is contained in:
commit
2f01943536
8 changed files with 92 additions and 26 deletions
|
@ -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 { connect } from 'react-redux';
|
||||||
import { push as historyPush } from 'connected-react-router';
|
import { push as historyPush } from 'connected-react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Logo } from '~/components/main/Logo';
|
import { Logo } from '~/components/main/Logo';
|
||||||
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
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 { Group } from '~/components/containers/Group';
|
||||||
import { DIALOGS } from '~/redux/modal/constants';
|
import { DIALOGS } from '~/redux/modal/constants';
|
||||||
import pick from 'ramda/es/pick';
|
import pick from 'ramda/es/pick';
|
||||||
|
@ -19,9 +19,12 @@ import classNames from 'classnames';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
|
import { IState } from '~/redux/store';
|
||||||
|
import isBefore from 'date-fns/isBefore';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
updates: pick(['boris_commented_at'])(selectAuthUpdates(state)),
|
||||||
pathname: path(['router', 'location', 'pathname'], state),
|
pathname: path(['router', 'location', 'pathname'], state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -35,7 +38,15 @@ const mapDispatchToProps = {
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
const HeaderUnconnected: FC<IProps> = memo(
|
const HeaderUnconnected: FC<IProps> = 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 [is_scrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]);
|
const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]);
|
||||||
|
@ -55,6 +66,13 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
return () => window.removeEventListener('scroll', onScroll);
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
}, [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(
|
return createPortal(
|
||||||
<div className={classNames(style.wrap, { [style.is_scrolled]: is_scrolled })}>
|
<div className={classNames(style.wrap, { [style.is_scrolled]: is_scrolled })}>
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
|
@ -71,7 +89,10 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(style.item, { [style.is_active]: pathname === URLS.BORIS })}
|
className={classNames(style.item, {
|
||||||
|
[style.is_active]: pathname === URLS.BORIS,
|
||||||
|
[style.has_dot]: hasBorisUpdates,
|
||||||
|
})}
|
||||||
to={URLS.BORIS}
|
to={URLS.BORIS}
|
||||||
>
|
>
|
||||||
БОРИС
|
БОРИС
|
||||||
|
|
|
@ -90,11 +90,27 @@
|
||||||
transition: transform 0.5s, opacity 0.25s;
|
transition: transform 0.5s, opacity 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
&::after {
|
||||||
padding: $gap;
|
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 {
|
&::after {
|
||||||
margin-left: $gap;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
import { selectNode } from '~/redux/node/selectors';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import { CommentForm } from '~/components/node/CommentForm';
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import boris from '~/sprites/boris_robot.svg';
|
import boris from '~/sprites/boris_robot.svg';
|
||||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { getRandomPhrase } from '~/constants/phrases';
|
import { getRandomPhrase } from '~/constants/phrases';
|
||||||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
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 => ({
|
const mapStateToProps = state => ({
|
||||||
node: selectNode(state),
|
node: selectNode(state),
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
|
@ -23,6 +25,7 @@ const mapDispatchToProps = {
|
||||||
nodeLockComment: NODE_ACTIONS.nodeLockComment,
|
nodeLockComment: NODE_ACTIONS.nodeLockComment,
|
||||||
nodeEditComment: NODE_ACTIONS.nodeEditComment,
|
nodeEditComment: NODE_ACTIONS.nodeEditComment,
|
||||||
nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
|
nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
|
||||||
|
authSetUser: AUTH_ACTIONS.authSetUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
|
@ -34,14 +37,24 @@ const id = 696;
|
||||||
const BorisLayoutUnconnected: FC<IProps> = ({
|
const BorisLayoutUnconnected: FC<IProps> = ({
|
||||||
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count },
|
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count },
|
||||||
user,
|
user,
|
||||||
user: { is_user },
|
user: { is_user, last_seen_boris },
|
||||||
nodeLoadNode,
|
nodeLoadNode,
|
||||||
nodeLockComment,
|
nodeLockComment,
|
||||||
nodeEditComment,
|
nodeEditComment,
|
||||||
nodeLoadMoreComments,
|
nodeLoadMoreComments,
|
||||||
|
authSetUser,
|
||||||
}) => {
|
}) => {
|
||||||
const title = getRandomPhrase('BORIS_TITLE');
|
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(() => {
|
useEffect(() => {
|
||||||
if (is_loading) return;
|
if (is_loading) return;
|
||||||
nodeLoadNode(id, 'DESC');
|
nodeLoadNode(id, 'DESC');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api';
|
import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api';
|
||||||
import { API } from '~/constants/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 { userLoginTransform } from '~/redux/auth/transforms';
|
||||||
import { IUser } from './types';
|
import { IUser } from './types';
|
||||||
|
|
||||||
|
@ -55,7 +55,9 @@ export const apiAuthGetUpdates = ({
|
||||||
access,
|
access,
|
||||||
exclude_dialogs,
|
exclude_dialogs,
|
||||||
last,
|
last,
|
||||||
}): Promise<IResultWithStatus<{ message: IMessage }>> =>
|
}): Promise<
|
||||||
|
IResultWithStatus<{ notifications: INotification[]; boris: { commented_at: string } }>
|
||||||
|
> =>
|
||||||
api
|
api
|
||||||
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } }))
|
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } }))
|
||||||
.then(resultMiddleware)
|
.then(resultMiddleware)
|
||||||
|
|
|
@ -62,6 +62,7 @@ export const EMPTY_USER: IUser = {
|
||||||
|
|
||||||
last_seen: null,
|
last_seen: null,
|
||||||
last_seen_messages: null,
|
last_seen_messages: null,
|
||||||
|
last_seen_boris: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IApiUser {
|
export interface IApiUser {
|
||||||
|
|
|
@ -14,6 +14,7 @@ const INITIAL_STATE: IAuthState = {
|
||||||
updates: {
|
updates: {
|
||||||
last: null,
|
last: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
boris_commented_at: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
|
|
|
@ -40,7 +40,7 @@ import {
|
||||||
selectAuthUpdates,
|
selectAuthUpdates,
|
||||||
selectAuthRestore,
|
selectAuthRestore,
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { IResultWithStatus, INotification, IMessageNotification } from '../types';
|
import { IResultWithStatus, INotification, IMessageNotification, Unwrap } from '../types';
|
||||||
import { IUser, IAuthState } from './types';
|
import { IUser, IAuthState } from './types';
|
||||||
import { REHYDRATE, RehydrateAction } from 'redux-persist';
|
import { REHYDRATE, RehydrateAction } from 'redux-persist';
|
||||||
import { selectModal } from '~/redux/modal/selectors';
|
import { selectModal } from '~/redux/modal/selectors';
|
||||||
|
@ -244,32 +244,42 @@ function* sendMessage({ message, onSuccess }: ReturnType<typeof authSendMessage>
|
||||||
}
|
}
|
||||||
|
|
||||||
function* getUpdates() {
|
function* getUpdates() {
|
||||||
const user = yield select(selectAuthUser);
|
const user: ReturnType<typeof selectAuthUser> = yield select(selectAuthUser);
|
||||||
|
|
||||||
if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return;
|
if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return;
|
||||||
|
|
||||||
const modal: IModalState = yield select(selectModal);
|
const modal: IModalState = yield select(selectModal);
|
||||||
const profile: IAuthState['profile'] = yield select(selectAuthProfile);
|
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 =
|
const exclude_dialogs =
|
||||||
modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null;
|
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<ReturnType<typeof apiAuthGetUpdates>> = yield call(
|
||||||
reqWrapper,
|
reqWrapper,
|
||||||
apiAuthGetUpdates,
|
apiAuthGetUpdates,
|
||||||
{ exclude_dialogs, last: last || user.last_seen_messages }
|
{ 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(
|
if (data.boris && data.boris.commented_at && boris_commented_at !== data.boris.commented_at) {
|
||||||
authSetUpdates({
|
yield put(
|
||||||
last: notifications[0].created_at,
|
authSetUpdates({
|
||||||
notifications,
|
boris_commented_at: data.boris.commented_at,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* startPollingSaga() {
|
function* startPollingSaga() {
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface IUser {
|
||||||
|
|
||||||
last_seen: string;
|
last_seen: string;
|
||||||
last_seen_messages: string;
|
last_seen_messages: string;
|
||||||
|
last_seen_boris: string;
|
||||||
|
|
||||||
is_activated: boolean;
|
is_activated: boolean;
|
||||||
is_user: boolean;
|
is_user: boolean;
|
||||||
|
@ -30,6 +31,7 @@ export type IAuthState = Readonly<{
|
||||||
updates: {
|
updates: {
|
||||||
last: string;
|
last: string;
|
||||||
notifications: INotification[];
|
notifications: INotification[];
|
||||||
|
boris_commented_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue