diff options
Diffstat (limited to 'FrontEnd')
-rw-r--r-- | FrontEnd/src/utilities/useValueWithRef.ts | 11 | ||||
-rw-r--r-- | FrontEnd/src/views/timeline/Timeline.tsx | 216 | ||||
-rw-r--r-- | FrontEnd/src/views/timeline/TimelineCard.tsx | 16 | ||||
-rw-r--r-- | FrontEnd/src/views/timeline/index.css | 2 | ||||
-rw-r--r-- | FrontEnd/src/views/timeline/index.tsx | 72 |
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> ); }; |