Newer
Older
taehui / qwilight-fe / src / site / SiteView.tsx
@Taehui Taehui on 13 Mar 10 KB v1.0.0
import {
  MutableRefObject,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { observer } from "mobx-react-lite";
import { Item, ItemParams, Menu, useContextMenu } from "react-contexify";
import { useTranslation } from "react-i18next";
import {
  Alert,
  Button,
  Col,
  Offcanvas,
  OffcanvasBody,
  OffcanvasHeader,
  Row,
} from "reactstrap";
import { isFirefox } from "react-device-detect";
import Swal from "sweetalert2";
import { toast } from "react-toastify";
import { sprintf } from "sprintf-js";
import { useTo } from "taehui-ts/fe-utility";
import { useQueryClient } from "@tanstack/react-query";

import { useSiteStore } from "src/Stores";
import SiteComponent from "src/site/SiteComponent";
import ConfigureWindow from "src/site/ConfigureWindow";
import SiteWindow from "src/site/SiteWindow";
import SiteYellItems from "src/site/SiteYellItems";
import AvatarItems from "src/site/AvatarItems";
import SiteInput from "src/site/SiteInput";
import SiteYellItem from "src/site/SiteYellItem";
import { getDefaultAvatarID } from "src/Utility";
import { SiteViewLoading } from "src/Loading";
import { OnSiteYellInput } from "src/site/Site";

import scss from "src/site/SiteView.module.scss";

const EventPB = require("src/Event_pb");

export default observer<{
  titleComponent: MutableRefObject<HTMLDivElement | null>;
  siteYellsView: MutableRefObject<HTMLDivElement | null>;
}>(({ titleComponent, siteYellsView }) => {
  const {
    avatars,
    siteNotify,
    isAvatarsOpened,
    isPendingSiteYellOpened,
    lastPendingSiteYell,
    targetSiteID,
    siteViews,
    siteAvatarID,
    siteYells,
    setSiteYellsViewLowest,
    getSiteView,
    onSiteIDModified,
    siteYellsViewMove,
    setPendingSiteYellOpened,
    setAvatarsOpened,
    isLoading,
    setSiteWindowOpened,
  } = useSiteStore();
  const { t } = useTranslation();
  const to = useTo();
  const { show: viewSiteNameInput } = useContextMenu({
    id: "siteName",
  });
  const { show: viewAvatarInput } = useContextMenu({
    id: "avatar",
  });
  const { show: viewSiteYellInput } = useContextMenu({
    id: "siteYell",
  });

  useEffect(() => {
    if (window.location.pathname === "/qwilight/site") {
      siteYellsViewMove(siteYellsView);
    }
  }, [siteYellsView, siteYellsViewMove]);

  const [siteYellsViewHeight, setSiteYellsViewHeight] = useState("");
  const siteViewsView = useRef<HTMLDivElement>(null);
  const inputComponent = useRef<HTMLDivElement>(null);

  const onViewAvatar = ({ props: { avatarID } }: ItemParams) => {
    if (avatarID.startsWith("*")) {
      toast.warning(t("notAvatarViewFault"));
    } else {
      to(
        `/qwilight/avatar/${encodeURIComponent("#")}${getDefaultAvatarID(
          avatarID,
        )}`,
      );
    }
  };

  useEffect(() => {
    window.Notification?.requestPermission();
  }, []);

  const onViewsModified = useCallback(() => {
    setSiteYellsViewHeight(
      `${
        document.documentElement.clientHeight -
        (titleComponent.current?.clientHeight ?? 0) -
        (siteViewsView.current?.clientHeight ?? 0) -
        (inputComponent.current?.clientHeight ?? 0) -
        (isFirefox ? 1 : 0)
      }px`,
    );
  }, [titleComponent]);

  useEffect(() => {
    onViewsModified();
  }, [onViewsModified, isLoading, avatars, lastPendingSiteYell]);

  useEffect(() => {
    const onModified = () => {
      onViewsModified();
      siteYellsViewMove(siteYellsView);
    };

    window.addEventListener("resize", onModified);

    return () => {
      window.removeEventListener("resize", onModified);
    };
  }, [onViewsModified, siteYellsView, siteYellsViewMove]);

  const onSiteYellsViewMove = useCallback(() => {
    const { current } = siteYellsView;
    if (current) {
      const siteYellID = siteYells[0]?.siteYellID;
      if (siteYellID && siteYellID > 0 && !current.scrollTop) {
        SiteComponent.send({
          eventID: EventPB.Event.EventID.GET_SITE_YELLS,
          text: JSON.stringify({
            siteID: targetSiteID,
            siteYellID: siteYellID,
          }),
        });
      }

      const isSiteYellsViewLowest =
        current.scrollTop + current.clientHeight >= current.scrollHeight;
      setSiteYellsViewLowest(isSiteYellsViewLowest);
      if (isSiteYellsViewLowest) {
        setPendingSiteYellOpened(false);
      }
    }
  }, [
    setPendingSiteYellOpened,
    setSiteYellsViewLowest,
    siteYells,
    siteYellsView,
    targetSiteID,
  ]);

  useEffect(() => {
    const { current } = siteYellsView;

    if (current) {
      current.addEventListener("scroll", onSiteYellsViewMove);

      return () => {
        current.removeEventListener("scroll", onSiteYellsViewMove);
      };
    }
  }, [onSiteYellsViewMove, isLoading, siteYellsView]);

  const isSiteHand = useMemo(
    () => getSiteView(targetSiteID)?.siteHand === siteAvatarID,
    [getSiteView, siteAvatarID, targetSiteID],
  );

  const queryClient = useQueryClient();

  if (isLoading) {
    return <SiteViewLoading />;
  }

  const onSiteYellInput: OnSiteYellInput = (event, avatarID) => {
    viewSiteYellInput({ event, props: { avatarID } });
  };

  return (
    <>
      <div ref={siteViewsView}>
        <Row className="g-0">
          {siteViews.map(({ siteID, siteName, isNew }) => {
            return (
              <Col className="m-1" xs="auto" key={siteID}>
                <Button
                  color={
                    isNew
                      ? "warning"
                      : siteID === targetSiteID
                        ? "primary"
                        : "secondary"
                  }
                  onClick={() => {
                    onSiteIDModified(siteID, siteYellsView);
                  }}
                  onContextMenu={(event) => {
                    event.preventDefault();
                    viewSiteNameInput({ event });
                  }}
                >
                  {siteName}
                </Button>
              </Col>
            );
          })}
          <Col className="m-1" xs="auto">
            <Button
              color="info"
              onClick={async () => {
                await queryClient.invalidateQueries({ queryKey: ["sites"] });
                setSiteWindowOpened(true);
              }}
            >
              {t("onSiteWindow")}
            </Button>
          </Col>
          {targetSiteID && (
            <Col className="m-1" xs="auto">
              <Button
                onClick={() => {
                  SiteComponent.send({
                    eventID: EventPB.Event.EventID.QUIT_SITE,
                    text: targetSiteID,
                  });
                }}
                color="danger"
              >
                {t("quitSite")}
              </Button>
            </Col>
          )}
        </Row>
      </div>

      <Row className="justify-content-center g-0">
        <Col className="m-1" xs="auto" style={{ position: "absolute" }}>
          <Alert isOpen={!!siteNotify} color="primary">
            <span>{siteNotify}</span>
          </Alert>
        </Col>
      </Row>

      <Row className="justify-content-center g-0 route">
        <Col className="m-1" xs="auto" style={{ position: "absolute" }}>
          <Alert
            isOpen={isPendingSiteYellOpened}
            onClick={() => {
              siteYellsViewMove(siteYellsView);
            }}
          >
            {lastPendingSiteYell && (
              <SiteYellItem
                data={lastPendingSiteYell}
                onSiteYellInput={onSiteYellInput}
              />
            )}
          </Alert>
        </Col>
      </Row>

      <div
        ref={siteYellsView}
        className={scss.siteView}
        style={{
          height: siteYellsViewHeight,
        }}
      >
        <SiteYellItems onSiteYellInput={onSiteYellInput} />
      </div>

      <Offcanvas
        direction="end"
        isOpen={isAvatarsOpened}
        toggle={() => setAvatarsOpened(false)}
      >
        <OffcanvasHeader toggle={() => setAvatarsOpened(false)}>
          {sprintf(t("avatarCountText"), avatars.length)}
        </OffcanvasHeader>
        <OffcanvasBody>
          <AvatarItems
            onAvatarInput={(event, avatarID) => {
              viewAvatarInput({ event, props: { avatarID } });
            }}
          />
        </OffcanvasBody>

        <Menu id="avatar">
          <Item
            disabled={!isSiteHand}
            onClick={({ props: { avatarID } }) => {
              SiteComponent.send({
                eventID: EventPB.Event.EventID.SET_SITE_OWNER,
                text: JSON.stringify({
                  siteID: targetSiteID,
                  avatarID,
                }),
              });
            }}
          >
            <span>{t("setSiteHand")}</span>
          </Item>
          <Item onClick={onViewAvatar}>
            <span>{t("viewAvatarView")}</span>
          </Item>
          <Item
            onClick={({ props: { avatarID } }) => {
              SiteComponent.send({
                eventID: EventPB.Event.EventID.NEW_SILENT_SITE,
                text: avatarID,
              });
            }}
          >
            <span>{t("silentSiteNew")}</span>
          </Item>
          <Item
            disabled={!isSiteHand}
            onClick={({ props: { avatarID } }) => {
              SiteComponent.send({
                eventID: EventPB.Event.EventID.EXILE_AVATAR,
                text: JSON.stringify({
                  siteID: targetSiteID,
                  avatarID,
                }),
              });
            }}
          >
            <span>{t("exileAvatar")}</span>
          </Item>
        </Menu>
      </Offcanvas>

      <Menu id="siteYell">
        <Item onClick={onViewAvatar}>
          <span>{t("viewAvatarView")}</span>
        </Item>
      </Menu>

      <Menu id="siteName">
        <Item
          disabled={!isSiteHand}
          onClick={async () => {
            const { isConfirmed, value } = await Swal.fire({
              title: t("setSiteNameText"),
              input: "text",
            });
            if (isConfirmed) {
              SiteComponent.send({
                eventID: EventPB.Event.EventID.SET_SITE_NAME,
                text: JSON.stringify({
                  siteID: targetSiteID,
                  siteName: value,
                }),
              });
            }
          }}
        >
          <span>{t("setSiteName")}</span>
        </Item>
      </Menu>

      <div ref={inputComponent}>
        <SiteInput />
      </div>

      <ConfigureWindow />
      <SiteWindow />
    </>
  );
});