PageStatusAlert.jsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ReactDOMServer from 'react-dom/server';
  4. import { useTranslation } from 'react-i18next';
  5. import AppContainer from '~/client/services/AppContainer';
  6. import PageContainer from '~/client/services/PageContainer';
  7. import Username from '~/components/User/Username';
  8. import { withUnstatedContainers } from './UnstatedUtils';
  9. /**
  10. *
  11. * @author Yuki Takei <yuki@weseek.co.jp>
  12. *
  13. * @export
  14. * @class PageStatusAlert
  15. * @extends {React.Component}
  16. */
  17. class PageStatusAlert extends React.Component {
  18. constructor(props) {
  19. super(props);
  20. this.state = {
  21. };
  22. this.getContentsForSomeoneEditingAlert = this.getContentsForSomeoneEditingAlert.bind(this);
  23. this.getContentsForDraftExistsAlert = this.getContentsForDraftExistsAlert.bind(this);
  24. this.getContentsForUpdatedAlert = this.getContentsForUpdatedAlert.bind(this);
  25. this.onClickResolveConflict = this.onClickResolveConflict.bind(this);
  26. }
  27. refreshPage() {
  28. window.location.reload();
  29. }
  30. onClickResolveConflict() {
  31. this.props.pageContainer.setState({
  32. isConflictDiffModalOpen: true,
  33. });
  34. }
  35. getContentsForSomeoneEditingAlert() {
  36. const { t } = this.props;
  37. return [
  38. ['bg-success', 'd-hackmd-none'],
  39. <>
  40. <i className="icon-fw icon-people"></i>
  41. {t('hackmd.someone_editing')}
  42. </>,
  43. <a href="#hackmd" className="btn btn-outline-white">
  44. <i className="fa fa-fw fa-file-text-o mr-1"></i>
  45. Open HackMD Editor
  46. </a>,
  47. ];
  48. }
  49. getContentsForDraftExistsAlert(isRealtime) {
  50. const { t } = this.props;
  51. return [
  52. ['bg-success', 'd-hackmd-none'],
  53. <>
  54. <i className="icon-fw icon-pencil"></i>
  55. {t('hackmd.this_page_has_draft')}
  56. </>,
  57. <a href="#hackmd" className="btn btn-outline-white">
  58. <i className="fa fa-fw fa-file-text-o mr-1"></i>
  59. Open HackMD Editor
  60. </a>,
  61. ];
  62. }
  63. getContentsForUpdatedAlert() {
  64. const { t, appContainer, pageContainer } = this.props;
  65. const pageEditor = appContainer.getComponentInstance('PageEditor');
  66. let isConflictOnEdit = false;
  67. if (pageEditor != null) {
  68. const markdownOnEdit = pageEditor.getMarkdown();
  69. isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
  70. }
  71. const usernameComponentToString = ReactDOMServer.renderToString(<Username user={pageContainer.state.lastUpdateUser} />);
  72. const label1 = isConflictOnEdit
  73. ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
  74. // eslint-disable-next-line react/no-danger
  75. : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
  76. return [
  77. ['bg-warning'],
  78. <>
  79. <i className="icon-fw icon-bulb"></i>
  80. {label1}
  81. </>,
  82. <>
  83. <button type="button" onClick={() => this.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. { isConflictOnEdit && (
  88. <button
  89. type="button"
  90. onClick={this.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. }
  100. render() {
  101. const {
  102. revisionId, revisionIdHackmdSynced, remoteRevisionId, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime,
  103. } = this.props.pageContainer.state;
  104. const isRevisionOutdated = revisionId !== remoteRevisionId;
  105. const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
  106. let getContentsFunc = null;
  107. // when remote revision is newer than both
  108. if (isHackmdDocumentOutdated && isRevisionOutdated) {
  109. getContentsFunc = this.getContentsForUpdatedAlert;
  110. }
  111. // when someone editing with HackMD
  112. else if (isHackmdDraftUpdatingInRealtime) {
  113. getContentsFunc = this.getContentsForSomeoneEditingAlert;
  114. }
  115. // when the draft of HackMD is newest
  116. else if (hasDraftOnHackmd) {
  117. getContentsFunc = this.getContentsForDraftExistsAlert;
  118. }
  119. // do not render anything
  120. else {
  121. return null;
  122. }
  123. const [additionalClasses, label, btn] = getContentsFunc();
  124. return (
  125. <div className={`card grw-page-status-alert text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
  126. <div className="card-body">
  127. <p className="card-text grw-card-label-container">
  128. {label}
  129. </p>
  130. <p className="card-text grw-card-btn-container">
  131. {btn}
  132. </p>
  133. </div>
  134. </div>
  135. );
  136. }
  137. }
  138. PageStatusAlert.propTypes = {
  139. t: PropTypes.func.isRequired, // i18next
  140. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  141. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  142. };
  143. const PageStatusAlertWrapperFC = (props) => {
  144. const { t } = useTranslation();
  145. return <PageStatusAlert t={t} {...props} />;
  146. };
  147. /**
  148. * Wrapper component for using unstated
  149. */
  150. const PageStatusAlertWrapper = withUnstatedContainers(PageStatusAlertWrapperFC, [AppContainer, PageContainer]);
  151. export default PageStatusAlertWrapper;