mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
added updates everywhere
This commit is contained in:
parent
a451e30499
commit
b1e68a8a6d
27 changed files with 246 additions and 79 deletions
|
@ -6,10 +6,8 @@ import classNames from 'classnames';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import markdown from '~/styles/common/markdown.module.scss';
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { flowSetCellView } from '~/redux/flow/actions';
|
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { NODE_TYPES } from '~/redux/node/constants';
|
import { NODE_TYPES } from '~/redux/node/constants';
|
||||||
import { Group } from '~/components/containers/Group';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const THUMBNAIL_SIZES = {
|
const THUMBNAIL_SIZES = {
|
||||||
|
@ -21,8 +19,8 @@ interface IProps {
|
||||||
is_text?: boolean;
|
is_text?: boolean;
|
||||||
can_edit?: boolean;
|
can_edit?: boolean;
|
||||||
|
|
||||||
onSelect: (id: INode['id'], type: INode['type']) => void;
|
onSelect: (id: INode['id']) => void;
|
||||||
onChangeCellView: typeof flowSetCellView;
|
onChangeCellView: (id: INode['id'], flow: INode['flow']) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Cell: FC<IProps> = ({
|
const Cell: FC<IProps> = ({
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import React, { FC, Fragment } from 'react';
|
import React, { FC, Fragment, useCallback } from 'react';
|
||||||
import { Cell } from '~/components/flow/Cell';
|
import { Cell } from '~/components/flow/Cell';
|
||||||
|
|
||||||
import { IFlowState } from '~/redux/flow/reducer';
|
import { IFlowState } from '~/redux/flow/reducer';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { canEditNode } from '~/utils/node';
|
import { canEditNode } from '~/utils/node';
|
||||||
import { IUser } from '~/redux/auth/types';
|
import { IUser } from '~/redux/auth/types';
|
||||||
import { flowSetCellView } from '~/redux/flow/actions';
|
import { useHistory } from 'react-router';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
|
||||||
type IProps = Partial<IFlowState> & {
|
type IProps = Partial<IFlowState> & {
|
||||||
user: Partial<IUser>;
|
user: Partial<IUser>;
|
||||||
onSelect: (id: INode['id'], type: INode['type']) => void;
|
onChangeCellView: (id: INode['id'], flow: INode['flow']) => void;
|
||||||
onChangeCellView: typeof flowSetCellView;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FlowGrid: FC<IProps> = ({ user, nodes, onSelect, onChangeCellView }) => {
|
export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const onSelect = useCallback((id: INode['id']) => history.push(URLS.NODE_URL(id)), [history]);
|
||||||
|
|
||||||
if (!nodes) {
|
if (!nodes) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
|
||||||
import { getPrettyDate } from '~/utils/dom';
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: Partial<INode>;
|
node: Partial<INode>;
|
||||||
|
@ -15,13 +16,22 @@ interface IProps {
|
||||||
const FlowRecentItem: FC<IProps> = ({ node, has_new }) => {
|
const FlowRecentItem: FC<IProps> = ({ node, has_new }) => {
|
||||||
return (
|
return (
|
||||||
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
|
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
|
||||||
<div className={classNames(styles.thumb, { [styles.new]: has_new })}>
|
<div
|
||||||
|
className={classNames(styles.thumb, {
|
||||||
|
[styles.new]: has_new,
|
||||||
|
[styles.lab]: !node.is_promoted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<NodeRelatedItem item={node} />
|
<NodeRelatedItem item={node} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.title}>{node.title || '...'}</div>
|
<div className={styles.title}>{node.title || '...'}</div>
|
||||||
<div className={styles.comment}>{getPrettyDate(node.created_at)}</div>
|
|
||||||
|
<div className={styles.comment}>
|
||||||
|
{!node.is_promoted && <Icon icon="lab" size={14} />}
|
||||||
|
<span>{getPrettyDate(node.created_at)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,6 +37,12 @@
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.lab {
|
||||||
|
&::after {
|
||||||
|
background: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
@ -55,8 +61,16 @@
|
||||||
.comment {
|
.comment {
|
||||||
font: $font_12_regular;
|
font: $font_12_regular;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
opacity: 0.5;
|
color: darken(white, 50%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
margin-right: $gap / 2;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,20 +13,16 @@ interface IProps {
|
||||||
recent: IFlowState['recent'];
|
recent: IFlowState['recent'];
|
||||||
updated: IFlowState['updated'];
|
updated: IFlowState['updated'];
|
||||||
search: IFlowState['search'];
|
search: IFlowState['search'];
|
||||||
flowChangeSearch: typeof FLOW_ACTIONS.flowChangeSearch;
|
onSearchChange: (text: string) => void;
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlowStamp: FC<IProps> = ({ recent, updated, search, flowChangeSearch, onLoadMore }) => {
|
const FlowStamp: FC<IProps> = ({ recent, updated, search, onSearchChange, onLoadMore }) => {
|
||||||
const onSearchChange = useCallback((text: string) => flowChangeSearch({ text }), [
|
|
||||||
flowChangeSearch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onSearchSubmit = useCallback((event: FormEvent) => {
|
const onSearchSubmit = useCallback((event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClearSearch = useCallback(() => flowChangeSearch({ text: '' }), [flowChangeSearch]);
|
const onClearSearch = useCallback(() => onSearchChange(''), [onSearchChange]);
|
||||||
|
|
||||||
const onKeyUp = useCallback(
|
const onKeyUp = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
|
|
@ -21,6 +21,9 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
import { Authorized } from '~/components/containers/Authorized';
|
import { Authorized } from '~/components/containers/Authorized';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectLabUpdates, selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
||||||
|
import { selectFlow, selectFlowUpdated } from '~/redux/flow/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
@ -48,7 +51,8 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
authOpenProfile,
|
authOpenProfile,
|
||||||
}) => {
|
}) => {
|
||||||
const [is_scrolled, setIsScrolled] = useState(false);
|
const [is_scrolled, setIsScrolled] = useState(false);
|
||||||
|
const labUpdates = useShallowSelect(selectLabUpdatesNodes);
|
||||||
|
const flowUpdates = useShallowSelect(selectFlowUpdated);
|
||||||
const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]);
|
const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]);
|
||||||
|
|
||||||
const onScroll = useCallback(() => {
|
const onScroll = useCallback(() => {
|
||||||
|
@ -74,6 +78,9 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
[boris_commented_at, last_seen_boris]
|
[boris_commented_at, last_seen_boris]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasLabUpdates = useMemo(() => labUpdates.length > 0, [labUpdates]);
|
||||||
|
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={classNames(styles.wrap, { [styles.is_scrolled]: is_scrolled })}>
|
<div className={classNames(styles.wrap, { [styles.is_scrolled]: is_scrolled })}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
@ -83,7 +90,10 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
|
|
||||||
<div className={styles.plugs}>
|
<div className={styles.plugs}>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
className={classNames(styles.item, {
|
||||||
|
[styles.is_active]: pathname === URLS.BASE,
|
||||||
|
[styles.has_dot]: hasFlowUpdates,
|
||||||
|
})}
|
||||||
to={URLS.BASE}
|
to={URLS.BASE}
|
||||||
>
|
>
|
||||||
ФЛОУ
|
ФЛОУ
|
||||||
|
@ -91,7 +101,10 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
|
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
className={classNames(styles.item, styles.lab, {
|
||||||
|
[styles.is_active]: pathname === URLS.LAB,
|
||||||
|
[styles.has_dot]: hasLabUpdates,
|
||||||
|
})}
|
||||||
to={URLS.LAB}
|
to={URLS.LAB}
|
||||||
>
|
>
|
||||||
ЛАБ
|
ЛАБ
|
||||||
|
@ -99,7 +112,7 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, {
|
className={classNames(styles.item, styles.boris, {
|
||||||
[styles.is_active]: pathname === URLS.BORIS,
|
[styles.is_active]: pathname === URLS.BORIS,
|
||||||
[styles.has_dot]: hasBorisUpdates,
|
[styles.has_dot]: hasBorisUpdates,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -116,6 +116,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.lab::after {
|
||||||
|
background: lighten($blue, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.boris::after {
|
||||||
|
background: lighten($wisegreen, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
|
|
||||||
|
|
|
@ -53,5 +53,6 @@ export const API = {
|
||||||
LAB: {
|
LAB: {
|
||||||
NODES: `/lab/`,
|
NODES: `/lab/`,
|
||||||
STATS: '/lab/stats',
|
STATS: '/lab/stats',
|
||||||
|
UPDATES: '/lab/updates',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,9 +11,11 @@ import {
|
||||||
selectLabStatsHeroes,
|
selectLabStatsHeroes,
|
||||||
selectLabStatsLoading,
|
selectLabStatsLoading,
|
||||||
selectLabStatsTags,
|
selectLabStatsTags,
|
||||||
|
selectLabUpdatesNodes,
|
||||||
} from '~/redux/lab/selectors';
|
} from '~/redux/lab/selectors';
|
||||||
import { LabTags } from '~/components/lab/LabTags';
|
import { LabTags } from '~/components/lab/LabTags';
|
||||||
import { LabHeroes } from '~/components/lab/LabHeroes';
|
import { LabHeroes } from '~/components/lab/LabHeroes';
|
||||||
|
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
|
@ -21,6 +23,7 @@ const LabStats: FC<IProps> = () => {
|
||||||
const tags = useShallowSelect(selectLabStatsTags);
|
const tags = useShallowSelect(selectLabStatsTags);
|
||||||
const heroes = useShallowSelect(selectLabStatsHeroes);
|
const heroes = useShallowSelect(selectLabStatsHeroes);
|
||||||
const isLoading = useShallowSelect(selectLabStatsLoading);
|
const isLoading = useShallowSelect(selectLabStatsLoading);
|
||||||
|
const updates = useShallowSelect(selectLabUpdatesNodes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
|
@ -42,6 +45,17 @@ const LabStats: FC<IProps> = () => {
|
||||||
<div />
|
<div />
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
|
{updates.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className={styles.title}>Новые</div>
|
||||||
|
<Group className={styles.updates}>
|
||||||
|
{updates.map(node => (
|
||||||
|
<FlowRecentItem node={node} key={node.id} has_new />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Placeholder height={14} width="100px" />
|
<Placeholder height={14} width="100px" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -23,3 +23,7 @@
|
||||||
background-color: $comment_bg;
|
background-color: $comment_bg;
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.updates {
|
||||||
|
padding: 0 $gap / 4 $gap * 2;
|
||||||
|
}
|
||||||
|
|
|
@ -1,42 +1,29 @@
|
||||||
import React, { FC, useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { FlowGrid } from '~/components/flow/FlowGrid';
|
import { FlowGrid } from '~/components/flow/FlowGrid';
|
||||||
import { selectFlow } from '~/redux/flow/selectors';
|
import { selectFlow } from '~/redux/flow/selectors';
|
||||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
import {
|
||||||
import * as FLOW_ACTIONS from '~/redux/flow/actions';
|
flowChangeSearch,
|
||||||
import { pick } from 'ramda';
|
flowGetMore,
|
||||||
|
flowLoadMoreSearch,
|
||||||
|
flowSetCellView,
|
||||||
|
} from '~/redux/flow/actions';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { FlowHero } from '~/components/flow/FlowHero';
|
import { FlowHero } from '~/components/flow/FlowHero';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { IState } from '~/redux/store';
|
|
||||||
import { FlowStamp } from '~/components/flow/FlowStamp';
|
import { FlowStamp } from '~/components/flow/FlowStamp';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const FlowLayout: FC = () => {
|
||||||
flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading', 'search'], selectFlow(state)),
|
const { nodes, heroes, recent, updated, is_loading, search } = useShallowSelect(selectFlow);
|
||||||
user: pick(['role', 'id'], selectUser(state)),
|
const labUpdates = useShallowSelect(selectLabUpdatesNodes);
|
||||||
});
|
const user = useShallowSelect(selectUser);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
nodeGotoNode: NODE_ACTIONS.nodeGotoNode,
|
|
||||||
flowSetCellView: FLOW_ACTIONS.flowSetCellView,
|
|
||||||
flowGetMore: FLOW_ACTIONS.flowGetMore,
|
|
||||||
flowChangeSearch: FLOW_ACTIONS.flowChangeSearch,
|
|
||||||
flowLoadMoreSearch: FLOW_ACTIONS.flowLoadMoreSearch,
|
|
||||||
};
|
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
|
||||||
|
|
||||||
const FlowLayoutUnconnected: FC<IProps> = ({
|
|
||||||
flow: { nodes, heroes, recent, updated, is_loading, search },
|
|
||||||
user,
|
|
||||||
nodeGotoNode,
|
|
||||||
flowSetCellView,
|
|
||||||
flowGetMore,
|
|
||||||
flowChangeSearch,
|
|
||||||
flowLoadMoreSearch,
|
|
||||||
}) => {
|
|
||||||
const onLoadMore = useCallback(() => {
|
const onLoadMore = useCallback(() => {
|
||||||
(window as any).flowScrollPos = window.scrollY;
|
(window as any).flowScrollPos = window.scrollY;
|
||||||
|
|
||||||
|
@ -44,13 +31,29 @@ const FlowLayoutUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
if (is_loading || pos < -600) return;
|
if (is_loading || pos < -600) return;
|
||||||
|
|
||||||
flowGetMore();
|
dispatch(flowGetMore());
|
||||||
}, [flowGetMore, is_loading]);
|
}, [dispatch, is_loading]);
|
||||||
|
|
||||||
const onLoadMoreSearch = useCallback(() => {
|
const onLoadMoreSearch = useCallback(() => {
|
||||||
if (search.is_loading_more) return;
|
if (search.is_loading_more) return;
|
||||||
flowLoadMoreSearch();
|
dispatch(flowLoadMoreSearch());
|
||||||
}, [search.is_loading_more, flowLoadMoreSearch]);
|
}, [search.is_loading_more, dispatch]);
|
||||||
|
|
||||||
|
const onChangeSearch = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
dispatch(flowChangeSearch({ text }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeCellView = useCallback(
|
||||||
|
(id: INode['id'], flow: INode['flow']) => {
|
||||||
|
dispatch(flowSetCellView(id, flow));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const cumulativeUpdates = useMemo(() => [...updated, ...labUpdates], [updated, labUpdates]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('scroll', onLoadMore);
|
window.addEventListener('scroll', onLoadMore);
|
||||||
|
@ -72,19 +75,14 @@ const FlowLayoutUnconnected: FC<IProps> = ({
|
||||||
<div className={styles.stamp}>
|
<div className={styles.stamp}>
|
||||||
<FlowStamp
|
<FlowStamp
|
||||||
recent={recent}
|
recent={recent}
|
||||||
updated={updated}
|
updated={cumulativeUpdates}
|
||||||
search={search}
|
search={search}
|
||||||
flowChangeSearch={flowChangeSearch}
|
onSearchChange={onChangeSearch}
|
||||||
onLoadMore={onLoadMoreSearch}
|
onLoadMore={onLoadMoreSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FlowGrid
|
<FlowGrid nodes={nodes} user={user} onChangeCellView={onChangeCellView} />
|
||||||
nodes={nodes}
|
|
||||||
user={user}
|
|
||||||
onSelect={nodeGotoNode}
|
|
||||||
onChangeCellView={flowSetCellView}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarRouter prefix="" />
|
<SidebarRouter prefix="" />
|
||||||
|
@ -92,6 +90,4 @@ const FlowLayoutUnconnected: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlowLayout = connect(mapStateToProps, mapDispatchToProps)(FlowLayoutUnconnected);
|
export { FlowLayout };
|
||||||
|
|
||||||
export { FlowLayout, FlowLayoutUnconnected };
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useScrollToTop } from '~/utils/hooks/useScrollToTop';
|
||||||
import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
|
import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
|
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
|
||||||
|
import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen';
|
||||||
|
|
||||||
type IProps = RouteComponentProps<{ id: string }> & {};
|
type IProps = RouteComponentProps<{ id: string }> & {};
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ const NodeLayout: FC<IProps> = memo(
|
||||||
useNodeCoverImage(current);
|
useNodeCoverImage(current);
|
||||||
useScrollToTop([id]);
|
useScrollToTop([id]);
|
||||||
useLoadNode(id, is_loading);
|
useLoadNode(id, is_loading);
|
||||||
|
useOnNodeSeen(current);
|
||||||
|
|
||||||
const { head, block } = useNodeBlocks(current, is_loading);
|
const { head, block } = useNodeBlocks(current, is_loading);
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { messagesSet } from '~/redux/messages/actions';
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { isEmpty } from 'ramda';
|
import { isEmpty } from 'ramda';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { labGetUpdates } from '~/redux/lab/actions';
|
||||||
|
|
||||||
function* setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
|
function* setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
|
@ -193,6 +194,7 @@ function* getUpdates() {
|
||||||
function* startPollingSaga() {
|
function* startPollingSaga() {
|
||||||
while (true) {
|
while (true) {
|
||||||
yield call(getUpdates);
|
yield call(getUpdates);
|
||||||
|
yield put(labGetUpdates());
|
||||||
yield delay(60000);
|
yield delay(60000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ import { FLOW_ACTIONS } from './constants';
|
||||||
import { IFlowState } from './reducer';
|
import { IFlowState } from './reducer';
|
||||||
import { INode } from '../types';
|
import { INode } from '../types';
|
||||||
|
|
||||||
|
export const flowGetFlow = () => ({
|
||||||
|
type: FLOW_ACTIONS.GET_FLOW,
|
||||||
|
});
|
||||||
|
|
||||||
export const flowSetNodes = (nodes: IFlowState['nodes']) => ({
|
export const flowSetNodes = (nodes: IFlowState['nodes']) => ({
|
||||||
nodes,
|
nodes,
|
||||||
type: FLOW_ACTIONS.SET_NODES,
|
type: FLOW_ACTIONS.SET_NODES,
|
||||||
|
@ -50,3 +54,8 @@ export const flowChangeSearch = (search: Partial<IFlowState['search']>) => ({
|
||||||
export const flowLoadMoreSearch = () => ({
|
export const flowLoadMoreSearch = () => ({
|
||||||
type: FLOW_ACTIONS.LOAD_MORE_SEARCH,
|
type: FLOW_ACTIONS.LOAD_MORE_SEARCH,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const flowSeenNode = (nodeId: INode['id']) => ({
|
||||||
|
type: FLOW_ACTIONS.SEEN_NODE,
|
||||||
|
nodeId,
|
||||||
|
});
|
||||||
|
|
|
@ -14,4 +14,6 @@ export const FLOW_ACTIONS = {
|
||||||
SET_SEARCH: `${prefix}SET_SEARCH`,
|
SET_SEARCH: `${prefix}SET_SEARCH`,
|
||||||
CHANGE_SEARCH: `${prefix}CHANGE_SEARCH`,
|
CHANGE_SEARCH: `${prefix}CHANGE_SEARCH`,
|
||||||
LOAD_MORE_SEARCH: `${prefix}LOAD_MORE_SEARCH`,
|
LOAD_MORE_SEARCH: `${prefix}LOAD_MORE_SEARCH`,
|
||||||
|
|
||||||
|
SEEN_NODE: `${prefix}SEEN_NODE`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,8 @@ import { Unwrap } from '../types';
|
||||||
import { selectFlow, selectFlowNodes } from './selectors';
|
import { selectFlow, selectFlowNodes } from './selectors';
|
||||||
import { getSearchResults, postCellView } from './api';
|
import { getSearchResults, postCellView } from './api';
|
||||||
import { uniq } from 'ramda';
|
import { uniq } from 'ramda';
|
||||||
|
import { labSeenNode, labSetUpdates } from '~/redux/lab/actions';
|
||||||
|
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
function hideLoader() {
|
function hideLoader() {
|
||||||
const loader = document.getElementById('main_loader');
|
const loader = document.getElementById('main_loader');
|
||||||
|
@ -185,10 +187,16 @@ function* loadMoreSearch() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* seenNode({ nodeId }: ReturnType<typeof labSeenNode>) {
|
||||||
|
const { updated }: ReturnType<typeof selectFlow> = yield select(selectFlow);
|
||||||
|
yield put(flowSetUpdated(updated.filter(node => node.id != nodeId)));
|
||||||
|
}
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
export default function* nodeSaga() {
|
||||||
yield takeLatest([FLOW_ACTIONS.GET_FLOW, REHYDRATE], onGetFlow);
|
yield takeLatest([FLOW_ACTIONS.GET_FLOW, REHYDRATE], onGetFlow);
|
||||||
yield takeLatest(FLOW_ACTIONS.SET_CELL_VIEW, onSetCellView);
|
yield takeLatest(FLOW_ACTIONS.SET_CELL_VIEW, onSetCellView);
|
||||||
yield takeLeading(FLOW_ACTIONS.GET_MORE, getMore);
|
yield takeLeading(FLOW_ACTIONS.GET_MORE, getMore);
|
||||||
yield takeLatest(FLOW_ACTIONS.CHANGE_SEARCH, changeSearch);
|
yield takeLatest(FLOW_ACTIONS.CHANGE_SEARCH, changeSearch);
|
||||||
yield takeLatest(FLOW_ACTIONS.LOAD_MORE_SEARCH, loadMoreSearch);
|
yield takeLatest(FLOW_ACTIONS.LOAD_MORE_SEARCH, loadMoreSearch);
|
||||||
|
yield takeLatest(FLOW_ACTIONS.SEEN_NODE, seenNode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,3 +3,4 @@ import { IFlowState } from './reducer';
|
||||||
|
|
||||||
export const selectFlow = (state: IState): IFlowState => state.flow;
|
export const selectFlow = (state: IState): IFlowState => state.flow;
|
||||||
export const selectFlowNodes = (state: IState) => state.flow.nodes;
|
export const selectFlowNodes = (state: IState) => state.flow.nodes;
|
||||||
|
export const selectFlowUpdated = (state: IState) => state.flow.updated;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
import { ILabState } from '~/redux/lab/types';
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
export const labGetList = (after?: string) => ({
|
export const labGetList = (after?: string) => ({
|
||||||
type: LAB_ACTIONS.GET_LIST,
|
type: LAB_ACTIONS.GET_LIST,
|
||||||
|
@ -19,3 +20,17 @@ export const labSetStats = (stats: Partial<ILabState['stats']>) => ({
|
||||||
type: LAB_ACTIONS.SET_STATS,
|
type: LAB_ACTIONS.SET_STATS,
|
||||||
stats,
|
stats,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const labSetUpdates = (updates: Partial<ILabState['updates']>) => ({
|
||||||
|
type: LAB_ACTIONS.SET_UPDATES,
|
||||||
|
updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labGetUpdates = () => ({
|
||||||
|
type: LAB_ACTIONS.GET_UPDATES,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labSeenNode = (nodeId: INode['id']) => ({
|
||||||
|
type: LAB_ACTIONS.SEEN_NODE,
|
||||||
|
nodeId,
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { api, cleanResult } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { GetLabNodesRequest, GetLabNodesResult, GetLabStatsResult } from '~/redux/lab/types';
|
import {
|
||||||
|
GetLabNodesRequest,
|
||||||
|
GetLabNodesResult,
|
||||||
|
GetLabStatsResult,
|
||||||
|
GetLabUpdatesResult,
|
||||||
|
} from '~/redux/lab/types';
|
||||||
|
|
||||||
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
api
|
api
|
||||||
|
@ -8,3 +13,4 @@ export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
export const getLabStats = () => api.get<GetLabStatsResult>(API.LAB.STATS).then(cleanResult);
|
export const getLabStats = () => api.get<GetLabStatsResult>(API.LAB.STATS).then(cleanResult);
|
||||||
|
export const getLabUpdates = () => api.get<GetLabUpdatesResult>(API.LAB.UPDATES).then(cleanResult);
|
||||||
|
|
|
@ -6,4 +6,8 @@ export const LAB_ACTIONS = {
|
||||||
|
|
||||||
GET_STATS: `${prefix}GET_STATS`,
|
GET_STATS: `${prefix}GET_STATS`,
|
||||||
SET_STATS: `${prefix}SET_STATS`,
|
SET_STATS: `${prefix}SET_STATS`,
|
||||||
|
|
||||||
|
SET_UPDATES: `${prefix}SET_UPDATES`,
|
||||||
|
GET_UPDATES: `${prefix}GET_UPDATES`,
|
||||||
|
SEEN_NODE: `${prefix}SET_UPDATE_SEEN`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
import { labSetList, labSetStats } from '~/redux/lab/actions';
|
import { labSetList, labSetStats, labSetUpdates } from '~/redux/lab/actions';
|
||||||
import { ILabState } from '~/redux/lab/types';
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
type LabHandler<T extends (...args: any) => any> = (
|
type LabHandler<T extends (...args: any) => any> = (
|
||||||
|
@ -23,7 +23,16 @@ const setStats: LabHandler<typeof labSetStats> = (state, { stats }) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setUpdates: LabHandler<typeof labSetUpdates> = (state, { updates }) => ({
|
||||||
|
...state,
|
||||||
|
updates: {
|
||||||
|
...state.updates,
|
||||||
|
...updates,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const LAB_HANDLERS = {
|
export const LAB_HANDLERS = {
|
||||||
[LAB_ACTIONS.SET_LIST]: setList,
|
[LAB_ACTIONS.SET_LIST]: setList,
|
||||||
[LAB_ACTIONS.SET_STATS]: setStats,
|
[LAB_ACTIONS.SET_STATS]: setStats,
|
||||||
|
[LAB_ACTIONS.SET_UPDATES]: setUpdates,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,10 @@ const INITIAL_STATE: ILabState = {
|
||||||
tags: [],
|
tags: [],
|
||||||
error: undefined,
|
error: undefined,
|
||||||
},
|
},
|
||||||
|
updates: {
|
||||||
|
nodes: [],
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { takeLeading, call, put } from 'redux-saga/effects';
|
import { takeLeading, call, put, select } from 'redux-saga/effects';
|
||||||
import { labGetList, labSetList, labSetStats } from '~/redux/lab/actions';
|
import {
|
||||||
|
labGetList,
|
||||||
|
labSetList,
|
||||||
|
labSetStats,
|
||||||
|
labSetUpdates,
|
||||||
|
labSeenNode,
|
||||||
|
} from '~/redux/lab/actions';
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
import { Unwrap } from '~/redux/types';
|
import { Unwrap } from '~/redux/types';
|
||||||
import { getLabNodes, getLabStats } from '~/redux/lab/api';
|
import { getLabNodes, getLabStats, getLabUpdates } from '~/redux/lab/api';
|
||||||
|
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
||||||
try {
|
try {
|
||||||
|
@ -28,7 +35,28 @@ function* getStats() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* getUpdates() {
|
||||||
|
try {
|
||||||
|
yield put(labSetUpdates({ isLoading: true }));
|
||||||
|
const { nodes }: Unwrap<typeof getLabUpdates> = yield call(getLabUpdates);
|
||||||
|
yield put(labSetUpdates({ nodes }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.message);
|
||||||
|
} finally {
|
||||||
|
yield put(labSetUpdates({ isLoading: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* seenNode({ nodeId }: ReturnType<typeof labSeenNode>) {
|
||||||
|
const nodes: ReturnType<typeof selectLabUpdatesNodes> = yield select(selectLabUpdatesNodes);
|
||||||
|
const newNodes = nodes.filter(node => node.id != nodeId);
|
||||||
|
yield put(labSetUpdates({ nodes: newNodes }));
|
||||||
|
}
|
||||||
|
|
||||||
export default function* labSaga() {
|
export default function* labSaga() {
|
||||||
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
||||||
yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
|
yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
|
||||||
|
|
||||||
|
yield takeLeading(LAB_ACTIONS.GET_UPDATES, getUpdates);
|
||||||
|
yield takeLeading(LAB_ACTIONS.SEEN_NODE, seenNode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,5 @@ export const selectLabList = (state: IState) => state.lab.list;
|
||||||
export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes;
|
export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes;
|
||||||
export const selectLabStatsTags = (state: IState) => state.lab.stats.tags;
|
export const selectLabStatsTags = (state: IState) => state.lab.stats.tags;
|
||||||
export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading;
|
export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading;
|
||||||
|
export const selectLabUpdates = (state: IState) => state.lab.updates;
|
||||||
|
export const selectLabUpdatesNodes = (state: IState) => state.lab.updates.nodes;
|
||||||
|
|
|
@ -13,6 +13,10 @@ export type ILabState = Readonly<{
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
updates: {
|
||||||
|
nodes: INode[];
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type GetLabNodesRequest = {
|
export type GetLabNodesRequest = {
|
||||||
|
@ -34,3 +38,7 @@ export type GetLabStatsResult = {
|
||||||
heroes: INode[];
|
heroes: INode[];
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetLabUpdatesResult = {
|
||||||
|
nodes: INode[];
|
||||||
|
};
|
||||||
|
|
|
@ -177,13 +177,6 @@ function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Remove current node from recently updated
|
|
||||||
const { updated } = yield select(selectFlow);
|
|
||||||
|
|
||||||
if (updated.some(item => item.id === id)) {
|
|
||||||
yield put(flowSetUpdated(updated.filter(item => item.id !== id)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
|
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
|
||||||
|
|
16
src/utils/hooks/node/useOnNodeSeen.ts
Normal file
16
src/utils/hooks/node/useOnNodeSeen.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { labSeenNode } from '~/redux/lab/actions';
|
||||||
|
import { flowSeenNode } from '~/redux/flow/actions';
|
||||||
|
|
||||||
|
// useOnNodeSeen updates node seen status across all needed places
|
||||||
|
export const useOnNodeSeen = (node: INode) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// Remove node from updated
|
||||||
|
if (node.is_promoted) {
|
||||||
|
dispatch(flowSeenNode(node.id));
|
||||||
|
} else {
|
||||||
|
dispatch(labSeenNode(node.id));
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue