PageStatusAlert.tsx 5.9 KB

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