aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/utilities/useValueWithRef.ts11
-rw-r--r--FrontEnd/src/views/timeline-common/ConnectionStatusBadge.css36
-rw-r--r--FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx2
-rw-r--r--FrontEnd/src/views/timeline-common/Timeline.tsx47
-rw-r--r--FrontEnd/src/views/timeline-common/TimelineEmptyItem.tsx25
-rw-r--r--FrontEnd/src/views/timeline-common/TimelineLoading.tsx4
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx85
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostListView.tsx4
-rw-r--r--FrontEnd/src/views/timeline-common/TimelineTop.tsx27
-rw-r--r--FrontEnd/src/views/timeline-common/index.css46
10 files changed, 112 insertions, 175 deletions
diff --git a/FrontEnd/src/utilities/useValueWithRef.ts b/FrontEnd/src/utilities/useValueWithRef.ts
new file mode 100644
index 00000000..8c5f2039
--- /dev/null
+++ b/FrontEnd/src/utilities/useValueWithRef.ts
@@ -0,0 +1,11 @@
+import React from "react";
+
+export default function useValueWithRef<T>(
+ value: T
+): React.MutableRefObject<T> {
+ const ref = React.useRef<T>(value);
+ React.useEffect(() => {
+ ref.current = value;
+ }, [value]);
+ return ref;
+}
diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.css b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.css
new file mode 100644
index 00000000..7fe83b9b
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.css
@@ -0,0 +1,36 @@
+.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-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
index df43d8d2..c8478557 100644
--- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
+++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
@@ -3,6 +3,8 @@ import classnames from "classnames";
import { HubConnectionState } from "@microsoft/signalr";
import { useTranslation } from "react-i18next";
+import "./ConnectionStatusBadge.css";
+
export interface ConnectionStatusBadgeProps {
status: HubConnectionState;
className?: string;
diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx
index c632badc..3aed2445 100644
--- a/FrontEnd/src/views/timeline-common/Timeline.tsx
+++ b/FrontEnd/src/views/timeline-common/Timeline.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { HubConnectionState } from "@microsoft/signalr";
+import classnames from "classnames";
import {
HttpForbiddenError,
@@ -14,19 +15,22 @@ import {
import { getTimelinePostUpdate$ } from "@/services/timeline";
+import useValueWithRef from "@/utilities/useValueWithRef";
+
import TimelinePagedPostListView from "./TimelinePagedPostListView";
-import TimelineTop from "./TimelineTop";
+import TimelineEmptyItem from "./TimelineEmptyItem";
import TimelineLoading from "./TimelineLoading";
+import TimelinePostEdit from "./TimelinePostEdit";
import "./index.css";
-import TimelinePostEdit from "./TimelinePostEdit";
export interface TimelineProps {
className?: string;
style?: React.CSSProperties;
- timelineName?: string;
+ timelineName: string;
reloadKey: number;
onReload: () => void;
+ onTimelineLoaded?: (timeline: HttpTimelineInfo) => void;
onConnectionStateChanged?: (state: HubConnectionState) => void;
}
@@ -45,19 +49,11 @@ const Timeline: React.FC<TimelineProps> = (props) => {
setPosts([]);
}, [timelineName]);
- const onReload = React.useRef<() => void>(props.onReload);
-
- React.useEffect(() => {
- onReload.current = props.onReload;
- }, [props.onReload]);
-
- const onConnectionStateChanged = React.useRef<
- ((state: HubConnectionState) => void) | null
- >(null);
-
- React.useEffect(() => {
- onConnectionStateChanged.current = props.onConnectionStateChanged ?? null;
- }, [props.onConnectionStateChanged]);
+ const onReload = useValueWithRef(props.onReload);
+ const onTimelineLoaded = useValueWithRef(props.onTimelineLoaded);
+ const onConnectionStateChanged = useValueWithRef(
+ props.onConnectionStateChanged
+ );
React.useEffect(() => {
if (timelineName != null && state === "loaded") {
@@ -74,7 +70,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
subscription.unsubscribe();
};
}
- }, [timelineName, state]);
+ }, [timelineName, state, onReload, onConnectionStateChanged]);
React.useEffect(() => {
if (timelineName != null) {
@@ -90,6 +86,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
setTimeline(t);
setPosts(p);
setState("loaded");
+ onTimelineLoaded.current?.(t);
}
},
(error) => {
@@ -112,7 +109,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
subscribe = false;
};
}
- }, [timelineName, reloadKey]);
+ }, [timelineName, reloadKey, onTimelineLoaded]);
switch (state) {
case "loading":
@@ -143,8 +140,8 @@ const Timeline: React.FC<TimelineProps> = (props) => {
);
default:
return (
- <>
- <TimelineTop height={40} />
+ <div style={style} className={classnames("timeline", className)}>
+ <TimelineEmptyItem height={40} />
<TimelinePagedPostListView
posts={posts}
onReload={onReload.current}
@@ -152,15 +149,9 @@ const Timeline: React.FC<TimelineProps> = (props) => {
{timeline?.postable ? (
<TimelinePostEdit timeline={timeline} onPosted={onReload.current} />
) : (
- <TimelineTop
- lineProps={{
- startSegmentLength: 20,
- center: "none",
- current: true,
- }}
- />
+ <TimelineEmptyItem startSegmentLength={20} center="none" current />
)}
- </>
+ </div>
);
}
};
diff --git a/FrontEnd/src/views/timeline-common/TimelineEmptyItem.tsx b/FrontEnd/src/views/timeline-common/TimelineEmptyItem.tsx
new file mode 100644
index 00000000..8638ad46
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/TimelineEmptyItem.tsx
@@ -0,0 +1,25 @@
+import 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-common/TimelineLoading.tsx b/FrontEnd/src/views/timeline-common/TimelineLoading.tsx
index fc42f4b4..57402811 100644
--- a/FrontEnd/src/views/timeline-common/TimelineLoading.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelineLoading.tsx
@@ -1,10 +1,10 @@
import React from "react";
-import TimelineTop from "./TimelineTop";
+import TimelineEmptyItem from "./TimelineEmptyItem";
const TimelineLoading: React.FC = () => {
return (
- <TimelineTop
+ <TimelineEmptyItem
className="timeline-top-loading-enter"
height={100}
lineProps={{
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
index 9b9ebbc2..4a56346a 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
@@ -1,19 +1,15 @@
import React from "react";
-import { useTranslation } from "react-i18next";
import { Container } from "react-bootstrap";
import { HubConnectionState } from "@microsoft/signalr";
-import { HttpNetworkError, HttpNotFoundError } from "@/http/common";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import { getAlertHost } from "@/services/alert";
-
-import Timeline from "./Timeline";
-import TimelinePostEdit from "./TimelinePostEdit";
+import { HttpTimelineInfo } from "@/http/timeline";
import useReverseScrollPositionRemember from "@/utilities/useReverseScrollPositionRemember";
+
import { generatePalette, setPalette } from "@/palette";
+import Timeline from "./Timeline";
+
export interface TimelinePageCardProps {
timeline: HttpTimelineInfo;
collapse: boolean;
@@ -34,11 +30,6 @@ export interface TimelinePageTemplateProps {
const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {
const { timelineName, reloadKey, onReload, CardComponent } = props;
- const { t } = useTranslation();
-
- const [state, setState] = React.useState<
- "loading" | "done" | "offline" | "notexist" | "error"
- >("loading");
const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
const [connectionStatus, setConnectionStatus] =
@@ -47,52 +38,11 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {
useReverseScrollPositionRemember();
React.useEffect(() => {
- setState("loading");
- setTimeline(null);
- }, [timelineName]);
-
- React.useEffect(() => {
- let subscribe = true;
- void getHttpTimelineClient()
- .getTimeline(timelineName)
- .then(
- (data) => {
- if (subscribe) {
- setState("done");
- setTimeline(data);
- }
- },
- (error) => {
- if (subscribe) {
- if (error instanceof HttpNetworkError) {
- setState("offline");
- } else if (error instanceof HttpNotFoundError) {
- setState("notexist");
- } else {
- console.error(error);
- setState("error");
- }
- setTimeline(null);
- }
- }
- );
- return () => {
- subscribe = false;
- };
- }, [timelineName, reloadKey]);
-
- React.useEffect(() => {
if (timeline != null && timeline.color != null) {
return setPalette(generatePalette({ primary: timeline.color }));
}
}, [timeline]);
- const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0);
-
- const reloadTimeline = (): void => {
- setTimelineReloadKey((old) => old + 1);
- };
-
const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`;
const [cardCollapse, setCardCollapse] = React.useState<boolean>(true);
@@ -126,26 +76,13 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {
/>
) : null}
<Container className="px-0">
- {(() => {
- if (state === "offline") {
- // TODO: i18n
- return <p className="text-danger">Offline!</p>;
- } else if (state === "notexist") {
- return <p className="text-danger">{t(props.notFoundI18nKey)}</p>;
- } else if (state === "error") {
- // TODO: i18n
- return <p className="text-danger">Error!</p>;
- } else {
- return (
- <Timeline
- timelineName={timeline?.name}
- reloadKey={timelineReloadKey}
- onReload={reloadTimeline}
- onConnectionStateChanged={setConnectionStatus}
- />
- );
- }
- })()}
+ <Timeline
+ timelineName={timelineName}
+ reloadKey={reloadKey}
+ onReload={onReload}
+ onTimelineLoaded={(t) => setTimeline(t)}
+ onConnectionStateChanged={setConnectionStatus}
+ />
</Container>
</>
);
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
index 3213f76d..43c61ea8 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
@@ -53,7 +53,7 @@ const TimelinePostListView: React.FC<TimelinePostListViewProps> = (props) => {
}, [posts]);
return (
- <div style={style} className={classnames("timeline", className)}>
+ <>
{groupedPosts.map((group) => {
return (
<Fragment key={group.date.toDateString()}>
@@ -71,7 +71,7 @@ const TimelinePostListView: React.FC<TimelinePostListViewProps> = (props) => {
</Fragment>
);
})}
- </div>
+ </>
);
};
diff --git a/FrontEnd/src/views/timeline-common/TimelineTop.tsx b/FrontEnd/src/views/timeline-common/TimelineTop.tsx
deleted file mode 100644
index dabbdf1e..00000000
--- a/FrontEnd/src/views/timeline-common/TimelineTop.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-import TimelineLine, { TimelineLineProps } from "./TimelineLine";
-
-export interface TimelineTopProps {
- height?: number | string;
- lineProps?: TimelineLineProps;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const TimelineTop: React.FC<TimelineTopProps> = (props) => {
- const { height, style, className } = props;
- const lineProps = props.lineProps ?? { center: "none" };
-
- return (
- <div
- style={{ ...style, height: height }}
- className={classnames("timeline-top", className)}
- >
- <TimelineLine {...lineProps} />
- </div>
- );
-};
-
-export default TimelineTop;
diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css
index 297e6156..6a5a6407 100644
--- a/FrontEnd/src/views/timeline-common/index.css
+++ b/FrontEnd/src/views/timeline-common/index.css
@@ -133,20 +133,15 @@
animation-name: timeline-line-node-loading;
}
-.timeline-item.timeline-post-edit {
- padding-bottom: 0;
-}
-
-.timeline-top {
- position: relative;
- text-align: right;
-}
-
.timeline-item {
position: relative;
padding: 0.5em;
}
+.timeline-item.timeline-post-edit {
+ padding-bottom: 0;
+}
+
.timeline-item-card {
position: relative;
padding: 0.3em 0.5em 1em 4em;
@@ -268,36 +263,3 @@
right: 10px;
top: 2px;
}
-
-.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;
-}