PageStatusAlert.tsx 5.6 KB

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