reply.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import React from "react"
  2. import Form from "misago/components/form"
  3. import * as attachments from "./utils/attachments"
  4. import { getPostValidators } from "./utils/validators"
  5. import ajax from "misago/services/ajax"
  6. import posting from "misago/services/posting"
  7. import snackbar from "misago/services/snackbar"
  8. import MarkupEditor from "../MarkupEditor"
  9. import PostingDialog from "./PostingDialog"
  10. import PostingDialogBody from "./PostingDialogBody"
  11. import PostingDialogError from "./PostingDialogError"
  12. import PostingDialogHeader from "./PostingDialogHeader"
  13. import { clearGlobalState, setGlobalState } from "./globalState"
  14. export default class extends Form {
  15. constructor(props) {
  16. super(props)
  17. this.state = {
  18. isReady: false,
  19. isLoading: false,
  20. error: null,
  21. minimized: false,
  22. fullscreen: false,
  23. post: this.props.default || "",
  24. attachments: [],
  25. validators: {
  26. post: getPostValidators(),
  27. },
  28. errors: {},
  29. }
  30. }
  31. componentDidMount() {
  32. ajax
  33. .get(this.props.config, this.props.context || null)
  34. .then(this.loadSuccess, this.loadError)
  35. setGlobalState(false, this.onQuote)
  36. }
  37. componentWillUnmount() {
  38. clearGlobalState()
  39. }
  40. componentWillReceiveProps(nextProps) {
  41. const context = this.props.context
  42. const newContext = nextProps.context
  43. if (context && newContext && context.reply === newContext.reply) return
  44. ajax
  45. .get(nextProps.config, nextProps.context || null)
  46. .then(this.appendData, snackbar.apiError)
  47. }
  48. loadSuccess = (data) => {
  49. this.setState({
  50. isReady: true,
  51. post: data.post
  52. ? '[quote="@' + data.poster + '"]\n' + data.post + "\n[/quote]"
  53. : this.state.post,
  54. })
  55. }
  56. loadError = (rejection) => {
  57. this.setState({
  58. error: rejection.detail,
  59. })
  60. }
  61. appendData = (data) => {
  62. const newPost = data.post
  63. ? '[quote="@' + data.poster + '"]\n' + data.post + "\n[/quote]\n\n"
  64. : ""
  65. this.setState((prevState, props) => {
  66. if (prevState.post.length > 0) {
  67. return {
  68. post: prevState.post + "\n\n" + newPost,
  69. }
  70. }
  71. return {
  72. post: newPost,
  73. }
  74. })
  75. this.open()
  76. }
  77. onCancel = () => {
  78. const cancel = window.confirm(
  79. pgettext("post reply", "Are you sure you want to discard your reply?")
  80. )
  81. if (cancel) {
  82. this.close()
  83. }
  84. }
  85. onPostChange = (event) => {
  86. this.changeValue("post", event.target.value)
  87. }
  88. onAttachmentsChange = (attachments) => {
  89. this.setState(attachments)
  90. }
  91. onQuote = (quote) => {
  92. this.setState(({ post }) => {
  93. if (post.length > 0) {
  94. return { post: post.trim() + "\n\n" + quote }
  95. }
  96. return { post: quote }
  97. })
  98. this.open()
  99. }
  100. clean() {
  101. if (!this.state.post.trim().length) {
  102. snackbar.error(gettext("You have to enter a message."))
  103. return false
  104. }
  105. const errors = this.validate()
  106. if (errors.post) {
  107. snackbar.error(errors.post[0])
  108. return false
  109. }
  110. return true
  111. }
  112. send() {
  113. setGlobalState(true, this.onQuote)
  114. return ajax.post(this.props.submit, {
  115. post: this.state.post,
  116. attachments: attachments.clean(this.state.attachments),
  117. })
  118. }
  119. handleSuccess(success) {
  120. snackbar.success(pgettext("post reply", "Your reply has been posted."))
  121. window.location = success.url.index
  122. // keep form loading
  123. this.setState({
  124. isLoading: true,
  125. })
  126. setGlobalState(false, this.onQuote)
  127. }
  128. handleError(rejection) {
  129. if (rejection.status === 400) {
  130. const errors = [].concat(
  131. rejection.non_field_errors || [],
  132. rejection.post || [],
  133. rejection.attachments || []
  134. )
  135. snackbar.error(errors[0])
  136. } else {
  137. snackbar.apiError(rejection)
  138. }
  139. setGlobalState(false, this.onQuote)
  140. }
  141. close = () => {
  142. this.minimize()
  143. posting.close()
  144. }
  145. minimize = () => {
  146. this.setState({ fullscreen: false, minimized: true })
  147. }
  148. open = () => {
  149. this.setState({ minimized: false })
  150. if (this.state.fullscreen) {
  151. }
  152. }
  153. fullscreenEnter = () => {
  154. this.setState({ fullscreen: true, minimized: false })
  155. }
  156. fullscreenExit = () => {
  157. this.setState({ fullscreen: false, minimized: false })
  158. }
  159. render() {
  160. const dialogProps = {
  161. thread: this.props.thread,
  162. minimized: this.state.minimized,
  163. minimize: this.minimize,
  164. open: this.open,
  165. fullscreen: this.state.fullscreen,
  166. fullscreenEnter: this.fullscreenEnter,
  167. fullscreenExit: this.fullscreenExit,
  168. close: this.onCancel,
  169. }
  170. if (this.state.error) {
  171. return (
  172. <PostingDialogReply {...dialogProps}>
  173. <PostingDialogError message={this.state.error} close={this.close} />
  174. </PostingDialogReply>
  175. )
  176. }
  177. if (!this.state.isReady) {
  178. return (
  179. <PostingDialogReply {...dialogProps}>
  180. <div className="posting-loading ui-preview">
  181. <MarkupEditor
  182. attachments={[]}
  183. value={""}
  184. submitText={pgettext("post reply submit", "Post reply")}
  185. disabled={true}
  186. onAttachmentsChange={() => {}}
  187. onChange={() => {}}
  188. />
  189. </div>
  190. </PostingDialogReply>
  191. )
  192. }
  193. return (
  194. <PostingDialogReply {...dialogProps}>
  195. <form
  196. className="posting-dialog-form"
  197. method="POST"
  198. onSubmit={this.handleSubmit}
  199. >
  200. <MarkupEditor
  201. attachments={this.state.attachments}
  202. value={this.state.post}
  203. submitText={pgettext("post reply submit", "Post reply")}
  204. disabled={this.state.isLoading}
  205. onAttachmentsChange={this.onAttachmentsChange}
  206. onChange={this.onPostChange}
  207. />
  208. </form>
  209. </PostingDialogReply>
  210. )
  211. }
  212. }
  213. const PostingDialogReply = ({
  214. children,
  215. close,
  216. minimized,
  217. minimize,
  218. open,
  219. fullscreen,
  220. fullscreenEnter,
  221. fullscreenExit,
  222. thread,
  223. }) => (
  224. <PostingDialog fullscreen={fullscreen} minimized={minimized}>
  225. <PostingDialogHeader
  226. fullscreen={fullscreen}
  227. fullscreenEnter={fullscreenEnter}
  228. fullscreenExit={fullscreenExit}
  229. minimized={minimized}
  230. minimize={minimize}
  231. open={open}
  232. close={close}
  233. >
  234. {interpolate(
  235. pgettext("post reply", "Reply to: %(thread)s"),
  236. { thread: thread.title },
  237. true
  238. )}
  239. </PostingDialogHeader>
  240. <PostingDialogBody>{children}</PostingDialogBody>
  241. </PostingDialog>
  242. )