aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/pages/timeline
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-09-14 18:58:44 +0800
committercrupest <crupest@outlook.com>2023-09-20 18:50:08 +0800
commitbdc69c18c1986544497b6974ffe5d8e073e4be6d (patch)
tree4283f2fcc4af9b8849588e8db21e4f52d136a1e7 /FrontEnd/src/pages/timeline
parentaef14be13c3bd9e93eeea598dbfbf707ba98d448 (diff)
downloadtimeline-bdc69c18c1986544497b6974ffe5d8e073e4be6d.tar.gz
timeline-bdc69c18c1986544497b6974ffe5d8e073e4be6d.tar.bz2
timeline-bdc69c18c1986544497b6974ffe5d8e073e4be6d.zip
...
Diffstat (limited to 'FrontEnd/src/pages/timeline')
-rw-r--r--FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx11
-rw-r--r--FrontEnd/src/pages/timeline/TimelinePostContentView.tsx190
-rw-r--r--FrontEnd/src/pages/timeline/TimelinePostView.tsx2
-rw-r--r--FrontEnd/src/pages/timeline/view/ImagePostView.css0
-rw-r--r--FrontEnd/src/pages/timeline/view/ImagePostView.tsx38
-rw-r--r--FrontEnd/src/pages/timeline/view/MarkdownPostView.css4
-rw-r--r--FrontEnd/src/pages/timeline/view/MarkdownPostView.tsx60
-rw-r--r--FrontEnd/src/pages/timeline/view/PlainTextPostView.css0
-rw-r--r--FrontEnd/src/pages/timeline/view/PlainTextPostView.tsx50
-rw-r--r--FrontEnd/src/pages/timeline/view/TimelinePostContentView.tsx37
10 files changed, 194 insertions, 198 deletions
diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
index 630ce4ca..d1af364b 100644
--- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
@@ -1,20 +1,17 @@
-import * as React from "react";
import { useNavigate } from "react-router-dom";
import { Trans } from "react-i18next";
import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline";
-import OperationDialog from "~src/components/dialog/OperationDialog";
+import { OperationDialog } from "~src/components/dialog";
interface TimelineDeleteDialog {
timeline: HttpTimelineInfo;
}
-const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
+export default function TimelineDeleteDialog({ timeline }: TimelineDeleteDialog) {
const navigate = useNavigate();
- const { timeline } = props;
-
return (
<OperationDialog
title="timeline.deleteDialog.title"
@@ -24,7 +21,7 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
i18nKey="timeline.deleteDialog.inputPrompt"
values={{ name: timeline.nameV2 }}
>
- 0<code className="mx-2">1</code>2
+ 0<code>1</code>2
</Trans>
}
inputs={{
@@ -54,4 +51,4 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
);
};
-export default TimelineDeleteDialog;
+TimelineDeleteDialog;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx b/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx
deleted file mode 100644
index 6c0d7387..00000000
--- a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { marked } from "marked";
-
-import { UiLogicError } from "~src/common";
-
-import { HttpNetworkError } from "~src/http/common";
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline";
-
-import { useUser } from "~src/services/user";
-
-import Skeleton from "~src/components/Skeleton";
-import LoadFailReload from "~src/components/LoadFailReload";
-
-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);
-
- const [reloadKey, setReloadKey] = React.useState<number>(0);
-
- React.useEffect(() => {
- let subscribe = true;
-
- setText(null);
- setError(null);
-
- void getHttpTimelineClient()
- .getPostDataAsString(post.timelineOwnerV2, post.timelineNameV2, post.id)
- .then(
- (data) => {
- if (subscribe) setText(data);
- },
- (error) => {
- if (subscribe) {
- if (error instanceof HttpNetworkError) {
- setError("offline");
- } else {
- setError("error");
- }
- }
- },
- );
-
- return () => {
- subscribe = false;
- };
- }, [post.timelineOwnerV2, post.timelineNameV2, post.id, reloadKey]);
-
- if (error != null) {
- return (
- <LoadFailReload
- className={className}
- style={style}
- onReload={() => setReloadKey(reloadKey + 1)}
- />
- );
- } else if (text == null) {
- return <Skeleton />;
- } 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.timelineOwnerV2,
- post.timelineNameV2,
- post.id,
- )}
- className={classnames(className, "timeline-content-image")}
- style={style}
- />
- );
-};
-
-const MarkdownView: React.FC<TimelinePostContentViewProps> = (props) => {
- const { post, className, style } = props;
-
- const [markdown, setMarkdown] = React.useState<string | null>(null);
- const [error, setError] = React.useState<"offline" | "error" | null>(null);
-
- const [reloadKey, setReloadKey] = React.useState<number>(0);
-
- React.useEffect(() => {
- let subscribe = true;
-
- setMarkdown(null);
- setError(null);
-
- void getHttpTimelineClient()
- .getPostDataAsString(post.timelineOwnerV2, post.timelineNameV2, post.id)
- .then(
- (data) => {
- if (subscribe) setMarkdown(data);
- },
- (error) => {
- if (subscribe) {
- if (error instanceof HttpNetworkError) {
- setError("offline");
- } else {
- setError("error");
- }
- }
- },
- );
-
- return () => {
- subscribe = false;
- };
- }, [post.timelineOwnerV2, post.timelineNameV2, post.id, reloadKey]);
-
- const markdownHtml = React.useMemo<string | null>(() => {
- if (markdown == null) return null;
- return marked.parse(markdown, {
- mangle: false,
- headerIds: false,
- });
- }, [markdown]);
-
- if (error != null) {
- return (
- <LoadFailReload
- className={className}
- style={style}
- onReload={() => setReloadKey(reloadKey + 1)}
- />
- );
- } else if (markdown == null) {
- return <Skeleton />;
- } else {
- if (markdownHtml == null) {
- throw new UiLogicError("Markdown is not null but markdown html is.");
- }
- return (
- <div
- className={classnames(className, "markdown-container")}
- style={style}
- dangerouslySetInnerHTML={{
- __html: markdownHtml,
- }}
- />
- );
- }
-};
-
-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
- console.error("Unknown post type", post);
- return <div>Error, unknown post type!</div>;
- }
-};
-
-export default TimelinePostContentView;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
index b09fe6f8..4f0460ff 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
@@ -11,7 +11,7 @@ import UserAvatar from "~src/components/user/UserAvatar";
import { DialogProvider, useDialog } from "~src/components/dialog";
import FlatButton from "~src/components/button/FlatButton";
import ConfirmDialog from "~src/components/dialog/ConfirmDialog";
-import TimelinePostContentView from "./TimelinePostContentView";
+import TimelinePostContentView from "./view/TimelinePostContentView";
import IconButton from "~src/components/button/IconButton";
import TimelinePostContainer from "./TimelinePostContainer";
diff --git a/FrontEnd/src/pages/timeline/view/ImagePostView.css b/FrontEnd/src/pages/timeline/view/ImagePostView.css
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/ImagePostView.css
diff --git a/FrontEnd/src/pages/timeline/view/ImagePostView.tsx b/FrontEnd/src/pages/timeline/view/ImagePostView.tsx
new file mode 100644
index 00000000..85179475
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/ImagePostView.tsx
@@ -0,0 +1,38 @@
+import { useEffect, useState } from "react";
+import classNames from "classnames";
+
+import {
+ HttpTimelinePostInfo,
+ getHttpTimelineClient,
+} from "~src/http/timeline";
+
+import "./ImagePostView.css";
+
+interface ImagePostViewProps {
+ post?: HttpTimelinePostInfo;
+ className?: string;
+}
+
+export default function ImagePostView({ post, className }: ImagePostViewProps) {
+ const [url, setUrl] = useState<string | null>(null);
+
+ useEffect(() => {
+ if (post) {
+ setUrl(
+ getHttpTimelineClient().generatePostDataUrl(
+ post.timelineOwnerV2,
+ post.timelineNameV2,
+ post.id,
+ ),
+ );
+ } else {
+ setUrl(null);
+ }
+ }, [post]);
+
+ return (
+ <div className={classNames("timeline-view-image-container", className)}>
+ <img src={url ?? undefined} className="timeline-view-image" />
+ </div>
+ );
+}
diff --git a/FrontEnd/src/pages/timeline/view/MarkdownPostView.css b/FrontEnd/src/pages/timeline/view/MarkdownPostView.css
new file mode 100644
index 00000000..48a893eb
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/MarkdownPostView.css
@@ -0,0 +1,4 @@
+.timeline-view-markdown img {
+ max-width: 100%;
+ max-height: 200px;
+}
diff --git a/FrontEnd/src/pages/timeline/view/MarkdownPostView.tsx b/FrontEnd/src/pages/timeline/view/MarkdownPostView.tsx
new file mode 100644
index 00000000..caf6eef0
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/MarkdownPostView.tsx
@@ -0,0 +1,60 @@
+import { useMemo, useState } from "react";
+import { marked } from "marked";
+import classNames from "classnames";
+
+import {
+ HttpTimelinePostInfo,
+ getHttpTimelineClient,
+} from "~src/http/timeline";
+
+import { useAutoUnsubscribePromise } from "~src/components/hooks";
+import Skeleton from "~src/components/Skeleton";
+
+import "./MarkdownPostView.css";
+
+interface MarkdownPostViewProps {
+ post?: HttpTimelinePostInfo;
+ className?: string;
+}
+
+export default function MarkdownPostView({
+ post,
+ className,
+}: MarkdownPostViewProps) {
+ const [markdown, setMarkdown] = useState<string | null>(null);
+
+ useAutoUnsubscribePromise(
+ () => {
+ if (post) {
+ return getHttpTimelineClient().getPostDataAsString(
+ post.timelineOwnerV2,
+ post.timelineNameV2,
+ post.id,
+ );
+ }
+ },
+ setMarkdown,
+ [post],
+ );
+
+ const markdownHtml = useMemo<string | null>(() => {
+ if (markdown == null) return null;
+ return marked.parse(markdown, {
+ mangle: false,
+ headerIds: false,
+ });
+ }, [markdown]);
+
+ return (
+ <div className={classNames("timeline-view-markdown-container", className)}>
+ {markdownHtml == null ? (
+ <Skeleton />
+ ) : (
+ <div
+ className="timeline-view-markdown"
+ dangerouslySetInnerHTML={{ __html: markdownHtml }}
+ />
+ )}
+ </div>
+ );
+}
diff --git a/FrontEnd/src/pages/timeline/view/PlainTextPostView.css b/FrontEnd/src/pages/timeline/view/PlainTextPostView.css
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/PlainTextPostView.css
diff --git a/FrontEnd/src/pages/timeline/view/PlainTextPostView.tsx b/FrontEnd/src/pages/timeline/view/PlainTextPostView.tsx
new file mode 100644
index 00000000..b964187d
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/PlainTextPostView.tsx
@@ -0,0 +1,50 @@
+import { useState } from "react";
+import classNames from "classnames";
+
+import {
+ HttpTimelinePostInfo,
+ getHttpTimelineClient,
+} from "~src/http/timeline";
+
+import Skeleton from "~src/components/Skeleton";
+import { useAutoUnsubscribePromise } from "~src/components/hooks";
+
+import "./PlainTextPostView.css";
+
+interface PlainTextPostViewProps {
+ post?: HttpTimelinePostInfo;
+ className?: string;
+}
+
+export default function PlainTextPostView({
+ post,
+ className,
+}: PlainTextPostViewProps) {
+ const [text, setText] = useState<string | null>(null);
+
+ useAutoUnsubscribePromise(
+ () => {
+ if (post) {
+ return getHttpTimelineClient().getPostDataAsString(
+ post.timelineOwnerV2,
+ post.timelineNameV2,
+ post.id,
+ );
+ }
+ },
+ setText,
+ [post],
+ );
+
+ return (
+ <div
+ className={classNames("timeline-view-plain-text-container", className)}
+ >
+ {text == null ? (
+ <Skeleton />
+ ) : (
+ <div className="timeline-view-plain-text">{text}</div>
+ )}
+ </div>
+ );
+}
diff --git a/FrontEnd/src/pages/timeline/view/TimelinePostContentView.tsx b/FrontEnd/src/pages/timeline/view/TimelinePostContentView.tsx
new file mode 100644
index 00000000..851a9a33
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/view/TimelinePostContentView.tsx
@@ -0,0 +1,37 @@
+import ImagePostView from "./ImagePostView";
+import MarkdownPostView from "./MarkdownPostView";
+import PlainTextPostView from "./PlainTextPostView";
+
+import type { HttpTimelinePostInfo } from "~src/http/timeline";
+
+interface TimelinePostContentViewProps {
+ post?: HttpTimelinePostInfo;
+ className?: string;
+}
+
+const viewMap: Record<string, React.FC<TimelinePostContentViewProps>> = {
+ "text/plain": PlainTextPostView,
+ "text/markdown": MarkdownPostView,
+ "image/png": ImagePostView,
+ "image/jpeg": ImagePostView,
+ "image/gif": ImagePostView,
+ "image/webp": ImagePostView,
+};
+
+export default function TimelinePostContentView({
+ post,
+ className,
+}: TimelinePostContentViewProps) {
+ if (post == null) {
+ return <div />;
+ }
+
+ const type = post.dataList[0].kind;
+
+ if (type in viewMap) {
+ const View = viewMap[type];
+ return <View post={post} className={className} />;
+ }
+
+ return <div>Unknown post type.</div>;
+}