aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx')
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx213
1 files changed, 133 insertions, 80 deletions
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
index 92eb0887..3087c20e 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
@@ -1,33 +1,39 @@
import React from "react";
-
-import { UiLogicError } from "@/common";
+import { useTranslation } from "react-i18next";
+import { Spinner } from "react-bootstrap";
import { HttpNetworkError, HttpNotFoundError } from "@/http/common";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import { TimelineMemberDialog } from "./TimelineMember";
-import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
-import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI";
-
-export interface TimelinePageTemplateProps<TManageItem> {
- name: string;
- onManage: (item: TManageItem) => void;
- UiComponent: React.ComponentType<
- Omit<TimelinePageTemplateUIProps<TManageItem>, "CardComponent">
- >;
+import {
+ getHttpTimelineClient,
+ HttpTimelineInfo,
+ HttpTimelinePostInfo,
+} from "@/http/timeline";
+
+import { getAlertHost } from "@/services/alert";
+
+import Timeline from "./Timeline";
+import TimelinePostEdit from "./TimelinePostEdit";
+
+export interface TimelinePageCardProps {
+ timeline: HttpTimelineInfo;
+ collapse: boolean;
+ toggleCollapse: () => void;
+ className?: string;
+ onReload: () => void;
+}
+
+export interface TimelinePageTemplateProps {
+ timelineName: string;
notFoundI18nKey: string;
reloadKey: number;
onReload: () => void;
+ CardComponent: React.ComponentType<TimelinePageCardProps>;
}
-export default function TimelinePageTemplate<TManageItem>(
- props: TimelinePageTemplateProps<TManageItem>
-): React.ReactElement | null {
- const { name, reloadKey, onReload } = props;
+const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {
+ const { timelineName, reloadKey, onReload, CardComponent } = props;
- const [dialog, setDialog] = React.useState<null | "property" | "member">(
- null
- );
+ const { t } = useTranslation();
const [timeline, setTimeline] = React.useState<
HttpTimelineInfo | "loading" | "offline" | "notexist" | "error"
@@ -38,7 +44,7 @@ export default function TimelinePageTemplate<TManageItem>(
let subscribe = true;
void getHttpTimelineClient()
- .getTimeline(name)
+ .getTimeline(timelineName)
.then(
(data) => {
if (subscribe) {
@@ -61,70 +67,117 @@ export default function TimelinePageTemplate<TManageItem>(
return () => {
subscribe = false;
};
- }, [name, reloadKey]);
+ }, [timelineName, reloadKey]);
- let dialogElement: React.ReactElement | undefined;
- const closeDialog = (): void => setDialog(null);
+ const scrollToBottom = React.useCallback(() => {
+ window.scrollTo(0, document.body.scrollHeight);
+ }, []);
- if (dialog === "property") {
- if (typeof timeline !== "object") {
- throw new UiLogicError(
- "Timeline is null but attempt to open change property dialog."
- );
+ const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0);
+
+ const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0);
+
+ const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]);
+
+ const reloadTimeline = (): void => {
+ setTimelineReloadKey((old) => old + 1);
+ setNewPosts([]);
+ };
+
+ const onPostEditHeightChange = React.useCallback((height: number): void => {
+ setBottomSpaceHeight(height);
+ if (height === 0) {
+ const alertHost = getAlertHost();
+ if (alertHost != null) {
+ alertHost.style.removeProperty("margin-bottom");
+ }
+ } else {
+ const alertHost = getAlertHost();
+ if (alertHost != null) {
+ alertHost.style.marginBottom = `${height}px`;
+ }
}
+ }, []);
+
+ const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`;
- dialogElement = (
- <TimelinePropertyChangeDialog
- open
- close={closeDialog}
- timeline={timeline}
- onChange={onReload}
- />
+ const [cardCollapse, setCardCollapse] = React.useState<boolean>(true);
+ React.useEffect(() => {
+ const savedCollapse =
+ window.localStorage.getItem(cardCollapseLocalStorageKey) === "true";
+ setCardCollapse(savedCollapse);
+ }, [cardCollapseLocalStorageKey]);
+
+ const toggleCardCollapse = (): void => {
+ const newState = !cardCollapse;
+ setCardCollapse(newState);
+ window.localStorage.setItem(
+ cardCollapseLocalStorageKey,
+ newState.toString()
);
- } else if (dialog === "member") {
- if (typeof timeline !== "object") {
- throw new UiLogicError(
- "Timeline is null but attempt to open change property dialog."
- );
- }
+ };
+
+ let body: React.ReactElement;
- dialogElement = (
- <TimelineMemberDialog
- open
- onClose={closeDialog}
- timeline={timeline}
- onChange={onReload}
- />
+ if (timeline == "loading") {
+ body = (
+ <div className="full-viewport-center-child">
+ <Spinner variant="primary" animation="grow" />
+ </div>
+ );
+ } else if (timeline === "offline") {
+ // TODO: i18n
+ body = <p className="text-danger">Offline!</p>;
+ } else if (timeline === "notexist") {
+ body = <p className="text-danger">{t(props.notFoundI18nKey)}</p>;
+ } else if (timeline === "error") {
+ // TODO: i18n
+ body = <p className="text-danger">Error!</p>;
+ } else {
+ body = (
+ <>
+ <CardComponent
+ className="timeline-template-card"
+ timeline={timeline}
+ collapse={cardCollapse}
+ toggleCollapse={toggleCardCollapse}
+ onReload={onReload}
+ />
+ <div
+ className="timeline-container"
+ style={{
+ minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`,
+ }}
+ >
+ <Timeline
+ top={40}
+ timelineName={timeline.name}
+ reloadKey={timelineReloadKey}
+ onReload={reloadTimeline}
+ additionalPosts={newPosts}
+ onLoad={scrollToBottom}
+ />
+ </div>
+ {timeline.postable ? (
+ <>
+ <div
+ style={{ height: bottomSpaceHeight }}
+ className="flex-fix-length"
+ />
+ <TimelinePostEdit
+ className="fixed-bottom"
+ timeline={timeline}
+ onHeightChange={onPostEditHeightChange}
+ onPosted={(newPost) => {
+ setNewPosts((old) => [...old, newPost]);
+ }}
+ />
+ </>
+ ) : null}
+ </>
);
}
+ return body;
+};
- const { UiComponent } = props;
-
- return (
- <>
- <UiComponent
- timeline={
- typeof timeline === "object"
- ? {
- ...timeline,
- operations: {
- onManage: timeline.manageable
- ? (item) => {
- if (item === "property") {
- setDialog(item);
- } else {
- props.onManage(item);
- }
- }
- : undefined,
- onMember: () => setDialog("member"),
- },
- }
- : timeline
- }
- notExistMessageI18nKey={props.notFoundI18nKey}
- />
- {dialogElement}
- </>
- );
-}
+export default TimelinePageTemplate;