route.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import React from "react"
  2. import Button from "misago/components/button"
  3. import {
  4. compareGlobalWeight,
  5. compareWeight
  6. } from "misago/components/threads/compare"
  7. import Container from "misago/components/threads/container"
  8. import Header from "misago/components/threads/header"
  9. import {
  10. diffThreads,
  11. getModerationActions,
  12. getPageTitle,
  13. getTitle
  14. } from "misago/components/threads/utils"
  15. import ThreadsList from "misago/components/threads-list"
  16. import ThreadsListEmpty from "misago/components/threads/list-empty"
  17. import WithDropdown from "misago/components/with-dropdown"
  18. import misago from "misago/index"
  19. import * as select from "misago/reducers/selection"
  20. import { append, deleteThread, hydrate, patch } from "misago/reducers/threads"
  21. import ajax from "misago/services/ajax"
  22. import polls from "misago/services/polls"
  23. import snackbar from "misago/services/snackbar"
  24. import store from "misago/services/store"
  25. import title from "misago/services/page-title"
  26. import * as sets from "misago/utils/sets"
  27. export default class extends WithDropdown {
  28. constructor(props) {
  29. super(props)
  30. this.state = {
  31. isMounted: true,
  32. isLoaded: false,
  33. isBusy: false,
  34. diff: {
  35. results: []
  36. },
  37. moderation: [],
  38. busyThreads: [],
  39. dropdown: false,
  40. subcategories: [],
  41. count: 0,
  42. more: 0,
  43. page: 1,
  44. pages: 1
  45. }
  46. let category = this.getCategory()
  47. if (misago.has("THREADS")) {
  48. this.initWithPreloadedData(category, misago.get("THREADS"))
  49. } else {
  50. this.initWithoutPreloadedData(category)
  51. }
  52. }
  53. getCategory() {
  54. if (!this.props.route.category.special_role) {
  55. return this.props.route.category.id
  56. } else {
  57. return null
  58. }
  59. }
  60. initWithPreloadedData(category, data) {
  61. this.state = Object.assign(this.state, {
  62. moderation: getModerationActions(data.results),
  63. subcategories: data.subcategories,
  64. count: data.count,
  65. more: data.more,
  66. page: data.page,
  67. pages: data.pages
  68. })
  69. this.startPolling(category)
  70. }
  71. initWithoutPreloadedData(category) {
  72. this.loadThreads(category)
  73. }
  74. loadThreads(category, page = 1) {
  75. ajax
  76. .get(
  77. this.props.options.api,
  78. {
  79. category: category,
  80. list: this.props.route.list.type,
  81. page: page || 1
  82. },
  83. "threads"
  84. )
  85. .then(
  86. data => {
  87. if (!this.state.isMounted) {
  88. // user changed route before loading completion
  89. return
  90. }
  91. if (page === 1) {
  92. store.dispatch(hydrate(data.results))
  93. } else {
  94. store.dispatch(append(data.results, this.getSorting()))
  95. }
  96. this.setState({
  97. isLoaded: true,
  98. isBusy: false,
  99. moderation: getModerationActions(store.getState().threads),
  100. subcategories: data.subcategories,
  101. count: data.count,
  102. more: data.more,
  103. page: data.page,
  104. pages: data.pages
  105. })
  106. this.startPolling(category)
  107. },
  108. rejection => {
  109. snackbar.apiError(rejection)
  110. }
  111. )
  112. }
  113. startPolling(category) {
  114. polls.start({
  115. poll: "threads",
  116. url: this.props.options.api,
  117. data: {
  118. category: category,
  119. list: this.props.route.list.type
  120. },
  121. frequency: 120 * 1000,
  122. update: this.pollResponse
  123. })
  124. }
  125. componentDidMount() {
  126. this.setPageTitle()
  127. if (misago.has("THREADS")) {
  128. // unlike in other components, routes are root components for threads
  129. // so we can't dispatch store action from constructor
  130. store.dispatch(hydrate(misago.pop("THREADS").results))
  131. this.setState({
  132. isLoaded: true
  133. })
  134. }
  135. store.dispatch(select.none())
  136. }
  137. componentWillUnmount() {
  138. this.state.isMounted = false
  139. polls.stop("threads")
  140. }
  141. getTitle() {
  142. if (this.props.options.title) {
  143. return this.props.options.title
  144. }
  145. return getTitle(this.props.route)
  146. }
  147. setPageTitle() {
  148. if (this.props.route.category.level || !misago.get("THREADS_ON_INDEX")) {
  149. title.set(getPageTitle(this.props.route))
  150. } else if (this.props.options.title) {
  151. title.set(this.props.options.title)
  152. } else {
  153. if (misago.get("SETTINGS").forum_index_title) {
  154. document.title = misago.get("SETTINGS").forum_index_title
  155. } else {
  156. document.title = misago.get("SETTINGS").forum_name
  157. }
  158. }
  159. }
  160. getSorting() {
  161. if (this.props.route.category.level) {
  162. return compareWeight
  163. } else {
  164. return compareGlobalWeight
  165. }
  166. }
  167. // AJAX
  168. loadMore = () => {
  169. this.setState({
  170. isBusy: true
  171. })
  172. this.loadThreads(this.getCategory(), this.state.page + 1)
  173. }
  174. pollResponse = data => {
  175. this.setState({
  176. diff: Object.assign({}, data, {
  177. results: diffThreads(this.props.threads, data.results)
  178. })
  179. })
  180. }
  181. addThreads = threads => {
  182. store.dispatch(append(threads, this.getSorting()))
  183. }
  184. applyDiff = () => {
  185. this.addThreads(this.state.diff.results)
  186. this.setState(
  187. Object.assign({}, this.state.diff, {
  188. moderation: getModerationActions(store.getState().threads),
  189. diff: {
  190. results: []
  191. }
  192. })
  193. )
  194. }
  195. // Thread state utils
  196. freezeThread = thread => {
  197. this.setState(function(currentState) {
  198. return {
  199. busyThreads: sets.toggle(currentState.busyThreads, thread)
  200. }
  201. })
  202. }
  203. updateThread = thread => {
  204. store.dispatch(patch(thread, thread, this.getSorting()))
  205. }
  206. deleteThread = thread => {
  207. store.dispatch(deleteThread(thread))
  208. }
  209. getMoreButton() {
  210. if (this.state.more) {
  211. return (
  212. <div className="pager-more">
  213. <Button
  214. className="btn btn-default btn-outline"
  215. loading={this.state.isBusy || this.state.busyThreads.length}
  216. onClick={this.loadMore}
  217. >
  218. {gettext("Show more")}
  219. </Button>
  220. </div>
  221. )
  222. } else {
  223. return null
  224. }
  225. }
  226. getClassName() {
  227. let className = "page page-threads"
  228. className += " page-threads-" + this.props.route.list.type
  229. if (this.props.route.category.css_class) {
  230. className += " page-threads-" + this.props.route.category.css_class
  231. }
  232. return className
  233. }
  234. render() {
  235. return (
  236. <div className={this.getClassName()}>
  237. <Header
  238. categories={this.props.route.categoriesMap}
  239. disabled={!this.state.isLoaded}
  240. startThread={this.props.options.startThread}
  241. threads={this.props.threads}
  242. title={this.getTitle()}
  243. toggleNav={this.toggleNav}
  244. route={this.props.route}
  245. user={this.props.user}
  246. />
  247. <Container
  248. api={this.props.options.api}
  249. route={this.props.route}
  250. subcategories={this.state.subcategories}
  251. user={this.props.user}
  252. pageLead={this.props.options.pageLead}
  253. threads={this.props.threads}
  254. threadsCount={this.state.count}
  255. moderation={this.state.moderation}
  256. selection={this.props.selection}
  257. busyThreads={this.state.busyThreads}
  258. addThreads={this.addThreads}
  259. freezeThread={this.freezeThread}
  260. deleteThread={this.deleteThread}
  261. updateThread={this.updateThread}
  262. isLoaded={this.state.isLoaded}
  263. isBusy={this.state.isBusy}
  264. >
  265. <ThreadsList
  266. category={this.props.route.category}
  267. categories={this.props.route.categoriesMap}
  268. list={this.props.route.list}
  269. selection={this.props.selection}
  270. threads={this.props.threads}
  271. diffSize={this.state.diff.results.length}
  272. applyDiff={this.applyDiff}
  273. showOptions={!!this.props.user.id}
  274. isLoaded={this.state.isLoaded}
  275. busyThreads={this.state.busyThreads}
  276. >
  277. <ThreadsListEmpty
  278. category={this.props.route.category}
  279. emptyMessage={this.props.options.emptyMessage}
  280. list={this.props.route.list}
  281. />
  282. </ThreadsList>
  283. {this.getMoreButton()}
  284. </Container>
  285. </div>
  286. )
  287. }
  288. }