route.js 8.6 KB

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