diff --git a/src/components/flow/Cell/index.tsx b/src/components/flow/Cell/index.tsx index d79c061b..71454949 100644 --- a/src/components/flow/Cell/index.tsx +++ b/src/components/flow/Cell/index.tsx @@ -9,6 +9,7 @@ import { flowSetCellView } from "~/redux/flow/actions"; import { PRESETS } from "~/constants/urls"; import { debounce } from "throttle-debounce"; import { NODE_TYPES } from "~/redux/node/constants"; +import { Group } from "~/components/containers/Group"; interface IProps { node: INode; @@ -135,7 +136,7 @@ const Cell: FC = ({
{title &&
{title}
} -
@@ -145,7 +146,7 @@ const Cell: FC = ({
{title &&
{title}
} -
diff --git a/src/containers/flow/FlowLayout/index.tsx b/src/containers/flow/FlowLayout/index.tsx index 6a17ee03..b6e59d69 100644 --- a/src/containers/flow/FlowLayout/index.tsx +++ b/src/containers/flow/FlowLayout/index.tsx @@ -1,40 +1,63 @@ -import React, { FC } from 'react'; -import { connect } from 'react-redux'; -import { FlowGrid } from '~/components/flow/FlowGrid'; -import { selectFlow } from '~/redux/flow/selectors'; -import * as NODE_ACTIONS from '~/redux/node/actions'; -import * as FLOW_ACTIONS from '~/redux/flow/actions'; -import pick from 'ramda/es/pick'; -import { selectUser } from '~/redux/auth/selectors'; +import React, { FC, useEffect, useCallback } from "react"; +import { connect } from "react-redux"; +import { FlowGrid } from "~/components/flow/FlowGrid"; +import { selectFlow } from "~/redux/flow/selectors"; +import * as NODE_ACTIONS from "~/redux/node/actions"; +import * as FLOW_ACTIONS from "~/redux/flow/actions"; +import pick from "ramda/es/pick"; +import { selectUser } from "~/redux/auth/selectors"; const mapStateToProps = state => ({ - flow: pick(['nodes', 'heroes', 'recent', 'updated'], selectFlow(state)), - user: pick(['role', 'id'], selectUser(state)), + flow: pick( + ["nodes", "heroes", "recent", "updated", "is_loading"], + selectFlow(state) + ), + user: pick(["role", "id"], selectUser(state)) }); const mapDispatchToProps = { nodeGotoNode: NODE_ACTIONS.nodeGotoNode, flowSetCellView: FLOW_ACTIONS.flowSetCellView, + flowGetMore: FLOW_ACTIONS.flowGetMore }; -type IProps = ReturnType & typeof mapDispatchToProps & {}; +type IProps = ReturnType & + typeof mapDispatchToProps & {}; const FlowLayoutUnconnected: FC = ({ - flow: { nodes, heroes, recent, updated }, + flow: { nodes, heroes, recent, updated, is_loading }, user, nodeGotoNode, flowSetCellView, -}) => ( - -); + flowGetMore +}) => { + const loadMore = useCallback(() => { + const pos = + window.scrollY + window.innerHeight - document.body.scrollHeight; + + if (is_loading || pos < -600) return; + + flowGetMore(); + }, [flowGetMore, is_loading]); + + useEffect(() => { + window.addEventListener("scroll", loadMore); + + return () => window.removeEventListener("scroll", loadMore); + }, [loadMore]); + + return ( + + ); +}; const FlowLayout = connect( mapStateToProps, diff --git a/src/index.tsx b/src/index.tsx index a0eddf95..7e4c2eda 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,11 +21,10 @@ render( /* [Stage 0]: -- fix: text nodes cell has no preview (actually, that's a problem of brief) - +- check if email is registered at social login +- friendship - password restore - signup? -- better node brief update - flow updates - flow infinite scroll - avatar upload @@ -43,6 +42,8 @@ render( - comment editing Done: +- better node brief update +- fix: text nodes cell has no preview (actually, that's a problem of brief) - relocate files - backend: exclude node covers on import - profile editing @@ -59,5 +60,4 @@ Done: - fix: select node and edit it. All images will be not loaded - fix: text nodes cell not clickable - fix: text nodes should not have 'no comments yet badge - */ diff --git a/src/redux/flow/actions.ts b/src/redux/flow/actions.ts index 5046ad9c..1ab8089b 100644 --- a/src/redux/flow/actions.ts +++ b/src/redux/flow/actions.ts @@ -1,29 +1,43 @@ -import { FLOW_ACTIONS } from './constants'; -import { IFlowState } from './reducer'; -import { INode } from '../types'; +import { FLOW_ACTIONS } from "./constants"; +import { IFlowState } from "./reducer"; +import { INode } from "../types"; -export const flowSetNodes = (nodes: IFlowState['nodes']) => ({ +export const flowSetNodes = (nodes: IFlowState["nodes"]) => ({ nodes, - type: FLOW_ACTIONS.SET_NODES, + type: FLOW_ACTIONS.SET_NODES }); -export const flowSetHeroes = (heroes: IFlowState['heroes']) => ({ +export const flowSetHeroes = (heroes: IFlowState["heroes"]) => ({ heroes, - type: FLOW_ACTIONS.SET_HEROES, + type: FLOW_ACTIONS.SET_HEROES }); -export const flowSetRecent = (recent: IFlowState['recent']) => ({ +export const flowSetRecent = (recent: IFlowState["recent"]) => ({ recent, - type: FLOW_ACTIONS.SET_RECENT, + type: FLOW_ACTIONS.SET_RECENT }); -export const flowSetUpdated = (updated: IFlowState['updated']) => ({ +export const flowSetUpdated = (updated: IFlowState["updated"]) => ({ updated, - type: FLOW_ACTIONS.SET_UPDATED, + type: FLOW_ACTIONS.SET_UPDATED }); -export const flowSetCellView = (id: INode['id'], flow: INode['flow']) => ({ +export const flowSetCellView = (id: INode["id"], flow: INode["flow"]) => ({ type: FLOW_ACTIONS.SET_CELL_VIEW, id, - flow, + flow +}); + +export const flowSetRange = (range: IFlowState["range"]) => ({ + range, + type: FLOW_ACTIONS.SET_RANGE +}); + +export const flowGetMore = () => ({ + type: FLOW_ACTIONS.GET_MORE +}); + +export const flowSetFlow = (data: Partial) => ({ + type: FLOW_ACTIONS.SET_FLOW, + data }); diff --git a/src/redux/flow/api.ts b/src/redux/flow/api.ts index acdcff66..3a7d83fe 100644 --- a/src/redux/flow/api.ts +++ b/src/redux/flow/api.ts @@ -1,11 +1,16 @@ -import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api'; -import { INode, IResultWithStatus } from '../types'; -import { API } from '~/constants/api'; -import { flowSetCellView } from '~/redux/flow/actions'; +import { + api, + configWithToken, + resultMiddleware, + errorMiddleware +} from "~/utils/api"; +import { INode, IResultWithStatus } from "../types"; +import { API } from "~/constants/api"; +import { flowSetCellView } from "~/redux/flow/actions"; export const postNode = ({ access, - node, + node }: { access: string; node: INode; @@ -15,24 +20,24 @@ export const postNode = ({ .then(resultMiddleware) .catch(errorMiddleware); -export const getNodes = ({ - skip = 0, -}: { - skip: number; -}): Promise> => - api - .get(API.NODE.GET, { params: { skip } }) - .then(resultMiddleware) - .catch(errorMiddleware); +// export const getNodes = ({ +// from = null +// }: { +// from: string; +// }): Promise> => +// api +// .get(API.NODE.GET, { params: { from } }) +// .then(resultMiddleware) +// .catch(errorMiddleware); export const postCellView = ({ id, flow, - access, + access }: ReturnType & { access: string }): Promise< - IResultWithStatus<{ is_liked: INode['is_liked'] }> + IResultWithStatus<{ is_liked: INode["is_liked"] }> > => api .post(API.NODE.SET_CELL_VIEW(id), { flow }, configWithToken(access)) .then(resultMiddleware) - .catch(errorMiddleware); \ No newline at end of file + .catch(errorMiddleware); diff --git a/src/redux/flow/constants.ts b/src/redux/flow/constants.ts index 5f7e02b8..2da99203 100644 --- a/src/redux/flow/constants.ts +++ b/src/redux/flow/constants.ts @@ -1,10 +1,13 @@ -const prefix = 'FLOW.'; +const prefix = "FLOW."; export const FLOW_ACTIONS = { GET_FLOW: `${prefix}GET_FLOW`, + SET_FLOW: `${prefix}SET_FLOW`, SET_NODES: `${prefix}SET_NODES`, SET_HEROES: `${prefix}SET_HEROES`, SET_RECENT: `${prefix}SET_RECENT`, SET_UPDATED: `${prefix}SET_UPDATED`, + SET_RANGE: `${prefix}SET_RANGE`, SET_CELL_VIEW: `${prefix}SET_CELL_VIEW`, + GET_MORE: `${prefix}GET_MORE` }; diff --git a/src/redux/flow/handlers.ts b/src/redux/flow/handlers.ts index 7a273178..bf85e649 100644 --- a/src/redux/flow/handlers.ts +++ b/src/redux/flow/handlers.ts @@ -1,23 +1,53 @@ -import assocPath from 'ramda/es/assocPath'; -import { FLOW_ACTIONS } from './constants'; -import { flowSetNodes, flowSetHeroes, flowSetRecent, flowSetUpdated } from './actions'; -import { IFlowState } from './reducer'; +import assocPath from "ramda/es/assocPath"; +import { FLOW_ACTIONS } from "./constants"; +import { + flowSetNodes, + flowSetHeroes, + flowSetRecent, + flowSetUpdated, + flowSetRange, + flowSetFlow +} from "./actions"; +import { IFlowState } from "./reducer"; -const setNodes = (state: IFlowState, { nodes }: ReturnType) => - assocPath(['nodes'], nodes, state); +const setNodes = ( + state: IFlowState, + { nodes }: ReturnType +) => assocPath(["nodes"], nodes, state); -const setHeroes = (state: IFlowState, { heroes }: ReturnType) => - assocPath(['heroes'], heroes, state); +const setHeroes = ( + state: IFlowState, + { heroes }: ReturnType +) => assocPath(["heroes"], heroes, state); -const setRecent = (state: IFlowState, { recent }: ReturnType) => - assocPath(['recent'], recent, state); +const setRecent = ( + state: IFlowState, + { recent }: ReturnType +) => assocPath(["recent"], recent, state); -const setUpdated = (state: IFlowState, { updated }: ReturnType) => - assocPath(['updated'], updated, state); +const setUpdated = ( + state: IFlowState, + { updated }: ReturnType +) => assocPath(["updated"], updated, state); + +const setRange = ( + state: IFlowState, + { range }: ReturnType +) => assocPath(["range"], range, state); + +const setFlow = ( + state: IFlowState, + { data }: ReturnType +): IFlowState => ({ + ...state, + ...data +}); export const FLOW_HANDLERS = { [FLOW_ACTIONS.SET_NODES]: setNodes, [FLOW_ACTIONS.SET_HEROES]: setHeroes, [FLOW_ACTIONS.SET_RECENT]: setRecent, [FLOW_ACTIONS.SET_UPDATED]: setUpdated, + [FLOW_ACTIONS.SET_RANGE]: setRange, + [FLOW_ACTIONS.SET_FLOW]: setFlow }; diff --git a/src/redux/flow/reducer.ts b/src/redux/flow/reducer.ts index d406bbd5..0b114df4 100644 --- a/src/redux/flow/reducer.ts +++ b/src/redux/flow/reducer.ts @@ -1,6 +1,6 @@ -import { createReducer } from '~/utils/reducer'; -import { INode, IError } from '../types'; -import { FLOW_HANDLERS } from './handlers'; +import { createReducer } from "~/utils/reducer"; +import { INode, IError } from "../types"; +import { FLOW_HANDLERS } from "./handlers"; export type IFlowState = Readonly<{ is_loading: boolean; @@ -8,6 +8,7 @@ export type IFlowState = Readonly<{ heroes: Partial[]; recent: Partial[]; updated: Partial[]; + range: [string, string]; error: IError; }>; @@ -16,8 +17,9 @@ const INITIAL_STATE: IFlowState = { heroes: [], recent: [], updated: [], + range: [null, null], // drop it, we use realtime range calc is_loading: false, - error: null, + error: null }; export default createReducer(INITIAL_STATE, FLOW_HANDLERS); diff --git a/src/redux/flow/sagas.ts b/src/redux/flow/sagas.ts index dfd738ce..93813ca0 100644 --- a/src/redux/flow/sagas.ts +++ b/src/redux/flow/sagas.ts @@ -1,56 +1,78 @@ -import { takeLatest, call, put, select } from 'redux-saga/effects'; -import { REHYDRATE } from 'redux-persist'; -import { FLOW_ACTIONS } from './constants'; -import { getNodes } from '../node/api'; +import { + takeLatest, + call, + put, + select, + takeLeading, + delay +} from "redux-saga/effects"; +import { REHYDRATE } from "redux-persist"; +import { FLOW_ACTIONS } from "./constants"; +import { getNodes } from "../node/api"; import { flowSetNodes, flowSetCellView, flowSetHeroes, flowSetRecent, flowSetUpdated, -} from './actions'; -import { IResultWithStatus, INode } from '../types'; -import { selectFlowNodes } from './selectors'; -import { reqWrapper } from '../auth/sagas'; -import { postCellView } from './api'; -import { IFlowState } from './reducer'; + flowSetFlow +} from "./actions"; +import { IResultWithStatus } from "../types"; +import { selectFlowNodes } from "./selectors"; +import { reqWrapper } from "../auth/sagas"; +import { postCellView } from "./api"; +import { IFlowState } from "./reducer"; function* onGetFlow() { + yield put(flowSetFlow({ is_loading: true })); + const { - data: { nodes = [], heroes = [], recent = [], updated = [] }, + data: { nodes = [], heroes = [], recent = [], updated = [], mode } }: IResultWithStatus<{ - nodes: IFlowState['nodes']; - heroes: IFlowState['heroes']; - recent: IFlowState['recent']; - updated: IFlowState['updated']; + nodes: IFlowState["nodes"]; + heroes: IFlowState["heroes"]; + recent: IFlowState["recent"]; + updated: IFlowState["updated"]; + mode: string; }> = yield call(reqWrapper, getNodes, {}); - // if (!nodes || !nodes.length) { - // yield put(flowSetNodes([])); - // yield put(flowSetHeroes([])); - // yield put(flowSetRecent([])); - // yield put(flowSetUpdated([])); - // return; - // } + yield put(flowSetFlow({ is_loading: false, nodes })); - yield put(flowSetNodes(nodes)); - yield put(flowSetHeroes(heroes)); - yield put(flowSetRecent(recent)); - yield put(flowSetUpdated(updated)); + if (heroes.length) yield put(flowSetHeroes(heroes)); + if (recent.length) yield put(flowSetRecent(recent)); + if (updated.length) yield put(flowSetUpdated(updated)); - document.getElementById('main_loader').style.display = 'none'; + document.getElementById("main_loader").style.display = "none"; } function* onSetCellView({ id, flow }: ReturnType) { const nodes = yield select(selectFlowNodes); - yield put(flowSetNodes(nodes.map(node => (node.id === id ? { ...node, flow } : node)))); + yield put( + flowSetNodes(nodes.map(node => (node.id === id ? { ...node, flow } : node))) + ); const { data, error } = yield call(reqWrapper, postCellView, { id, flow }); +} - console.log({ data, error }); +function* getMore() { + yield put(flowSetFlow({ is_loading: true })); + const nodes: IFlowState["nodes"] = yield select(selectFlowNodes); + const from = + nodes && nodes[nodes.length - 1] && nodes[nodes.length - 1].created_at; + + const { error, data } = yield call(reqWrapper, getNodes, { from }); + + if (error || !data || !data.nodes) return; + + yield put( + flowSetFlow({ is_loading: false, nodes: [...nodes, ...data.nodes] }) + ); + + yield delay(data.nodes.length > 0 ? 2000 : 30000); } export default function* nodeSaga() { yield takeLatest([FLOW_ACTIONS.GET_FLOW, REHYDRATE], onGetFlow); yield takeLatest(FLOW_ACTIONS.SET_CELL_VIEW, onSetCellView); + yield takeLeading(FLOW_ACTIONS.GET_MORE, getMore); } diff --git a/src/redux/node/api.ts b/src/redux/node/api.ts index 35049cd7..ab4c9c1b 100644 --- a/src/redux/node/api.ts +++ b/src/redux/node/api.ts @@ -1,12 +1,17 @@ -import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api'; -import { INode, IResultWithStatus, IComment } from '../types'; -import { API } from '~/constants/api'; -import { nodeUpdateTags, nodeLike, nodeStar } from './actions'; -import { INodeState } from './reducer'; +import { + api, + configWithToken, + resultMiddleware, + errorMiddleware +} from "~/utils/api"; +import { INode, IResultWithStatus, IComment } from "../types"; +import { API } from "~/constants/api"; +import { nodeUpdateTags, nodeLike, nodeStar } from "./actions"; +import { INodeState } from "./reducer"; export const postNode = ({ access, - node, + node }: { access: string; node: INode; @@ -18,20 +23,20 @@ export const postNode = ({ // .then(console.log); export const getNodes = ({ - skip = 0, - access, + from = null, + access }: { - skip?: number; + from?: string; access: string; }): Promise> => api - .get(API.NODE.GET, configWithToken(access, { params: { skip } })) + .get(API.NODE.GET, configWithToken(access, { params: { from } })) .then(resultMiddleware) .catch(errorMiddleware); export const getNode = ({ id, - access, + access }: { id: string | number; access: string; @@ -44,7 +49,7 @@ export const getNode = ({ export const postNodeComment = ({ id, data, - access, + access }: { access: string; id: number; @@ -58,11 +63,11 @@ export const postNodeComment = ({ export const getNodeComments = ({ id, access, - order = 'ASC', + order = "ASC" }: { id: number; access: string; - order: 'ASC' | 'DESC'; + order: "ASC" | "DESC"; }): Promise> => api .get(API.NODE.COMMENT(id), configWithToken(access, { params: { order } })) @@ -71,11 +76,11 @@ export const getNodeComments = ({ export const getNodeRelated = ({ id, - access, + access }: { id: number; access: string; -}): Promise> => +}): Promise> => api .get(API.NODE.RELATED(id), configWithToken(access)) .then(resultMiddleware) @@ -84,7 +89,7 @@ export const getNodeRelated = ({ export const updateNodeTags = ({ id, tags, - access, + access }: ReturnType & { access: string }): Promise< IResultWithStatus<{ node: INode }> > => @@ -95,9 +100,9 @@ export const updateNodeTags = ({ export const postNodeLike = ({ id, - access, + access }: ReturnType & { access: string }): Promise< - IResultWithStatus<{ is_liked: INode['is_liked'] }> + IResultWithStatus<{ is_liked: INode["is_liked"] }> > => api .post(API.NODE.POST_LIKE(id), {}, configWithToken(access)) @@ -106,9 +111,9 @@ export const postNodeLike = ({ export const postNodeStar = ({ id, - access, + access }: ReturnType & { access: string }): Promise< - IResultWithStatus<{ is_liked: INode['is_liked'] }> + IResultWithStatus<{ is_liked: INode["is_liked"] }> > => api .post(API.NODE.POST_STAR(id), {}, configWithToken(access)) diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 321d40de..d58a3a6d 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -1,19 +1,23 @@ -import { IFile } from '~/redux/types'; -import formatDistanceToNow from 'date-fns/formatDistanceToNow'; -import { ru } from 'date-fns/locale'; -import Axios from 'axios'; -import { PRESETS } from '~/constants/urls'; +import { IFile } from "~/redux/types"; +import formatDistanceToNow from "date-fns/formatDistanceToNow"; +import { ru } from "date-fns/locale"; +import Axios from "axios"; +import { PRESETS } from "~/constants/urls"; export const getStyle = (oElm: any, strCssRule: string) => { if (document.defaultView && document.defaultView.getComputedStyle) { - return document.defaultView.getComputedStyle(oElm, '').getPropertyValue(strCssRule); + return document.defaultView + .getComputedStyle(oElm, "") + .getPropertyValue(strCssRule); } if (oElm.currentStyle) { - return oElm.currentStyle[strCssRule.replace(/-(\w)/g, (strMatch, p1) => p1.toUpperCase())]; + return oElm.currentStyle[ + strCssRule.replace(/-(\w)/g, (strMatch, p1) => p1.toUpperCase()) + ]; } - return ''; + return ""; }; function polarToCartesian(centerX, centerY, radius, angleInDegrees) { @@ -21,7 +25,7 @@ function polarToCartesian(centerX, centerY, radius, angleInDegrees) { return { x: centerX + radius * Math.cos(angleInRadians), - y: centerY + radius * Math.sin(angleInRadians), + y: centerY + radius * Math.sin(angleInRadians) }; } @@ -38,10 +42,10 @@ export const describeArc = ( const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1; return [ - 'M', + "M", start.x, start.y, - 'A', + "A", radius, radius, 0, @@ -49,63 +53,76 @@ export const describeArc = ( 0, end.x, end.y, - 'L', + "L", x, y, - 'L', + "L", start.x, - start.y, - ].join(' '); + start.y + ].join(" "); }; -export const getURL = (file: Partial, size?: typeof PRESETS[keyof typeof PRESETS]) => { +export const getURL = ( + file: Partial, + size?: typeof PRESETS[keyof typeof PRESETS] +) => { if (!file || !file.url) return null; if (size) { return file.url - .replace('REMOTE_CURRENT://', `${process.env.REMOTE_CURRENT}cache/${size}/`) - .replace('REMOTE_OLD://', process.env.REMOTE_OLD); + .replace( + "REMOTE_CURRENT://", + `${process.env.REMOTE_CURRENT}cache/${size}/` + ) + .replace("REMOTE_OLD://", process.env.REMOTE_OLD); } return file.url - .replace('REMOTE_CURRENT://', process.env.REMOTE_CURRENT) - .replace('REMOTE_OLD://', process.env.REMOTE_OLD); + .replace("REMOTE_CURRENT://", process.env.REMOTE_CURRENT) + .replace("REMOTE_OLD://", process.env.REMOTE_OLD); }; export const formatText = (text: string): string => !text - ? '' + ? "" : text - .replace(/(\n{2,})/gi, '\n') - .replace(//g, '>') + .replace(/\n{1,}/gim, "\n") + .replace(//g, ">") .replace( /~([\wа-яА-Я\-]+)/giu, - '~$1' + "~$1" ) - .replace(/:\/\//gim, ':|--|') + .replace(/:\/\//gim, ":|--|") .replace(/(\/\/[^\n]+)/gim, '$1') .replace(/(\/\*[\s\S]*?\*\/)/gim, '$1') - .replace(/:\|--\|/gim, '://') - .split('\n') + .replace(/:\|--\|/gim, "://") + .split("\n") + .filter(el => el.trim().length) .map(el => `

${el}

`) - .join(''); + .join(""); export const formatCommentText = (author: string, text: string): string => text ? formatText(text).replace( /^

/, - author ? `

${author}: ` : '

' + author ? `

${author}: ` : "

" ) - : ''; + : ""; export const formatCellText = (text: string): string => formatText(text); export const getPrettyDate = (date: string): string => - formatDistanceToNow(new Date(date), { locale: ru, includeSeconds: true, addSuffix: true }); + formatDistanceToNow(new Date(date), { + locale: ru, + includeSeconds: true, + addSuffix: true + }); export const getYoutubeTitle = async (id: string) => { - Axios.get(`http://youtube.com/get_video_info?video_id=${id}`).then(console.log); + Axios.get(`http://youtube.com/get_video_info?video_id=${id}`).then( + console.log + ); }; (window).getYoutubeTitle = getYoutubeTitle;