DisplaySwitcher.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import React, { useCallback, useEffect, useMemo } from 'react';
  2. import { pagePathUtils } from '@growi/core';
  3. import { useTranslation } from 'next-i18next';
  4. import dynamic from 'next/dynamic';
  5. import { Link } from 'react-scroll';
  6. import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
  7. import { SocketEventName } from '~/interfaces/websocket';
  8. import {
  9. useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
  10. } from '~/stores/context';
  11. import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
  12. import { useDescendantsPageListModal } from '~/stores/modal';
  13. import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
  14. import {
  15. useSetRemoteLatestPageData,
  16. } from '~/stores/remote-latest-page';
  17. import { EditorMode, useEditorMode } from '~/stores/ui';
  18. import { useGlobalSocket } from '~/stores/websocket';
  19. import CountBadge from '../Common/CountBadge';
  20. import { ContentLinkButtonsProps } from '../ContentLinkButtons';
  21. import CustomTabContent from '../CustomNavigation/CustomTabContent';
  22. import PageListIcon from '../Icons/PageListIcon';
  23. import { Page } from '../Page';
  24. import TableOfContents from '../TableOfContents';
  25. import { UserInfoProps } from '../User/UserInfo';
  26. import styles from './DisplaySwitcher.module.scss';
  27. const { isTopPage, isUsersHomePage } = pagePathUtils;
  28. const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
  29. const PageEditorByHackmd = dynamic(() => import('../PageEditorByHackmd').then(mod => mod.PageEditorByHackmd), { ssr: false });
  30. const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
  31. const HashChanged = dynamic(() => import('../EventListeneres/HashChanged'), { ssr: false });
  32. const ContentLinkButtons = dynamic<ContentLinkButtonsProps>(() => import('../ContentLinkButtons').then(mod => mod.ContentLinkButtons), { ssr: false });
  33. const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
  34. const UserInfo = dynamic<UserInfoProps>(() => import('../User/UserInfo').then(mod => mod.UserInfo), { ssr: false });
  35. const PageView = React.memo((): JSX.Element => {
  36. const { t } = useTranslation();
  37. const { data: currentPagePath } = useCurrentPagePath();
  38. const { data: isSharedUser } = useIsSharedUser();
  39. const { data: shareLinkId } = useShareLinkId();
  40. const { data: isNotFound } = useIsNotFound();
  41. const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
  42. const { open: openDescendantPageListModal } = useDescendantsPageListModal();
  43. const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
  44. const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
  45. const isTopPagePath = isTopPage(currentPagePath ?? '');
  46. const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
  47. const { data: socket } = useGlobalSocket();
  48. const setLatestRemotePageData = useCallback((data) => {
  49. const { s2cMessagePageUpdated } = data;
  50. const remoteData = {
  51. remoteRevisionId: s2cMessagePageUpdated.revisionId,
  52. remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
  53. remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
  54. remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
  55. revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
  56. hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
  57. };
  58. setRemoteLatestPageData(remoteData);
  59. }, [setRemoteLatestPageData]);
  60. const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
  61. const { s2cMessagePageUpdated } = data;
  62. if (s2cMessagePageUpdated.pageId === currentPage?._id) {
  63. mutateIsHackmdDraftUpdatingInRealtime(true);
  64. }
  65. }, [currentPage?._id, mutateIsHackmdDraftUpdatingInRealtime]);
  66. // listen socket for someone updating this page
  67. useEffect(() => {
  68. if (socket == null) { return }
  69. socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
  70. return () => {
  71. socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
  72. };
  73. }, [setLatestRemotePageData, socket]);
  74. // listen socket for hackmd saved
  75. useEffect(() => {
  76. if (socket == null) { return }
  77. socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
  78. return () => {
  79. socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
  80. };
  81. }, [setIsHackmdDraftUpdatingInRealtime, socket]);
  82. return (
  83. <div className="d-flex flex-column flex-lg-row">
  84. <div className="flex-grow-1 flex-basis-0 mw-0">
  85. { isUsersHomePagePath && <UserInfo author={currentPage?.creator} /> }
  86. { !isNotFound && <Page /> }
  87. { isNotFound && <NotFoundPage /> }
  88. </div>
  89. { !isNotFound && (
  90. <div className="grw-side-contents-container">
  91. <div className="grw-side-contents-sticky-container">
  92. {/* Page list */}
  93. <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
  94. { currentPagePath != null && !isSharedUser && (
  95. <button
  96. type="button"
  97. className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
  98. onClick={() => openDescendantPageListModal(currentPagePath)}
  99. data-testid="pageListButton"
  100. >
  101. <div className="grw-page-accessories-control-icon">
  102. <PageListIcon />
  103. </div>
  104. {t('page_list')}
  105. <CountBadge count={currentPage?.descendantCount} offset={1} />
  106. </button>
  107. ) }
  108. </div>
  109. {/* Comments */}
  110. { !isTopPagePath && (
  111. <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
  112. <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
  113. <button
  114. type="button"
  115. className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
  116. >
  117. <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
  118. <span>Comments</span>
  119. <CountBadge count={currentPage?.commentCount} />
  120. </button>
  121. </Link>
  122. </div>
  123. ) }
  124. <div className="d-none d-lg-block">
  125. <TableOfContents />
  126. { isUsersHomePagePath && <ContentLinkButtons author={currentPage?.creator} /> }
  127. </div>
  128. </div>
  129. </div>
  130. ) }
  131. </div>
  132. );
  133. });
  134. PageView.displayName = 'PageView';
  135. const DisplaySwitcher = React.memo((): JSX.Element => {
  136. const { data: isEditable } = useIsEditable();
  137. const { data: editorMode = EditorMode.View } = useEditorMode();
  138. const isViewMode = editorMode === EditorMode.View;
  139. const navTabMapping = useMemo(() => {
  140. return {
  141. [EditorMode.View]: {
  142. Content: () => (
  143. <div data-testid="page-view" id="page-view">
  144. <PageView />
  145. </div>
  146. ),
  147. },
  148. [EditorMode.Editor]: {
  149. Content: () => (
  150. isEditable
  151. ? (
  152. <div data-testid="page-editor" id="page-editor">
  153. <PageEditor />
  154. </div>
  155. )
  156. : <></>
  157. ),
  158. },
  159. [EditorMode.HackMD]: {
  160. Content: () => (
  161. isEditable
  162. ? (
  163. <div id="page-editor-with-hackmd">
  164. <PageEditorByHackmd />
  165. </div>
  166. )
  167. : <></>
  168. ),
  169. },
  170. };
  171. }, [isEditable]);
  172. return (
  173. <>
  174. <CustomTabContent activeTab={editorMode} navTabMapping={navTabMapping} />
  175. { isEditable && !isViewMode && <EditorNavbarBottom /> }
  176. { isEditable && <HashChanged></HashChanged> }
  177. </>
  178. );
  179. });
  180. DisplaySwitcher.displayName = 'DisplaySwitcher';
  181. export default DisplaySwitcher;