diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx')
-rw-r--r-- | Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx | 324 |
1 files changed, 0 insertions, 324 deletions
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx deleted file mode 100644 index e6514478..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import React, { CSSProperties } from "react"; -import { Spinner } from "reactstrap"; -import { useTranslation } from "react-i18next"; -import { fromEvent } from "rxjs"; -import Svg from "react-inlinesvg"; -import clsx from "clsx"; -import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; -import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; - -import { getAlertHost } from "../common/alert-service"; -import { useEventEmiiter, UiLogicError } from "../common"; -import { - TimelineInfo, - TimelinePostsWithSyncState, - timelineService, -} from "../data/timeline"; -import { userService } from "../data/user"; -import AppBar from "../common/AppBar"; - -import Timeline, { - TimelinePostInfoEx, - TimelineDeleteCallback, -} from "./Timeline"; -import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; - -type TimelinePostSyncState = "syncing" | "synced" | "offline"; - -const TimelinePostSyncStateBadge: React.FC<{ - state: TimelinePostSyncState; - style?: CSSProperties; - className?: string; -}> = ({ state, style, className }) => { - const { t } = useTranslation(); - - return ( - <div style={style} className={clsx("timeline-sync-state-badge", className)}> - {(() => { - switch (state) { - case "syncing": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-warning" /> - <span className="text-warning"> - {t("timeline.postSyncState.syncing")} - </span> - </> - ); - } - case "synced": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-success" /> - <span className="text-success"> - {t("timeline.postSyncState.synced")} - </span> - </> - ); - } - case "offline": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-danger" /> - <span className="text-danger"> - {t("timeline.postSyncState.offline")} - </span> - </> - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} - </div> - ); -}; - -export interface TimelineCardComponentProps<TManageItems> { - timeline: TimelineInfo; - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - className?: string; - onHeight?: (height: number) => void; -} - -export interface TimelinePageTemplateUIProps<TManageItems> { - avatarKey?: string | number; - timeline?: TimelineInfo; - postListState?: TimelinePostsWithSyncState; - CardComponent: React.ComponentType<TimelineCardComponentProps<TManageItems>>; - onMember: () => void; - onManage?: (item: TManageItems | "property") => void; - onPost?: TimelinePostSendCallback; - onDelete: TimelineDeleteCallback; - error?: string; -} - -export default function TimelinePageTemplateUI<TManageItems>( - props: TimelinePageTemplateUIProps<TManageItems> -): React.ReactElement | null { - const { timeline, postListState } = props; - - const { t } = useTranslation(); - - const bottomSpaceRef = React.useRef<HTMLDivElement | null>(null); - - const onPostEditHeightChange = React.useCallback((height: number): void => { - const { current: bottomSpaceDiv } = bottomSpaceRef; - if (bottomSpaceDiv != null) { - bottomSpaceDiv.style.height = `${height}px`; - } - 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 timelineRef = React.useRef<HTMLDivElement | null>(null); - - const [getResizeEvent, triggerResizeEvent] = useEventEmiiter(); - - React.useEffect(() => { - const { current: timelineElement } = timelineRef; - if (timelineElement != null) { - let loadingScrollToBottom = true; - let pinBottom = false; - - const isAtBottom = (): boolean => - window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight; - - const disableLoadingScrollToBottom = (): void => { - loadingScrollToBottom = false; - if (isAtBottom()) pinBottom = true; - }; - - const checkAndScrollToBottom = (): void => { - if (loadingScrollToBottom || pinBottom) { - window.scrollTo(0, document.body.scrollHeight); - } - }; - - const subscriptions = [ - fromEvent(timelineElement, "wheel").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "pointerdown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "keydown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(window, "scroll").subscribe(() => { - if (loadingScrollToBottom) return; - - if (isAtBottom()) { - pinBottom = true; - } else { - pinBottom = false; - } - }), - fromEvent(window, "resize").subscribe(checkAndScrollToBottom), - getResizeEvent().subscribe(checkAndScrollToBottom), - ]; - - return () => { - subscriptions.forEach((s) => s.unsubscribe()); - }; - } - }, [getResizeEvent, triggerResizeEvent, timeline, postListState]); - - const [cardHeight, setCardHeight] = React.useState<number>(0); - - const genCardCollapseLocalStorageKey = (uniqueId: string): string => - `timeline.${uniqueId}.cardCollapse`; - - const cardCollapseLocalStorageKey = - timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null; - - const [infoCardCollapse, setInfoCardCollapse] = React.useState<boolean>(true); - React.useEffect(() => { - if (cardCollapseLocalStorageKey != null) { - const savedCollapse = - window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; - setInfoCardCollapse(savedCollapse); - } - }, [cardCollapseLocalStorageKey]); - - let body: React.ReactElement; - - if (props.error != null) { - body = <p className="text-danger">{t(props.error)}</p>; - } else { - if (timeline != null) { - let timelineBody: React.ReactElement; - if (postListState != null) { - if (postListState.type === "notexist") { - throw new UiLogicError( - "Timeline is not null but post list state is notexist." - ); - } - if (postListState.type === "forbid") { - timelineBody = ( - <p className="text-danger">{t("timeline.messageCantSee")}</p> - ); - } else { - const posts: TimelinePostInfoEx[] = postListState.posts.map( - (post) => ({ - ...post, - deletable: timelineService.hasModifyPostPermission( - userService.currentUser, - timeline, - post - ), - }) - ); - - const topHeight: string = infoCardCollapse - ? "calc(68px + 1.5em)" - : `${cardHeight + 60}px`; - - const syncState: TimelinePostSyncState = postListState.syncing - ? "syncing" - : postListState.type === "synced" - ? "synced" - : "offline"; - - timelineBody = ( - <div> - <TimelinePostSyncStateBadge - style={{ top: topHeight }} - state={syncState} - /> - <Timeline - containerRef={timelineRef} - posts={posts} - onDelete={props.onDelete} - onResize={triggerResizeEvent} - /> - </div> - ); - if (props.onPost != null) { - timelineBody = ( - <> - {timelineBody} - <div ref={bottomSpaceRef} className="flex-fix-length" /> - <TimelinePostEdit - onPost={props.onPost} - onHeightChange={onPostEditHeightChange} - timelineUniqueId={timeline.uniqueId} - /> - </> - ); - } - } - } else { - timelineBody = ( - <div className="full-viewport-center-child"> - <Spinner color="primary" type="grow" /> - </div> - ); - } - const { CardComponent } = props; - - body = ( - <> - <div - className="fixed-top mt-appbar info-card-container" - data-collapse={infoCardCollapse ? "true" : "false"} - > - <Svg - src={ - infoCardCollapse - ? arrowsAngleExpandIcon - : arrowsAngleContractIcon - } - onClick={() => { - const newState = !infoCardCollapse; - setInfoCardCollapse(newState); - window.localStorage.setItem( - genCardCollapseLocalStorageKey(timeline.uniqueId), - newState.toString() - ); - }} - className="float-right m-1 info-card-collapse-button text-primary icon-button" - /> - <CardComponent - timeline={timeline} - onManage={props.onManage} - onMember={props.onMember} - onHeight={setCardHeight} - className="info-card-content" - /> - </div> - {timelineBody} - </> - ); - } else { - body = ( - <div className="full-viewport-center-child"> - <Spinner color="primary" type="grow" /> - </div> - ); - } - } - - return ( - <> - <AppBar /> - <div> - <div - style={{ height: 56 + cardHeight }} - className="timeline-page-top-space flex-fix-length" - /> - {body} - </div> - </> - ); -} |