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

added updates everywhere

This commit is contained in:
Fedor Katurov 2021-04-02 17:49:03 +07:00
parent a451e30499
commit b1e68a8a6d
27 changed files with 246 additions and 79 deletions

View file

@ -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> = ({

View file

@ -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;
} }

View file

@ -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>
); );

View file

@ -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;
}
} }

View file

@ -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 => {

View file

@ -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,
})} })}

View file

@ -116,6 +116,15 @@
} }
} }
&.lab::after {
background: lighten($blue, 10%);
}
&.boris::after {
background: lighten($wisegreen, 10%);
}
@include tablet { @include tablet {
padding: $gap; padding: $gap;

View file

@ -53,5 +53,6 @@ export const API = {
LAB: { LAB: {
NODES: `/lab/`, NODES: `/lab/`,
STATS: '/lab/stats', STATS: '/lab/stats',
UPDATES: '/lab/updates',
}, },
}; };

View file

@ -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" />
) : ( ) : (

View file

@ -23,3 +23,7 @@
background-color: $comment_bg; background-color: $comment_bg;
padding: $gap; padding: $gap;
} }
.updates {
padding: 0 $gap / 4 $gap * 2;
}

View file

@ -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 };

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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,
});

View file

@ -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`,
}; };

View file

@ -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);
} }

View file

@ -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;

View file

@ -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,
});

View file

@ -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);

View file

@ -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`,
}; };

View file

@ -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,
}; };

View file

@ -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);

View file

@ -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);
} }

View file

@ -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;

View file

@ -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[];
};

View file

@ -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>) {

View 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));
}
};