diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index b215379e..318380f3 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -33,7 +33,8 @@ const CommentContent: FC = memo(({ comment, can_edit, onDelete, modalSho const groupped = useMemo>( () => reduce( - (group, file) => assocPath([file.type], append(file, group[file.type]), group), + (group, file) => + file.type ? assocPath([file.type], append(file, group[file.type]), group) : group, {}, comment.files ), diff --git a/src/components/comment/CommentEmbedBlock/index.tsx b/src/components/comment/CommentEmbedBlock/index.tsx index 46bdd08a..77a374d2 100644 --- a/src/components/comment/CommentEmbedBlock/index.tsx +++ b/src/components/comment/CommentEmbedBlock/index.tsx @@ -6,6 +6,7 @@ import { selectPlayer } from '~/redux/player/selectors'; import { connect } from 'react-redux'; import * as PLAYER_ACTIONS from '~/redux/player/actions'; import { Icon } from '~/components/input/Icon'; +import { path } from 'ramda'; const mapStateToProps = state => ({ youtubes: selectPlayer(state).youtubes, @@ -21,30 +22,32 @@ type Props = ReturnType & const CommentEmbedBlockUnconnected: FC = memo( ({ block, youtubes, playerGetYoutubeInfo }) => { - const link = useMemo( - () => - block.content.match( - /https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?([\w\-\=]+)/ - ), - [block.content] - ); + const id = useMemo(() => { + const match = block.content.match( + /https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-\=]+)/ + ); + + return (match && match[1]) || ''; + }, [block.content]); const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]); useEffect(() => { - if (!link[5] || youtubes[link[5]]) return; - playerGetYoutubeInfo(link[5]); - }, [link, playerGetYoutubeInfo]); + if (!id) return; + playerGetYoutubeInfo(id); + }, [id, playerGetYoutubeInfo]); - const title = useMemo( - () => - (youtubes[link[5]] && youtubes[link[5]].metadata && youtubes[link[5]].metadata.title) || '', - [link, youtubes] - ); + const title = useMemo(() => { + if (!id) { + return block.content; + } + + return path([id, 'metadata', 'title'], youtubes) || block.content; + }, [id, youtubes, block.content]); return ( diff --git a/src/components/comment/CommentForm/index.tsx b/src/components/comment/CommentForm/index.tsx index 8b0a1c8c..b644509d 100644 --- a/src/components/comment/CommentForm/index.tsx +++ b/src/components/comment/CommentForm/index.tsx @@ -67,7 +67,13 @@ const CommentForm: FC = ({ comment, nodeId, onCancelEdit }) => { - + + {!!textarea && ( + + )} {isLoading && } diff --git a/src/components/comment/CommentFormAttaches/index.tsx b/src/components/comment/CommentFormAttaches/index.tsx index 1c764f14..7422c134 100644 --- a/src/components/comment/CommentFormAttaches/index.tsx +++ b/src/components/comment/CommentFormAttaches/index.tsx @@ -10,7 +10,8 @@ import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants'; import { useFileUploaderContext } from '~/utils/hooks/fileUploader'; const CommentFormAttaches: FC = () => { - const { files, pending, setFiles, uploadFiles } = useFileUploaderContext(); + const uploader = useFileUploaderContext(); + const { files, pending, setFiles, uploadFiles } = uploader!; const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [ files, @@ -70,7 +71,7 @@ const CommentFormAttaches: FC = () => { ); const onAudioTitleChange = useCallback( - (fileId: IFile['id'], title: IFile['metadata']['title']) => { + (fileId: IFile['id'], title: string) => { setFiles( files.map(file => file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file @@ -80,36 +81,36 @@ const CommentFormAttaches: FC = () => { [files, setFiles] ); - return ( - hasAttaches && ( -
- {hasImageAttaches && ( - - )} + if (!hasAttaches) return null; - {hasAudioAttaches && ( - - )} -
- ) + return ( +
+ {hasImageAttaches && ( + + )} + + {hasAudioAttaches && ( + + )} +
); }; diff --git a/src/components/comment/LocalCommentFormTextarea/index.tsx b/src/components/comment/LocalCommentFormTextarea/index.tsx index 79e4a524..2dab51fb 100644 --- a/src/components/comment/LocalCommentFormTextarea/index.tsx +++ b/src/components/comment/LocalCommentFormTextarea/index.tsx @@ -13,7 +13,7 @@ const LocalCommentFormTextarea: FC = ({ setRef }) => { const onKeyDown = useCallback>( ({ ctrlKey, key }) => { - if (!!ctrlKey && key === 'Enter') handleSubmit(null); + if (ctrlKey && key === 'Enter') handleSubmit(undefined); }, [handleSubmit] ); diff --git a/src/components/containers/CoverBackdrop/index.tsx b/src/components/containers/CoverBackdrop/index.tsx index 2e491137..0a6b7f80 100644 --- a/src/components/containers/CoverBackdrop/index.tsx +++ b/src/components/containers/CoverBackdrop/index.tsx @@ -1,16 +1,16 @@ -import React, { FC, useState, useCallback, useEffect, useRef } from "react"; -import { IUser } from "~/redux/auth/types"; +import React, { FC, useState, useCallback, useEffect, useRef } from 'react'; +import { IUser } from '~/redux/auth/types'; import styles from './styles.module.scss'; -import { getURL } from "~/utils/dom"; -import { PRESETS } from "~/constants/urls"; -import classNames from "classnames"; +import { getURL } from '~/utils/dom'; +import { PRESETS } from '~/constants/urls'; +import classNames from 'classnames'; interface IProps { - cover: IUser["cover"]; + cover: IUser['cover']; } const CoverBackdrop: FC = ({ cover }) => { - const ref = useRef(); + const ref = useRef(null); const [is_loaded, setIsLoaded] = useState(false); @@ -21,7 +21,7 @@ const CoverBackdrop: FC = ({ cover }) => { useEffect(() => { if (!cover || !cover.url || !ref || !ref.current) return; - ref.current.src = ""; + ref.current.src = ''; setIsLoaded(false); ref.current.src = getURL(cover, PRESETS.cover); }, [cover]); diff --git a/src/components/containers/FullWidth/index.tsx b/src/components/containers/FullWidth/index.tsx index 3c3eefed..b0e4dd0b 100644 --- a/src/components/containers/FullWidth/index.tsx +++ b/src/components/containers/FullWidth/index.tsx @@ -16,7 +16,7 @@ const FullWidth: FC = ({ children, onRefresh }) => { const { width } = sample.current.getBoundingClientRect(); const { clientWidth } = document.documentElement; - onRefresh(clientWidth); + if (onRefresh) onRefresh(clientWidth); return { width: clientWidth, diff --git a/src/components/containers/Sticky/index.tsx b/src/components/containers/Sticky/index.tsx index e3bc031b..79d57d3c 100644 --- a/src/components/containers/Sticky/index.tsx +++ b/src/components/containers/Sticky/index.tsx @@ -11,7 +11,7 @@ interface IProps extends DetailsHTMLAttributes {} const Sticky: FC = ({ children }) => { const ref = useRef(null); - let sb = null; + let sb; useEffect(() => { if (!ref.current) return; diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx index a51c300d..94acddc4 100644 --- a/src/components/editors/AudioEditor/index.tsx +++ b/src/components/editors/AudioEditor/index.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { INode } from '~/redux/types'; import { connect } from 'react-redux'; import { UPLOAD_TYPES } from '~/redux/uploads/constants'; import { ImageGrid } from '../ImageGrid'; @@ -8,19 +7,14 @@ import { selectUploads } from '~/redux/uploads/selectors'; import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; import styles from './styles.module.scss'; +import { NodeEditorProps } from '~/redux/node/types'; const mapStateToProps = selectUploads; const mapDispatchToProps = { uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles, }; -type IProps = ReturnType & - typeof mapDispatchToProps & { - data: INode; - setData: (val: INode) => void; - temp: string[]; - setTemp: (val: string[]) => void; - }; +type IProps = ReturnType & typeof mapDispatchToProps & NodeEditorProps; const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) => { const images = useMemo( @@ -69,9 +63,6 @@ const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) = ); }; -const AudioEditor = connect( - mapStateToProps, - mapDispatchToProps -)(AudioEditorUnconnected); +const AudioEditor = connect(mapStateToProps, mapDispatchToProps)(AudioEditorUnconnected); export { AudioEditor }; diff --git a/src/components/editors/AudioGrid/index.tsx b/src/components/editors/AudioGrid/index.tsx index 1640754f..d94c521b 100644 --- a/src/components/editors/AudioGrid/index.tsx +++ b/src/components/editors/AudioGrid/index.tsx @@ -35,7 +35,7 @@ const AudioGrid: FC = ({ files, setFiles, locked }) => { ); const onTitleChange = useCallback( - (changeId: IFile['id'], title: IFile['metadata']['title']) => { + (changeId: IFile['id'], title: string) => { setFiles( files.map(file => file && file.id === changeId ? { ...file, metadata: { ...file.metadata, title } } : file diff --git a/src/components/editors/EditorPanel/index.tsx b/src/components/editors/EditorPanel/index.tsx index c91a41d6..2506452b 100644 --- a/src/components/editors/EditorPanel/index.tsx +++ b/src/components/editors/EditorPanel/index.tsx @@ -2,6 +2,7 @@ import React, { FC, createElement } from 'react'; import styles from './styles.module.scss'; import { INode } from '~/redux/types'; import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants'; +import { has } from 'ramda'; interface IProps { data: INode; @@ -10,13 +11,19 @@ interface IProps { setTemp: (val: string[]) => void; } -const EditorPanel: FC = ({ data, setData, temp, setTemp }) => ( -
- {NODE_PANEL_COMPONENTS[data.type] && - NODE_PANEL_COMPONENTS[data.type].map((el, key) => - createElement(el, { key, data, setData, temp, setTemp }) - )} -
-); +const EditorPanel: FC = ({ data, setData, temp, setTemp }) => { + if (!data.type || !has(data.type, NODE_PANEL_COMPONENTS)) { + return null; + } + + return ( +
+ {NODE_PANEL_COMPONENTS[data.type] && + NODE_PANEL_COMPONENTS[data.type].map((el, key) => + createElement(el, { key, data, setData, temp, setTemp }) + )} +
+ ); +}; export { EditorPanel }; diff --git a/src/components/editors/EditorUploadButton/index.tsx b/src/components/editors/EditorUploadButton/index.tsx index 34e90a44..21b7cc50 100644 --- a/src/components/editors/EditorUploadButton/index.tsx +++ b/src/components/editors/EditorUploadButton/index.tsx @@ -64,7 +64,10 @@ const EditorUploadButtonUnconnected: FC = ({ }) ); - const temps = items.map(file => file.temp_id).slice(0, limit); + const temps = items + .filter(file => file?.temp_id) + .map(file => file.temp_id!) + .slice(0, limit); setTemp([...temp, ...temps]); uploadUploadFiles(items); diff --git a/src/components/editors/EditorUploadCoverButton/index.tsx b/src/components/editors/EditorUploadCoverButton/index.tsx index 38cf84c3..9fd4d6cc 100644 --- a/src/components/editors/EditorUploadCoverButton/index.tsx +++ b/src/components/editors/EditorUploadCoverButton/index.tsx @@ -33,16 +33,16 @@ const EditorUploadCoverButtonUnconnected: FC = ({ statuses, uploadUploadFiles, }) => { - const [cover_temp, setCoverTemp] = useState(null); + const [coverTemp, setCoverTemp] = useState(''); useEffect(() => { Object.entries(statuses).forEach(([id, status]) => { - if (cover_temp === id && !!status.uuid && files[status.uuid]) { + if (coverTemp === id && !!status.uuid && files[status.uuid]) { setData({ ...data, cover: files[status.uuid] }); - setCoverTemp(null); + setCoverTemp(''); } }); - }, [statuses, files, cover_temp, setData, data]); + }, [statuses, files, coverTemp, setData, data]); const onUpload = useCallback( (uploads: File[]) => { @@ -56,7 +56,7 @@ const EditorUploadCoverButtonUnconnected: FC = ({ }) ); - setCoverTemp(path([0, 'temp_id'], items)); + setCoverTemp(path([0, 'temp_id'], items) || ''); uploadUploadFiles(items); }, [uploadUploadFiles, setCoverTemp] @@ -73,11 +73,11 @@ const EditorUploadCoverButtonUnconnected: FC = ({ [onUpload] ); const onDropCover = useCallback(() => { - setData({ ...data, cover: null }); + setData({ ...data, cover: undefined }); }, [setData, data]); const background = data.cover ? getURL(data.cover, PRESETS['300']) : null; - const status = cover_temp && path([cover_temp], statuses); + const status = coverTemp && path([coverTemp], statuses); const preview = status && path(['preview'], status); return ( diff --git a/src/components/editors/ImageEditor/index.tsx b/src/components/editors/ImageEditor/index.tsx index 5dfe29e8..104fa220 100644 --- a/src/components/editors/ImageEditor/index.tsx +++ b/src/components/editors/ImageEditor/index.tsx @@ -5,19 +5,14 @@ import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; import { selectUploads } from '~/redux/uploads/selectors'; import { ImageGrid } from '~/components/editors/ImageGrid'; import styles from './styles.module.scss'; +import { NodeEditorProps } from '~/redux/node/types'; const mapStateToProps = selectUploads; const mapDispatchToProps = { uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles, }; -type IProps = ReturnType & - typeof mapDispatchToProps & { - data: INode; - setData: (val: INode) => void; - temp: string[]; - setTemp: (val: string[]) => void; - }; +type IProps = ReturnType & typeof mapDispatchToProps & NodeEditorProps; const ImageEditorUnconnected: FC = ({ data, setData, temp, statuses }) => { const pending_files = useMemo(() => temp.filter(id => !!statuses[id]).map(id => statuses[id]), [ @@ -34,9 +29,6 @@ const ImageEditorUnconnected: FC = ({ data, setData, temp, statuses }) = ); }; -const ImageEditor = connect( - mapStateToProps, - mapDispatchToProps -)(ImageEditorUnconnected); +const ImageEditor = connect(mapStateToProps, mapDispatchToProps)(ImageEditorUnconnected); export { ImageEditor }; diff --git a/src/components/editors/SortableAudioGrid/index.tsx b/src/components/editors/SortableAudioGrid/index.tsx index 38368863..fc1bf578 100644 --- a/src/components/editors/SortableAudioGrid/index.tsx +++ b/src/components/editors/SortableAudioGrid/index.tsx @@ -17,7 +17,7 @@ const SortableAudioGrid = SortableContainer( items: IFile[]; locked: IUploadStatus[]; onDelete: (file_id: IFile['id']) => void; - onTitleChange: (file_id: IFile['id'], title: IFile['metadata']['title']) => void; + onTitleChange: (file_id: IFile['id'], title: string) => void; }) => { return (
diff --git a/src/components/editors/TextEditor/index.tsx b/src/components/editors/TextEditor/index.tsx index 2cb1e6fb..113d4df5 100644 --- a/src/components/editors/TextEditor/index.tsx +++ b/src/components/editors/TextEditor/index.tsx @@ -3,11 +3,9 @@ import { INode } from '~/redux/types'; import styles from './styles.module.scss'; import { Textarea } from '~/components/input/Textarea'; import { path } from 'ramda'; +import { NodeEditorProps } from '~/redux/node/types'; -interface IProps { - data: INode; - setData: (val: INode) => void; -} +type IProps = NodeEditorProps & {}; const TextEditor: FC = ({ data, setData }) => { const setText = useCallback( diff --git a/src/components/editors/VideoEditor/index.tsx b/src/components/editors/VideoEditor/index.tsx index 443dae4c..fa9803f9 100644 --- a/src/components/editors/VideoEditor/index.tsx +++ b/src/components/editors/VideoEditor/index.tsx @@ -5,11 +5,9 @@ import { path } from 'ramda'; import { InputText } from '~/components/input/InputText'; import classnames from 'classnames'; import { getYoutubeThumb } from '~/utils/dom'; +import { NodeEditorProps } from '~/redux/node/types'; -interface IProps { - data: INode; - setData: (val: INode) => void; -} +type IProps = NodeEditorProps & {}; const VideoEditor: FC = ({ data, setData }) => { const setUrl = useCallback( @@ -19,9 +17,10 @@ const VideoEditor: FC = ({ data, setData }) => { const url = (path(['blocks', 0, 'url'], data) as string) || ''; const preview = useMemo(() => getYoutubeThumb(url), [url]); + const backgroundImage = (preview && `url("${preview}")`) || ''; return ( -
+
diff --git a/src/components/flow/Cell/index.tsx b/src/components/flow/Cell/index.tsx index db1f47c9..2823dfb6 100644 --- a/src/components/flow/Cell/index.tsx +++ b/src/components/flow/Cell/index.tsx @@ -119,7 +119,7 @@ const Cell: FC = ({ } }, [title]); - const cellText = useMemo(() => formatCellText(text), [text]); + const cellText = useMemo(() => formatCellText(text || ''), [text]); return (
diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx index 98ff4ff3..df0765ce 100644 --- a/src/components/flow/FlowGrid/index.tsx +++ b/src/components/flow/FlowGrid/index.tsx @@ -13,16 +13,22 @@ type IProps = Partial & { onChangeCellView: typeof flowSetCellView; }; -export const FlowGrid: FC = ({ user, nodes, onSelect, onChangeCellView }) => ( - - {nodes.map(node => ( - - ))} - -); +export const FlowGrid: FC = ({ user, nodes, onSelect, onChangeCellView }) => { + if (!nodes) { + return null; + } + + return ( + + {nodes.map(node => ( + + ))} + + ); +}; diff --git a/src/components/flow/FlowHero/index.tsx b/src/components/flow/FlowHero/index.tsx index 409b2d5c..2c1af190 100644 --- a/src/components/flow/FlowHero/index.tsx +++ b/src/components/flow/FlowHero/index.tsx @@ -7,7 +7,7 @@ import { getURL } from '~/utils/dom'; import { withRouter, RouteComponentProps, useHistory } from 'react-router'; import { URLS, PRESETS } from '~/constants/urls'; import { Icon } from '~/components/input/Icon'; -import { INode } from "~/redux/types"; +import { INode } from '~/redux/types'; type IProps = RouteComponentProps & { heroes: IFlowState['heroes']; @@ -18,46 +18,54 @@ const FlowHeroUnconnected: FC = ({ heroes }) => { const [limit, setLimit] = useState(6); const [current, setCurrent] = useState(0); const [loaded, setLoaded] = useState[]>([]); - const timer = useRef(null) + const timer = useRef(null); const history = useHistory(); - const onLoad = useCallback((i: number) => { - setLoaded([...loaded, heroes[i]]) - }, [heroes, loaded, setLoaded]) + const onLoad = useCallback( + (i: number) => { + setLoaded([...loaded, heroes[i]]); + }, + [heroes, loaded, setLoaded] + ); - const items = Math.min(heroes.length, limit) + const items = Math.min(heroes.length, limit); const title = useMemo(() => { return loaded[current]?.title || ''; }, [loaded, current, heroes]); const onNext = useCallback(() => { - if (heroes.length > limit) setLimit(limit + 1) - setCurrent(current < items - 1 ? current + 1 : 0) - }, [current, items, limit, heroes.length]) - const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [current, items]) + if (heroes.length > limit) setLimit(limit + 1); + setCurrent(current < items - 1 ? current + 1 : 0); + }, [current, items, limit, heroes.length]); + const onPrev = useCallback(() => setCurrent(current > 0 ? current - 1 : items - 1), [ + current, + items, + ]); const goToNode = useCallback(() => { - history.push(URLS.NODE_URL(loaded[current].id)) + history.push(URLS.NODE_URL(loaded[current].id)); }, [current, loaded]); useEffect(() => { - timer.current = setTimeout(onNext, 5000) - return () => clearTimeout(timer.current) - }, [current, timer.current]) + timer.current = setTimeout(onNext, 5000); + return () => clearTimeout(timer.current); + }, [current, timer.current]); useEffect(() => { - if (loaded.length === 1) onNext() - }, [loaded]) + if (loaded.length === 1) onNext(); + }, [loaded]); return (
- { - heroes.slice(0, items).map((hero, i) => ( - onLoad(i)} /> - )) - } + {heroes.slice(0, items).map((hero, i) => ( + onLoad(i)} + /> + ))}
{loaded.length > 0 && ( @@ -87,10 +95,7 @@ const FlowHeroUnconnected: FC = ({ heroes }) => { key={hero.id} onClick={goToNode} > - {hero.thumbnail} + {hero.thumbnail}
))}
diff --git a/src/components/input/ArcProgress/index.tsx b/src/components/input/ArcProgress/index.tsx index f9db81ea..12309968 100644 --- a/src/components/input/ArcProgress/index.tsx +++ b/src/components/input/ArcProgress/index.tsx @@ -4,19 +4,11 @@ import { describeArc } from '~/utils/dom'; interface IProps { size: number; - progress: number; + progress?: number; } -export const ArcProgress: FC = ({ size, progress }) => ( +export const ArcProgress: FC = ({ size, progress = 0 }) => ( - + ); diff --git a/src/components/input/Button/index.tsx b/src/components/input/Button/index.tsx index d34c4b0a..8c263770 100644 --- a/src/components/input/Button/index.tsx +++ b/src/components/input/Button/index.tsx @@ -50,7 +50,7 @@ const Button: FC = memo( ref, ...props }) => { - const tooltip = useRef(); + const tooltip = useRef(null); const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, { placement: 'top', modifiers: [ diff --git a/src/components/input/InputText/index.tsx b/src/components/input/InputText/index.tsx index e5d50552..5495c068 100644 --- a/src/components/input/InputText/index.tsx +++ b/src/components/input/InputText/index.tsx @@ -20,10 +20,16 @@ const InputText: FC = ({ ...props }) => { const [focused, setFocused] = useState(false); - const [inner_ref, setInnerRef] = useState(null); + const [inner_ref, setInnerRef] = useState(null); const onInput = useCallback( - ({ target }: ChangeEvent) => handler(target.value), + ({ target }: ChangeEvent) => { + if (!handler) { + return; + } + + handler(target.value); + }, [handler] ); diff --git a/src/components/main/GodRays/index.tsx b/src/components/main/GodRays/index.tsx index 306d9b19..24a18dfb 100644 --- a/src/components/main/GodRays/index.tsx +++ b/src/components/main/GodRays/index.tsx @@ -34,6 +34,10 @@ export class GodRays extends React.Component { const ctx = this.canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.globalCompositeOperation = 'luminosity'; ctx.clearRect(0, 0, width, height + 100); // clear canvas ctx.save(); @@ -123,7 +127,7 @@ export class GodRays extends React.Component { ); } - canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement | null | undefined; inc; } diff --git a/src/components/main/Notifications/index.tsx b/src/components/main/Notifications/index.tsx index 2b02db7b..d5a04046 100644 --- a/src/components/main/Notifications/index.tsx +++ b/src/components/main/Notifications/index.tsx @@ -42,8 +42,12 @@ const NotificationsUnconnected: FC = ({ (notification: INotification) => { switch (notification.type) { case 'message': + if (!(notification as IMessageNotification)?.content?.from?.username) { + return; + } + return authOpenProfile( - (notification as IMessageNotification).content.from.username, + (notification as IMessageNotification).content.from!.username, 'messages' ); default: @@ -78,9 +82,6 @@ const NotificationsUnconnected: FC = ({ ); }; -const Notifications = connect( - mapStateToProps, - mapDispatchToProps -)(NotificationsUnconnected); +const Notifications = connect(mapStateToProps, mapDispatchToProps)(NotificationsUnconnected); export { Notifications }; diff --git a/src/components/main/UserButton/index.tsx b/src/components/main/UserButton/index.tsx index b0b3af3b..711b9319 100644 --- a/src/components/main/UserButton/index.tsx +++ b/src/components/main/UserButton/index.tsx @@ -15,10 +15,12 @@ interface IProps { const UserButton: FC = ({ user: { username, photo }, authOpenProfile, onLogout }) => { const onProfileOpen = useCallback(() => { + if (!username) return; authOpenProfile(username, 'profile'); }, [authOpenProfile, username]); const onSettingsOpen = useCallback(() => { + if (!username) return; authOpenProfile(username, 'settings'); }, [authOpenProfile, username]); diff --git a/src/components/media/AudioPlayer/index.tsx b/src/components/media/AudioPlayer/index.tsx index d5942f2f..a8419e2e 100644 --- a/src/components/media/AudioPlayer/index.tsx +++ b/src/components/media/AudioPlayer/index.tsx @@ -26,7 +26,7 @@ type Props = ReturnType & file: IFile; isEditing?: boolean; onDelete?: (id: IFile['id']) => void; - onTitleChange?: (file_id: IFile['id'], title: IFile['metadata']['title']) => void; + onTitleChange?: (file_id: IFile['id'], title: string) => void; }; const AudioPlayerUnconnected = memo( @@ -93,14 +93,18 @@ const AudioPlayerUnconnected = memo( [file.metadata] ); - const onRename = useCallback((val: string) => onTitleChange(file.id, val), [ - onTitleChange, - file.id, - ]); + const onRename = useCallback( + (val: string) => { + if (!onTitleChange) return; + + onTitleChange(file.id, val); + }, + [onTitleChange, file.id] + ); useEffect(() => { const active = current && current.id === file.id; - setPlaying(current && current.id === file.id); + setPlaying(!!current && current.id === file.id); if (active) Player.on('playprogress', onProgress); diff --git a/src/components/node/ImageSwitcher/index.tsx b/src/components/node/ImageSwitcher/index.tsx index dceb1eec..32c03ea3 100644 --- a/src/components/node/ImageSwitcher/index.tsx +++ b/src/components/node/ImageSwitcher/index.tsx @@ -19,7 +19,10 @@ const ImageSwitcher: FC = ({ total, current, onChange, loaded }) => {
{range(0, total).map(item => (
onChange(item)} /> diff --git a/src/components/node/NodeComments/index.tsx b/src/components/node/NodeComments/index.tsx index 899b337c..075c68a8 100644 --- a/src/components/node/NodeComments/index.tsx +++ b/src/components/node/NodeComments/index.tsx @@ -14,7 +14,7 @@ import { modalShowPhotoswipe } from '~/redux/modal/actions'; import { useDispatch } from 'react-redux'; interface IProps { - comments?: IComment[]; + comments: IComment[]; count: INodeState['comment_count']; user: IUser; order?: 'ASC' | 'DESC'; diff --git a/src/components/node/NodeImageSlideBlock/index.tsx b/src/components/node/NodeImageSlideBlock/index.tsx index ab844c35..85c5cbbd 100644 --- a/src/components/node/NodeImageSlideBlock/index.tsx +++ b/src/components/node/NodeImageSlideBlock/index.tsx @@ -36,8 +36,8 @@ const NodeImageSlideBlock: FC = ({ const [is_dragging, setIsDragging] = useState(false); const [drag_start, setDragStart] = useState(0); - const slide = useRef(); - const wrap = useRef(); + const slide = useRef(null); + const wrap = useRef(null); const setHeightThrottled = useCallback(throttle(100, setHeight), [setHeight]); @@ -221,6 +221,8 @@ const NodeImageSlideBlock: FC = ({ const changeCurrent = useCallback( (item: number) => { + if (!wrap.current) return; + const { width } = wrap.current.getBoundingClientRect(); setOffset(-1 * item * width); }, @@ -266,10 +268,10 @@ const NodeImageSlideBlock: FC = ({ [styles.is_active]: index === current, })} ref={setRef(index)} - key={node.updated_at + file.id} + key={`${node?.updated_at || ''} + ${file?.id || ''} + ${index}`} > = memo( ({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => { const [stack, setStack] = useState(false); - const ref = useRef(null); + const ref = useRef(null); const getPlace = useCallback(() => { if (!ref.current) return; - const { bottom } = ref.current.getBoundingClientRect(); + const { bottom } = ref.current!.getBoundingClientRect(); setStack(bottom > window.innerHeight); }, [ref]); @@ -75,7 +75,7 @@ const NodePanel: FC = memo( can_edit={can_edit} can_like={can_like} can_star={can_star} - is_loading={is_loading} + is_loading={!!is_loading} />
); diff --git a/src/components/node/NodePanelInner/index.tsx b/src/components/node/NodePanelInner/index.tsx index 0ed58eab..41e66d05 100644 --- a/src/components/node/NodePanelInner/index.tsx +++ b/src/components/node/NodePanelInner/index.tsx @@ -96,7 +96,9 @@ const NodePanelInner: FC = memo( )} - {like_count > 0 &&
{like_count}
} + {!!like_count && like_count > 0 && ( +
{like_count}
+ )}
)}
diff --git a/src/components/node/NodeRelatedItem/index.tsx b/src/components/node/NodeRelatedItem/index.tsx index 5c71eb50..88cf1bcd 100644 --- a/src/components/node/NodeRelatedItem/index.tsx +++ b/src/components/node/NodeRelatedItem/index.tsx @@ -1,16 +1,16 @@ -import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import styles from "./styles.module.scss"; -import classNames from "classnames"; -import { INode } from "~/redux/types"; -import { PRESETS, URLS } from "~/constants/urls"; -import { RouteComponentProps, withRouter } from "react-router"; -import { getURL, stringToColour } from "~/utils/dom"; +import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import styles from './styles.module.scss'; +import classNames from 'classnames'; +import { INode } from '~/redux/types'; +import { PRESETS, URLS } from '~/constants/urls'; +import { RouteComponentProps, withRouter } from 'react-router'; +import { getURL, stringToColour } from '~/utils/dom'; type IProps = RouteComponentProps & { item: Partial; }; -type CellSize = 'small' | 'medium' | 'large' +type CellSize = 'small' | 'medium' | 'large'; const getTitleLetters = (title: string): string => { const words = (title && title.split(' ')) || []; @@ -43,17 +43,21 @@ const NodeRelatedItemUnconnected: FC = memo(({ item, history }) => { useEffect(() => { if (!ref.current) return; - const cb = () => setWidth(ref.current.getBoundingClientRect().width) + + const cb = () => setWidth(ref.current!.getBoundingClientRect().width); + window.addEventListener('resize', cb); + cb(); + return () => window.removeEventListener('resize', cb); - }, [ref.current]) + }, [ref.current]); const size = useMemo(() => { if (width > 90) return 'large'; if (width > 76) return 'medium'; return 'small'; - }, [width]) + }, [width]); return (
= ({ node }) => { - const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node)), [ + const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [ node.blocks, ]); diff --git a/src/components/node/NodeVideoBlock/index.tsx b/src/components/node/NodeVideoBlock/index.tsx index b4e663fc..2fd8df93 100644 --- a/src/components/node/NodeVideoBlock/index.tsx +++ b/src/components/node/NodeVideoBlock/index.tsx @@ -7,7 +7,7 @@ interface IProps extends INodeComponentProps {} const NodeVideoBlock: FC = ({ node }) => { const video = useMemo(() => { - const url: string = path(['blocks', 0, 'url'], node); + const url: string = path(['blocks', 0, 'url'], node) || ''; const match = url && url.match( diff --git a/src/components/notifications/NotificationMessage/index.tsx b/src/components/notifications/NotificationMessage/index.tsx index f75f3dfb..77849eec 100644 --- a/src/components/notifications/NotificationMessage/index.tsx +++ b/src/components/notifications/NotificationMessage/index.tsx @@ -21,7 +21,7 @@ const NotificationMessage: FC = ({
-
Сообщение от ~{from.username}:
+
Сообщение от ~{from?.username}:
{text}
diff --git a/src/components/profile/MessageForm/index.tsx b/src/components/profile/MessageForm/index.tsx index 2ac14bcc..0d6a018d 100644 --- a/src/components/profile/MessageForm/index.tsx +++ b/src/components/profile/MessageForm/index.tsx @@ -39,7 +39,7 @@ const MessageFormUnconnected: FC = ({ const onSuccess = useCallback(() => { setText(''); - if (isEditing) { + if (isEditing && onCancel) { onCancel(); } }, [setText, isEditing, onCancel]); @@ -50,7 +50,7 @@ const MessageFormUnconnected: FC = ({ const onKeyDown = useCallback>( ({ ctrlKey, key }) => { - if (!!ctrlKey && key === 'Enter') onSubmit(); + if (ctrlKey && key === 'Enter') onSubmit(); }, [onSubmit] ); diff --git a/src/components/profile/ProfileDescription/index.tsx b/src/components/profile/ProfileDescription/index.tsx index 4e471a97..f3b43ea1 100644 --- a/src/components/profile/ProfileDescription/index.tsx +++ b/src/components/profile/ProfileDescription/index.tsx @@ -17,15 +17,15 @@ const ProfileDescriptionUnconnected: FC = ({ profile: { user, is_loading return (
- {user.description && ( + {!!user?.description && ( )} - {!user.description && ( + {!user?.description && (
- {user.fullname || user.username} пока ничего не рассказал о себе + {user?.fullname || user?.username} пока ничего не рассказал о себе
)}
diff --git a/src/components/tags/Tag/index.tsx b/src/components/tags/Tag/index.tsx index 8207e65a..5045ba12 100644 --- a/src/components/tags/Tag/index.tsx +++ b/src/components/tags/Tag/index.tsx @@ -3,7 +3,7 @@ import { ITag } from '~/redux/types'; import { TagWrapper } from '~/components/tags/TagWrapper'; const getTagFeature = (tag: Partial) => { - if (tag.title.substr(0, 1) === '/') return 'green'; + if (tag?.title?.substr(0, 1) === '/') return 'green'; return ''; }; diff --git a/src/components/tags/TagAutocomplete/index.tsx b/src/components/tags/TagAutocomplete/index.tsx index 485a6fe1..2dd80338 100644 --- a/src/components/tags/TagAutocomplete/index.tsx +++ b/src/components/tags/TagAutocomplete/index.tsx @@ -87,7 +87,10 @@ const TagAutocompleteUnconnected: FC = ({ useEffect(() => { tagSetAutocomplete({ options: [] }); - return () => tagSetAutocomplete({ options: [] }); + + return () => { + tagSetAutocomplete({ options: [] }); + }; }, [tagSetAutocomplete]); useEffect(() => { diff --git a/src/components/tags/TagInput/index.tsx b/src/components/tags/TagInput/index.tsx index e7c5c767..b3fb0eeb 100644 --- a/src/components/tags/TagInput/index.tsx +++ b/src/components/tags/TagInput/index.tsx @@ -77,6 +77,10 @@ const TagInput: FC = ({ exclude, onAppend, onClearTag, onSubmit }) => { const onFocus = useCallback(() => setFocused(true), []); const onBlur = useCallback( event => { + if (!wrapper.current || !ref.current) { + return; + } + if (wrapper.current.contains(event.target)) { ref.current.focus(); return; @@ -126,7 +130,7 @@ const TagInput: FC = ({ exclude, onAppend, onClearTag, onSubmit }) => { /> - {onInput && focused && input?.length > 0 && ( + {onInput && focused && input?.length > 0 && ref.current && ( = ({ tags, is_editable, onTagsChange, onTagClick, const onSubmit = useCallback( (last: string[]) => { + if (!onTagsChange) { + return; + } + const exist = tags.map(tag => tag.title); - onTagsChange(uniq([...exist, ...data, ...last])); + onTagsChange(uniq([...exist, ...data, ...last]).filter(el => el) as string[]); }, [data] ); useEffect(() => { - setData(data.filter(title => !tags.some(tag => tag.title.trim() === title.trim()))); + setData(data.filter(title => !tags.some(tag => tag?.title?.trim() === title.trim()))); }, [tags]); const onAppendTag = useCallback( @@ -44,10 +48,10 @@ export const Tags: FC = ({ tags, is_editable, onTagsChange, onTagClick, return last; }, [data, setData]); - const exclude = useMemo(() => [...(data || []), ...(tags || []).map(({ title }) => title)], [ - data, - tags, - ]); + const exclude = useMemo( + () => [...(data || []), ...(tags || []).filter(el => el.title).map(({ title }) => title!)], + [data, tags] + ); return ( diff --git a/src/constants/urls.ts b/src/constants/urls.ts index 606e6407..67eb4a39 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -1,3 +1,5 @@ +import { INode } from '~/redux/types'; + export const URLS = { BASE: '/', BORIS: '/boris', @@ -12,7 +14,7 @@ export const URLS = { NOT_FOUND: '/lost', BACKEND_DOWN: '/oopsie', }, - NODE_URL: (id: number | string) => `/post${id}`, + NODE_URL: (id: INode['id'] | string) => `/post${id}`, NODE_TAG_URL: (id: number, tagName: string) => `/post${id}/tag/${tagName}`, PROFILE: (username: string) => `/~${username}`, PROFILE_PAGE: (username: string) => `/profile/${username}`, diff --git a/src/containers/dialogs/BetterScrollDialog/index.tsx b/src/containers/dialogs/BetterScrollDialog/index.tsx index 4f9708a8..5122fc98 100644 --- a/src/containers/dialogs/BetterScrollDialog/index.tsx +++ b/src/containers/dialogs/BetterScrollDialog/index.tsx @@ -1,9 +1,9 @@ -import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from "react"; -import styles from "./styles.module.scss"; -import { clearAllBodyScrollLocks, disableBodyScroll } from "body-scroll-lock"; -import { Icon } from "~/components/input/Icon"; -import { LoaderCircle } from "~/components/input/LoaderCircle"; -import { useCloseOnEscape } from "~/utils/hooks"; +import React, { FC, MouseEventHandler, ReactElement, useEffect, useRef } from 'react'; +import styles from './styles.module.scss'; +import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'; +import { Icon } from '~/components/input/Icon'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; +import { useCloseOnEscape } from '~/utils/hooks'; interface IProps { children: React.ReactChild; @@ -14,7 +14,7 @@ interface IProps { width?: number; error?: string; is_loading?: boolean; - overlay?: ReactElement; + overlay?: JSX.Element; onOverlayClick?: MouseEventHandler; onRefCapture?: (ref: any) => void; diff --git a/src/containers/dialogs/EditorDialog/index.tsx b/src/containers/dialogs/EditorDialog/index.tsx index 4a2fa1d4..a5a42243 100644 --- a/src/containers/dialogs/EditorDialog/index.tsx +++ b/src/containers/dialogs/EditorDialog/index.tsx @@ -1,4 +1,12 @@ -import React, { createElement, FC, FormEvent, useCallback, useEffect, useState } from 'react'; +import React, { + createElement, + FC, + FormEvent, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { connect } from 'react-redux'; import { IDialogProps } from '~/redux/modal/constants'; import { useCloseOnEscape } from '~/utils/hooks'; @@ -16,6 +24,7 @@ import { EMPTY_NODE, NODE_EDITORS } from '~/redux/node/constants'; import { BetterScrollDialog } from '../BetterScrollDialog'; import { CoverBackdrop } from '~/components/containers/CoverBackdrop'; import { IEditorComponentProps } from '~/redux/node/types'; +import { has, values } from 'ramda'; const mapStateToProps = state => { const { editor, errors } = selectNode(state); @@ -32,7 +41,7 @@ const mapDispatchToProps = { type IProps = IDialogProps & ReturnType & typeof mapDispatchToProps & { - type: keyof typeof NODE_EDITORS; + type: string; }; const EditorDialogUnconnected: FC = ({ @@ -44,7 +53,7 @@ const EditorDialogUnconnected: FC = ({ type, }) => { const [data, setData] = useState(EMPTY_NODE); - const [temp, setTemp] = useState([]); + const [temp, setTemp] = useState([]); useEffect(() => setData(editor), [editor]); @@ -93,9 +102,18 @@ const EditorDialogUnconnected: FC = ({ useCloseOnEscape(onRequestClose); - const error = errors && Object.values(errors)[0]; + const error = values(errors)[0]; + const component = useMemo(() => { + if (!has(type, NODE_EDITORS)) { + return undefined; + } - if (!Object.prototype.hasOwnProperty.call(NODE_EDITORS, type)) return null; + return NODE_EDITORS[type]; + }, [type]); + + if (!component) { + return null; + } return (
@@ -107,7 +125,7 @@ const EditorDialogUnconnected: FC = ({ onClose={onRequestClose} >
- {createElement(NODE_EDITORS[type], { + {createElement(component, { data, setData, temp, diff --git a/src/containers/dialogs/LoginDialog/index.tsx b/src/containers/dialogs/LoginDialog/index.tsx index 83c7bc16..23792200 100644 --- a/src/containers/dialogs/LoginDialog/index.tsx +++ b/src/containers/dialogs/LoginDialog/index.tsx @@ -80,7 +80,7 @@ const LoginDialogUnconnected: FC = ({ ); useEffect(() => { - if (error) userSetLoginError(null); + if (error) userSetLoginError(''); }, [username, password]); useEffect(() => { diff --git a/src/containers/dialogs/LoginDialogButtons/index.tsx b/src/containers/dialogs/LoginDialogButtons/index.tsx index f328f11d..45d55f50 100644 --- a/src/containers/dialogs/LoginDialogButtons/index.tsx +++ b/src/containers/dialogs/LoginDialogButtons/index.tsx @@ -3,9 +3,10 @@ import { Button } from '~/components/input/Button'; import { Grid } from '~/components/containers/Grid'; import { Group } from '~/components/containers/Group'; import styles from './styles.module.scss'; +import { ISocialProvider } from '~/redux/auth/types'; interface IProps { - openOauthWindow: (provider: string) => MouseEventHandler; + openOauthWindow: (provider: ISocialProvider) => MouseEventHandler; } const LoginDialogButtons: FC = ({ openOauthWindow }) => ( diff --git a/src/containers/dialogs/Modal/index.tsx b/src/containers/dialogs/Modal/index.tsx index dd7b768a..918e7786 100644 --- a/src/containers/dialogs/Modal/index.tsx +++ b/src/containers/dialogs/Modal/index.tsx @@ -24,7 +24,7 @@ const ModalUnconnected: FC = ({ }) => { const onRequestClose = useCallback(() => { modalSetShown(false); - modalSetDialog(null); + modalSetDialog(''); }, [modalSetShown, modalSetDialog]); if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null; @@ -43,10 +43,7 @@ const ModalUnconnected: FC = ({ ); }; -const Modal = connect( - mapStateToProps, - mapDispatchToProps -)(ModalUnconnected); +const Modal = connect(mapStateToProps, mapDispatchToProps)(ModalUnconnected); export { ModalUnconnected, Modal }; diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index 668c55eb..8094ef77 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -78,7 +78,9 @@ const PhotoSwipeUnconnected: FC = ({ photoswipe, modalSetShown }) => { useEffect(() => { window.location.hash = 'preview'; - return () => (window.location.hash = ''); + return () => { + window.location.hash = ''; + }; }, []); return ( diff --git a/src/containers/dialogs/RestorePasswordDialog/index.tsx b/src/containers/dialogs/RestorePasswordDialog/index.tsx index 905c6126..d768b1ba 100644 --- a/src/containers/dialogs/RestorePasswordDialog/index.tsx +++ b/src/containers/dialogs/RestorePasswordDialog/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState, useMemo, useCallback, useEffect } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { IDialogProps } from '~/redux/types'; import { connect } from 'react-redux'; import { BetterScrollDialog } from '../BetterScrollDialog'; @@ -49,7 +49,7 @@ const RestorePasswordDialogUnconnected: FC = ({ useEffect(() => { if (error || is_succesfull) { - authSetRestore({ error: null, is_succesfull: false }); + authSetRestore({ error: '', is_succesfull: false }); } }, [password, password_again]); @@ -69,7 +69,7 @@ const RestorePasswordDialogUnconnected: FC = ({
Пароль обновлен
-
Добро пожаловать домой, ~{user.username}!
+
Добро пожаловать домой, ~{user?.username}!
@@ -77,14 +77,16 @@ const RestorePasswordDialogUnconnected: FC = ({ Ура! - ) : null, + ) : ( + undefined + ), [is_succesfull] ); - const not_ready = useMemo(() => (is_loading && !user ?
: null), [ - is_loading, - user, - ]); + const not_ready = useMemo( + () => (is_loading && !user ?
: undefined), + [is_loading, user] + ); const invalid_code = useMemo( () => @@ -100,7 +102,9 @@ const RestorePasswordDialogUnconnected: FC = ({ Очень жаль - ) : null, + ) : ( + undefined + ), [is_loading, user, error] ); @@ -135,7 +139,7 @@ const RestorePasswordDialogUnconnected: FC = ({ type="password" value={password_again} handler={setPasswordAgain} - error={password_again && doesnt_match && ERROR_LITERAL[ERRORS.DOESNT_MATCH]} + error={password_again && doesnt_match ? ERROR_LITERAL[ERRORS.DOESNT_MATCH] : ''} /> diff --git a/src/containers/dialogs/RestoreRequestDialog/index.tsx b/src/containers/dialogs/RestoreRequestDialog/index.tsx index 5a77c6d9..616bd55c 100644 --- a/src/containers/dialogs/RestoreRequestDialog/index.tsx +++ b/src/containers/dialogs/RestoreRequestDialog/index.tsx @@ -43,7 +43,7 @@ const RestoreRequestDialogUnconnected: FC = ({ useEffect(() => { if (error || is_succesfull) { - authSetRestore({ error: null, is_succesfull: false }); + authSetRestore({ error: '', is_succesfull: false }); } }, [field]); @@ -72,7 +72,9 @@ const RestoreRequestDialogUnconnected: FC = ({ Отлично! - ) : null, + ) : ( + undefined + ), [is_succesfull] ); diff --git a/src/containers/node/BorisLayout/index.tsx b/src/containers/node/BorisLayout/index.tsx index fb005d40..49ef20d2 100644 --- a/src/containers/node/BorisLayout/index.tsx +++ b/src/containers/node/BorisLayout/index.tsx @@ -37,6 +37,7 @@ const BorisLayout: FC = () => { if ( user.last_seen_boris && + last_comment.created_at && !isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at)) ) return; diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index a36e6ada..00c3ef7c 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -12,9 +12,14 @@ import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeRelated } from '~/components/node/NodeRelated'; import { NodeComments } from '~/components/node/NodeComments'; import { NodeTags } from '~/components/node/NodeTags'; -import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/redux/node/constants'; +import { + INodeComponentProps, + NODE_COMPONENTS, + NODE_HEADS, + NODE_INLINES, +} from '~/redux/node/constants'; import { selectUser } from '~/redux/auth/selectors'; -import { pick } from 'ramda'; +import { path, pick, prop } from 'ramda'; import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; import { NodeCommentForm } from '~/components/node/NodeCommentForm'; @@ -71,9 +76,6 @@ const NodeLayoutUnconnected: FC = memo( nodeStar, nodeLock, nodeSetCoverImage, - nodeLockComment, - nodeEditComment, - nodeLoadMoreComments, modalShowPhotoswipe, }) => { const [layout, setLayout] = useState({}); @@ -84,7 +86,6 @@ const NodeLayoutUnconnected: FC = memo( comments = [], current: node, related, - comment_data, comment_count, } = useShallowSelect(selectNode); const updateLayout = useCallback(() => setLayout({}), []); @@ -103,6 +104,10 @@ const NodeLayoutUnconnected: FC = memo( const onTagClick = useCallback( (tag: Partial) => { + if (!node?.id || !tag?.title) { + return; + } + history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title))); }, [history, node.id] @@ -112,9 +117,9 @@ const NodeLayoutUnconnected: FC = memo( const can_like = useMemo(() => canLikeNode(node, user), [node, user]); const can_star = useMemo(() => canStarNode(node, user), [node, user]); - const head = node && node.type && NODE_HEADS[node.type]; - const block = node && node.type && NODE_COMPONENTS[node.type]; - const inline = node && node.type && NODE_INLINES[node.type]; + const head = useMemo(() => node?.type && prop(node?.type, NODE_HEADS), [node.type]); + const block = useMemo(() => node?.type && prop(node?.type, NODE_COMPONENTS), [node.type]); + const inline = useMemo(() => node?.type && prop(node?.type, NODE_INLINES), [node.type]); const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]); const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]); @@ -147,10 +152,10 @@ const NodeLayoutUnconnected: FC = memo( return ( <> - {createNodeBlock(head)} + {!!head && createNodeBlock(head)} - {createNodeBlock(block)} + {!!block && createNodeBlock(block)} = memo( {!is_loading && related && related.albums && + !!node?.id && Object.keys(related.albums) .filter(album => related.albums[album].length > 0) .map(album => ( + {album} } diff --git a/src/containers/profile/ProfileAvatar/index.tsx b/src/containers/profile/ProfileAvatar/index.tsx index 07869b7d..4de56cd7 100644 --- a/src/containers/profile/ProfileAvatar/index.tsx +++ b/src/containers/profile/ProfileAvatar/index.tsx @@ -1,43 +1,42 @@ -import React, { FC, useCallback, useEffect, useState } from "react"; -import styles from "./styles.module.scss"; -import { connect } from "react-redux"; -import { getURL } from "~/utils/dom"; -import { pick } from "ramda"; -import { selectAuthProfile, selectAuthUser } from "~/redux/auth/selectors"; -import { PRESETS } from "~/constants/urls"; -import { selectUploads } from "~/redux/uploads/selectors"; -import { IFileWithUUID } from "~/redux/types"; -import uuid from "uuid4"; -import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from "~/redux/uploads/constants"; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import styles from './styles.module.scss'; +import { connect } from 'react-redux'; +import { getURL } from '~/utils/dom'; +import { pick } from 'ramda'; +import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors'; +import { PRESETS } from '~/constants/urls'; +import { selectUploads } from '~/redux/uploads/selectors'; +import { IFileWithUUID } from '~/redux/types'; +import uuid from 'uuid4'; +import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants'; import { path } from 'ramda'; -import * as UPLOAD_ACTIONS from "~/redux/uploads/actions"; -import * as AUTH_ACTIONS from "~/redux/auth/actions"; -import { Icon } from "~/components/input/Icon"; +import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; +import * as AUTH_ACTIONS from '~/redux/auth/actions'; +import { Icon } from '~/components/input/Icon'; const mapStateToProps = state => ({ - user: pick(["id"], selectAuthUser(state)), - profile: pick(["is_loading", "user"], selectAuthProfile(state)), - uploads: pick(["statuses", "files"], selectUploads(state)) + user: pick(['id'], selectAuthUser(state)), + profile: pick(['is_loading', 'user'], selectAuthProfile(state)), + uploads: pick(['statuses', 'files'], selectUploads(state)), }); const mapDispatchToProps = { uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles, - authPatchUser: AUTH_ACTIONS.authPatchUser + authPatchUser: AUTH_ACTIONS.authPatchUser, }; -type IProps = ReturnType & - typeof mapDispatchToProps & {}; +type IProps = ReturnType & typeof mapDispatchToProps & {}; const ProfileAvatarUnconnected: FC = ({ user: { id }, profile: { is_loading, user }, uploads: { statuses, files }, uploadUploadFiles, - authPatchUser + authPatchUser, }) => { - const can_edit = !is_loading && id && id === user.id; + const can_edit = !is_loading && id && id === user?.id; - const [temp, setTemp] = useState(null); + const [temp, setTemp] = useState(''); useEffect(() => { if (!can_edit) return; @@ -45,7 +44,7 @@ const ProfileAvatarUnconnected: FC = ({ Object.entries(statuses).forEach(([id, status]) => { if (temp === id && !!status.uuid && files[status.uuid]) { authPatchUser({ photo: files[status.uuid] }); - setTemp(null); + setTemp(''); } }); }, [statuses, files, temp, can_edit, authPatchUser]); @@ -58,11 +57,11 @@ const ProfileAvatarUnconnected: FC = ({ temp_id: uuid(), subject: UPLOAD_SUBJECTS.AVATAR, target: UPLOAD_TARGETS.PROFILES, - type: UPLOAD_TYPES.IMAGE + type: UPLOAD_TYPES.IMAGE, }) ); - setTemp(path([0, "temp_id"], items)); + setTemp(path([0, 'temp_id'], items) || ''); uploadUploadFiles(items.slice(0, 1)); }, [uploadUploadFiles, setTemp] @@ -81,13 +80,15 @@ const ProfileAvatarUnconnected: FC = ({ [onUpload, can_edit] ); + const backgroundImage = is_loading + ? undefined + : `url("${user && getURL(user.photo, PRESETS.avatar)}")`; + return (
{can_edit && } @@ -100,9 +101,6 @@ const ProfileAvatarUnconnected: FC = ({ ); }; -const ProfileAvatar = connect( - mapStateToProps, - mapDispatchToProps -)(ProfileAvatarUnconnected); +const ProfileAvatar = connect(mapStateToProps, mapDispatchToProps)(ProfileAvatarUnconnected); export { ProfileAvatar }; diff --git a/src/containers/profile/ProfileInfo/index.tsx b/src/containers/profile/ProfileInfo/index.tsx index f5739f12..7cdacccc 100644 --- a/src/containers/profile/ProfileInfo/index.tsx +++ b/src/containers/profile/ProfileInfo/index.tsx @@ -1,5 +1,5 @@ import React, { FC, ReactNode } from 'react'; -import { IUser } from '~/redux/auth/types'; +import { IAuthState, IUser } from '~/redux/auth/types'; import styles from './styles.module.scss'; import { Group } from '~/components/containers/Group'; import { Placeholder } from '~/components/placeholders/Placeholder'; @@ -14,7 +14,7 @@ interface IProps { is_loading?: boolean; is_own?: boolean; - setTab?: (tab: string) => void; + setTab?: (tab: IAuthState['profile']['tab']) => void; content?: ReactNode; } @@ -26,16 +26,16 @@ const ProfileInfo: FC = ({ user, tab, is_loading, is_own, setTab, conten
- {is_loading ? : user.fullname || user.username} + {is_loading ? : user?.fullname || user?.username}
- {is_loading ? : getPrettyDate(user.last_seen)} + {is_loading ? : getPrettyDate(user?.last_seen)}
- + {content}
diff --git a/src/containers/profile/ProfileLayout/index.tsx b/src/containers/profile/ProfileLayout/index.tsx index 9db6fc73..24d623dd 100644 --- a/src/containers/profile/ProfileLayout/index.tsx +++ b/src/containers/profile/ProfileLayout/index.tsx @@ -20,10 +20,10 @@ const ProfileLayoutUnconnected: FC = ({ history, nodeSetCoverImage }) => const { params: { username }, } = useRouteMatch<{ username: string }>(); - const [user, setUser] = useState(null); + const [user, setUser] = useState(undefined); useEffect(() => { - if (user) setUser(null); + if (user) setUser(undefined); }, [username]); useEffect(() => { diff --git a/src/containers/profile/ProfileMessages/index.tsx b/src/containers/profile/ProfileMessages/index.tsx index 61948d2d..92357851 100644 --- a/src/containers/profile/ProfileMessages/index.tsx +++ b/src/containers/profile/ProfileMessages/index.tsx @@ -31,7 +31,7 @@ const ProfileMessagesUnconnected: FC = ({ messagesRefreshMessages, }) => { const wasAtBottom = useRef(true); - const [wrap, setWrap] = useState(null); + const [wrap, setWrap] = useState(undefined); const [editingMessageId, setEditingMessageId] = useState(0); const onEditMessage = useCallback((id: number) => setEditingMessageId(id), [setEditingMessageId]); @@ -95,31 +95,33 @@ const ProfileMessagesUnconnected: FC = ({ if (!messages.messages.length || profile.is_loading) return ; - return ( - messages.messages.length > 0 && ( -
- {messages.messages - .filter(message => !!message.text) - .map(( - message // TODO: show files / memo - ) => ( - - ))} + if (messages.messages.length <= 0) { + return null; + } - {!messages.is_loading_messages && messages.messages.length > 0 && ( -
Когда-нибудь здесь будут еще сообщения
- )} -
- ) + return ( +
+ {messages.messages + .filter(message => !!message.text) + .map(( + message // TODO: show files / memo + ) => ( + + ))} + + {!messages.is_loading_messages && messages.messages.length > 0 && ( +
Когда-нибудь здесь будут еще сообщения
+ )} +
); }; diff --git a/src/containers/profile/ProfilePageLeft/index.tsx b/src/containers/profile/ProfilePageLeft/index.tsx index dd4fdb58..fb2af296 100644 --- a/src/containers/profile/ProfilePageLeft/index.tsx +++ b/src/containers/profile/ProfilePageLeft/index.tsx @@ -1,11 +1,14 @@ import React, { FC, useMemo } from 'react'; -import styles from './styles.module.scss'; import { IAuthState } from '~/redux/auth/types'; -import { getURL } from '~/utils/dom'; +import { formatText, getURL } from '~/utils/dom'; import { PRESETS, URLS } from '~/constants/urls'; import { Placeholder } from '~/components/placeholders/Placeholder'; import { Link } from 'react-router-dom'; import { Icon } from '~/components/input/Icon'; +import classNames from 'classnames'; + +import styles from './styles.module.scss'; +import markdown from '~/styles/common/markdown.module.scss'; interface IProps { profile: IAuthState['profile']; @@ -26,11 +29,11 @@ const ProfilePageLeft: FC = ({ username, profile }) => {
- {profile.is_loading ? : profile.user.fullname} + {profile.is_loading ? : profile?.user?.fullname}
- {profile.is_loading ? : `~${profile.user.username}`} + {profile.is_loading ? : `~${profile?.user?.username}`}
@@ -53,7 +56,9 @@ const ProfilePageLeft: FC = ({ username, profile }) => {
{profile && profile.user && profile.user.description && false && ( -
{profile.user.description}
+
+ {formatText(profile?.user?.description || '')} +
)}
); diff --git a/src/containers/profile/ProfileTabs/index.tsx b/src/containers/profile/ProfileTabs/index.tsx index 4fe97bef..d576d4e2 100644 --- a/src/containers/profile/ProfileTabs/index.tsx +++ b/src/containers/profile/ProfileTabs/index.tsx @@ -1,38 +1,49 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import styles from './styles.module.scss'; import classNames from 'classnames'; +import { IAuthState } from '~/redux/auth/types'; interface IProps { tab: string; is_own: boolean; - setTab: (tab: string) => void; + setTab?: (tab: IAuthState['profile']['tab']) => void; } -const ProfileTabs: FC = ({ tab, is_own, setTab }) => ( -
-
setTab('profile')} - > - Профиль +const ProfileTabs: FC = ({ tab, is_own, setTab }) => { + const changeTab = useCallback( + (tab: IAuthState['profile']['tab']) => () => { + if (!setTab) return; + setTab(tab); + }, + [setTab] + ); + + return ( +
+
+ Профиль +
+
+ Сообщения +
+ {is_own && ( + <> +
+ Настройки +
+ + )}
-
setTab('messages')} - > - Сообщения -
- {is_own && ( - <> -
setTab('settings')} - > - Настройки -
- - )} -
-); + ); +}; export { ProfileTabs }; diff --git a/src/containers/sidebars/ProfileSidebar/index.tsx b/src/containers/sidebars/ProfileSidebar/index.tsx index 308cad8f..9032856d 100644 --- a/src/containers/sidebars/ProfileSidebar/index.tsx +++ b/src/containers/sidebars/ProfileSidebar/index.tsx @@ -56,7 +56,7 @@ const ProfileSidebarUnconnected: FC = ({
- + {!!user && }
diff --git a/src/containers/sidebars/TagSidebar/index.tsx b/src/containers/sidebars/TagSidebar/index.tsx index e5b849ff..ff045a81 100644 --- a/src/containers/sidebars/TagSidebar/index.tsx +++ b/src/containers/sidebars/TagSidebar/index.tsx @@ -35,7 +35,10 @@ const TagSidebarUnconnected: FC = ({ nodes, tagLoadNodes, tagSetNodes }) useEffect(() => { tagLoadNodes(tag); - return () => tagSetNodes({ list: [], count: 0 }); + + return () => { + tagSetNodes({ list: [], count: 0 }); + }; }, [tag]); const loadMore = useCallback(() => { diff --git a/src/redux/boris/reducer.ts b/src/redux/boris/reducer.ts index cb2ff72e..2032c793 100644 --- a/src/redux/boris/reducer.ts +++ b/src/redux/boris/reducer.ts @@ -31,7 +31,7 @@ export type IStatBackend = { export type IBorisState = Readonly<{ stats: { git: Partial[]; - backend: IStatBackend; + backend?: IStatBackend; is_loading: boolean; }; }>; @@ -39,7 +39,7 @@ export type IBorisState = Readonly<{ const BORIS_INITIAL_STATE: IBorisState = { stats: { git: [], - backend: null, + backend: undefined, is_loading: false, }, }; diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts index d2f95963..05012f98 100644 --- a/src/redux/node/actions.ts +++ b/src/redux/node/actions.ts @@ -17,7 +17,7 @@ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({ type: NODE_ACTIONS.SET_SAVE_ERRORS, }); -export const nodeGotoNode = (id: number, node_type: INode['type']) => ({ +export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({ id, node_type, type: NODE_ACTIONS.GOTO_NODE, diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index acd3f348..b5b11e48 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, ReactElement } from 'react'; import { IComment, INode, ValueOf } from '../types'; import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock'; import { NodeTextBlock } from '~/components/node/NodeTextBlock'; @@ -13,7 +13,7 @@ import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadB import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton'; import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton'; import { modalShowPhotoswipe } from '../modal/actions'; -import { IEditorComponentProps } from '~/redux/node/types'; +import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types'; import { EditorFiller } from '~/components/editors/EditorFiller'; const prefix = 'NODE.'; @@ -50,15 +50,13 @@ export const NODE_ACTIONS = { }; export const EMPTY_NODE: INode = { - id: null, - - user: null, - + id: 0, + user: undefined, title: '', files: [], - cover: null, - type: null, + cover: undefined, + type: undefined, blocks: [], tags: [], @@ -102,13 +100,16 @@ export const NODE_INLINES: INodeComponents = { }; export const EMPTY_COMMENT: IComment = { - id: null, + id: 0, text: '', files: [], - user: null, + user: undefined, }; -export const NODE_EDITORS = { +export const NODE_EDITORS: Record< + typeof NODE_TYPES[keyof typeof NODE_TYPES], + FC +> = { [NODE_TYPES.IMAGE]: ImageEditor, [NODE_TYPES.TEXT]: TextEditor, [NODE_TYPES.VIDEO]: VideoEditor, diff --git a/src/redux/node/reducer.ts b/src/redux/node/reducer.ts index eb0b3ecb..438524cd 100644 --- a/src/redux/node/reducer.ts +++ b/src/redux/node/reducer.ts @@ -8,12 +8,12 @@ export type INodeState = Readonly<{ current: INode; comments: IComment[]; related: { - albums: Record>; - similar: Partial; + albums: Record; + similar: INode[]; }; comment_data: Record; comment_count: number; - current_cover_image: IFile; + current_cover_image?: IFile; error: string; errors: Record; @@ -38,14 +38,17 @@ const INITIAL_STATE: INodeState = { }, comment_count: 0, comments: [], - related: null, - current_cover_image: null, + related: { + albums: {}, + similar: [], + }, + current_cover_image: undefined, is_loading: false, is_loading_comments: false, is_sending_comment: false, - error: null, + error: '', errors: {}, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index 2e95ff8b..8a629dfe 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -51,6 +51,7 @@ import { selectNode } from './selectors'; import { Unwrap } from '../types'; import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs'; import { DIALOGS } from '~/redux/modal/constants'; +import { has } from 'ramda'; export function* updateNodeEverywhere(node) { const { @@ -103,6 +104,9 @@ function* onNodeSave({ node }: ReturnType) { } function* onNodeGoto({ id, node_type }: ReturnType) { + if (!id) { + return; + } if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type })); yield put(nodeLoadNode(id)); @@ -224,7 +228,7 @@ function* onUpdateTags({ id, tags }: ReturnType) { } function* onCreateSaga({ node_type: type }: ReturnType) { - if (!NODE_EDITOR_DIALOGS[type]) return; + if (!type || !has(type, NODE_EDITOR_DIALOGS)) return; yield put(nodeSetEditor({ ...EMPTY_NODE, ...(NODE_EDITOR_DATA[type] || {}), type })); yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type])); @@ -240,6 +244,8 @@ function* onEditSaga({ id }: ReturnType) { const { node }: Unwrap = yield call(apiGetNode, { id }); + if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return; + if (!NODE_EDITOR_DIALOGS[node?.type]) { throw new Error('Unknown node type'); } diff --git a/src/redux/node/types.ts b/src/redux/node/types.ts index d7973326..4dba0cc3 100644 --- a/src/redux/node/types.ts +++ b/src/redux/node/types.ts @@ -83,3 +83,9 @@ export type ApiLockCommentRequest = { export type ApiLockcommentResult = { deleted_at: string; }; +export type NodeEditorProps = { + data: INode; + setData: (val: INode) => void; + temp: string[]; + setTemp: (val: string[]) => void; +}; diff --git a/src/redux/types.ts b/src/redux/types.ts index 9155b1ff..8dd92998 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -71,7 +71,7 @@ export interface IFile { url: string; size: number; - type: IUploadType; + type?: IUploadType; mime: string; metadata?: { id3title?: string; @@ -92,7 +92,7 @@ export interface IFileWithUUID { file: File; subject?: string; target: string; - type: string; + type?: string; onSuccess?: (file: IFile) => void; onFail?: () => void; } @@ -111,13 +111,13 @@ export type IBlock = IBlockText | IBlockEmbed; export interface INode { id?: number; - user: Partial; + user?: Partial; title: string; files: IFile[]; - cover: IFile; - type: string; + cover?: IFile; + type?: string; blocks: IBlock[]; thumbnail?: string; @@ -143,7 +143,7 @@ export interface IComment { id: number; text: string; files: IFile[]; - user: IUser; + user?: IUser; created_at?: string; update_at?: string; diff --git a/src/redux/uploads/constants.ts b/src/redux/uploads/constants.ts index 27f91abf..8d8cf134 100644 --- a/src/redux/uploads/constants.ts +++ b/src/redux/uploads/constants.ts @@ -15,9 +15,9 @@ export const UPLOAD_ACTIONS = { }; export const EMPTY_FILE: IFile = { - id: null, - user_id: null, - node_id: null, + id: undefined, + user_id: undefined, + node_id: undefined, name: '', orig_name: '', @@ -25,21 +25,21 @@ export const EMPTY_FILE: IFile = { full_path: '', url: '', size: 0, - type: null, + type: undefined, mime: '', }; export const EMPTY_UPLOAD_STATUS: IUploadStatus = { is_uploading: false, - preview: null, - error: null, - uuid: null, - url: null, + preview: '', + error: '', + uuid: 0, + url: '', progress: 0, - thumbnail_url: null, - type: null, - temp_id: null, - name: null, + thumbnail_url: '', + type: '', + temp_id: '', + name: '', }; // for targeted cancellation diff --git a/src/redux/uploads/handlers.ts b/src/redux/uploads/handlers.ts index fe3140bf..57a4b86b 100644 --- a/src/redux/uploads/handlers.ts +++ b/src/redux/uploads/handlers.ts @@ -2,38 +2,41 @@ import { assocPath } from 'ramda'; import { omit } from 'ramda'; import { UPLOAD_ACTIONS, EMPTY_UPLOAD_STATUS } from './constants'; -import { - uploadAddStatus, uploadDropStatus, uploadSetStatus, uploadAddFile -} from './actions'; +import { uploadAddStatus, uploadDropStatus, uploadSetStatus, uploadAddFile } from './actions'; import { IUploadState } from './reducer'; const addStatus = ( state: IUploadState, - { temp_id, status, }: ReturnType -): IUploadState => assocPath( - ['statuses'], - { ...state.statuses, [temp_id]: { ...EMPTY_UPLOAD_STATUS, ...status, }, }, - state -); + { temp_id, status }: ReturnType +): IUploadState => + assocPath( + ['statuses'], + { ...state.statuses, [temp_id]: { ...EMPTY_UPLOAD_STATUS, ...status } }, + state + ); const dropStatus = ( state: IUploadState, - { temp_id, }: ReturnType + { temp_id }: ReturnType ): IUploadState => assocPath(['statuses'], omit([temp_id], state.statuses), state); const setStatus = ( state: IUploadState, - { temp_id, status, }: ReturnType -): IUploadState => assocPath( - ['statuses'], - { - ...state.statuses, - [temp_id]: { ...(state.statuses[temp_id] || EMPTY_UPLOAD_STATUS), ...status, }, - }, - state -); + { temp_id, status }: ReturnType +): IUploadState => + assocPath( + ['statuses'], + { + ...state.statuses, + [temp_id]: { ...(state.statuses[temp_id] || EMPTY_UPLOAD_STATUS), ...status }, + }, + state + ); -const addFile = (state: IUploadState, { file, }: ReturnType): IUploadState => assocPath(['files'], { ...state.files, [file.id]: file, }, state); +const addFile = (state: IUploadState, { file }: ReturnType): IUploadState => { + if (!file.id) return state; + return assocPath(['files', file.id], file, state); +}; export const UPLOAD_HANDLERS = { [UPLOAD_ACTIONS.ADD_STATUS]: addStatus, diff --git a/src/redux/uploads/mocks.ts b/src/redux/uploads/mocks.ts deleted file mode 100644 index e8ccc9e9..00000000 --- a/src/redux/uploads/mocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -import uuid from 'uuid4'; -import { IResultWithStatus, IFile, UUID } from '../types'; -import { HTTP_RESPONSES } from '~/utils/api'; -import { EMPTY_FILE } from './constants'; - -export const uploadMock = ({ temp_id, file }: { temp_id: UUID; file: File }): Promise> => ( - Promise.resolve({ - status: HTTP_RESPONSES.CREATED, - data: { - ...EMPTY_FILE, - id: uuid(), - temp_id, - }, - error: null, - })); diff --git a/src/redux/uploads/sagas.ts b/src/redux/uploads/sagas.ts index c7b168fd..ff1b68a9 100644 --- a/src/redux/uploads/sagas.ts +++ b/src/redux/uploads/sagas.ts @@ -73,7 +73,7 @@ function* uploadFile({ file, temp_id, type, target, onSuccess, onFail }: IFileWi if (!temp_id) return; try { - if (!file.type || !FILE_MIMES[type] || !FILE_MIMES[type].includes(file.type)) { + if (!file.type || !type || !FILE_MIMES[type] || !FILE_MIMES[type].includes(file.type)) { return { error: 'File_Not_Image', status: HTTP_RESPONSES.BAD_REQUEST, diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 6d414348..eae9655f 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -89,7 +89,10 @@ export const getURLFromString = ( return url.replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT); }; -export const getURL = (file: Partial, size?: typeof PRESETS[keyof typeof PRESETS]) => { +export const getURL = ( + file: Partial | undefined, + size?: typeof PRESETS[keyof typeof PRESETS] +) => { return file?.url ? getURLFromString(file.url, size) : ''; }; diff --git a/src/utils/fn.ts b/src/utils/fn.ts index def79fbb..8f9a9487 100644 --- a/src/utils/fn.ts +++ b/src/utils/fn.ts @@ -10,16 +10,20 @@ export const objFromArray = (array: any[], key: string) => array.reduce((obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), {}); export const groupCommentsByUser = ( - result: ICommentGroup[], + grouppedComments: ICommentGroup[], comment: IComment ): ICommentGroup[] => { - const last: ICommentGroup = path([result.length - 1], result) || null; + const last: ICommentGroup | undefined = path([grouppedComments.length - 1], grouppedComments); + + if (!comment.user) { + return grouppedComments; + } return [ ...(!last || path(['user', 'id'], last) !== path(['user', 'id'], comment) ? [ // add new group - ...result, + ...grouppedComments, { user: comment.user, comments: [comment], @@ -28,7 +32,7 @@ export const groupCommentsByUser = ( ] : [ // append to last group - ...result.slice(0, result.length - 1), + ...grouppedComments.slice(0, grouppedComments.length - 1), { ...last, comments: [...last.comments, comment], @@ -37,6 +41,3 @@ export const groupCommentsByUser = ( ]), ]; }; - -// const isSameComment = (comments, index) => -// comments[index - 1] && comments[index - 1].user.id === comments[index].user.id; diff --git a/src/utils/hooks/fileUploader.tsx b/src/utils/hooks/fileUploader.tsx index 207a86f3..3e2910e6 100644 --- a/src/utils/hooks/fileUploader.tsx +++ b/src/utils/hooks/fileUploader.tsx @@ -1,4 +1,12 @@ -import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { + createContext, + FC, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { IFile, IFileWithUUID } from '~/redux/types'; import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants'; import { getFileType } from '~/utils/uploader'; @@ -7,6 +15,8 @@ import { useDispatch } from 'react-redux'; import { uploadUploadFiles } from '~/redux/uploads/actions'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { selectUploads } from '~/redux/uploads/selectors'; +import { has, path } from 'ramda'; +import { IUploadStatus } from '~/redux/uploads/reducer'; export const useFileUploader = ( subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS], @@ -31,7 +41,7 @@ export const useFileUploader = ( }) ); - const temps = items.map(file => file.temp_id); + const temps = items.filter(el => !!el.temp_id).map(file => file.temp_id!); setPendingIDs([...pendingIDs, ...temps]); dispatch(uploadUploadFiles(items)); @@ -41,9 +51,10 @@ export const useFileUploader = ( useEffect(() => { const added = pendingIDs - .map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid) - .map(el => !!el && uploadedFiles[el]) - .filter(el => !!el && !files.some(file => file && file.id === el.id)); + .map(temp_uuid => path([temp_uuid, 'uuid'], statuses) as IUploadStatus['uuid']) + .filter(el => el) + .map(el => (path([String(el)], uploadedFiles) as IFile) || undefined) + .filter(el => !!el! && !files.some(file => file && file.id === el.id)); const newPending = pendingIDs.filter( temp_id => @@ -68,7 +79,7 @@ export const useFileUploader = ( }; export type FileUploader = ReturnType; -const FileUploaderContext = createContext(null); +const FileUploaderContext = createContext(undefined); export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({ value, diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 80230f2a..563bacb0 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect } from 'react'; -export const useCloseOnEscape = (onRequestClose: () => void, ignore_inputs = false) => { +export const useCloseOnEscape = (onRequestClose?: () => void, ignore_inputs = false) => { const onEscape = useCallback( event => { - if (event.key !== 'Escape') return; + if (event.key !== 'Escape' || !onRequestClose) return; if ( ignore_inputs && (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') diff --git a/src/utils/hooks/useCommentFormFormik.ts b/src/utils/hooks/useCommentFormFormik.ts index dd3fc139..cd222db5 100644 --- a/src/utils/hooks/useCommentFormFormik.ts +++ b/src/utils/hooks/useCommentFormFormik.ts @@ -12,7 +12,7 @@ const validationSchema = object().shape({ }); const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers) => ( - e: string + e?: string ) => { setSubmitting(false); diff --git a/src/utils/player.ts b/src/utils/player.ts index e7d747aa..82930dc4 100644 --- a/src/utils/player.ts +++ b/src/utils/player.ts @@ -32,15 +32,12 @@ export class PlayerClass { }); } - public current: number = 0; + public current = 0; + public total = 0; + public element = new Audio(); + public duration = 0; - public total: number = 0; - - public element: HTMLAudioElement = typeof Audio !== 'undefined' ? new Audio() : null; - - public duration: number = 0; - - public set = (src: string): void => { + public set = (src: string) => { this.element.src = src; }; diff --git a/src/utils/tag.ts b/src/utils/tag.ts index 2913ba95..2eb7fa49 100644 --- a/src/utils/tag.ts +++ b/src/utils/tag.ts @@ -3,11 +3,11 @@ import { ITag } from '~/redux/types'; export const separateTags = (tags: Partial[]): Partial[][] => (tags || []).reduce( (obj, tag) => - tag.title.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]], - [[], []] + tag?.title?.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]], + [[], []] as Partial[][] ); export const separateTagOptions = (options: string[]): string[][] => separateTags(options.map((title): Partial => ({ title }))).map(item => - item.map(({ title }) => title) + item.filter(tag => tag.title).map(({ title }) => title!) );