From fe5128137f530daf8ca315cb89811121c6c2c9da Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 13 Feb 2021 21:23:30 +0800 Subject: ... --- .../src/app/views/timeline-common/Timeline.tsx | 10 +- .../timeline-common/TimelinePageTemplateUI.tsx | 10 +- .../timeline-common/TimelinePostContentView.tsx | 114 ++++++++++++++++++ .../app/views/timeline-common/TimelinePostEdit.tsx | 129 +++++++++++---------- .../views/timeline-common/TimelinePostListView.tsx | 4 +- .../app/views/timeline-common/TimelinePostView.tsx | 26 ++++- 6 files changed, 221 insertions(+), 72 deletions(-) create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx (limited to 'FrontEnd/src/app/views/timeline-common') diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx index d970af84..d41588bb 100644 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -13,10 +13,12 @@ export interface TimelineProps { className?: string; style?: React.CSSProperties; timelineName: string; + reloadKey: number; + onReload: () => void; } const Timeline: React.FC = (props) => { - const { timelineName, className, style } = props; + const { timelineName, className, style, reloadKey, onReload } = props; const [posts, setPosts] = React.useState< | HttpTimelinePostInfo[] @@ -30,6 +32,8 @@ const Timeline: React.FC = (props) => { React.useEffect(() => { let subscribe = true; + setPosts("loading"); + void getHttpTimelineClient() .listPost(timelineName) .then( @@ -53,7 +57,7 @@ const Timeline: React.FC = (props) => { return () => { subscribe = false; }; - }, [timelineName]); + }, [timelineName, reloadKey]); switch (posts) { case "loading": @@ -87,7 +91,7 @@ const Timeline: React.FC = (props) => { ); default: - return ; + return ; } }; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 56be8cfe..d133bd34 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -49,6 +49,9 @@ export default function TimelinePageTemplateUI( const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState(0); + const [timelineReloadKey, setTimelineReloadKey] = React.useState(0); + const reloadTimeline = (): void => setTimelineReloadKey((old) => old + 1); + const onPostEditHeightChange = React.useCallback((height: number): void => { setBottomSpaceHeight(height); if (height === 0) { @@ -122,7 +125,11 @@ export default function TimelinePageTemplateUI( minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, }} > - + {timeline.postable ? ( <> @@ -134,6 +141,7 @@ export default function TimelinePageTemplateUI( className="fixed-bottom" timeline={timeline} onHeightChange={onPostEditHeightChange} + onPosted={reloadTimeline} /> ) : null} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx new file mode 100644 index 00000000..69954040 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { Spinner } from "react-bootstrap"; + +import { HttpNetworkError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; + +import { useUser } from "@/services/user"; + +const TextView: React.FC = (props) => { + const { post, className, style } = props; + + const [text, setText] = React.useState(null); + const [error, setError] = React.useState<"offline" | "error" | null>(null); + + React.useEffect(() => { + let subscribe = true; + + setText(null); + setError(null); + + void getHttpTimelineClient() + .getPostDataAsString(post.timelineName, post.id) + .then( + (data) => { + if (subscribe) setText(data); + }, + (error) => { + if (subscribe) { + if (error instanceof HttpNetworkError) { + setError("offline"); + } else { + setError("error"); + } + } + } + ); + + return () => { + subscribe = false; + }; + }, [post]); + + if (error != null) { + // TODO: i18n + return ( +
+ Error! +
+ ); + } else if (text == null) { + return ; + } else { + return ( +
+ {text} +
+ ); + } +}; + +const ImageView: React.FC = (props) => { + const { post, className, style } = props; + + useUser(); + + return ( + + ); +}; + +const MarkdownView: React.FC = (_props) => { + // TODO: Implement this. + return
Unsupported now!
; +}; + +export interface TimelinePostContentViewProps { + post: HttpTimelinePostInfo; + className?: string; + style?: React.CSSProperties; +} + +const viewMap: Record> = { + "text/plain": TextView, + "text/markdown": MarkdownView, + "image/png": ImageView, + "image/jpeg": ImageView, + "image/gif": ImageView, + "image/webp": ImageView, +}; + +const TimelinePostContentView: React.FC = ( + props +) => { + const { post, className, style } = props; + + const type = post.dataList[0].kind; + + if (type in viewMap) { + const View = viewMap[type]; + return ; + } else { + // TODO: i18n + return
Error, unknown post type!
; + } +}; + +export default TimelinePostContentView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx index 488b627c..7c49e5bb 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -5,8 +5,14 @@ import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; import { UiLogicError } from "@/common"; +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePostPostRequestData, +} from "@/http/timeline"; + import { pushAlert } from "@/services/alert"; -import { HttpTimelineInfo } from "@/http/timeline"; +import { base64 } from "@/http/common"; interface TimelinePostEditImageProps { onSelect: (blob: Blob | null) => void; @@ -77,19 +83,21 @@ const TimelinePostEditImage: React.FC = (props) => { export interface TimelinePostEditProps { className?: string; timeline: HttpTimelineInfo; + onPosted: () => void; onHeightChange?: (height: number) => void; } const TimelinePostEdit: React.FC = (props) => { - const { t } = useTranslation(); + const { timeline, onHeightChange, className, onPosted } = props; - const { timeline } = 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.timelineUniqueId}.postDraft`; + const draftLocalStorageKey = `timeline.${timeline.name}.postDraft`; React.useEffect(() => { setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); @@ -101,77 +109,76 @@ const TimelinePostEdit: React.FC = (props) => { const containerRef = React.useRef(null!); const notifyHeightChange = (): void => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); + if (onHeightChange) { + onHeightChange(containerRef.current.clientHeight); } }; React.useEffect(() => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); + if (onHeightChange) { + onHeightChange(containerRef.current.clientHeight); } return () => { - if (props.onHeightChange) { - props.onHeightChange(0); + if (onHeightChange) { + onHeightChange(0); } }; }); const toggleKind = React.useCallback(() => { - // TODO: Implement this. - // setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - // setImageBlob(null); + setKind((oldKind) => (oldKind === "text" ? "image" : "text")); + setImageBlob(null); }, []); - const onSend = React.useCallback(() => { + const onSend = async (): Promise => { setState("process"); - // TODO: Implement this. - - // const req: TimelineCreatePostRequest = (() => { - // switch (kind) { - // case "text": - // return { - // content: { - // type: "text", - // text: text, - // }, - // } as TimelineCreatePostRequest; - // case "image": - // if (imageBlob == null) { - // throw new UiLogicError( - // "Content type is image but image blob is null." - // ); - // } - // return { - // content: { - // type: "image", - // data: imageBlob, - // }, - // } as TimelineCreatePostRequest; - // 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"); - // } - // ); - }, []); + let requestData: HttpTimelinePostPostRequestData; + switch (kind) { + case "text": + requestData = { + contentType: "text/plain", + data: await base64(new Blob([text])), + }; + break; + case "image": + if (imageBlob == null) { + throw new UiLogicError( + "Content type is image but image blob is null." + ); + } + requestData = { + contentType: imageBlob.type, + data: await base64(imageBlob), + }; + break; + default: + throw new UiLogicError("Unknown content type."); + } + + getHttpTimelineClient() + .postPost(timeline.name, { + dataList: [requestData], + }) + .then( + (_) => { + if (kind === "text") { + setText(""); + window.localStorage.removeItem(draftLocalStorageKey); + } + setState("input"); + setKind("text"); + onPosted(); + }, + (_) => { + pushAlert({ + type: "danger", + message: t("timeline.sendPostFailed"), + }); + setState("input"); + } + ); + }; const onImageSelect = React.useCallback((blob: Blob | null) => { setImageBlob(blob); @@ -180,7 +187,7 @@ const TimelinePostEdit: React.FC = (props) => { return (
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx index 5acc1c21..63255619 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx @@ -18,10 +18,11 @@ export interface TimelinePostListViewProps { className?: string; style?: React.CSSProperties; posts: HttpTimelinePostInfo[]; + onReload: () => void; } const TimelinePostListView: React.FC = (props) => { - const { className, style, posts } = props; + const { className, style, posts, onReload } = props; const groupedPosts = React.useMemo< { date: Date; posts: (HttpTimelinePostInfo & { index: number })[] }[] @@ -60,6 +61,7 @@ const TimelinePostListView: React.FC = (props) => { key={post.id} post={post} current={posts.length - 1 === post.index} + onDeleted={onReload} /> ); })} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx index a2ae72cf..7fd98310 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx @@ -2,10 +2,13 @@ import React from "react"; import clsx from "clsx"; import { Link } from "react-router-dom"; -import { HttpTimelinePostInfo } from "@/http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; + +import { pushAlert } from "@/services/alert"; import UserAvatar from "../common/user/UserAvatar"; import TimelineLine from "./TimelineLine"; +import TimelinePostContentView from "./TimelinePostContentView"; import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; export interface TimelinePostViewProps { @@ -13,10 +16,11 @@ export interface TimelinePostViewProps { current?: boolean; className?: string; style?: React.CSSProperties; + onDeleted?: () => void; } const TimelinePostView: React.FC = (props) => { - const { post, className, style } = props; + const { post, className, style, onDeleted } = props; const current = props.current === true; const [ @@ -25,8 +29,6 @@ const TimelinePostView: React.FC = (props) => { ] = React.useState(false); const [deleteDialog, setDeleteDialog] = React.useState(false); - // TODO: Load content. - return (
= (props) => {
-
{/** TODO: Load content. */}
+
+ +
{operationMaskVisible ? (
= (props) => { setOperationMaskVisible(false); }} onConfirm={() => { - // TODO: Implement this! + void getHttpTimelineClient() + .deletePost(post.timelineName, post.id) + .then(onDeleted, () => { + pushAlert({ + type: "danger", + message: { + type: "i18n", + key: "timeline.deletePostFailed", + }, + }); + }); }} /> ) : null} -- cgit v1.2.3