route.js 8.4 KB

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