PageStatusAlert.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import React, { useCallback, useMemo } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import * as ReactDOMServer from 'react-dom/server';
  4. import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
  5. import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
  6. import {
  7. useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
  8. } from '~/stores/hackmd';
  9. import { useConflictDiffModal } from '~/stores/modal';
  10. import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
  11. import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
  12. import { EditorMode, useEditorMode } from '~/stores/ui';
  13. import { Username } from './User/Username';
  14. import styles from './PageStatusAlert.module.scss';
  15. type AlertComponentContents = {
  16. additionalClasses: string[],
  17. label: JSX.Element,
  18. btn: JSX.Element
  19. }
  20. export const PageStatusAlert = (): JSX.Element => {
  21. const { t } = useTranslation();
  22. const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
  23. const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
  24. const { data: isConflict } = useIsConflict();
  25. const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
  26. const { open: openConflictDiffModal } = useConflictDiffModal();
  27. const { mutate: mutateEditorMode } = useEditorMode();
  28. const { data: isGuestUser } = useIsGuestUser();
  29. const { data: isReadOnlyUser } = useIsReadOnlyUser();
  30. // store remote latest page data
  31. const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
  32. const { data: remoteRevisionId } = useRemoteRevisionId();
  33. const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
  34. const { data: pageData } = useSWRxCurrentPage();
  35. const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
  36. const revision = pageData?.revision;
  37. const refreshPage = useCallback(async() => {
  38. const updatedPageData = await mutatePageData();
  39. mutateEditingMarkdown(updatedPageData?.revision.body);
  40. }, [mutateEditingMarkdown, mutatePageData]);
  41. const onClickResolveConflict = useCallback(() => {
  42. openConflictDiffModal();
  43. }, [openConflictDiffModal]);
  44. const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
  45. return {
  46. additionalClasses: ['bg-success', 'd-hackmd-none'],
  47. label:
  48. <>
  49. <i className="icon-fw icon-people"></i>
  50. {t('hackmd.someone_editing')}
  51. </>,
  52. btn:
  53. <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
  54. <i className="fa fa-fw fa-file-text-o me-1"></i>
  55. Open HackMD Editor
  56. </a>,
  57. };
  58. }, [t]);
  59. const getContentsForDraftExistsAlert = useCallback((): AlertComponentContents => {
  60. return {
  61. additionalClasses: ['bg-success', 'd-hackmd-none'],
  62. label:
  63. <>
  64. <i className="icon-fw icon-pencil"></i>
  65. {t('hackmd.this_page_has_draft')}
  66. </>,
  67. btn:
  68. <button type="button" onClick={() => mutateEditorMode(EditorMode.HackMD)} className="btn btn-outline-white">
  69. <i className="fa fa-fw fa-file-text-o me-1"></i>
  70. Open HackMD Editor
  71. </button>,
  72. };
  73. }, [mutateEditorMode, t]);
  74. const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
  75. const usernameComponentToString = ReactDOMServer.renderToString(<Username user={remoteRevisionLastUpdateUser} />);
  76. const label1 = isConflict
  77. ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
  78. // eslint-disable-next-line react/no-danger
  79. : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
  80. return {
  81. additionalClasses: ['bg-warning text-dark'],
  82. label:
  83. <>
  84. <i className="icon-fw icon-bulb"></i>
  85. {label1}
  86. </>,
  87. btn:
  88. <>
  89. <button type="button" onClick={() => refreshPage()} className="btn btn-outline-white me-4">
  90. <i className="icon-fw icon-reload me-1"></i>
  91. {t('Load latest')}
  92. </button>
  93. { isConflict && (
  94. <button
  95. type="button"
  96. onClick={onClickResolveConflict}
  97. className="btn btn-outline-white"
  98. >
  99. <i className="fa fa-fw fa-file-text-o me-1"></i>
  100. {t('modal_resolve_conflict.resolve_conflict')}
  101. </button>
  102. )}
  103. </>,
  104. };
  105. }, [remoteRevisionLastUpdateUser, isConflict, t, onClickResolveConflict, refreshPage]);
  106. const alertComponentContents = useMemo(() => {
  107. const isRevisionOutdated = revision?._id !== remoteRevisionId;
  108. const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
  109. // 'revision?._id' and 'remoteRevisionId' are can not be undefined
  110. if (revision?._id == null || remoteRevisionId == null) { return }
  111. // when remote revision is newer than both
  112. if (isHackmdDocumentOutdated && isRevisionOutdated) {
  113. return getContentsForUpdatedAlert();
  114. }
  115. // when someone editing with HackMD
  116. if (isHackmdDraftUpdatingInRealtime) {
  117. return getContentsForSomeoneEditingAlert();
  118. }
  119. // when the draft of HackMD is newest
  120. if (hasDraftOnHackmd) {
  121. return getContentsForDraftExistsAlert();
  122. }
  123. return null;
  124. }, [
  125. revision?._id,
  126. remoteRevisionId,
  127. revisionIdHackmdSynced,
  128. isHackmdDraftUpdatingInRealtime,
  129. hasDraftOnHackmd,
  130. getContentsForUpdatedAlert,
  131. getContentsForSomeoneEditingAlert,
  132. getContentsForDraftExistsAlert,
  133. ]);
  134. if (!!isGuestUser || !!isReadOnlyUser || alertComponentContents == null) { return <></> }
  135. const { additionalClasses, label, btn } = alertComponentContents;
  136. return (
  137. <div className={`${styles['grw-page-status-alert']} card text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
  138. <div className="card-body">
  139. <p className="card-text grw-card-label-container">
  140. {label}
  141. </p>
  142. <p className="card-text grw-card-btn-container">
  143. {btn}
  144. </p>
  145. </div>
  146. </div>
  147. );
  148. };