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

Merge pull request #6977 from weseek/support/108172-ForUpdatedAlert

support: 108172 for updated alert
Yuki Takei 3 лет назад
Родитель
Сommit
f6a84900dc

+ 28 - 1
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
@@ -6,12 +6,15 @@ import dynamic from 'next/dynamic';
 import { Link } from 'react-scroll';
 
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
+import { SocketEventName } from '~/interfaces/websocket';
 import {
   useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
+import { useGlobalSocket } from '~/stores/websocket';
 
 import CountBadge from '../Common/CountBadge';
 import { ContentLinkButtonsProps } from '../ContentLinkButtons';
@@ -44,10 +47,34 @@ const PageView = React.memo((): JSX.Element => {
   const { data: isNotFound } = useIsNotFound();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
 
   const isTopPagePath = isTopPage(currentPagePath ?? '');
   const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
 
+  const { data: socket } = useGlobalSocket();
+
+  const setLatestRemotePageData = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+
+    mutateRemoteRevisionId(s2cMessagePageUpdated.revisionId);
+    mutateRemoteRevisionLastUpdateUser(s2cMessagePageUpdated.remoteLastUpdateUser);
+  }, [mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser]);
+
+  // listen socket for someone updating this page
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
+
+    return () => {
+      socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
+    };
+
+  }, [setLatestRemotePageData, socket]);
+
   return (
     <div className="d-flex flex-column flex-lg-row">
 

+ 27 - 1
packages/app/src/components/PageEditor.tsx

@@ -15,13 +15,15 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { IEditorMethods } from '~/interfaces/editor-methods';
+import { SocketEventName } from '~/interfaces/websocket';
 import {
   useCurrentPathname, useCurrentPageId,
-  useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown, useIsNotFound,
+  useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useIsNotFound, useEditingMarkdown,
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
+  useIsConflict,
 } from '~/stores/editor';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
@@ -29,6 +31,7 @@ import {
   EditorMode,
   useEditorMode, useIsMobile, useSelectedGrant,
 } from '~/stores/ui';
+import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
 
@@ -84,10 +87,33 @@ const PageEditor = React.memo((): JSX.Element => {
 
   const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), [slackChannelsData]);
 
+  const { data: socket } = useGlobalSocket();
+
+  const { mutate: mutateIsConflict } = useIsConflict();
+
 
   const editorRef = useRef<IEditorMethods>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
+  const checkIsConflict = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+
+    const isConflict = markdownToPreview !== s2cMessagePageUpdated.revisionBody;
+
+    mutateIsConflict(isConflict);
+
+  }, [markdownToPreview, mutateIsConflict]);
+
+  useEffect(() => {
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.PageUpdated, checkIsConflict);
+
+    return () => {
+      socket.off(SocketEventName.PageUpdated, checkIsConflict);
+    };
+
+  }, [socket, checkIsConflict]);
 
   // const optionsToSave = useMemo(() => {
   //   if (grantData == null) {

+ 2 - 1
packages/app/src/components/PageEditorByHackmd.tsx

@@ -22,9 +22,10 @@ import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 import {
-  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId, useIsHackmdDraftUpdatingInRealtime,
+  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   EditorMode,
   useEditorMode, useSelectedGrant,

+ 40 - 0
packages/app/src/components/PageStatusAlert.module.scss

@@ -0,0 +1,40 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+
+.grw-page-status-alert :global {
+  $margin-bottom: var.$grw-navbar-bottom-height + 10px;
+
+  box-shadow: 0px 2px 4px #0000004d;
+  opacity: 0.9;
+
+  @include bs.media-breakpoint-down(sm) {
+    margin: 0 10px $margin-bottom;
+
+    .grw-card-label-container {
+      text-align: center;
+    }
+    .grw-card-btn-container {
+      text-align: center;
+
+      .btn {
+        @include bs.button-size(bs.$btn-padding-y-lg, bs.$btn-padding-x-lg, bs.$btn-font-size-lg, bs.$btn-line-height-lg, bs.$btn-border-radius-lg);
+      }
+    }
+  }
+
+  @include bs.media-breakpoint-up(md) {
+    width: 700px;
+    margin: 0 auto $margin-bottom;
+
+    .card-body {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+
+    .grw-card-label-container,
+    .grw-card-btn-container {
+      margin: 0;
+    }
+  }
+}

+ 22 - 35
packages/app/src/components/PageStatusAlert.tsx

@@ -1,13 +1,18 @@
-import React, { useCallback, useEffect, useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import * as ReactDOMServer from 'react-dom/server';
 
-import { SocketEventName } from '~/interfaces/websocket';
+import { useIsConflict } from '~/stores/editor';
 import {
-  useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRemoteRevisionId, useRevisionIdHackmdSynced,
+  useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
 } from '~/stores/hackmd';
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useGlobalSocket } from '~/stores/websocket';
+import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
+
+import { Username } from './User/Username';
+
+import styles from './PageStatusAlert.module.scss';
 
 type AlertComponentContents = {
   additionalClasses: string[],
@@ -20,24 +25,16 @@ export const PageStatusAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
+  const { data: isConflict } = useIsConflict();
+
+  // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: remoteRevisionId } = useRemoteRevisionId();
+  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
+
   const { data: pageData } = useSWRxCurrentPage();
   const revision = pageData?.revision;
 
-  const { data: socket } = useGlobalSocket();
-
-  useEffect(() => {
-    if (socket == null) { return }
-
-    socket.on(SocketEventName.PageUpdated, () => {
-      console.log('page updated');
-    });
-
-    return () => { socket.off(SocketEventName.PageUpdated) };
-
-  }, [socket]);
-
   const refreshPage = useCallback(() => {
     window.location.reload();
   }, []);
@@ -81,23 +78,13 @@ export const PageStatusAlert = (): JSX.Element => {
   }, [t]);
 
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
-    // const pageEditor = appContainer.getComponentInstance('PageEditor');
-
-    const isConflictOnEdit = false;
-
-    // if (pageEditor != null) {
-    //   const markdownOnEdit = pageEditor.getMarkdown();
-    //   isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
-    // }
 
-    // TODO: re-impl with Next.js way
-    // const usernameComponentToString = ReactDOMServer.renderToString(<Username user={pageContainer.state.lastUpdateUser} />);
+    const usernameComponentToString = ReactDOMServer.renderToString(<Username user={remoteRevisionLastUpdateUser} />);
 
-    // const label1 = isConflictOnEdit
-    //   ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
-    //   // eslint-disable-next-line react/no-danger
-    //   : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
-    const label1 = '(TBD -- 2022.09.13)';
+    const label1 = isConflict
+      ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
+      // eslint-disable-next-line react/no-danger
+      : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
 
     return {
       additionalClasses: ['bg-warning'],
@@ -112,7 +99,7 @@ export const PageStatusAlert = (): JSX.Element => {
             <i className="icon-fw icon-reload mr-1"></i>
             {t('Load latest')}
           </button>
-          { isConflictOnEdit && (
+          { isConflict && (
             <button
               type="button"
               onClick={onClickResolveConflict}
@@ -124,7 +111,7 @@ export const PageStatusAlert = (): JSX.Element => {
           )}
         </>,
     };
-  }, [t, onClickResolveConflict, refreshPage]);
+  }, [remoteRevisionLastUpdateUser, isConflict, t, onClickResolveConflict, refreshPage]);
 
   const alertComponentContents = useMemo(() => {
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
@@ -162,7 +149,7 @@ export const PageStatusAlert = (): JSX.Element => {
   const { additionalClasses, label, btn } = alertComponentContents;
 
   return (
-    <div className={`card grw-page-status-alert text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
+    <div className={`${styles['grw-page-status-alert']} card text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
       <div className="card-body">
         <p className="card-text grw-card-label-container">
           {label}

+ 4 - 0
packages/app/src/stores/editor.tsx

@@ -117,3 +117,7 @@ export const usePageTagsForEditors = (pageId: Nullable<string>): SWRResponse<str
 export const useIsEnabledUnsavedWarning = (): SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>('isEnabledUnsavedWarning');
 };
+
+export const useIsConflict = (): SWRResponse<boolean, Error> => {
+  return useStaticSWR<boolean, Error>('isConflict', undefined, { fallbackData: false });
+};

+ 0 - 4
packages/app/src/stores/hackmd.ts

@@ -17,10 +17,6 @@ export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRRespo
   return useStaticSWR<Nullable<any>, Error>('revisionIdHackmdSynced', initialData);
 };
 
-export const useRemoteRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('remoteRevisionId', initialData);
-};
-
 export const useIsHackmdDraftUpdatingInRealtime = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
   return useStaticSWR<Nullable<boolean>, Error>('isHackmdDraftUpdatingInRealtime', initialData);
 };

+ 1 - 1
packages/app/src/stores/page.tsx

@@ -29,7 +29,7 @@ export const useSWRxPage = (
     revisionId?: string,
     initialData?: IPagePopulatedToShowRevision|null,
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
-  const swrResponse = useSWR<IPagePopulatedToShowRevision|null, Error>(
+  const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
     pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
     (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
       .then(result => result.data.page)

+ 18 - 0
packages/app/src/stores/remote-latest-page.ts

@@ -0,0 +1,18 @@
+import { SWRResponse } from 'swr';
+
+import { IUser } from '~/interfaces/user';
+
+import { useStaticSWR } from './use-static-swr';
+
+
+export const useRemoteRevisionId = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR<string, Error>('remoteRevisionId', initialData);
+};
+
+export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR<string, Error>('remoteRevisionId', initialData);
+};
+
+export const useRemoteRevisionLastUpdatUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
+  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdatUser', initialData);
+};

+ 0 - 38
packages/app/src/styles/_page.scss

@@ -41,41 +41,3 @@
     line-height: 1;
   }
 }
-
-.card.grw-page-status-alert {
-  $margin-bottom: $grw-navbar-bottom-height + 10px;
-
-  box-shadow: 0px 2px 4px #0000004d;
-  opacity: 0.9;
-
-  @include media-breakpoint-down(sm) {
-    margin: 0 10px $margin-bottom;
-
-    .grw-card-label-container {
-      text-align: center;
-    }
-    .grw-card-btn-container {
-      text-align: center;
-
-      .btn {
-        @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-line-height-lg, $btn-border-radius-lg);
-      }
-    }
-  }
-
-  @include media-breakpoint-up(md) {
-    width: 700px;
-    margin: 0 auto $margin-bottom;
-
-    .card-body {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-    }
-
-    .grw-card-label-container,
-    .grw-card-btn-container {
-      margin: 0;
-    }
-  }
-}