diff options
| author | crupest <crupest@outlook.com> | 2020-08-25 00:05:01 +0800 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-25 00:05:01 +0800 | 
| commit | 738a17688ca481db62755295f8b732200ca12511 (patch) | |
| tree | 034b470eff54a9f725cd9e472966cf554cd2ee26 /Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx | |
| parent | b9b7334ac5d5cf83b6206af024ddd4c55a6e07da (diff) | |
| parent | fe687f82d9c5af2869b679222bec11b1a00b9ee9 (diff) | |
| download | timeline-738a17688ca481db62755295f8b732200ca12511.tar.gz timeline-738a17688ca481db62755295f8b732200ca12511.tar.bz2 timeline-738a17688ca481db62755295f8b732200ca12511.zip  | |
Merge pull request #154 from crupest/toolchain
Upgrade toolchain of front end.
Diffstat (limited to 'Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx')
| -rw-r--r-- | Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx | 649 | 
1 files changed, 324 insertions, 325 deletions
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx index 18b3323d..e6514478 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx @@ -1,325 +1,324 @@ -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 Timeline, {
 -  TimelinePostInfoEx,
 -  TimelineDeleteCallback,
 -} from './Timeline';
 -import AppBar from '../common/AppBar';
 -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>
 -    </>
 -  );
 -}
 +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> +    </> +  ); +}  | 
