diff options
Diffstat (limited to 'FrontEnd/src')
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; -} |