PageStatusAlert.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import React, { useCallback, useEffect, useMemo } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import * as ReactDOMServer from 'react-dom/server';
  4. import { SocketEventName } from '~/interfaces/websocket';
  5. import { useGetEditingMarkdown } from '~/stores/editor';
  6. import {
  7. useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
  8. } from '~/stores/hackmd';
  9. import { useSWRxCurrentPage } from '~/stores/page';
  10. import { useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
  11. import { useGlobalSocket } from '~/stores/websocket';
  12. import { Username } from './User/Username';
  13. import styles from './PageStatusAlert.module.scss';
  14. type AlertComponentContents = {
  15. additionalClasses: string[],
  16. label: JSX.Element,
  17. btn: JSX.Element
  18. }
  19. export const PageStatusAlert = (): JSX.Element => {
  20. const { t } = useTranslation();
  21. const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
  22. const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
  23. const { data: getEditingMarkdown } = useGetEditingMarkdown();
  24. // store remote latest page data
  25. const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
  26. const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
  27. const { data: remoteRevisionBody, mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
  28. const { data: remoteRevisionLastUpdateUser, mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
  29. const { data: pageData } = useSWRxCurrentPage();
  30. const revision = pageData?.revision;
  31. const pageId = pageData?._id;
  32. const { data: socket } = useGlobalSocket();
  33. // method from page container
  34. // setLatestRemotePageData(s2cMessagePageUpdated) {
  35. // const newState = {
  36. // remoteRevisionId: s2cMessagePageUpdated.revisionId,
  37. // remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
  38. // remoteRevisionUpdateAt: s2cMessagePageUpdated.revisionUpdateAt,
  39. // revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
  40. // // TODO // TODO remove lastUpdateUsername and refactor parts that lastUpdateUsername is used
  41. // lastUpdateUsername: s2cMessagePageUpdated.lastUpdateUsername,
  42. // lastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
  43. // };
  44. // if (s2cMessagePageUpdated.hasDraftOnHackmd != null) {
  45. // newState.hasDraftOnHackmd = s2cMessagePageUpdated.hasDraftOnHackmd;
  46. // }
  47. // this.setState(newState);
  48. // }
  49. const setLatestRemotePageData = useCallback((s2cMessagePageUpdated: any) => {
  50. mutateRemoteRevisionId(s2cMessagePageUpdated.revisionId);
  51. mutateRemoteRevisionBody(s2cMessagePageUpdated.revisionBody);
  52. mutateRemoteRevisionLastUpdateUser(s2cMessagePageUpdated.remoteLastUpdateUser);
  53. }, [mutateRemoteRevisionBody, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser]);
  54. useEffect(() => {
  55. if (socket == null) { return }
  56. socket.on(SocketEventName.PageUpdated, (data) => {
  57. const { s2cMessagePageUpdated } = data;
  58. if (s2cMessagePageUpdated.pageId === pageId) {
  59. setLatestRemotePageData(s2cMessagePageUpdated);
  60. }
  61. });
  62. return () => { socket.off(SocketEventName.PageUpdated) };
  63. }, [pageId, setLatestRemotePageData, socket]);
  64. const refreshPage = useCallback(() => {
  65. window.location.reload();
  66. }, []);
  67. const onClickResolveConflict = useCallback(() => {
  68. // this.props.pageContainer.setState({
  69. // isConflictDiffModalOpen: true,
  70. // });
  71. }, []);
  72. const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
  73. return {
  74. additionalClasses: ['bg-success', 'd-hackmd-none'],
  75. label:
  76. <>
  77. <i className="icon-fw icon-people"></i>
  78. {t('hackmd.someone_editing')}
  79. </>,
  80. btn:
  81. <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
  82. <i className="fa fa-fw fa-file-text-o mr-1"></i>
  83. Open HackMD Editor
  84. </a>,
  85. };
  86. }, [t]);
  87. const getContentsForDraftExistsAlert = useCallback((): AlertComponentContents => {
  88. return {
  89. additionalClasses: ['bg-success', 'd-hackmd-none'],
  90. label:
  91. <>
  92. <i className="icon-fw icon-pencil"></i>
  93. {t('hackmd.this_page_has_draft')}
  94. </>,
  95. btn:
  96. <a href="#hackmd" key="btnOpenHackmdPageHasDraft" className="btn btn-outline-white">
  97. <i className="fa fa-fw fa-file-text-o mr-1"></i>
  98. Open HackMD Editor
  99. </a>,
  100. };
  101. }, [t]);
  102. const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
  103. let isConflictOnEdit = false;
  104. if (getEditingMarkdown != null) {
  105. const editingMarkdown = getEditingMarkdown();
  106. isConflictOnEdit = editingMarkdown !== remoteRevisionBody;
  107. }
  108. // TODO: re-impl with Next.js way
  109. const usernameComponentToString = ReactDOMServer.renderToString(<Username user={remoteRevisionLastUpdateUser} />);
  110. const label1 = isConflictOnEdit
  111. ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
  112. // eslint-disable-next-line react/no-danger
  113. : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
  114. return {
  115. additionalClasses: ['bg-warning'],
  116. label:
  117. <>
  118. <i className="icon-fw icon-bulb"></i>
  119. {label1}
  120. </>,
  121. btn:
  122. <>
  123. <button type="button" onClick={() => refreshPage()} className="btn btn-outline-white mr-4">
  124. <i className="icon-fw icon-reload mr-1"></i>
  125. {t('Load latest')}
  126. </button>
  127. { isConflictOnEdit && (
  128. <button
  129. type="button"
  130. onClick={onClickResolveConflict}
  131. className="btn btn-outline-white"
  132. >
  133. <i className="fa fa-fw fa-file-text-o mr-1"></i>
  134. {t('modal_resolve_conflict.resolve_conflict')}
  135. </button>
  136. )}
  137. </>,
  138. };
  139. }, [getEditingMarkdown, remoteRevisionLastUpdateUser, t, onClickResolveConflict, remoteRevisionBody, refreshPage]);
  140. const alertComponentContents = useMemo(() => {
  141. const isRevisionOutdated = revision?._id !== remoteRevisionId;
  142. const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
  143. // when remote revision is newer than both
  144. if (isHackmdDocumentOutdated && isRevisionOutdated) {
  145. return getContentsForUpdatedAlert();
  146. }
  147. // when someone editing with HackMD
  148. if (isHackmdDraftUpdatingInRealtime) {
  149. return getContentsForSomeoneEditingAlert();
  150. }
  151. // when the draft of HackMD is newest
  152. if (hasDraftOnHackmd) {
  153. return getContentsForDraftExistsAlert();
  154. }
  155. return null;
  156. }, [
  157. revision?._id,
  158. remoteRevisionId,
  159. revisionIdHackmdSynced,
  160. isHackmdDraftUpdatingInRealtime,
  161. hasDraftOnHackmd,
  162. getContentsForUpdatedAlert,
  163. getContentsForSomeoneEditingAlert,
  164. getContentsForDraftExistsAlert,
  165. ]);
  166. if (alertComponentContents == null) { return <></> }
  167. const { additionalClasses, label, btn } = alertComponentContents;
  168. return (
  169. <div className={`${styles['grw-page-status-alert']} card text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
  170. <div className="card-body">
  171. <p className="card-text grw-card-label-container">
  172. {label}
  173. </p>
  174. <p className="card-text grw-card-btn-container">
  175. {btn}
  176. </p>
  177. </div>
  178. </div>
  179. );
  180. };