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 (
{(() => { switch (state) { case "syncing": { return ( <> {t("timeline.postSyncState.syncing")} ); } case "synced": { return ( <> {t("timeline.postSyncState.synced")} ); } case "offline": { return ( <> {t("timeline.postSyncState.offline")} ); } default: throw new UiLogicError("Unknown sync state."); } })()}
); }; export interface TimelineCardComponentProps { timeline: TimelineInfo; onManage?: (item: TManageItems | "property") => void; onMember: () => void; className?: string; onHeight?: (height: number) => void; } export interface TimelinePageTemplateUIProps { avatarKey?: string | number; timeline?: TimelineInfo; postListState?: TimelinePostsWithSyncState; CardComponent: React.ComponentType>; onMember: () => void; onManage?: (item: TManageItems | "property") => void; onPost?: TimelinePostSendCallback; onDelete: TimelineDeleteCallback; error?: string; } export default function TimelinePageTemplateUI( props: TimelinePageTemplateUIProps ): React.ReactElement | null { const { timeline, postListState } = props; const { t } = useTranslation(); const bottomSpaceRef = React.useRef(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(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(0); const genCardCollapseLocalStorageKey = (uniqueId: string): string => `timeline.${uniqueId}.cardCollapse`; const cardCollapseLocalStorageKey = timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null; const [infoCardCollapse, setInfoCardCollapse] = React.useState(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 =

{t(props.error)}

; } 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 = (

{t("timeline.messageCantSee")}

); } 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 = (
); if (props.onPost != null) { timelineBody = ( <> {timelineBody}
); } } } else { timelineBody = (
); } const { CardComponent } = props; body = ( <>
{ 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" />
{timelineBody} ); } else { body = (
); } } return ( <>
{body}
); }