route.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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, 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. title.set(getPageTitle(this.props.route));
  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. getSorting() {
  129. if (this.props.route.category.special_role) {
  130. return compareGlobalWeight;
  131. } else {
  132. return compareWeight;
  133. }
  134. }
  135. /* jshint ignore:start */
  136. // AJAX
  137. loadMore = () => {
  138. this.setState({
  139. isBusy: true
  140. });
  141. this.loadThreads(this.getCategory(), this.state.page + 1);
  142. };
  143. pollResponse = (data) => {
  144. this.setState({
  145. diff: Object.assign({}, data, {
  146. results: diffThreads(this.props.threads, data.results)
  147. })
  148. });
  149. };
  150. applyDiff = () => {
  151. store.dispatch(append(this.state.diff.results, this.getSorting()));
  152. this.setState(Object.assign({}, this.state.diff, {
  153. moderation: getModerationActions(store.getState().threads),
  154. diff: {
  155. results: []
  156. }
  157. }));
  158. };
  159. // Thread state utils
  160. freezeThread = (thread) => {
  161. this.setState({
  162. busyThreads: sets.toggle(this.state.busyThreads, thread)
  163. });
  164. };
  165. updateThread = (thread) => {
  166. store.dispatch(patch(thread, thread, this.getSorting()));
  167. };
  168. /* jshint ignore:end */
  169. getCompactNav() {
  170. if (this.props.route.lists.length > 1) {
  171. /* jshint ignore:start */
  172. return <div className={this.getCompactNavClassName()}>
  173. <CompactNav baseUrl={this.props.route.category.absolute_url}
  174. list={this.props.route.list}
  175. lists={this.props.route.lists}
  176. hideNav={this.hideNav} />
  177. </div>;
  178. /* jshint ignore:end */
  179. } else {
  180. return null;
  181. }
  182. }
  183. getMoreButton() {
  184. if (this.state.more) {
  185. /* jshint ignore:start */
  186. return <div className="pager-more">
  187. <Button loading={this.state.isBusy || this.state.busyThreads.length}
  188. onClick={this.loadMore}>
  189. {gettext("Show more")}
  190. </Button>
  191. </div>;
  192. /* jshint ignore:end */
  193. } else {
  194. return null;
  195. }
  196. }
  197. getClassName() {
  198. let className = 'page page-threads';
  199. className += ' page-threads-' + this.props.route.list;
  200. if (this.props.route.category.css_class) {
  201. className += ' page-' + this.props.route.category.css_class;
  202. }
  203. return className;
  204. }
  205. render() {
  206. /* jshint ignore:start */
  207. return <div className={this.getClassName()}>
  208. <Header disabled={!this.state.isLoaded}
  209. threads={this.props.threads}
  210. title={this.getTitle()}
  211. toggleNav={this.toggleNav}
  212. route={this.props.route}
  213. user={this.props.user} />
  214. {this.getCompactNav()}
  215. <Container route={this.props.route}
  216. subcategories={this.state.subcategories}
  217. user={this.props.user}
  218. threads={this.props.threads}
  219. threadsCount={this.state.count}
  220. moderation={this.state.moderation}
  221. selection={this.props.selection}
  222. busyThreads={this.state.busyThreads}
  223. freezeThread={this.freezeThread}
  224. updateThread={this.updateThread}
  225. isLoaded={this.state.isLoaded}
  226. isBusy={this.state.isBusy}>
  227. <ThreadsList categories={this.props.route.categoriesMap}
  228. list={this.props.route.list}
  229. selection={this.props.selection}
  230. threads={this.props.threads}
  231. diffSize={this.state.diff.results.length}
  232. applyDiff={this.applyDiff}
  233. showOptions={!!this.props.user.id}
  234. isLoaded={this.state.isLoaded}
  235. busyThreads={this.state.busyThreads}>
  236. <ThreadsListEmpty category={this.props.route.category}
  237. list={this.props.route.list} />
  238. </ThreadsList>
  239. {this.getMoreButton()}
  240. </Container>
  241. </div>;
  242. /* jshint ignore:end */
  243. }
  244. }