aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/pages/timeline/Timeline.css7
-rw-r--r--FrontEnd/src/pages/timeline/Timeline.tsx15
-rw-r--r--FrontEnd/src/pages/timeline/TimelineCard.css18
-rw-r--r--FrontEnd/src/pages/timeline/TimelineCard.tsx59
-rw-r--r--FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx4
-rw-r--r--FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx4
-rw-r--r--FrontEnd/src/views/timeline/CollapseButton.tsx21
-rw-r--r--FrontEnd/src/views/timeline/ConnectionStatusBadge.css36
-rw-r--r--FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx41
-rw-r--r--FrontEnd/src/views/timeline/MarkdownPostEdit.css21
-rw-r--r--FrontEnd/src/views/timeline/MarkdownPostEdit.tsx215
-rw-r--r--FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx42
-rw-r--r--FrontEnd/src/views/timeline/Timeline.css244
-rw-r--r--FrontEnd/src/views/timeline/Timeline.tsx207
-rw-r--r--FrontEnd/src/views/timeline/TimelineCard.tsx167
-rw-r--r--FrontEnd/src/views/timeline/TimelineDateLabel.tsx19
-rw-r--r--FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx61
-rw-r--r--FrontEnd/src/views/timeline/TimelineEmptyItem.tsx25
-rw-r--r--FrontEnd/src/views/timeline/TimelineLine.tsx51
-rw-r--r--FrontEnd/src/views/timeline/TimelineLoading.tsx16
-rw-r--r--FrontEnd/src/views/timeline/TimelineMember.css8
-rw-r--r--FrontEnd/src/views/timeline/TimelineMember.tsx202
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostContentView.tsx187
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostEdit.css10
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostEdit.tsx267
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostEditCard.tsx31
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostEditNoLogin.tsx18
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostListView.tsx76
-rw-r--r--FrontEnd/src/views/timeline/TimelinePostView.tsx159
-rw-r--r--FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx82
-rw-r--r--FrontEnd/src/views/timeline/index.tsx23
31 files changed, 55 insertions, 2281 deletions
diff --git a/FrontEnd/src/pages/timeline/Timeline.css b/FrontEnd/src/pages/timeline/Timeline.css
index 4dd4fdcc..f071f163 100644
--- a/FrontEnd/src/pages/timeline/Timeline.css
+++ b/FrontEnd/src/pages/timeline/Timeline.css
@@ -230,13 +230,6 @@
margin-right: 0.6em;
}
-.timeline-card {
- position: fixed;
- z-index: 1029;
- top: 56px;
- right: 0;
- margin: 0.5em;
-}
.timeline-top {
position: sticky;
diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx
index 3a7fbd00..f93e1623 100644
--- a/FrontEnd/src/pages/timeline/Timeline.tsx
+++ b/FrontEnd/src/pages/timeline/Timeline.tsx
@@ -41,7 +41,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
const [posts, setPosts] = React.useState<HttpTimelinePostInfo[] | null>(null);
const [signalrState, setSignalrState] = React.useState<HubConnectionState>(
- HubConnectionState.Connecting
+ HubConnectionState.Connecting,
);
const [error, setError] = React.useState<
"offline" | "forbid" | "notfound" | "error" | null
@@ -81,7 +81,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
console.error(error);
setError("error");
}
- }
+ },
);
}, [timelineOwner, timelineName, timelineReloadKey]);
@@ -91,7 +91,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
.then(
(page) => {
setPosts(
- page.items.filter((p): p is HttpTimelinePostInfo => !p.deleted)
+ page.items.filter((p): p is HttpTimelinePostInfo => !p.deleted),
);
setTotalPage(page.totalPageCount);
},
@@ -106,14 +106,14 @@ const Timeline: React.FC<TimelineProps> = (props) => {
console.error(error);
setError("error");
}
- }
+ },
);
}, [timelineOwner, timelineName, postsReloadKey]);
React.useEffect(() => {
const timelinePostUpdate$ = getTimelinePostUpdate$(
timelineOwner,
- timelineName
+ timelineName,
);
const subscription = timelinePostUpdate$.subscribe(({ update, state }) => {
if (update) {
@@ -134,7 +134,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
.then(
(page) => {
const ps = page.items.filter(
- (p): p is HttpTimelinePostInfo => !p.deleted
+ (p): p is HttpTimelinePostInfo => !p.deleted,
);
setPosts((old) => [...(old ?? []), ...ps]);
},
@@ -149,7 +149,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
console.error(error);
setError("error");
}
- }
+ },
);
}, currentPage < totalPage);
@@ -183,7 +183,6 @@ const Timeline: React.FC<TimelineProps> = (props) => {
{timeline == null && posts == null && <TimelineLoading />}
{timeline && (
<TimelineCard
- className="timeline-card"
timeline={timeline}
connectionStatus={signalrState}
onReload={updateTimeline}
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.css b/FrontEnd/src/pages/timeline/TimelineCard.css
new file mode 100644
index 00000000..75ce6c51
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelineCard.css
@@ -0,0 +1,18 @@
+.timeline-card {
+ position: fixed;
+ z-index: 1029;
+ top: 56px;
+ right: 0;
+ margin: 0.5em;
+}
+
+.timeline-card-title {
+ display: inline-block;
+ vertical-align: middle;
+ color: var(--cru-key-on-color);
+}
+
+.timeline-card-title-name {
+ margin-inline-start: 1em;
+ color: var(--cru-surface-on-color);
+} \ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx
index 8ce133c0..bcdfa4c2 100644
--- a/FrontEnd/src/pages/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx
@@ -1,5 +1,4 @@
-import * as React from "react";
-import { useTranslation } from "react-i18next";
+import { useState } from "react";
import classnames from "classnames";
import { HubConnectionState } from "@microsoft/signalr";
@@ -10,6 +9,8 @@ import { pushAlert } from "@/services/alert";
import { HttpTimelineInfo } from "@/http/timeline";
import { getHttpBookmarkClient } from "@/http/bookmark";
+import { useC } from "@/views/common/common";
+import { useDialog } from "@/views/common/dialog";
import UserAvatar from "@/views/common/user/UserAvatar";
import PopupMenu from "@/views/common/menu/PopupMenu";
import FullPageDialog from "@/views/common/dialog/FullPageDialog";
@@ -21,36 +22,36 @@ import { TimelineMemberDialog } from "./TimelineMember";
import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
import IconButton from "@/views/common/button/IconButton";
-export interface TimelinePageCardProps {
+import "./TimelineCard.css";
+
+interface TimelinePageCardProps {
timeline: HttpTimelineInfo;
connectionStatus: HubConnectionState;
- className?: string;
onReload: () => void;
}
-const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
- const { timeline, connectionStatus, onReload, className } = props;
+export default function TimelineCard(props: TimelinePageCardProps) {
+ const { timeline, connectionStatus, onReload } = props;
- const { t } = useTranslation();
+ const user = useUser();
- const [dialog, setDialog] = React.useState<
- "member" | "property" | "delete" | null
- >(null);
+ const c = useC();
- const [collapse, setCollapse] = React.useState(true);
+ const [collapse, setCollapse] = useState(true);
const toggleCollapse = (): void => {
setCollapse((o) => !o);
};
const isSmallScreen = useIsSmallScreen();
- const user = useUser();
+ const { createDialogSwitch, dialog, dialogPropsMap, switchDialog } =
+ useDialog(["member", "property", "delete"]);
const content = (
- <>
- <h3 className="cru-color-primary d-inline-block align-middle">
+ <div className="cru-primary">
+ <h3 className="timeline-card-title">
{timeline.title}
- <small className="ms-3 cru-color-secondary">{timeline.nameV2}</small>
+ <small className="timeline-card-title-name">{timeline.nameV2}</small>
</h3>
<div>
<UserAvatar
@@ -64,7 +65,7 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
</div>
<p className="mb-0">{timeline.description}</p>
<small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
+ {c(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
</small>
<div className="mt-2 cru-text-end">
{user != null ? (
@@ -92,7 +93,7 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
<IconButton
icon="people"
className="me-3"
- onClick={() => setDialog("member")}
+ onClick={createDialogSwitch("member")}
/>
{timeline.manageable ? (
<PopupMenu
@@ -100,12 +101,12 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
{
type: "button",
text: "timeline.manageItem.property",
- onClick: () => setDialog("property"),
+ onClick: createDialogSwitch("property"),
},
{ type: "divider" },
{
type: "button",
- onClick: () => setDialog("delete"),
+ onClick: createDialogSwitch("delete"),
color: "danger",
text: "timeline.manageItem.delete",
},
@@ -116,12 +117,12 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
</PopupMenu>
) : null}
</div>
- </>
+ </div>
);
return (
<>
- <Card className={classnames("p-2 cru-clearfix", className)}>
+ <Card className="timeline-card">
<div
className={classnames(
"cru-float-right d-flex align-items-center",
@@ -145,23 +146,15 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
</Card>
<TimelineMemberDialog
timeline={timeline}
- onClose={() => setDialog(null)}
- open={dialog === "member"}
onChange={onReload}
+ {...dialogPropsMap["member"]}
/>
<TimelinePropertyChangeDialog
timeline={timeline}
- close={() => setDialog(null)}
- open={dialog === "property"}
onChange={onReload}
+ {...dialogPropsMap["property"]}
/>
- <TimelineDeleteDialog
- timeline={timeline}
- open={dialog === "delete"}
- close={() => setDialog(null)}
- />
+ <TimelineDeleteDialog timeline={timeline} {...dialogPropsMap["delete"]} />
</>
);
-};
-
-export default TimelineCard;
+}
diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
index d5b22aee..0a5a2491 100644
--- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
@@ -9,7 +9,7 @@ import OperationDialog from "@/views/common/dialog/OperationDialog";
interface TimelineDeleteDialog {
timeline: HttpTimelineInfo;
open: boolean;
- close: () => void;
+ onClose: () => void;
}
const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
@@ -20,7 +20,7 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
return (
<OperationDialog
open={props.open}
- onClose={props.close}
+ onClose={props.onClose}
title="timeline.deleteDialog.title"
color="danger"
inputPrompt={() => (
diff --git a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
index e26df3eb..b57135bb 100644
--- a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
@@ -12,7 +12,7 @@ import OperationDialog from "@/views/common/dialog/OperationDialog";
export interface TimelinePropertyChangeDialogProps {
open: boolean;
- close: () => void;
+ onClose: () => void;
timeline: HttpTimelineInfo;
onChange: () => void;
}
@@ -64,7 +64,7 @@ const TimelinePropertyChangeDialog: React.FC<
},
}}
open={props.open}
- onClose={props.close}
+ onClose={props.onClose}
onProcess={({ title, visibility, description }) => {
const req: HttpTimelinePatchRequest = {};
if (title !== timeline.title) {
diff --git a/FrontEnd/src/views/timeline/CollapseButton.tsx b/FrontEnd/src/views/timeline/CollapseButton.tsx
deleted file mode 100644
index 374ccc2e..00000000
--- a/FrontEnd/src/views/timeline/CollapseButton.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as React from "react";
-
-import IconButton from "../common/button/IconButton";
-
-const CollapseButton: React.FC<{
- collapse: boolean;
- onClick: () => void;
- className?: string;
- style?: React.CSSProperties;
-}> = ({ collapse, onClick, className, style }) => {
- return (
- <IconButton
- icon={collapse ? "arrows-angle-expand" : "arrows-angle-contract"}
- onClick={onClick}
- className={className}
- style={style}
- />
- );
-};
-
-export default CollapseButton;
diff --git a/FrontEnd/src/views/timeline/ConnectionStatusBadge.css b/FrontEnd/src/views/timeline/ConnectionStatusBadge.css
deleted file mode 100644
index 7fe83b9b..00000000
--- a/FrontEnd/src/views/timeline/ConnectionStatusBadge.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.connection-status-badge {
- font-size: 0.8em;
- border-radius: 5px;
- padding: 0.1em 1em;
- background-color: #eaf2ff;
-}
-.connection-status-badge::before {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- display: inline-block;
- content: "";
- margin-right: 0.6em;
-}
-.connection-status-badge.success {
- color: #006100;
-}
-.connection-status-badge.success::before {
- background-color: #006100;
-}
-
-.connection-status-badge.warning {
- color: #e4a700;
-}
-
-.connection-status-badge.warning::before {
- background-color: #e4a700;
-}
-
-.connection-status-badge.danger {
- color: #fd1616;
-}
-
-.connection-status-badge.danger::before {
- background-color: #fd1616;
-}
diff --git a/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx
deleted file mode 100644
index 2b820454..00000000
--- a/FrontEnd/src/views/timeline/ConnectionStatusBadge.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { HubConnectionState } from "@microsoft/signalr";
-import { useTranslation } from "react-i18next";
-
-import "./ConnectionStatusBadge.css";
-
-export interface ConnectionStatusBadgeProps {
- status: HubConnectionState;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const classNameMap: Record<HubConnectionState, string> = {
- Connected: "success",
- Connecting: "warning",
- Disconnected: "danger",
- Disconnecting: "warning",
- Reconnecting: "warning",
-};
-
-const ConnectionStatusBadge: React.FC<ConnectionStatusBadgeProps> = (props) => {
- const { status, className, style } = props;
-
- const { t } = useTranslation();
-
- return (
- <div
- className={classnames(
- "connection-status-badge",
- classNameMap[status],
- className
- )}
- style={style}
- >
- {t(`connectionState.${status}`)}
- </div>
- );
-};
-
-export default ConnectionStatusBadge;
diff --git a/FrontEnd/src/views/timeline/MarkdownPostEdit.css b/FrontEnd/src/views/timeline/MarkdownPostEdit.css
deleted file mode 100644
index e36be992..00000000
--- a/FrontEnd/src/views/timeline/MarkdownPostEdit.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.timeline-markdown-post-edit-page {
- overflow: auto;
- max-height: 300px;
-}
-
-.timeline-markdown-post-edit-image-container {
- position: relative;
- text-align: center;
- margin-bottom: 1em;
-}
-
-.timeline-markdown-post-edit-image {
- max-width: 100%;
- max-height: 200px;
-}
-
-.timeline-markdown-post-edit-image-delete-button {
- position: absolute;
- right: 10px;
- top: 2px;
-}
diff --git a/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx
deleted file mode 100644
index 6401cfaa..00000000
--- a/FrontEnd/src/views/timeline/MarkdownPostEdit.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
-
-import TimelinePostBuilder from "@/services/TimelinePostBuilder";
-
-import FlatButton from "../common/button/FlatButton";
-import TabPages from "../common/tab/TabPages";
-import ConfirmDialog from "../common/dialog/ConfirmDialog";
-import Spinner from "../common/Spinner";
-import IconButton from "../common/button/IconButton";
-
-import "./MarkdownPostEdit.css";
-
-export interface MarkdownPostEditProps {
- owner: string;
- timeline: string;
- onPosted: (post: HttpTimelinePostInfo) => void;
- onPostError: () => void;
- onClose: () => void;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
- owner: ownerUsername,
- timeline: timelineName,
- onPosted,
- onClose,
- onPostError,
- className,
- style,
-}) => {
- const { t } = useTranslation();
-
- const [canLeave, setCanLeave] = React.useState<boolean>(true);
-
- const [process, setProcess] = React.useState<boolean>(false);
-
- const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] =
- React.useState<boolean>(false);
-
- const [text, _setText] = React.useState<string>("");
- const [images, _setImages] = React.useState<{ file: File; url: string }[]>(
- []
- );
- const [previewHtml, _setPreviewHtml] = React.useState<string>("");
-
- const _builder = React.useRef<TimelinePostBuilder | null>(null);
-
- const getBuilder = (): TimelinePostBuilder => {
- if (_builder.current == null) {
- const builder = new TimelinePostBuilder(() => {
- setCanLeave(builder.isEmpty);
- _setText(builder.text);
- _setImages(builder.images);
- _setPreviewHtml(builder.renderHtml());
- });
- _builder.current = builder;
- }
- return _builder.current;
- };
-
- const canSend = text.length > 0;
-
- React.useEffect(() => {
- return () => {
- getBuilder().dispose();
- };
- }, []);
-
- React.useEffect(() => {
- window.onbeforeunload = (): unknown => {
- if (!canLeave) {
- return t("timeline.confirmLeave");
- }
- };
-
- return () => {
- window.onbeforeunload = null;
- };
- }, [canLeave, t]);
-
- const send = async (): Promise<void> => {
- setProcess(true);
- try {
- const dataList = await getBuilder().build();
- const post = await getHttpTimelineClient().postPost(
- ownerUsername,
- timelineName,
- {
- dataList,
- }
- );
- onPosted(post);
- onClose();
- } catch (e) {
- setProcess(false);
- onPostError();
- }
- };
-
- return (
- <>
- <TabPages
- className={className}
- style={style}
- pageContainerClassName="py-2"
- dense
- actions={
- process ? (
- <Spinner />
- ) : (
- <div>
- <IconButton
- icon="x"
- color="danger"
- large
- className="cru-align-middle me-2"
- onClick={() => {
- if (canLeave) {
- onClose();
- } else {
- setShowLeaveConfirmDialog(true);
- }
- }}
- />
- {canSend && (
- <FlatButton text="timeline.send" onClick={() => void send()} />
- )}
- </div>
- )
- }
- pages={[
- {
- name: "text",
- text: "edit",
- page: (
- <textarea
- value={text}
- disabled={process}
- className="cru-fill-parent"
- onChange={(event) => {
- getBuilder().setMarkdownText(event.currentTarget.value);
- }}
- />
- ),
- },
- {
- name: "images",
- text: "image",
- page: (
- <div className="timeline-markdown-post-edit-page">
- {images.map((image, index) => (
- <div
- key={image.url}
- className="timeline-markdown-post-edit-image-container"
- >
- <img
- src={image.url}
- className="timeline-markdown-post-edit-image"
- />
- <IconButton
- icon="trash"
- color="danger"
- className={classnames(
- "timeline-markdown-post-edit-image-delete-button",
- process && "d-none"
- )}
- onClick={() => {
- getBuilder().deleteImage(index);
- }}
- />
- </div>
- ))}
- <input
- type="file"
- accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
- onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
- const { files } = event.currentTarget;
- if (files != null && files.length !== 0) {
- getBuilder().appendImage(files[0]);
- }
- }}
- disabled={process}
- />
- </div>
- ),
- },
- {
- name: "preview",
- text: "preview",
- page: (
- <div
- className="markdown-container timeline-markdown-post-edit-page"
- dangerouslySetInnerHTML={{ __html: previewHtml }}
- />
- ),
- },
- ]}
- />
- <ConfirmDialog
- onClose={() => setShowLeaveConfirmDialog(false)}
- onConfirm={onClose}
- open={showLeaveConfirmDialog}
- title="timeline.dropDraft"
- body="timeline.confirmLeave"
- />
- </>
- );
-};
-
-export default MarkdownPostEdit;
diff --git a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx
deleted file mode 100644
index fc55185c..00000000
--- a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import * as React from "react";
-
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-
-function PostPropertyChangeDialog(props: {
- open: boolean;
- onClose: () => void;
- post: HttpTimelinePostInfo;
- onSuccess: (post: HttpTimelinePostInfo) => void;
-}): React.ReactElement | null {
- const { open, onClose, post, onSuccess } = props;
-
- return (
- <OperationDialog
- title="timeline.changePostPropertyDialog.title"
- onClose={onClose}
- open={open}
- inputScheme={[
- {
- label: "timeline.changePostPropertyDialog.time",
- type: "datetime",
- initValue: post.time,
- },
- ]}
- onProcess={([time]) => {
- return getHttpTimelineClient().patchPost(
- post.timelineOwnerV2,
- post.timelineNameV2,
- post.id,
- {
- time: time === "" ? undefined : new Date(time).toISOString(),
- }
- );
- }}
- onSuccessAndClose={onSuccess}
- />
- );
-}
-
-export default PostPropertyChangeDialog;
diff --git a/FrontEnd/src/views/timeline/Timeline.css b/FrontEnd/src/views/timeline/Timeline.css
deleted file mode 100644
index 4dd4fdcc..00000000
--- a/FrontEnd/src/views/timeline/Timeline.css
+++ /dev/null
@@ -1,244 +0,0 @@
-.timeline {
- z-index: 0;
- position: relative;
- width: 100%;
-}
-
-@keyframes timeline-line-node {
- to {
- box-shadow: 0 0 20px 3px var(--cru-primary-l1-color);
- }
-}
-
-@keyframes timeline-line-node-current {
- to {
- box-shadow: 0 0 20px 3px var(--cru-primary-enhance-l1-color);
- }
-}
-
-@keyframes timeline-line-node-loading {
- to {
- box-shadow: 0 0 20px 3px var(--cru-primary-l1-color);
- }
-}
-
-@keyframes timeline-line-node-loading-edge {
- from {
- transform: rotate(0turn);
- }
- to {
- transform: rotate(1turn);
- }
-}
-
-@keyframes timeline-top-loading-enter {
- from {
- transform: translate(0, -100%);
- }
-}
-
-@keyframes timeline-post-enter {
- from {
- transform: translate(0, 100%);
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-.timeline-top-loading-enter {
- animation: 1s timeline-top-loading-enter;
-}
-
-.timeline-line {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 30px;
- position: absolute;
- z-index: 1;
- left: 2em;
- top: 0;
- bottom: 0;
- transition: left 0.5s;
-}
-
-@media (max-width: 575.98px) {
- .timeline-line {
- left: 1em;
- }
-}
-
-.timeline-line .segment {
- width: 7px;
- background: var(--cru-primary-color);
-}
-.timeline-line .segment.start {
- height: 1.8em;
- flex: 0 0 auto;
-}
-.timeline-line .segment.end {
- flex: 1 1 auto;
-}
-.timeline-line .segment.current-end {
- height: 2em;
- flex: 0 0 auto;
- background: linear-gradient(var(--cru-primary-enhance-color), white);
-}
-.timeline-line .node-container {
- flex: 0 0 auto;
- position: relative;
- width: 18px;
- height: 18px;
-}
-.timeline-line .node {
- width: 20px;
- height: 20px;
- position: absolute;
- background: var(--cru-primary-color);
- left: -1px;
- top: -1px;
- border-radius: 50%;
- box-sizing: border-box;
- z-index: 1;
- animation: 1s infinite alternate;
- animation-name: timeline-line-node;
-}
-.timeline-line .node-loading-edge {
- color: var(--cru-primary-color);
- width: 38px;
- height: 38px;
- position: absolute;
- left: -10px;
- top: -10px;
- box-sizing: border-box;
- z-index: 2;
- animation: 1.5s linear infinite timeline-line-node-loading-edge;
-}
-.timeline-line.current .segment.start {
- background: linear-gradient(
- var(--cru-primary-color),
- var(--cru-primary-enhance-color)
- );
-}
-
-.timeline-line.current .segment.end {
- background: var(--cru-primary-enhance-color);
-}
-
-.timeline-line.current .node {
- background: var(--cru-primary-enhance-color);
- animation-name: timeline-line-node-current;
-}
-
-.timeline-line.loading .node {
- background: var(--cru-primary-color);
- animation-name: timeline-line-node-loading;
-}
-
-.timeline-item {
- position: relative;
- padding: 0.5em;
-}
-
-.timeline-item-card {
- position: relative;
- padding: 0.5em 0.5em 0.5em 4em;
-}
-
-.timeline-item-card.enter-animation {
- animation: 0.6s forwards;
- opacity: 0;
-}
-
-@media (max-width: 575.98px) {
- .timeline-item-card {
- padding-left: 3em;
- }
-}
-
-.timeline-item-header {
- display: flex;
- align-items: center;
-}
-
-.timeline-avatar {
- border-radius: 50%;
- width: 2em;
- height: 2em;
-}
-
-.timeline-item-delete-button {
- position: absolute;
- right: 0;
- bottom: 0;
-}
-
-.timeline-content {
- white-space: pre-line;
-}
-
-.timeline-content-image {
- max-width: 80%;
- max-height: 200px;
-}
-
-.timeline-date-item {
- position: relative;
- padding: 0.3em 0 0.3em 4em;
-}
-
-.timeline-date-item-badge {
- display: inline-block;
- padding: 0.1em 0.4em;
- border-radius: 0.4em;
- background: #7c7c7c;
- color: white;
- font-size: 0.8em;
-}
-
-.timeline-post-item-options-mask {
- background: rgba(255, 255, 255, 0.85);
- z-index: 100;
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
-
- display: flex;
- justify-content: space-around;
- align-items: center;
-
- border-radius: var(--cru-card-border-radius);
-}
-
-.timeline-sync-state-badge {
- font-size: 0.8em;
- padding: 3px 8px;
- border-radius: 5px;
- background: #e8fbff;
-}
-
-.timeline-sync-state-badge-pin {
- display: inline-block;
- width: 0.4em;
- height: 0.4em;
- border-radius: 50%;
- vertical-align: middle;
- margin-right: 0.6em;
-}
-
-.timeline-card {
- position: fixed;
- z-index: 1029;
- top: 56px;
- right: 0;
- margin: 0.5em;
-}
-
-.timeline-top {
- position: sticky;
- top: 56px;
-}
diff --git a/FrontEnd/src/views/timeline/Timeline.tsx b/FrontEnd/src/views/timeline/Timeline.tsx
deleted file mode 100644
index 3a7fbd00..00000000
--- a/FrontEnd/src/views/timeline/Timeline.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { useScrollToBottom } from "@/utilities/hooks";
-import { HubConnectionState } from "@microsoft/signalr";
-
-import {
- HttpForbiddenError,
- HttpNetworkError,
- HttpNotFoundError,
-} from "@/http/common";
-import {
- getHttpTimelineClient,
- HttpTimelineInfo,
- HttpTimelinePostInfo,
-} from "@/http/timeline";
-
-import { useUser } from "@/services/user";
-import { getTimelinePostUpdate$ } from "@/services/timeline";
-
-import TimelinePostListView from "./TimelinePostListView";
-import TimelineEmptyItem from "./TimelineEmptyItem";
-import TimelineLoading from "./TimelineLoading";
-import TimelinePostEdit from "./TimelinePostEdit";
-import TimelinePostEditNoLogin from "./TimelinePostEditNoLogin";
-import TimelineCard from "./TimelineCard";
-
-import "./Timeline.css";
-
-export interface TimelineProps {
- className?: string;
- style?: React.CSSProperties;
- timelineOwner: string;
- timelineName: string;
-}
-
-const Timeline: React.FC<TimelineProps> = (props) => {
- const { timelineOwner, timelineName, className, style } = props;
-
- const user = useUser();
-
- const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
- const [posts, setPosts] = React.useState<HttpTimelinePostInfo[] | null>(null);
- const [signalrState, setSignalrState] = React.useState<HubConnectionState>(
- HubConnectionState.Connecting
- );
- const [error, setError] = React.useState<
- "offline" | "forbid" | "notfound" | "error" | null
- >(null);
-
- const [currentPage, setCurrentPage] = React.useState(1);
- const [totalPage, setTotalPage] = React.useState(0);
-
- const [timelineReloadKey, setTimelineReloadKey] = React.useState(0);
- const [postsReloadKey, setPostsReloadKey] = React.useState(0);
-
- const updateTimeline = (): void => setTimelineReloadKey((o) => o + 1);
- const updatePosts = (): void => setPostsReloadKey((o) => o + 1);
-
- React.useEffect(() => {
- setTimeline(null);
- setPosts(null);
- setError(null);
- setSignalrState(HubConnectionState.Connecting);
- }, [timelineOwner, timelineName]);
-
- React.useEffect(() => {
- getHttpTimelineClient()
- .getTimeline(timelineOwner, timelineName)
- .then(
- (t) => {
- setTimeline(t);
- },
- (error) => {
- if (error instanceof HttpNetworkError) {
- setError("offline");
- } else if (error instanceof HttpForbiddenError) {
- setError("forbid");
- } else if (error instanceof HttpNotFoundError) {
- setError("notfound");
- } else {
- console.error(error);
- setError("error");
- }
- }
- );
- }, [timelineOwner, timelineName, timelineReloadKey]);
-
- React.useEffect(() => {
- getHttpTimelineClient()
- .listPost(timelineOwner, timelineName, 1)
- .then(
- (page) => {
- setPosts(
- page.items.filter((p): p is HttpTimelinePostInfo => !p.deleted)
- );
- setTotalPage(page.totalPageCount);
- },
- (error) => {
- if (error instanceof HttpNetworkError) {
- setError("offline");
- } else if (error instanceof HttpForbiddenError) {
- setError("forbid");
- } else if (error instanceof HttpNotFoundError) {
- setError("notfound");
- } else {
- console.error(error);
- setError("error");
- }
- }
- );
- }, [timelineOwner, timelineName, postsReloadKey]);
-
- React.useEffect(() => {
- const timelinePostUpdate$ = getTimelinePostUpdate$(
- timelineOwner,
- timelineName
- );
- const subscription = timelinePostUpdate$.subscribe(({ update, state }) => {
- if (update) {
- setPostsReloadKey((o) => o + 1);
- }
- setSignalrState(state);
- });
- return () => {
- subscription.unsubscribe();
- };
- }, [timelineOwner, timelineName]);
-
- useScrollToBottom(() => {
- console.log(`Load page ${currentPage + 1}.`);
- setCurrentPage(currentPage + 1);
- void getHttpTimelineClient()
- .listPost(timelineOwner, timelineName, currentPage + 1)
- .then(
- (page) => {
- const ps = page.items.filter(
- (p): p is HttpTimelinePostInfo => !p.deleted
- );
- setPosts((old) => [...(old ?? []), ...ps]);
- },
- (error) => {
- if (error instanceof HttpNetworkError) {
- setError("offline");
- } else if (error instanceof HttpForbiddenError) {
- setError("forbid");
- } else if (error instanceof HttpNotFoundError) {
- setError("notfound");
- } else {
- console.error(error);
- setError("error");
- }
- }
- );
- }, currentPage < totalPage);
-
- if (error === "offline") {
- return (
- <div className={className} style={style}>
- Offline.
- </div>
- );
- } else if (error === "notfound") {
- return (
- <div className={className} style={style}>
- Not exist.
- </div>
- );
- } else if (error === "forbid") {
- return (
- <div className={className} style={style}>
- Forbid.
- </div>
- );
- } else if (error === "error") {
- return (
- <div className={className} style={style}>
- Error.
- </div>
- );
- }
- return (
- <>
- {timeline == null && posts == null && <TimelineLoading />}
- {timeline && (
- <TimelineCard
- className="timeline-card"
- timeline={timeline}
- connectionStatus={signalrState}
- onReload={updateTimeline}
- />
- )}
- {posts && (
- <div style={style} className={classnames("timeline", className)}>
- <TimelineEmptyItem className="timeline-top" height={50} />
- {timeline?.postable ? (
- <TimelinePostEdit timeline={timeline} onPosted={updatePosts} />
- ) : user == null ? (
- <TimelinePostEditNoLogin />
- ) : null}
- <TimelinePostListView posts={posts} onReload={updatePosts} />
- </div>
- )}
- </>
- );
-};
-
-export default Timeline;
diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx
deleted file mode 100644
index fdf7f0a0..00000000
--- a/FrontEnd/src/views/timeline/TimelineCard.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import classnames from "classnames";
-import { HubConnectionState } from "@microsoft/signalr";
-
-import { useIsSmallScreen } from "@/utilities/hooks";
-import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline";
-import { useUser } from "@/services/user";
-import { pushAlert } from "@/services/alert";
-import { HttpTimelineInfo } from "@/http/timeline";
-import { getHttpBookmarkClient } from "@/http/bookmark";
-
-import UserAvatar from "../common/user/UserAvatar";
-import PopupMenu from "../common/menu/PopupMenu";
-import FullPageDialog from "../common/dialog/FullPageDialog";
-import Card from "../common/Card";
-import TimelineDeleteDialog from "./TimelineDeleteDialog";
-import ConnectionStatusBadge from "./ConnectionStatusBadge";
-import CollapseButton from "./CollapseButton";
-import { TimelineMemberDialog } from "./TimelineMember";
-import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
-import IconButton from "../common/button/IconButton";
-
-export interface TimelinePageCardProps {
- timeline: HttpTimelineInfo;
- connectionStatus: HubConnectionState;
- className?: string;
- onReload: () => void;
-}
-
-const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
- const { timeline, connectionStatus, onReload, className } = props;
-
- const { t } = useTranslation();
-
- const [dialog, setDialog] = React.useState<
- "member" | "property" | "delete" | null
- >(null);
-
- const [collapse, setCollapse] = React.useState(true);
- const toggleCollapse = (): void => {
- setCollapse((o) => !o);
- };
-
- const isSmallScreen = useIsSmallScreen();
-
- const user = useUser();
-
- const content = (
- <>
- <h3 className="cru-color-primary d-inline-block align-middle">
- {timeline.title}
- <small className="ms-3 cru-color-secondary">{timeline.nameV2}</small>
- </h3>
- <div>
- <UserAvatar
- username={timeline.owner.username}
- className="cru-avatar small cru-round me-3"
- />
- {timeline.owner.nickname}
- <small className="ms-3 cru-color-secondary">
- @{timeline.owner.username}
- </small>
- </div>
- <p className="mb-0">{timeline.description}</p>
- <small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
- </small>
- <div className="mt-2 cru-text-end">
- {user != null ? (
- <IconButton
- icon={timeline.isBookmark ? "bookmark-fill" : "bookmark"}
- className="me-3"
- onClick={() => {
- getHttpBookmarkClient()
- [timeline.isBookmark ? "delete" : "post"](
- user.username,
- timeline.owner.username,
- timeline.nameV2
- )
- .then(onReload, () => {
- pushAlert({
- message: timeline.isBookmark
- ? "timeline.removeBookmarkFail"
- : "timeline.addBookmarkFail",
- type: "danger",
- });
- });
- }}
- />
- ) : null}
- <IconButton
- icon="people"
- className="me-3"
- onClick={() => setDialog("member")}
- />
- {timeline.manageable ? (
- <PopupMenu
- items={[
- {
- type: "button",
- text: "timeline.manageItem.property",
- onClick: () => setDialog("property"),
- },
- { type: "divider" },
- {
- type: "button",
- onClick: () => setDialog("delete"),
- color: "danger",
- text: "timeline.manageItem.delete",
- },
- ]}
- containerClassName="d-inline"
- >
- <IconButton icon="three-dots-vertical" />
- </PopupMenu>
- ) : null}
- </div>
- </>
- );
-
- return (
- <>
- <Card className={classnames("p-2 cru-clearfix", className)}>
- <div
- className={classnames(
- "cru-float-right d-flex align-items-center",
- !collapse && "ms-3"
- )}
- >
- <ConnectionStatusBadge status={connectionStatus} className="me-2" />
- <CollapseButton collapse={collapse} onClick={toggleCollapse} />
- </div>
- {isSmallScreen ? (
- <FullPageDialog
- onBack={toggleCollapse}
- show={!collapse}
- contentContainerClassName="p-2"
- >
- {content}
- </FullPageDialog>
- ) : (
- <div style={{ display: collapse ? "none" : "inline" }}>{content}</div>
- )}
- </Card>
- <TimelineMemberDialog
- timeline={timeline}
- onClose={() => setDialog(null)}
- open={dialog === "member"}
- onChange={onReload}
- />
- <TimelinePropertyChangeDialog
- timeline={timeline}
- close={() => setDialog(null)}
- open={dialog === "property"}
- onChange={onReload}
- />
- <TimelineDeleteDialog
- timeline={timeline}
- open={dialog === "delete"}
- close={() => setDialog(null)}
- />
- </>
- );
-};
-
-export default TimelineCard;
diff --git a/FrontEnd/src/views/timeline/TimelineDateLabel.tsx b/FrontEnd/src/views/timeline/TimelineDateLabel.tsx
deleted file mode 100644
index 5f4ac706..00000000
--- a/FrontEnd/src/views/timeline/TimelineDateLabel.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as React from "react";
-import TimelineLine from "./TimelineLine";
-
-export interface TimelineDateItemProps {
- date: Date;
-}
-
-const TimelineDateLabel: React.FC<TimelineDateItemProps> = ({ date }) => {
- return (
- <div className="timeline-date-item">
- <TimelineLine center="none" />
- <div className="timeline-date-item-badge">
- {date.toLocaleDateString()}
- </div>
- </div>
- );
-};
-
-export default TimelineDateLabel;
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
deleted file mode 100644
index c960b3c2..00000000
--- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import * as React from "react";
-import { useNavigate } from "react-router-dom";
-import { Trans } from "react-i18next";
-
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-
-interface TimelineDeleteDialog {
- timeline: HttpTimelineInfo;
- open: boolean;
- close: () => void;
-}
-
-const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
- const navigate = useNavigate();
-
- const { timeline } = props;
-
- return (
- <OperationDialog
- open={props.open}
- onClose={props.close}
- title="timeline.deleteDialog.title"
- themeColor="danger"
- inputPrompt={() => {
- return (
- <Trans
- i18nKey="timeline.deleteDialog.inputPrompt"
- values={{ name: timeline.nameV2 }}
- >
- 0<code className="mx-2">1</code>2
- </Trans>
- );
- }}
- inputScheme={[
- {
- type: "text",
- },
- ]}
- inputValidator={([value]) => {
- if (value !== timeline.nameV2) {
- return { 0: "timeline.deleteDialog.notMatch" };
- } else {
- return null;
- }
- }}
- onProcess={() => {
- return getHttpTimelineClient().deleteTimeline(
- timeline.owner.username,
- timeline.nameV2
- );
- }}
- onSuccessAndClose={() => {
- navigate("/", { replace: true });
- }}
- />
- );
-};
-
-export default TimelineDeleteDialog;
diff --git a/FrontEnd/src/views/timeline/TimelineEmptyItem.tsx b/FrontEnd/src/views/timeline/TimelineEmptyItem.tsx
deleted file mode 100644
index 5e0728d4..00000000
--- a/FrontEnd/src/views/timeline/TimelineEmptyItem.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import TimelineLine, { TimelineLineProps } from "./TimelineLine";
-
-export interface TimelineEmptyItemProps extends Partial<TimelineLineProps> {
- height?: number | string;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const TimelineEmptyItem: React.FC<TimelineEmptyItemProps> = (props) => {
- const { height, style, className, center, ...lineProps } = props;
-
- return (
- <div
- style={{ ...style, height: height }}
- className={classnames("timeline-item", className)}
- >
- <TimelineLine center={center ?? "none"} {...lineProps} />
- </div>
- );
-};
-
-export default TimelineEmptyItem;
diff --git a/FrontEnd/src/views/timeline/TimelineLine.tsx b/FrontEnd/src/views/timeline/TimelineLine.tsx
deleted file mode 100644
index 4a87e6e0..00000000
--- a/FrontEnd/src/views/timeline/TimelineLine.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-export interface TimelineLineProps {
- current?: boolean;
- startSegmentLength?: string | number;
- center: "node" | "loading" | "none";
- className?: string;
- style?: React.CSSProperties;
-}
-
-const TimelineLine: React.FC<TimelineLineProps> = ({
- startSegmentLength,
- center,
- current,
- className,
- style,
-}) => {
- return (
- <div
- className={classnames(
- "timeline-line",
- current && "current",
- center === "loading" && "loading",
- className
- )}
- style={style}
- >
- <div className="segment start" style={{ height: startSegmentLength }} />
- {center !== "none" ? (
- <div className="node-container">
- <div className="node"></div>
- {center === "loading" ? (
- <svg className="node-loading-edge" viewBox="0 0 100 100">
- <path
- d="M 50,10 A 40 40 45 0 1 78.28,21.72"
- stroke="currentcolor"
- strokeLinecap="square"
- strokeWidth="8"
- />
- </svg>
- ) : null}
- </div>
- ) : null}
- {center !== "loading" ? <div className="segment end"></div> : null}
- {current && <div className="segment current-end" />}
- </div>
- );
-};
-
-export default TimelineLine;
diff --git a/FrontEnd/src/views/timeline/TimelineLoading.tsx b/FrontEnd/src/views/timeline/TimelineLoading.tsx
deleted file mode 100644
index f876cba9..00000000
--- a/FrontEnd/src/views/timeline/TimelineLoading.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as React from "react";
-
-import TimelineEmptyItem from "./TimelineEmptyItem";
-
-const TimelineLoading: React.FC = () => {
- return (
- <TimelineEmptyItem
- className="timeline-top-loading-enter"
- height={100}
- center="loading"
- startSegmentLength={56}
- />
- );
-};
-
-export default TimelineLoading;
diff --git a/FrontEnd/src/views/timeline/TimelineMember.css b/FrontEnd/src/views/timeline/TimelineMember.css
deleted file mode 100644
index adb78764..00000000
--- a/FrontEnd/src/views/timeline/TimelineMember.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.timeline-member-item {
- border: var(--cru-background-1-color) solid;
- border-width: 0.5px 1px;
-}
-
-.timeline-member-item > div {
- padding: 0.5em;
-}
diff --git a/FrontEnd/src/views/timeline/TimelineMember.tsx b/FrontEnd/src/views/timeline/TimelineMember.tsx
deleted file mode 100644
index aaafd173..00000000
--- a/FrontEnd/src/views/timeline/TimelineMember.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import { useState } from "react";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText } from "@/common";
-
-import { HttpUser } from "@/http/user";
-import { getHttpSearchClient } from "@/http/search";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import SearchInput from "../common/SearchInput";
-import UserAvatar from "../common/user/UserAvatar";
-import Button from "../common/button/Button";
-import Dialog from "../common/dialog/Dialog";
-
-import "./TimelineMember.css";
-
-const TimelineMemberItem: React.FC<{
- user: HttpUser;
- add?: boolean;
- onAction?: (username: string) => void;
-}> = ({ user, add, onAction }) => {
- return (
- <div className="container timeline-member-item">
- <div className="row">
- <div className="col col-auto">
- <UserAvatar username={user.username} className="cru-avatar small" />
- </div>
- <div className="col">
- <div className="row">{user.nickname}</div>
- <small className="row">{"@" + user.username}</small>
- </div>
- {onAction ? (
- <div className="col col-auto">
- <Button
- text={`timeline.member.${add ? "add" : "remove"}`}
- color={add ? "success" : "danger"}
- onClick={() => {
- onAction(user.username);
- }}
- />
- </div>
- ) : null}
- </div>
- </div>
- );
-};
-
-const TimelineMemberUserSearch: React.FC<{
- timeline: HttpTimelineInfo;
- onChange: () => void;
-}> = ({ timeline, onChange }) => {
- const { t } = useTranslation();
-
- const [userSearchText, setUserSearchText] = useState<string>("");
- const [userSearchState, setUserSearchState] = useState<
- | {
- type: "users";
- data: HttpUser[];
- }
- | { type: "error"; data: I18nText }
- | { type: "loading" }
- | { type: "init" }
- >({ type: "init" });
-
- return (
- <>
- <SearchInput
- className="mt-3"
- value={userSearchText}
- onChange={(v) => {
- setUserSearchText(v);
- }}
- loading={userSearchState.type === "loading"}
- onButtonClick={() => {
- if (userSearchText === "") {
- setUserSearchState({
- type: "error",
- data: "login.emptyUsername",
- });
- return;
- }
- setUserSearchState({ type: "loading" });
- getHttpSearchClient()
- .searchUsers(userSearchText)
- .then(
- (users) => {
- users = users.filter(
- (user) =>
- timeline.members.findIndex(
- (m) => m.username === user.username
- ) === -1 && timeline.owner.username !== user.username
- );
- setUserSearchState({ type: "users", data: users });
- },
- (e) => {
- setUserSearchState({
- type: "error",
- data: { type: "custom", value: String(e) },
- });
- }
- );
- }}
- />
- {(() => {
- if (userSearchState.type === "users") {
- const users = userSearchState.data;
- if (users.length === 0) {
- return <div>{t("timeline.member.noUserAvailableToAdd")}</div>;
- } else {
- return (
- <div className="mt-2">
- {users.map((user) => (
- <TimelineMemberItem
- key={user.username}
- user={user}
- add
- onAction={() => {
- void getHttpTimelineClient()
- .memberPut(
- timeline.owner.username,
- timeline.nameV2,
- user.username
- )
- .then(() => {
- setUserSearchText("");
- setUserSearchState({ type: "init" });
- onChange();
- });
- }}
- />
- ))}
- </div>
- );
- }
- } else if (userSearchState.type === "error") {
- return (
- <div className="cru-color-danger">
- {convertI18nText(userSearchState.data, t)}
- </div>
- );
- }
- })()}
- </>
- );
-};
-
-export interface TimelineMemberProps {
- timeline: HttpTimelineInfo;
- onChange: () => void;
-}
-
-const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
- const { timeline, onChange } = props;
- const members = [timeline.owner, ...timeline.members];
-
- return (
- <div className="container px-4 py-3">
- <div>
- {members.map((member, index) => (
- <TimelineMemberItem
- key={member.username}
- user={member}
- onAction={
- timeline.manageable && index !== 0
- ? () => {
- void getHttpTimelineClient()
- .memberDelete(
- timeline.owner.username,
- timeline.nameV2,
- member.username
- )
- .then(onChange);
- }
- : undefined
- }
- />
- ))}
- </div>
- {timeline.manageable ? (
- <TimelineMemberUserSearch timeline={timeline} onChange={onChange} />
- ) : null}
- </div>
- );
-};
-
-export default TimelineMember;
-
-export interface TimelineMemberDialogProps extends TimelineMemberProps {
- open: boolean;
- onClose: () => void;
-}
-
-export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = (
- props
-) => {
- return (
- <Dialog open={props.open} onClose={props.onClose}>
- <TimelineMember {...props} />
- </Dialog>
- );
-};
diff --git a/FrontEnd/src/views/timeline/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline/TimelinePostContentView.tsx
deleted file mode 100644
index 9ed192e5..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostContentView.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { marked } from "marked";
-
-import { UiLogicError } from "@/common";
-
-import { HttpNetworkError } from "@/http/common";
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
-
-import { useUser } from "@/services/user";
-
-import Skeleton from "../common/Skeleton";
-import LoadFailReload from "../common/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);
- }, [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/views/timeline/TimelinePostEdit.css b/FrontEnd/src/views/timeline/TimelinePostEdit.css
deleted file mode 100644
index 9b7629e2..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostEdit.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.timeline-post-edit {
- position: sticky !important;
- top: 106px;
- z-index: 100;
-}
-
-.timeline-post-edit-image {
- max-width: 100px;
- max-height: 100px;
-}
diff --git a/FrontEnd/src/views/timeline/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline/TimelinePostEdit.tsx
deleted file mode 100644
index 38e72264..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostEdit.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-
-import { UiLogicError } from "@/common";
-
-import {
- getHttpTimelineClient,
- HttpTimelineInfo,
- HttpTimelinePostInfo,
- HttpTimelinePostPostRequestData,
-} from "@/http/timeline";
-
-import { pushAlert } from "@/services/alert";
-
-import base64 from "@/utilities/base64";
-
-import BlobImage from "../common/BlobImage";
-import LoadingButton from "../common/button/LoadingButton";
-import PopupMenu from "../common/menu/PopupMenu";
-import MarkdownPostEdit from "./MarkdownPostEdit";
-import TimelinePostEditCard from "./TimelinePostEditCard";
-import IconButton from "../common/button/IconButton";
-
-import "./TimelinePostEdit.css";
-
-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 (
- <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, disabled } = props;
-
- const { t } = useTranslation();
-
- const [file, setFile] = React.useState<File | null>(null);
- const [error, setError] = React.useState<boolean>(false);
-
- const onInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
- setError(false);
- const files = e.target.files;
- if (files == null || files.length === 0) {
- setFile(null);
- onSelect(null);
- } else {
- setFile(files[0]);
- }
- };
-
- React.useEffect(() => {
- return () => {
- onSelect(null);
- };
- }, [onSelect]);
-
- return (
- <>
- <input
- type="file"
- onChange={onInputChange}
- accept="image/*"
- disabled={disabled}
- className="mx-3 my-1"
- />
- {file != null && !error && (
- <BlobImage
- blob={file}
- className="timeline-post-edit-image"
- onLoad={() => onSelect(file)}
- onError={() => {
- onSelect(null);
- setError(true);
- }}
- />
- )}
- {error ? <div className="text-danger">{t("loadImageError")}</div> : null}
- </>
- );
-};
-
-type PostKind = "text" | "markdown" | "image";
-
-const postKindIconMap: Record<PostKind, string> = {
- text: "fonts",
- markdown: "markdown",
- image: "image",
-};
-
-export interface TimelinePostEditProps {
- className?: string;
- style?: React.CSSProperties;
- timeline: HttpTimelineInfo;
- onPosted: (newPost: HttpTimelinePostInfo) => void;
-}
-
-const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
- const { timeline, style, className, onPosted } = props;
-
- const { t } = useTranslation();
-
- const [process, setProcess] = React.useState<boolean>(false);
-
- const [kind, setKind] = React.useState<Exclude<PostKind, "markdown">>("text");
- const [showMarkdown, setShowMarkdown] = React.useState<boolean>(false);
-
- const [text, setText] = React.useState<string>("");
- const [image, setImage] = React.useState<File | null>(null);
-
- const draftTextLocalStorageKey = `timeline.${timeline.owner.username}.${timeline.nameV2}.postDraft.text`;
-
- React.useEffect(() => {
- setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? "");
- }, [draftTextLocalStorageKey]);
-
- const canSend =
- (kind === "text" && text.length !== 0) ||
- (kind === "image" && image != null);
-
- const onPostError = (): void => {
- pushAlert({
- type: "danger",
- message: "timeline.sendPostFailed",
- });
- };
-
- const onSend = async (): Promise<void> => {
- setProcess(true);
-
- let requestData: HttpTimelinePostPostRequestData;
- switch (kind) {
- case "text":
- requestData = {
- contentType: "text/plain",
- data: await base64(text),
- };
- break;
- case "image":
- if (image == null) {
- throw new UiLogicError(
- "Content type is image but image blob is null.",
- );
- }
- requestData = {
- contentType: image.type,
- data: await base64(image),
- };
- break;
- default:
- throw new UiLogicError("Unknown content type.");
- }
-
- getHttpTimelineClient()
- .postPost(timeline.owner.username, timeline.nameV2, {
- dataList: [requestData],
- })
- .then(
- (data) => {
- if (kind === "text") {
- setText("");
- window.localStorage.removeItem(draftTextLocalStorageKey);
- }
- setProcess(false);
- setKind("text");
- onPosted(data);
- },
- () => {
- setProcess(false);
- onPostError();
- },
- );
- };
-
- return (
- <TimelinePostEditCard className={className} style={style}>
- {showMarkdown ? (
- <MarkdownPostEdit
- className="cru-fill-parent"
- onClose={() => setShowMarkdown(false)}
- owner={timeline.owner.username}
- timeline={timeline.nameV2}
- onPosted={onPosted}
- onPostError={onPostError}
- />
- ) : (
- <div className="row">
- <div className="col px-1 py-1">
- {(() => {
- if (kind === "text") {
- return (
- <TimelinePostEditText
- className="cru-fill-parent 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}
- />
- );
- }
- })()}
- </div>
- <div className="col col-auto align-self-end m-1">
- <div className="d-block cru-text-center mt-1 mb-2">
- <PopupMenu
- items={(["text", "image", "markdown"] as const).map((kind) => ({
- type: "button",
- text: `timeline.post.type.${kind}`,
- iconClassName: postKindIconMap[kind],
- onClick: () => {
- if (kind === "markdown") {
- setShowMarkdown(true);
- } else {
- setKind(kind);
- }
- },
- }))}
- >
- <IconButton large icon={postKindIconMap[kind]} />
- </PopupMenu>
- </div>
- <LoadingButton
- onClick={() => void onSend()}
- disabled={!canSend}
- loading={process}
- >
- {t("timeline.send")}
- </LoadingButton>
- </div>
- </div>
- )}
- </TimelinePostEditCard>
- );
-};
-
-export default TimelinePostEdit;
diff --git a/FrontEnd/src/views/timeline/TimelinePostEditCard.tsx b/FrontEnd/src/views/timeline/TimelinePostEditCard.tsx
deleted file mode 100644
index d2f7bd72..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostEditCard.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import Card from "../common/Card";
-import TimelineLine from "./TimelineLine";
-
-import "./TimelinePostEdit.css";
-
-export interface TimelinePostEditCardProps {
- className?: string;
- style?: React.CSSProperties;
- children?: React.ReactNode;
-}
-
-const TimelinePostEdit: React.FC<TimelinePostEditCardProps> = ({
- className,
- style,
- children,
-}) => {
- return (
- <div
- className={classnames("timeline-item timeline-post-edit", className)}
- style={style}
- >
- <TimelineLine center="node" />
- <Card className="timeline-item-card">{children}</Card>
- </div>
- );
-};
-
-export default TimelinePostEdit;
diff --git a/FrontEnd/src/views/timeline/TimelinePostEditNoLogin.tsx b/FrontEnd/src/views/timeline/TimelinePostEditNoLogin.tsx
deleted file mode 100644
index 1ef0a287..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostEditNoLogin.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from "react";
-import { Trans } from "react-i18next";
-import { Link } from "react-router-dom";
-
-import TimelinePostEditCard from "./TimelinePostEditCard";
-
-export default function TimelinePostEditNoLogin(): React.ReactElement | null {
- return (
- <TimelinePostEditCard>
- <div className="mt-3 mb-4">
- <Trans
- i18nKey="timeline.postNoLogin"
- components={{ l: <Link to="/login" /> }}
- />
- </div>
- </TimelinePostEditCard>
- );
-}
diff --git a/FrontEnd/src/views/timeline/TimelinePostListView.tsx b/FrontEnd/src/views/timeline/TimelinePostListView.tsx
deleted file mode 100644
index f878b004..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostListView.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Fragment } from "react";
-import * as React from "react";
-
-import { HttpTimelinePostInfo } from "@/http/timeline";
-
-import TimelinePostView from "./TimelinePostView";
-import TimelineDateLabel from "./TimelineDateLabel";
-
-function dateEqual(left: Date, right: Date): boolean {
- return (
- left.getDate() == right.getDate() &&
- left.getMonth() == right.getMonth() &&
- left.getFullYear() == right.getFullYear()
- );
-}
-
-export interface TimelinePostListViewProps {
- posts: HttpTimelinePostInfo[];
- onReload: () => void;
-}
-
-const TimelinePostListView: React.FC<TimelinePostListViewProps> = (props) => {
- const { posts, onReload } = props;
-
- const groupedPosts = React.useMemo<
- {
- date: Date;
- posts: (HttpTimelinePostInfo & { index: number })[];
- }[]
- >(() => {
- const result: {
- date: Date;
- posts: (HttpTimelinePostInfo & { index: number })[];
- }[] = [];
- let index = 0;
- for (const post of posts) {
- const time = new Date(post.time);
- if (result.length === 0) {
- result.push({ date: time, posts: [{ ...post, index }] });
- } else {
- const lastGroup = result[result.length - 1];
- if (dateEqual(lastGroup.date, time)) {
- lastGroup.posts.push({ ...post, index });
- } else {
- result.push({ date: time, posts: [{ ...post, index }] });
- }
- }
- index++;
- }
- return result;
- }, [posts]);
-
- return (
- <>
- {groupedPosts.map((group) => {
- return (
- <Fragment key={group.date.toDateString()}>
- <TimelineDateLabel date={group.date} />
- {group.posts.map((post) => {
- return (
- <TimelinePostView
- key={post.id}
- post={post}
- onChanged={onReload}
- onDeleted={onReload}
- />
- );
- })}
- </Fragment>
- );
- })}
- </>
- );
-};
-
-export default TimelinePostListView;
diff --git a/FrontEnd/src/views/timeline/TimelinePostView.tsx b/FrontEnd/src/views/timeline/TimelinePostView.tsx
deleted file mode 100644
index e3eac0f4..00000000
--- a/FrontEnd/src/views/timeline/TimelinePostView.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
-
-import { pushAlert } from "@/services/alert";
-
-import { useClickOutside } from "@/utilities/hooks";
-
-import UserAvatar from "../common/user/UserAvatar";
-import Card from "../common/Card";
-import FlatButton from "../common/button/FlatButton";
-import ConfirmDialog from "../common/dialog/ConfirmDialog";
-import TimelineLine from "./TimelineLine";
-import TimelinePostContentView from "./TimelinePostContentView";
-import PostPropertyChangeDialog from "./PostPropertyChangeDialog";
-import IconButton from "../common/button/IconButton";
-
-export interface TimelinePostViewProps {
- post: HttpTimelinePostInfo;
- className?: string;
- style?: React.CSSProperties;
- cardStyle?: React.CSSProperties;
- onChanged: (post: HttpTimelinePostInfo) => void;
- onDeleted: () => void;
-}
-
-const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
- const { post, className, style, cardStyle, onChanged, onDeleted } = props;
-
- const [operationMaskVisible, setOperationMaskVisible] =
- React.useState<boolean>(false);
- const [dialog, setDialog] = React.useState<
- "delete" | "changeproperty" | null
- >(null);
-
- const [maskElement, setMaskElement] = React.useState<HTMLElement | null>(
- null
- );
-
- useClickOutside(maskElement, () => setOperationMaskVisible(false));
-
- const cardRef = React.useRef<HTMLDivElement>(null);
- React.useEffect(() => {
- const cardIntersectionObserver = new IntersectionObserver(([e]) => {
- if (e.intersectionRatio > 0) {
- if (cardRef.current != null) {
- cardRef.current.style.animationName = "timeline-post-enter";
- }
- }
- });
- if (cardRef.current) {
- cardIntersectionObserver.observe(cardRef.current);
- }
-
- return () => {
- cardIntersectionObserver.disconnect();
- };
- }, []);
-
- return (
- <div
- id={`timeline-post-${post.id}`}
- className={classnames("timeline-item", className)}
- style={style}
- >
- <TimelineLine center="node" />
- <Card
- ref={cardRef}
- className="timeline-item-card enter-animation"
- style={cardStyle}
- >
- {post.editable ? (
- <IconButton
- icon="chevron-down"
- color="primary-enhance"
- className="cru-float-right"
- onClick={(e) => {
- setOperationMaskVisible(true);
- e.stopPropagation();
- }}
- />
- ) : null}
- <div className="timeline-item-header">
- <span className="me-2">
- <span>
- <UserAvatar
- username={post.author.username}
- className="timeline-avatar me-1"
- />
- <small className="text-dark me-2">{post.author.nickname}</small>
- <small className="text-secondary white-space-no-wrap">
- {new Date(post.time).toLocaleTimeString()}
- </small>
- </span>
- </span>
- </div>
- <div className="timeline-content">
- <TimelinePostContentView post={post} />
- </div>
- {operationMaskVisible ? (
- <div
- ref={setMaskElement}
- className="timeline-post-item-options-mask"
- onClick={() => {
- setOperationMaskVisible(false);
- }}
- >
- <FlatButton
- text="changeProperty"
- onClick={(e) => {
- setDialog("changeproperty");
- e.stopPropagation();
- }}
- />
- <FlatButton
- text="delete"
- color="danger"
- onClick={(e) => {
- setDialog("delete");
- e.stopPropagation();
- }}
- />
- </div>
- ) : null}
- </Card>
- <ConfirmDialog
- title="timeline.post.deleteDialog.title"
- body="timeline.post.deleteDialog.prompt"
- open={dialog === "delete"}
- onClose={() => {
- setDialog(null);
- setOperationMaskVisible(false);
- }}
- onConfirm={() => {
- void getHttpTimelineClient()
- .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id)
- .then(onDeleted, () => {
- pushAlert({
- type: "danger",
- message: "timeline.deletePostFailed",
- });
- });
- }}
- />
- <PostPropertyChangeDialog
- open={dialog === "changeproperty"}
- onClose={() => {
- setDialog(null);
- setOperationMaskVisible(false);
- }}
- post={post}
- onSuccess={onChanged}
- />
- </div>
- );
-};
-
-export default TimelinePostView;
diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
deleted file mode 100644
index bd5bef4c..00000000
--- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import * as React from "react";
-
-import {
- getHttpTimelineClient,
- HttpTimelineInfo,
- HttpTimelinePatchRequest,
- kTimelineVisibilities,
- TimelineVisibility,
-} from "@/http/timeline";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-
-export interface TimelinePropertyChangeDialogProps {
- open: boolean;
- close: () => void;
- timeline: HttpTimelineInfo;
- onChange: () => void;
-}
-
-const labelMap: { [key in TimelineVisibility]: string } = {
- Private: "timeline.visibility.private",
- Public: "timeline.visibility.public",
- Register: "timeline.visibility.register",
-};
-
-const TimelinePropertyChangeDialog: React.FC<
- TimelinePropertyChangeDialogProps
-> = (props) => {
- const { timeline, onChange } = props;
-
- return (
- <OperationDialog
- title={"timeline.dialogChangeProperty.title"}
- inputScheme={
- [
- {
- type: "text",
- label: "timeline.dialogChangeProperty.titleField",
- initValue: timeline.title,
- },
- {
- type: "select",
- label: "timeline.dialogChangeProperty.visibility",
- options: kTimelineVisibilities.map((v) => ({
- label: labelMap[v],
- value: v,
- })),
- initValue: timeline.visibility,
- },
- {
- type: "text",
- label: "timeline.dialogChangeProperty.description",
- initValue: timeline.description,
- },
- ] as const
- }
- open={props.open}
- onClose={props.close}
- onProcess={([newTitle, newVisibility, newDescription, newColor]) => {
- const req: HttpTimelinePatchRequest = {};
- if (newTitle !== timeline.title) {
- req.title = newTitle;
- }
- if (newVisibility !== timeline.visibility) {
- req.visibility = newVisibility as TimelineVisibility;
- }
- if (newDescription !== timeline.description) {
- req.description = newDescription;
- }
- const nc = newColor ?? "";
- if (nc !== timeline.color) {
- req.color = nc;
- }
- return getHttpTimelineClient()
- .patchTimeline(timeline.owner.username, timeline.nameV2, req)
- .then(onChange);
- }}
- />
- );
-};
-
-export default TimelinePropertyChangeDialog;
diff --git a/FrontEnd/src/views/timeline/index.tsx b/FrontEnd/src/views/timeline/index.tsx
deleted file mode 100644
index 1dffdcc1..00000000
--- a/FrontEnd/src/views/timeline/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from "react";
-import { useParams } from "react-router-dom";
-
-import { UiLogicError } from "@/common";
-
-import Timeline from "./Timeline";
-
-const TimelinePage: React.FC = () => {
- const { owner, timeline: timelineNameParam } = useParams();
-
- if (owner == null || owner == "")
- throw new UiLogicError("Route param owner is not set.");
-
- const timeline = timelineNameParam || "self";
-
- return (
- <div className="container">
- <Timeline timelineOwner={owner} timelineName={timeline} />
- </div>
- );
-};
-
-export default TimelinePage;