diff options
author | crupest <crupest@outlook.com> | 2021-02-13 21:23:30 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-02-13 21:23:30 +0800 |
commit | c8bd19aacf9059f740df4f6fa9890127c20c1f6d (patch) | |
tree | 01b3d74965415aff1ab1ef36fb9c92551c2166a4 | |
parent | b9703104b5b416dd3211adedb878d1916072c96d (diff) | |
download | timeline-c8bd19aacf9059f740df4f6fa9890127c20c1f6d.tar.gz timeline-c8bd19aacf9059f740df4f6fa9890127c20c1f6d.tar.bz2 timeline-c8bd19aacf9059f740df4f6fa9890127c20c1f6d.zip |
...
-rw-r--r-- | FrontEnd/src/app/App.tsx | 6 | ||||
-rw-r--r-- | FrontEnd/src/app/http/timeline.ts | 33 | ||||
-rw-r--r-- | FrontEnd/src/app/http/user.ts | 6 | ||||
-rw-r--r-- | FrontEnd/src/app/utilities/url.ts | 3 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/AppBar.tsx | 10 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/user/UserAvatar.tsx | 13 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/TimelineBoard.tsx | 12 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/Timeline.tsx | 10 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx | 10 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx | 114 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx | 129 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx | 26 |
13 files changed, 274 insertions, 102 deletions
diff --git a/FrontEnd/src/app/App.tsx b/FrontEnd/src/app/App.tsx index 0a7513e4..fb57bd1e 100644 --- a/FrontEnd/src/app/App.tsx +++ b/FrontEnd/src/app/App.tsx @@ -12,7 +12,6 @@ import TimelinePage from "./views/timeline"; import Search from "./views/search"; import AlertHost from "./views/common/alert/AlertHost"; -import { dataStorage } from "./services/common"; import { userService, useRawUser } from "./services/user"; const NoMatch: React.FC = () => { @@ -24,16 +23,13 @@ const LazyAdmin = React.lazy( ); const App: React.FC = () => { - const [loading, setLoading] = React.useState<boolean>(true); - const user = useRawUser(); React.useEffect(() => { void userService.checkLoginState(); - void dataStorage.ready().then(() => setLoading(false)); }, []); - if (user === undefined || loading) { + if (user === undefined) { return <LoadingPage />; } else { return ( diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts index 375a2325..50af259e 100644 --- a/FrontEnd/src/app/http/timeline.ts +++ b/FrontEnd/src/app/http/timeline.ts @@ -7,6 +7,7 @@ import { apiBaseUrl, extractResponseData, convertToIfErrorCodeIs, + getHttpToken, } from "./common"; import { HttpUser } from "./user"; @@ -50,20 +51,22 @@ export interface HttpTimelinePostInfo { id: number; time: string; author: HttpUser; - dataList: HttpTimelinePostDataDigest; + dataList: HttpTimelinePostDataDigest[]; color: string; lastUpdated: string; timelineName: string; editable: boolean; } +export interface HttpTimelinePostPostRequestData { + contentType: string; + data: string; +} + export interface HttpTimelinePostPostRequest { time?: string; color?: string; - dataList: { - contentType: string; - data: string; - }[]; + dataList: HttpTimelinePostPostRequestData[]; } export interface HttpTimelinePatchRequest { @@ -91,6 +94,8 @@ export interface IHttpTimelineClient { memberPut(timelineName: string, username: string): Promise<void>; memberDelete(timelineName: string, username: string): Promise<void>; listPost(timelineName: string): Promise<HttpTimelinePostInfo[]>; + generatePostDataUrl(timelineName: string, postId: number): string; + getPostDataAsString(timelineName: string, postId: number): Promise<string>; postPost( timelineName: string, req: HttpTimelinePostPostRequest @@ -153,6 +158,24 @@ export class HttpTimelineClient implements IHttpTimelineClient { .then(extractResponseData); } + generatePostDataUrl(timelineName: string, postId: number): string { + return applyQueryParameters( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`, + { token: getHttpToken() } + ); + } + + getPostDataAsString(timelineName: string, postId: number): Promise<string> { + return axios + .get<string>( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`, + { + responseType: "text", + } + ) + .then(extractResponseData); + } + postPost( timelineName: string, req: HttpTimelinePostPostRequest diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index c6a567d3..dcb222bf 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -61,7 +61,7 @@ export interface IHttpUserClient { get(username: string): Promise<HttpUser>; patch(username: string, req: HttpUserPatchRequest): Promise<HttpUser>; delete(username: string): Promise<void>; - // return etag + generateAvatarUrl(username: string): string; putAvatar(username: string, data: Blob): Promise<string>; changePassword(req: HttpChangePasswordRequest): Promise<void>; putUserPermission( @@ -100,6 +100,10 @@ export class HttpUserClient implements IHttpUserClient { return axios.delete(`${apiBaseUrl}/users/${username}`).then(); } + generateAvatarUrl(username: string): string { + return `${apiBaseUrl}/users/${username}/avatar`; + } + putAvatar(username: string, data: Blob): Promise<string> { return axios .put(`${apiBaseUrl}/users/${username}/avatar`, data, { diff --git a/FrontEnd/src/app/utilities/url.ts b/FrontEnd/src/app/utilities/url.ts index 21ad6304..4f2a6ecd 100644 --- a/FrontEnd/src/app/utilities/url.ts +++ b/FrontEnd/src/app/utilities/url.ts @@ -4,7 +4,8 @@ export function applyQueryParameters<T>(url: string, query: T): string { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { - if (typeof value === "string") params.set(key, value); + if (value == null) void 0; + else if (typeof value === "string") params.set(key, value); else if (typeof value === "number") params.set(key, String(value)); else if (typeof value === "boolean") params.set(key, String(value)); else if (value instanceof Date) params.set(key, value.toISOString()); diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx index d0e39f98..e682a308 100644 --- a/FrontEnd/src/app/views/common/AppBar.tsx +++ b/FrontEnd/src/app/views/common/AppBar.tsx @@ -4,14 +4,13 @@ import { LinkContainer } from "react-router-bootstrap"; import { Navbar, Nav } from "react-bootstrap"; import { NavLink } from "react-router-dom"; -import { useUser, useAvatar } from "@/services/user"; +import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; -import BlobImage from "./BlobImage"; +import UserAvatar from "./user/UserAvatar"; const AppBar: React.FC = (_) => { const user = useUser(); - const avatar = useAvatar(user?.username); const { t } = useTranslation(); @@ -70,10 +69,9 @@ const AppBar: React.FC = (_) => { <Nav className="ml-auto mr-2 align-items-center"> {user != null ? ( <LinkContainer to={`/users/${user.username}`}> - <BlobImage + <UserAvatar + username={user.username} className="avatar small rounded-circle bg-white cursor-pointer ml-auto" - onClick={collapse} - blob={avatar} /> </LinkContainer> ) : ( diff --git a/FrontEnd/src/app/views/common/user/UserAvatar.tsx b/FrontEnd/src/app/views/common/user/UserAvatar.tsx index 73273298..9e822528 100644 --- a/FrontEnd/src/app/views/common/user/UserAvatar.tsx +++ b/FrontEnd/src/app/views/common/user/UserAvatar.tsx @@ -1,8 +1,6 @@ import React from "react"; -import { useAvatar } from "@/services/user"; - -import BlobImage from "../BlobImage"; +import { getHttpUserClient } from "@/http/user"; export interface UserAvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> { @@ -10,9 +8,12 @@ export interface UserAvatarProps } const UserAvatar: React.FC<UserAvatarProps> = ({ username, ...otherProps }) => { - const avatar = useAvatar(username); - - return <BlobImage blob={avatar} {...otherProps} />; + return ( + <img + src={getHttpUserClient().generateAvatarUrl(username)} + {...otherProps} + /> + ); }; export default UserAvatar; diff --git a/FrontEnd/src/app/views/home/TimelineBoard.tsx b/FrontEnd/src/app/views/home/TimelineBoard.tsx index c3f01aed..58988b17 100644 --- a/FrontEnd/src/app/views/home/TimelineBoard.tsx +++ b/FrontEnd/src/app/views/home/TimelineBoard.tsx @@ -4,10 +4,10 @@ import { Link } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { Spinner } from "react-bootstrap"; -import { TimelineInfo } from "@/services/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; + import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; -import { HttpTimelineInfo } from "@/http/timeline"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; @@ -98,7 +98,7 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({ }; interface TimelineBoardItemContainerProps { - timelines: TimelineInfo[]; + timelines: HttpTimelineInfo[]; editHandler?: { // offset may exceed index range plusing index. onMove: (timeline: string, index: number, offset: number) => void; @@ -206,7 +206,7 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ interface TimelineBoardUIProps { title?: string; - timelines: TimelineInfo[] | "offline" | "loading"; + timelines: HttpTimelineInfo[] | "offline" | "loading"; onReload: () => void; className?: string; editHandler?: { @@ -304,7 +304,7 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => { export interface TimelineBoardProps { title?: string; className?: string; - load: () => Promise<TimelineInfo[]>; + load: () => Promise<HttpTimelineInfo[]>; editHandler?: { onMove: (timeline: string, index: number, offset: number) => Promise<void>; onDelete: (timeline: string) => Promise<void>; @@ -318,7 +318,7 @@ const TimelineBoard: React.FC<TimelineBoardProps> = ({ editHandler, }) => { const [timelines, setTimelines] = React.useState< - TimelineInfo[] | "offline" | "loading" + HttpTimelineInfo[] | "offline" | "loading" >("loading"); React.useEffect(() => { 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<TimelineProps> = (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<TimelineProps> = (props) => { React.useEffect(() => { let subscribe = true; + setPosts("loading"); + void getHttpTimelineClient() .listPost(timelineName) .then( @@ -53,7 +57,7 @@ const Timeline: React.FC<TimelineProps> = (props) => { return () => { subscribe = false; }; - }, [timelineName]); + }, [timelineName, reloadKey]); switch (posts) { case "loading": @@ -87,7 +91,7 @@ const Timeline: React.FC<TimelineProps> = (props) => { </div> ); default: - return <TimelinePostListView posts={posts} />; + return <TimelinePostListView posts={posts} onReload={onReload} />; } }; 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<TManageItems>( const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0); + const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(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<TManageItems>( minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, }} > - <Timeline timelineName={timeline.name} /> + <Timeline + timelineName={timeline.name} + reloadKey={timelineReloadKey} + onReload={reloadTimeline} + /> </div> {timeline.postable ? ( <> @@ -134,6 +141,7 @@ export default function TimelinePageTemplateUI<TManageItems>( 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<TimelinePostContentViewProps> = (props) => { + const { post, className, style } = props; + + const [text, setText] = React.useState<string | null>(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 ( + <div className={className} style={style}> + Error! + </div> + ); + } else if (text == null) { + return <Spinner variant="primary" animation="grow" />; + } else { + return ( + <div className={className} style={style}> + {text} + </div> + ); + } +}; + +const ImageView: React.FC<TimelinePostContentViewProps> = (props) => { + const { post, className, style } = props; + + useUser(); + + return ( + <img + src={getHttpTimelineClient().generatePostDataUrl( + post.timelineName, + post.id + )} + className={className} + style={style} + /> + ); +}; + +const MarkdownView: React.FC<TimelinePostContentViewProps> = (_props) => { + // TODO: Implement this. + return <div>Unsupported now!</div>; +}; + +export interface TimelinePostContentViewProps { + post: HttpTimelinePostInfo; + className?: string; + style?: React.CSSProperties; +} + +const viewMap: Record<string, React.FC<TimelinePostContentViewProps>> = { + "text/plain": TextView, + "text/markdown": MarkdownView, + "image/png": ImageView, + "image/jpeg": ImageView, + "image/gif": ImageView, + "image/webp": ImageView, +}; + +const TimelinePostContentView: React.FC<TimelinePostContentViewProps> = ( + props +) => { + const { post, className, style } = props; + + const type = post.dataList[0].kind; + + if (type in viewMap) { + const View = viewMap[type]; + return <View post={post} className={className} style={style} />; + } else { + // TODO: i18n + return <div>Error, unknown post type!</div>; + } +}; + +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<TimelinePostEditImageProps> = (props) => { export interface TimelinePostEditProps { className?: string; timeline: HttpTimelineInfo; + onPosted: () => void; onHeightChange?: (height: number) => void; } const TimelinePostEdit: React.FC<TimelinePostEditProps> = (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<string>(""); + const [imageBlob, setImageBlob] = React.useState<Blob | null>(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<TimelinePostEditProps> = (props) => { const containerRef = React.useRef<HTMLDivElement>(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<void> => { 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<TimelinePostEditProps> = (props) => { return ( <div ref={containerRef} - className={clsx("container-fluid bg-light", props.className)} + className={clsx("container-fluid bg-light", className)} > <Row> <Col className="px-1 py-1"> 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<TimelinePostListViewProps> = (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<TimelinePostListViewProps> = (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<TimelinePostViewProps> = (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<TimelinePostViewProps> = (props) => { ] = React.useState<boolean>(false); const [deleteDialog, setDeleteDialog] = React.useState<boolean>(false); - // TODO: Load content. - return ( <div className={clsx("timeline-item", current && "current", className)} @@ -59,7 +61,9 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => { </span> </span> </div> - <div className="timeline-content">{/** TODO: Load content. */}</div> + <div className="timeline-content"> + <TimelinePostContentView post={post} /> + </div> {operationMaskVisible ? ( <div className="position-absolute position-lt w-100 h-100 mask d-flex justify-content-center align-items-center" @@ -84,7 +88,17 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (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} |