Просмотр исходного кода

Merge branch 'feat/gw7907-compare-page-versions-on-history-tab' into feat/gw7908-compare-all-page-versions

jam411 3 лет назад
Родитель
Сommit
ace261b318

+ 1 - 6
packages/app/src/client/services/user-ui-settings.ts

@@ -1,11 +1,9 @@
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
 import { AxiosResponse } from 'axios';
-
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { useIsGuestUser } from '~/stores/context';
 
 
 let settingsForBulk: Partial<IUserUISettings> = {};
 let settingsForBulk: Partial<IUserUISettings> = {};
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
@@ -33,11 +31,8 @@ type UserUISettingsUtil = {
   scheduleToPut: ScheduleToPutFunction | (() => void),
   scheduleToPut: ScheduleToPutFunction | (() => void),
 }
 }
 export const useUserUISettings = (): UserUISettingsUtil => {
 export const useUserUISettings = (): UserUISettingsUtil => {
-  const { data: isGuestUser } = useIsGuestUser();
 
 
   return {
   return {
-    scheduleToPut: isGuestUser
-      ? () => {}
-      : scheduleToPut,
+    scheduleToPut,
   };
   };
 };
 };

+ 44 - 39
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -25,7 +25,7 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
   const { isAuthenticated } = props;
   const { isAuthenticated } = props;
 
 
   const {
   const {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
   } = useNextThemes();
   } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
@@ -114,55 +114,60 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
 
         {/* sidebar mode */}
         {/* sidebar mode */}
         {renderSidebarModeSwitch(false)}
         {renderSidebarModeSwitch(false)}
-        {dropdownDivider}
 
 
         {/* side bar mode on editor */}
         {/* side bar mode on editor */}
         {isAuthenticated && (
         {isAuthenticated && (
           <>
           <>
-            {renderSidebarModeSwitch(true)}
             {dropdownDivider}
             {dropdownDivider}
+            {renderSidebarModeSwitch(true)}
           </>
           </>
         )}
         )}
 
 
         {/* color mode */}
         {/* color mode */}
-        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-        <form className="px-4">
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto d-flex align-items-center">
-              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <SunIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swUserPreference"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={isDarkMode}
-                  disabled={useOsSettings}
-                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+        { !isForcedByGrowiTheme && (
+          <>
+            {dropdownDivider}
+            <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
+            <form className="px-4">
+              <div className="form-row justify-content-center">
+                <div className="form-group col-auto d-flex align-items-center">
+                  <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <SunIcon />
+                  </IconWithTooltip>
+                  <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                    <input
+                      id="swUserPreference"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={isDarkMode}
+                      disabled={useOsSettings}
+                      onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label" htmlFor="swUserPreference"></label>
+                  </div>
+                  <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <MoonIcon />
+                  </IconWithTooltip>
+                </div>
               </div>
               </div>
-              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <MoonIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-          <div className="form-row">
-            <div className="form-group col-auto">
-              <div className="custom-control custom-checkbox">
-                <input
-                  id="cbFollowOs"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={useOsSettings}
-                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+              <div className="form-row">
+                <div className="form-group col-auto">
+                  <div className="custom-control custom-checkbox">
+                    <input
+                      id="cbFollowOs"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={useOsSettings}
+                      onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+                  </div>
+                </div>
               </div>
               </div>
-            </div>
-          </div>
-        </form>
+            </form>
+          </>
+        ) }
+
       </div>
       </div>
 
 
     </>
     </>

+ 3 - 3
packages/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -18,21 +18,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
       if (pageData.grant === 2) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')} only</strong>
+            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 4) {
       if (pageData.grant === 4) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-lock"></i><strong>{t('Only me')} only</strong>
+            <i className="icon-fw icon-lock"></i><strong>{t('Only me')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 5) {
       if (pageData.grant === 5) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name} only</strong>
+            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name}</strong>
           </>
           </>
         );
         );
       }
       }

+ 21 - 8
packages/app/src/components/PagePresentationModal.tsx

@@ -1,13 +1,13 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import type { PresentationProps } from '@growi/presentation';
 import type { PresentationProps } from '@growi/presentation';
+import { useFullScreen } from '@growi/ui';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import {
 import {
   Modal, ModalBody,
   Modal, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-
 import { usePagePresentationModal } from '~/stores/modal';
 import { usePagePresentationModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
 import { usePresentationViewOptions } from '~/stores/renderer';
@@ -30,13 +30,26 @@ const PagePresentationModal = (): JSX.Element => {
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
 
 
   const { isDarkMode } = useNextThemes();
   const { isDarkMode } = useNextThemes();
+  const fullscreen = useFullScreen();
 
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: rendererOptions } = usePresentationViewOptions();
   const { data: rendererOptions } = usePresentationViewOptions();
 
 
-  const requestFullscreen = useCallback(() => {
-    document.documentElement.requestFullscreen();
-  }, []);
+  const toggleFullscreenHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    else {
+      fullscreen.enter();
+    }
+  }, [fullscreen]);
+
+  const closeHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    closePresentationModal();
+  }, [fullscreen, closePresentationModal]);
 
 
   const isOpen = presentationModalData?.isOpened ?? false;
   const isOpen = presentationModalData?.isOpened ?? false;
 
 
@@ -49,15 +62,15 @@ const PagePresentationModal = (): JSX.Element => {
   return (
   return (
     <Modal
     <Modal
       isOpen={isOpen}
       isOpen={isOpen}
-      toggle={closePresentationModal}
+      toggle={closeHandler}
       data-testid="page-presentation-modal"
       data-testid="page-presentation-modal"
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
     >
     >
       <div className="grw-presentation-controls d-flex">
       <div className="grw-presentation-controls d-flex">
-        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={requestFullscreen}>
-          <i className="ti ti-fullscreen" aria-hidden></i>
+        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={toggleFullscreenHandler}>
+          <i className={`${fullscreen.active ? 'icon-size-actual' : 'icon-size-fullscreen'}`} aria-hidden></i>
         </button>
         </button>
-        <button className="close btn-close" type="button" aria-label="close" onClick={closePresentationModal}>
+        <button className="close btn-close" type="button" aria-label="close" onClick={closeHandler}>
           <i className="ti ti-close" aria-hidden></i>
           <i className="ti ti-close" aria-hidden></i>
         </button>
         </button>
       </div>
       </div>

+ 2 - 0
packages/app/src/interfaces/crowi-request.ts

@@ -9,6 +9,8 @@ export interface CrowiRequest<U extends IUser = IUserHasId> extends Request {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   crowi: any,
   crowi: any,
 
 
+  session: any,
+
   // provided by csurf
   // provided by csurf
   csrfToken: () => string,
   csrfToken: () => string,
 
 

+ 26 - 29
packages/app/src/pages/[[...path]].page.tsx

@@ -10,7 +10,7 @@ import type {
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
 } from '@growi/core';
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -31,39 +31,36 @@ import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
+import {
+  useCurrentUser,
+  useIsLatestRevision,
+  useIsForbidden, useIsNotFound, useIsSharedUser,
+  useIsEnabledStaleNotification, useIsIdenticalPath,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
+  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
+  useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useIsSlackConfigured, useRendererConfig,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
+} from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import {
-  useSelectedGrant,
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
+import { useSelectedGrant } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
-import {
-  useCurrentUser,
-  useIsLatestRevision,
-  useIsForbidden, useIsNotFound, useIsSharedUser,
-  useIsEnabledStaleNotification, useIsIdenticalPath,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
-  useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
-  useIsSlackConfigured, useRendererConfig,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
-} from '../stores/context';
 
 
-import { NextPageWithLayout } from './_app.page';
+import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
@@ -73,6 +70,7 @@ declare global {
 }
 }
 
 
 
 
+const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
@@ -185,16 +183,15 @@ type Props = CommonProps & {
 };
 };
 
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
 const Page: NextPageWithLayout<Props> = (props: Props) => {
-  // const { t } = useTranslation();
-  const router = useRouter();
-
-  const { data: currentUser } = useCurrentUser(props.currentUser ?? null);
-
   // register global EventEmitter
   // register global EventEmitter
   if (isClient() && window.globalEmitter == null) {
   if (isClient() && window.globalEmitter == null) {
     window.globalEmitter = new EventEmitter();
     window.globalEmitter = new EventEmitter();
   }
   }
 
 
+  const router = useRouter();
+
+  useCurrentUser(props.currentUser ?? null);
+
   // commons
   // commons
   useEditorConfig(props.editorConfig);
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
@@ -241,7 +238,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
 
 
-  const { pageWithMeta, userUISettings } = props;
+  const { pageWithMeta } = props;
 
 
   const pageId = pageWithMeta?.data._id;
   const pageId = pageWithMeta?.data._id;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
@@ -466,9 +463,9 @@ async function injectUserUISettings(context: GetServerSidePropsContext, props: P
   const { user } = req;
   const { user } = req;
   const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
 
 
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+  const userUISettings = user == null ? req.session.uiSettings : await UserUISettings.findOne({ user: user._id }).exec();
   if (userUISettings != null) {
   if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
+    props.userUISettings = userUISettings.toObject?.() ?? userUISettings;
   }
   }
 }
 }
 
 
@@ -530,7 +527,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
   const {
   const {
-    appService, searchService, configManager, aclService, slackNotificationService, mailService,
+    searchService, configManager, aclService,
   } = crowi;
   } = crowi;
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;

+ 8 - 2
packages/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:routes:apiv3:user-ui-settings');
 const router = express.Router();
 const router = express.Router();
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
 
   const validatorForPut = [
   const validatorForPut = [
     body('settings').exists().withMessage('The body param \'settings\' is required'),
     body('settings').exists().withMessage('The body param \'settings\' is required'),
@@ -25,7 +24,7 @@ module.exports = (crowi) => {
   ];
   ];
 
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  router.put('/', loginRequiredStrictly, validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
+  router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
     const { user } = req;
     const { user } = req;
     const { settings } = req.body;
     const { settings } = req.body;
 
 
@@ -37,6 +36,13 @@ module.exports = (crowi) => {
       preferDrawerModeByUser: settings.preferDrawerModeByUser,
       preferDrawerModeByUser: settings.preferDrawerModeByUser,
       preferDrawerModeOnEditByUser: settings.preferDrawerModeOnEditByUser,
       preferDrawerModeOnEditByUser: settings.preferDrawerModeOnEditByUser,
     };
     };
+
+    if (user == null) {
+      req.session.uiSettings = updateData;
+      return res.apiv3(updateData);
+    }
+
+
     // remove the keys that have null value
     // remove the keys that have null value
     Object.keys(updateData).forEach((key) => {
     Object.keys(updateData).forEach((key) => {
       if (updateData[key] == null) {
       if (updateData[key] == null) {

+ 4 - 9
packages/app/src/stores/ui.tsx

@@ -22,10 +22,9 @@ import type { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
-  useCurrentPageId, useIsEditable, useIsGuestUser,
+  useCurrentPageId, useIsEditable,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
 } from './context';
 } from './context';
-import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
@@ -235,18 +234,14 @@ type PreferDrawerModeByUserUtils = {
 }
 }
 
 
 export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
 export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
-  const { data: isGuestUser } = useIsGuestUser();
   const { scheduleToPut } = useUserUISettings();
   const { scheduleToPut } = useUserUISettings();
 
 
-  const swrResponse: SWRResponse<boolean, Error> = useStaticSWR('preferDrawerModeByUser', initialData, { use: isGuestUser ? [localStorageMiddleware] : [] });
+  const swrResponse: SWRResponse<boolean, Error> = useStaticSWR('preferDrawerModeByUser', initialData);
 
 
   const utils: PreferDrawerModeByUserUtils = {
   const utils: PreferDrawerModeByUserUtils = {
     update: (preferDrawerMode: boolean) => {
     update: (preferDrawerMode: boolean) => {
       swrResponse.mutate(preferDrawerMode);
       swrResponse.mutate(preferDrawerMode);
-
-      if (!isGuestUser) {
-        scheduleToPut({ preferDrawerModeByUser: preferDrawerMode });
-      }
+      scheduleToPut({ preferDrawerModeByUser: preferDrawerMode });
     },
     },
   };
   };
 
 
@@ -292,7 +287,7 @@ export const useDrawerMode = (): SWRResponse<boolean, Error> => {
   };
   };
 
 
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
-  const isEditModeWithPreferDrawerMode = editorMode === EditorMode.Editor && preferDrawerModeOnEditByUser;
+  const isEditModeWithPreferDrawerMode = editorMode !== EditorMode.View && preferDrawerModeOnEditByUser;
   const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
   const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
 
 
   return useSWRImmutable(
   return useSWRImmutable(

+ 7 - 2
packages/app/src/stores/use-next-themes.tsx

@@ -24,17 +24,22 @@ type UseThemeExtendedProps = Omit<UseThemeProps, 'theme'|'resolvedTheme'> & {
   resolvedTheme: ColorScheme,
   resolvedTheme: ColorScheme,
   useOsSettings: boolean,
   useOsSettings: boolean,
   isDarkMode: boolean,
   isDarkMode: boolean,
+  isForcedByGrowiTheme: boolean,
   resolvedThemeByAttributes?: ColorScheme,
   resolvedThemeByAttributes?: ColorScheme,
 }
 }
 
 
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
   const props = useTheme();
   const props = useTheme();
+  const { data: forcedColorScheme } = useForcedColorScheme();
+
+  const resolvedTheme = forcedColorScheme ?? props.resolvedTheme as ColorScheme;
 
 
   return Object.assign(props, {
   return Object.assign(props, {
     theme: props.theme as Themes,
     theme: props.theme as Themes,
-    resolvedTheme: props.resolvedTheme as ColorScheme,
+    resolvedTheme,
     useOsSettings: props.theme === Themes.SYSTEM,
     useOsSettings: props.theme === Themes.SYSTEM,
-    isDarkMode: props.resolvedTheme === ColorScheme.DARK,
+    isDarkMode: resolvedTheme === ColorScheme.DARK,
+    isForcedByGrowiTheme: forcedColorScheme != null,
     resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ColorScheme : undefined,
     resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ColorScheme : undefined,
   });
   });
 };
 };

+ 9 - 3
packages/presentation/src/components/Slides.tsx

@@ -32,9 +32,15 @@ type Props = {
 
 
 export const Slides = (props: Props): JSX.Element => {
 export const Slides = (props: Props): JSX.Element => {
   const { options, children } = props;
   const { options, children } = props;
-  const { rendererOptions, isDarkMode } = options;
-
-  rendererOptions.remarkPlugins?.push([extractSections.remarkPlugin, { isDarkMode }]);
+  const { rendererOptions, isDarkMode, disableSeparationByHeader } = options;
+
+  rendererOptions.remarkPlugins?.push([
+    extractSections.remarkPlugin,
+    {
+      isDarkMode,
+      disableSeparationByHeader,
+    },
+  ]);
 
 
   const { css } = marp.render('', { htmlAsArray: true });
   const { css } = marp.render('', { htmlAsArray: true });
 
 

+ 1 - 0
packages/presentation/src/consts/index.ts

@@ -5,4 +5,5 @@ export type PresentationOptions = {
   rendererOptions: ReactMarkdownOptions,
   rendererOptions: ReactMarkdownOptions,
   revealOptions?: RevealOptions,
   revealOptions?: RevealOptions,
   isDarkMode?: boolean,
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 }

+ 19 - 4
packages/presentation/src/services/renderer/extract-sections.ts

@@ -37,33 +37,48 @@ function removeElement(parentNode: Parent, elem: Node): void {
 
 
 export type ExtractSectionsPluginParams = {
 export type ExtractSectionsPluginParams = {
   isDarkMode?: boolean,
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 }
 
 
 export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
 export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
-  const { isDarkMode } = options;
+  const { isDarkMode, disableSeparationByHeader } = options;
+
+  const startCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type !== 'thematicBreak';
+  };
+  const endCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type === 'thematicBreak';
+  };
 
 
   return (tree) => {
   return (tree) => {
     // wrap with <section>
     // wrap with <section>
     visit(
     visit(
       tree,
       tree,
-      node => node.type !== 'thematicBreak',
+      startCondition,
       (node, index, parent: Parent) => {
       (node, index, parent: Parent) => {
         if (parent == null || parent.type !== 'root') {
         if (parent == null || parent.type !== 'root') {
           return;
           return;
         }
         }
 
 
         const startElem = node;
         const startElem = node;
-        const endElem = findAfter(parent, startElem, node => node.type === 'thematicBreak');
+        const endElem = findAfter(parent, startElem, endCondition);
 
 
         wrapWithSection(parent, startElem, endElem, isDarkMode);
         wrapWithSection(parent, startElem, endElem, isDarkMode);
 
 
         // remove <hr>
         // remove <hr>
-        if (endElem != null) {
+        if (endElem != null && endElem.type === 'thematicBreak') {
           removeElement(parent, endElem);
           removeElement(parent, endElem);
         }
         }
       },
       },
     );
     );
   };
   };
+
 };
 };
 
 
 
 

+ 2 - 1
packages/ui/src/index.ts

@@ -3,7 +3,8 @@ export * from './interfaces/breakpoints';
 export * from './components/Attachment/Attachment';
 export * from './components/Attachment/Attachment';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/PagePath/PagePathLabel';
+export * from './components/SearchPage/FootstampIcon';
 export * from './components/User/UserPicture';
 export * from './components/User/UserPicture';
 
 
 export * from './utils/browser-utils';
 export * from './utils/browser-utils';
-export * from './components/SearchPage/FootstampIcon';
+export * from './utils/use-fullscreen';

+ 50 - 0
packages/ui/src/utils/use-fullscreen.ts

@@ -0,0 +1,50 @@
+import {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+export interface FullScreenHandle {
+  active: boolean;
+  enter: () => Promise<void>;
+  exit: () => Promise<void>;
+}
+
+export const useFullScreen = (): FullScreenHandle => {
+  const [active, setActive] = useState(false);
+
+  useEffect(() => {
+    const handleChange = () => {
+      setActive(document.fullscreenElement != null);
+    };
+
+    document.addEventListener('fullscreenchange', handleChange);
+    return function cleanup() {
+      document.removeEventListener('fullscreenchange', handleChange);
+    };
+  }, []);
+
+  const enter = useCallback((elem?: HTMLElement) => {
+    if (document.fullscreenElement != null) {
+      return Promise.resolve();
+    }
+
+    const targetElem = elem ?? document.documentElement;
+    return targetElem.requestFullscreen();
+  }, []);
+
+  const exit = useCallback(() => {
+    if (document.fullscreenElement == null) {
+      return Promise.resolve();
+    }
+
+    return document.exitFullscreen();
+  }, []);
+
+  return useMemo(
+    () => ({
+      active,
+      enter,
+      exit,
+    }),
+    [active, enter, exit],
+  );
+};