PageStatusAlert.tsx 5.9 KB

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