route.js 8.1 KB

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