aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd')
-rw-r--r--FrontEnd/src/utilities/useValueWithRef.ts11
-rw-r--r--FrontEnd/src/views/timeline/Timeline.tsx216
-rw-r--r--FrontEnd/src/views/timeline/TimelineCard.tsx16
-rw-r--r--FrontEnd/src/views/timeline/index.css2
-rw-r--r--FrontEnd/src/views/timeline/index.tsx72
5 files changed, 134 insertions, 183 deletions
diff --git a/FrontEnd/src/utilities/useValueWithRef.ts b/FrontEnd/src/utilities/useValueWithRef.ts
deleted file mode 100644
index 8c5f2039..00000000
--- a/FrontEnd/src/utilities/useValueWithRef.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-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/Timeline.tsx b/FrontEnd/src/views/timeline/Timeline.tsx
index e8b1147f..6399b6bc 100644
--- a/FrontEnd/src/views/timeline/Timeline.tsx
+++ b/FrontEnd/src/views/timeline/Timeline.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { HubConnectionState } from "@microsoft/signalr";
import classnames from "classnames";
+import { HubConnectionState } from "@microsoft/signalr";
import {
HttpForbiddenError,
@@ -13,16 +13,15 @@ import {
HttpTimelinePostInfo,
} from "@/http/timeline";
-import { getTimelinePostUpdate$ } from "@/services/timeline";
import { useUser } from "@/services/user";
-
-import useValueWithRef from "@/utilities/useValueWithRef";
+import { getTimelinePostUpdate$ } from "@/services/timeline";
import TimelinePagedPostListView from "./TimelinePagedPostListView";
import TimelineEmptyItem from "./TimelineEmptyItem";
import TimelineLoading from "./TimelineLoading";
import TimelinePostEdit from "./TimelinePostEdit";
import TimelinePostEditNoLogin from "./TimelinePostEditNoLogin";
+import TimelineCard from "./TimelineCard";
import "./index.css";
@@ -31,143 +30,170 @@ export interface TimelineProps {
style?: React.CSSProperties;
timelineOwner: string;
timelineName: string;
- reloadKey: number;
- onReload: () => void;
- onTimelineLoaded?: (timeline: HttpTimelineInfo) => void;
- onConnectionStateChanged?: (state: HubConnectionState) => void;
}
const Timeline: React.FC<TimelineProps> = (props) => {
- const { timelineOwner, timelineName, className, style, reloadKey } = props;
+ const { timelineOwner, timelineName, className, style } = props;
const user = useUser();
- const [state, setState] = React.useState<
- "loading" | "loaded" | "offline" | "notexist" | "forbid" | "error"
- >("loading");
const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
- const [posts, setPosts] = React.useState<HttpTimelinePostInfo[]>([]);
+ 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 [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(() => {
- setState("loading");
setTimeline(null);
- setPosts([]);
- }, [timelineName]);
-
- const onReload = useValueWithRef(props.onReload);
- const onTimelineLoaded = useValueWithRef(props.onTimelineLoaded);
- const onConnectionStateChanged = useValueWithRef(
- props.onConnectionStateChanged
- );
+ setPosts(null);
+ setError(null);
+ setSignalrState(HubConnectionState.Connecting);
+ }, [timelineOwner, timelineName]);
React.useEffect(() => {
- if (timelineName != null && state === "loaded") {
- const timelinePostUpdate$ = getTimelinePostUpdate$(
- timelineOwner,
- timelineName
- );
- const subscription = timelinePostUpdate$.subscribe(
- ({ update, state }) => {
- if (update) {
- onReload.current();
+ if (timelineName != null) {
+ let subscribe = true;
+
+ getHttpTimelineClient()
+ .getTimeline(timelineOwner, timelineName)
+ .then(
+ (t) => {
+ if (subscribe) {
+ setTimeline(t);
+ }
+ },
+ (error) => {
+ if (subscribe) {
+ 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");
+ }
+ }
}
- onConnectionStateChanged.current?.(state);
- }
- );
+ );
+
return () => {
- subscription.unsubscribe();
+ subscribe = false;
};
}
- }, [timelineOwner, timelineName, state, onReload, onConnectionStateChanged]);
+ }, [timelineOwner, timelineName, timelineReloadKey]);
React.useEffect(() => {
- if (timelineName != null) {
- let subscribe = true;
-
- const client = getHttpTimelineClient();
- Promise.all([
- client.getTimeline(timelineOwner, timelineName),
- client.listPost(timelineOwner, timelineName),
- ]).then(
- ([t, p]) => {
+ let subscribe = true;
+ void getHttpTimelineClient()
+ .listPost(timelineOwner, timelineName)
+ .then(
+ (ps) => {
if (subscribe) {
- setTimeline(t);
setPosts(
- p.items.filter(
+ ps.items.filter(
(p): p is HttpTimelinePostInfo => p.deleted === false
)
);
- setState("loaded");
- onTimelineLoaded.current?.(t);
}
},
(error) => {
if (subscribe) {
if (error instanceof HttpNetworkError) {
- setState("offline");
+ setError("offline");
} else if (error instanceof HttpForbiddenError) {
- setState("forbid");
+ setError("forbid");
} else if (error instanceof HttpNotFoundError) {
- setState("notexist");
+ setError("notfound");
} else {
console.error(error);
- setState("error");
+ setError("error");
}
}
}
);
+ return () => {
+ subscribe = false;
+ };
+ }, [timelineOwner, timelineName, postsReloadKey]);
- return () => {
- subscribe = false;
- };
- }
- }, [timelineOwner, timelineName, reloadKey, onTimelineLoaded]);
-
- switch (state) {
- case "loading":
- return <TimelineLoading />;
- case "offline":
- return (
- <div className={className} style={style}>
- Offline.
- </div>
- );
- case "notexist":
- return (
- <div className={className} style={style}>
- Not exist.
- </div>
- );
- case "forbid":
- return (
- <div className={className} style={style}>
- Forbid.
- </div>
- );
- case "error":
- return (
- <div className={className} style={style}>
- Error.
- </div>
- );
- default:
- return (
+ 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]);
+
+ 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 height={40} />
- <TimelinePagedPostListView
- posts={posts}
- onReload={onReload.current}
- />
+ <TimelinePagedPostListView posts={posts} onReload={updatePosts} />
{timeline?.postable ? (
- <TimelinePostEdit timeline={timeline} onPosted={onReload.current} />
+ <TimelinePostEdit timeline={timeline} onPosted={updatePosts} />
) : user == null ? (
<TimelinePostEditNoLogin />
) : (
<TimelineEmptyItem startSegmentLength={20} center="none" current />
)}
</div>
- );
- }
+ )}
+ </>
+ );
};
export default Timeline;
diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx
index 872ad6d3..08eae3e0 100644
--- a/FrontEnd/src/views/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/views/timeline/TimelineCard.tsx
@@ -22,22 +22,13 @@ import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
export interface TimelinePageCardProps {
timeline: HttpTimelineInfo;
- collapse: boolean;
- toggleCollapse: () => void;
connectionStatus: HubConnectionState;
className?: string;
onReload: () => void;
}
const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
- const {
- timeline,
- collapse,
- toggleCollapse,
- connectionStatus,
- onReload,
- className,
- } = props;
+ const { timeline, connectionStatus, onReload, className } = props;
const { t } = useTranslation();
@@ -45,6 +36,11 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
"member" | "property" | "delete" | null
>(null);
+ const [collapse, setCollapse] = React.useState(false);
+ const toggleCollapse = (): void => {
+ setCollapse((o) => !o);
+ };
+
const isSmallScreen = useIsSmallScreen();
const user = useUser();
diff --git a/FrontEnd/src/views/timeline/index.css b/FrontEnd/src/views/timeline/index.css
index 6929f9ae..fa3542dd 100644
--- a/FrontEnd/src/views/timeline/index.css
+++ b/FrontEnd/src/views/timeline/index.css
@@ -237,7 +237,7 @@
margin-right: 0.6em;
}
-.timeline-template-card {
+.timeline-card {
position: fixed;
z-index: 1029;
top: 56px;
diff --git a/FrontEnd/src/views/timeline/index.tsx b/FrontEnd/src/views/timeline/index.tsx
index 65bb90f6..cb9fb46f 100644
--- a/FrontEnd/src/views/timeline/index.tsx
+++ b/FrontEnd/src/views/timeline/index.tsx
@@ -1,82 +1,22 @@
import React from "react";
-import { HubConnectionState } from "@microsoft/signalr";
import { useParams } from "react-router-dom";
import { UiLogicError } from "@/common";
-import { HttpTimelineInfo } from "@/http/timeline";
-import { generatePalette, setPalette } from "@/palette";
import Timeline from "./Timeline";
-import TimelineCard from "./TimelineCard";
const TimelinePage: React.FC = () => {
- const { owner: ownerUsername, timeline: timelineNameParam } = useParams();
+ const { owner, timeline: timelineNameParam } = useParams();
- if (ownerUsername == null || ownerUsername == "")
+ if (owner == null || owner == "")
throw new UiLogicError("Route param owner is not set.");
- const timelineName =
- timelineNameParam == null || timelineNameParam === ""
- ? "self"
- : timelineNameParam;
-
- const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
-
- const [reloadKey, setReloadKey] = React.useState<number>(0);
- const reload = (): void => setReloadKey(reloadKey + 1);
-
- const [connectionStatus, setConnectionStatus] =
- React.useState<HubConnectionState>(HubConnectionState.Connecting);
-
- React.useEffect(() => {
- if (timeline != null && timeline.color != null) {
- return setPalette(generatePalette({ primary: timeline.color }));
- }
- }, [timeline]);
-
- const cardCollapseLocalStorageKey = `timeline.${ownerUsername}.${timelineName}.cardCollapse`;
-
- const [cardCollapse, setCardCollapse] = React.useState<boolean>(true);
-
- React.useEffect(() => {
- const savedCollapse = window.localStorage.getItem(
- cardCollapseLocalStorageKey
- );
- setCardCollapse(savedCollapse == null ? true : savedCollapse === "true");
- }, [cardCollapseLocalStorageKey]);
-
- const toggleCardCollapse = (): void => {
- const newState = !cardCollapse;
- setCardCollapse(newState);
- window.localStorage.setItem(
- cardCollapseLocalStorageKey,
- newState.toString()
- );
- };
+ const timeline = timelineNameParam || "self";
return (
- <>
- {timeline != null ? (
- <TimelineCard
- className="timeline-template-card"
- timeline={timeline}
- collapse={cardCollapse}
- toggleCollapse={toggleCardCollapse}
- onReload={reload}
- connectionStatus={connectionStatus}
- />
- ) : null}
- <div className="container">
- <Timeline
- timelineOwner={ownerUsername}
- timelineName={timelineName}
- reloadKey={reloadKey}
- onReload={reload}
- onTimelineLoaded={(t) => setTimeline(t)}
- onConnectionStateChanged={setConnectionStatus}
- />
- </div>
- </>
+ <div className="container">
+ <Timeline timelineOwner={owner} timelineName={timeline} />
+ </div>
);
};