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> -    </> -  ); -}  | 
