diff options
Diffstat (limited to 'FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx')
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx | 156 |
1 files changed, 118 insertions, 38 deletions
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx index 5bc5b166..6c428b74 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -17,13 +17,40 @@ import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; import LoadingButton from "../common/LoadingButton"; +import { PopupMenu } from "../common/Menu"; + +interface TimelinePostEditTextProps { + text: string; + disabled: boolean; + onChange: (text: string) => void; + className?: string; + style?: React.CSSProperties; +} + +const TimelinePostEditText: React.FC<TimelinePostEditTextProps> = (props) => { + const { text, disabled, onChange, className, style } = props; + + return ( + <Form.Control + as="textarea" + value={text} + disabled={disabled} + onChange={(event) => { + onChange(event.target.value); + }} + className={className} + style={style} + /> + ); +}; interface TimelinePostEditImageProps { onSelect: (file: File | null) => void; + disabled: boolean; } const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { - const { onSelect } = props; + const { onSelect, disabled } = props; const { t } = useTranslation(); @@ -41,12 +68,19 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { } }; + React.useEffect(() => { + return () => { + onSelect(null); + }; + }, [onSelect]); + return ( <> <Form.File label={t("chooseImage")} onChange={onInputChange} accept="image/*" + disabled={disabled} className="mx-3 my-1 d-inline-block" /> {file != null && !error && ( @@ -65,6 +99,14 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { ); }; +type PostKind = "text" | "markdown" | "image"; + +const postKindIconClassNameMap: Record<PostKind, string> = { + text: "bi-fonts", + markdown: "bi-markdown", + image: "bi-image", +}; + export interface TimelinePostEditProps { className?: string; timeline: HttpTimelineInfo; @@ -78,19 +120,27 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { const { t } = useTranslation(); const [process, setProcess] = React.useState<boolean>(false); - const [kind, setKind] = React.useState<"text" | "image">("text"); + + const [kind, setKind] = React.useState<PostKind>("text"); + const [text, setText] = React.useState<string>(""); + const [markdown, setMarkdown] = React.useState<string>(""); const [image, setImage] = React.useState<File | null>(null); - const draftLocalStorageKey = `timeline.${timeline.name}.postDraft`; + const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; + const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`; React.useEffect(() => { - setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); - }, [draftLocalStorageKey]); + setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); + setMarkdown( + window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? "" + ); + }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]); const canSend = (kind === "text" && text.length !== 0) || - (kind === "image" && image != null); + (kind === "image" && image != null) || + (kind === "markdown" && markdown.length !== 0); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef<HTMLDivElement>(null!); @@ -102,9 +152,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }; React.useEffect(() => { - if (onHeightChange) { - onHeightChange(containerRef.current.clientHeight); - } + notifyHeightChange(); return () => { if (onHeightChange) { onHeightChange(0); @@ -112,11 +160,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }; }); - const toggleKind = React.useCallback(() => { - setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImage(null); - }, []); - const onSend = async (): Promise<void> => { setProcess(true); @@ -128,6 +171,12 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { data: await base64(new Blob([text])), }; break; + case "markdown": + requestData = { + contentType: "text/markdown", + data: await base64(new Blob([markdown])), + }; + break; case "image": if (image == null) { throw new UiLogicError( @@ -151,7 +200,10 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { (data) => { if (kind === "text") { setText(""); - window.localStorage.removeItem(draftLocalStorageKey); + window.localStorage.removeItem(draftTextLocalStorageKey); + } else if (kind === "markdown") { + setMarkdown(""); + window.localStorage.removeItem(draftMarkdownLocalStorageKey); } setProcess(false); setKind("text"); @@ -174,32 +226,60 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { > <Row> <Col className="px-1 py-1"> - {kind === "text" ? ( - <Form.Control - as="textarea" - className="w-100 h-100 timeline-post-edit" - value={text} - disabled={process} - onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => { - const value = event.currentTarget.value; - setText(value); - window.localStorage.setItem(draftLocalStorageKey, value); - }} - /> - ) : ( - <TimelinePostEditImage onSelect={setImage} /> - )} + {(() => { + if (kind === "text") { + return ( + <TimelinePostEditText + className="w-100 h-100 timeline-post-edit" + text={text} + disabled={process} + onChange={(t) => { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + <TimelinePostEditImage onSelect={setImage} disabled={process} /> + ); + } else if (kind === "markdown") { + return ( + <TimelinePostEditText + className="w-100 h-100 timeline-post-edit" + text={markdown} + disabled={process} + onChange={(t) => { + setMarkdown(t); + window.localStorage.setItem( + draftMarkdownLocalStorageKey, + t + ); + }} + /> + ); + } + })()} </Col> <Col xs="auto" className="align-self-end m-1"> <div className="d-block text-center mt-1 mb-2"> - <i - onLoad={notifyHeightChange} - className={clsx( - kind === "text" ? "bi-image" : "bi-card-text", - "icon-button" - )} - onClick={process ? undefined : toggleKind} - /> + <PopupMenu + items={(["text", "image", "markdown"] as const).map((kind) => ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconClassNameMap[kind], + onClick: () => { + setKind(kind); + }, + }))} + > + <i + className={clsx( + postKindIconClassNameMap[kind], + "icon-button large" + )} + /> + </PopupMenu> </div> <LoadingButton variant="primary" |