From cf6cfe87b46a2a3eb2913209092ab4c5639e75c3 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 11 Jun 2020 17:27:15 +0800 Subject: feat(front): Service worker is coming! --- .../src/app/timeline/TimelinePostEdit.tsx | 224 +++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx (limited to 'Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx') diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx new file mode 100644 index 00000000..f12b6892 --- /dev/null +++ b/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Button, Spinner, Row, Col } from 'reactstrap'; +import { useTranslation } from 'react-i18next'; + +import { pushAlert } from '../common/alert-service'; +import { CreatePostRequest } from '../data/timeline'; + +import FileInput from '../common/FileInput'; +import { UiLogicError } from '../common'; + +interface TimelinePostEditImageProps { + onSelect: (blob: Blob | null) => void; +} + +const TimelinePostEditImage: React.FC = (props) => { + const { onSelect } = props; + const { t } = useTranslation(); + + const [file, setFile] = React.useState(null); + const [fileUrl, setFileUrl] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + if (file != null) { + const url = URL.createObjectURL(file); + setFileUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } + }, [file]); + + const onInputChange: React.ChangeEventHandler = React.useCallback( + (e) => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + setFileUrl(null); + } else { + setFile(files[0]); + } + onSelect(null); + setError(null); + }, + [onSelect] + ); + + const onImgLoad = React.useCallback(() => { + onSelect(file); + }, [onSelect, file]); + + const onImgError = React.useCallback(() => { + setError('loadImageError'); + }, []); + + return ( + <> + + {fileUrl && error == null && ( + + )} + {error != null &&
{t(error)}
} + + ); +}; + +export type TimelinePostSendCallback = ( + content: CreatePostRequest +) => Promise; + +export interface TimelinePostEditProps { + className?: string; + onPost: TimelinePostSendCallback; + onHeightChange?: (height: number) => void; + timelineName: string; +} + +const TimelinePostEdit: React.FC = (props) => { + const { onPost } = props; + + const { t } = useTranslation(); + + const [state, setState] = React.useState<'input' | 'process'>('input'); + const [kind, setKind] = React.useState<'text' | 'image'>('text'); + const [text, setText] = React.useState(''); + const [imageBlob, setImageBlob] = React.useState(null); + + const draftLocalStorageKey = `timeline.${props.timelineName}.postDraft`; + + React.useEffect(() => { + setText(window.localStorage.getItem(draftLocalStorageKey) ?? ''); + }, [draftLocalStorageKey]); + + const canSend = kind === 'text' || (kind === 'image' && imageBlob != null); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + + React.useEffect(() => { + if (props.onHeightChange) { + props.onHeightChange(containerRef.current.clientHeight); + } + return () => { + if (props.onHeightChange) { + props.onHeightChange(0); + } + }; + }); + + const toggleKind = React.useCallback(() => { + setKind((oldKind) => (oldKind === 'text' ? 'image' : 'text')); + setImageBlob(null); + }, []); + + const onSend = React.useCallback(() => { + setState('process'); + + const req: CreatePostRequest = (() => { + switch (kind) { + case 'text': + return { + content: { + type: 'text', + text: text, + }, + } as CreatePostRequest; + case 'image': + if (imageBlob == null) { + throw new UiLogicError( + 'Content type is image but image blob is null.' + ); + } + return { + content: { + type: 'image', + data: imageBlob, + }, + } as CreatePostRequest; + default: + throw new UiLogicError('Unknown content type.'); + } + })(); + + onPost(req).then( + (_) => { + if (kind === 'text') { + setText(''); + window.localStorage.removeItem(draftLocalStorageKey); + } + setState('input'); + setKind('text'); + }, + (_) => { + pushAlert({ + type: 'danger', + message: t('timeline.sendPostFailed'), + }); + setState('input'); + } + ); + }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); + + const onImageSelect = React.useCallback((blob: Blob | null) => { + setImageBlob(blob); + }, []); + + return ( +
+ + + {kind === 'text' ? ( +