route.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import React from 'react'; // jshint ignore:line
  2. import Button from 'misago/components/button'; // jshint ignore:line
  3. import Header from 'misago/components/threads/header'; // jshint ignore:line
  4. import { CompactNav } from 'misago/components/threads/navs'; // jshint ignore:line
  5. import { getPageTitle, getTitle } from 'misago/components/threads/utils';
  6. import ThreadsList from 'misago/components/threads-list/root'; // jshint ignore:line
  7. import ThreadsListEmpty from 'misago/components/threads/list-empty'; // jshint ignore:line
  8. import Toolbar from 'misago/components/threads/toolbar'; // jshint ignore:line
  9. import WithDropdown from 'misago/components/with-dropdown';
  10. import misago from 'misago/index';
  11. import { append, hydrate } from 'misago/reducers/threads'; // jshint ignore:line
  12. import ajax from 'misago/services/ajax';
  13. import snackbar from 'misago/services/snackbar';
  14. import store from 'misago/services/store';
  15. import title from 'misago/services/page-title';
  16. import * as sets from 'misago/utils/sets'; // jshint ignore:line
  17. export default class extends WithDropdown {
  18. constructor(props) {
  19. super(props);
  20. this.state = {
  21. isLoaded: false,
  22. isBusy: false,
  23. selection: [],
  24. busyThreads: [],
  25. dropdown: false,
  26. subcategories: [],
  27. count: 0,
  28. more: 0,
  29. page: 1,
  30. pages: 1
  31. };
  32. if (misago.has('THREADS')) {
  33. this.initWithPreloadedData(misago.get('THREADS'));
  34. } else {
  35. this.initWithoutPreloadedData();
  36. }
  37. }
  38. initWithPreloadedData(data) {
  39. this.state = Object.assign(this.state, {
  40. subcategories: data.subcategories,
  41. count: data.count,
  42. more: data.more,
  43. page: data.page,
  44. pages: data.pages
  45. });
  46. }
  47. initWithoutPreloadedData() {
  48. this.loadThreads();
  49. }
  50. loadThreads(page=1) {
  51. let category = null;
  52. if (!this.props.route.category.special_role) {
  53. category = this.props.route.category.id;
  54. }
  55. ajax.get(misago.get('THREADS_API'), {
  56. category: category,
  57. list: this.props.route.list.type,
  58. page: page || 1
  59. }, 'threads').then((data) => {
  60. if (page === 1) {
  61. store.dispatch(hydrate(data.results));
  62. } else {
  63. store.dispatch(append(data.results));
  64. }
  65. this.setState({
  66. isLoaded: true,
  67. isBusy: false,
  68. subcategories: data.subcategories,
  69. count: data.count,
  70. more: data.more,
  71. page: data.page,
  72. pages: data.pages
  73. });
  74. }, (rejection) => {
  75. snackbar.apiError(rejection);
  76. });
  77. }
  78. componentDidMount() {
  79. title.set(getPageTitle(this.props.route));
  80. if (misago.has('THREADS')) {
  81. // unlike in other components, routes are root components for threads
  82. // so we can't dispatch store action from constructor
  83. store.dispatch(hydrate(misago.pop('THREADS').results));
  84. this.setState({
  85. isLoaded: true
  86. });
  87. }
  88. }
  89. getTitle() {
  90. return getTitle(this.props.route);
  91. }
  92. /* jshint ignore:start */
  93. loadMore = () => {
  94. this.setState({
  95. isBusy: true
  96. });
  97. this.loadThreads(this.state.page + 1);
  98. };
  99. selectThread = (thread) => {
  100. this.setState({
  101. selection: sets.toggle(this.state.selection, thread)
  102. });
  103. };
  104. /* jshint ignore:end */
  105. getClassName() {
  106. let className = 'page page-threads';
  107. className += ' page-threads-' + this.props.route.list;
  108. if (this.props.route.category.css_class) {
  109. className += ' page-' + this.props.route.category.css_class;
  110. }
  111. return className;
  112. }
  113. getCompactNav() {
  114. if (this.props.route.lists.length > 1) {
  115. /* jshint ignore:start */
  116. return <CompactNav baseUrl={this.props.route.category.absolute_url}
  117. list={this.props.route.list}
  118. lists={this.props.route.lists}
  119. hideNav={this.hideNav} />;
  120. /* jshint ignore:end */
  121. } else {
  122. return null;
  123. }
  124. }
  125. getCategoryDescription() {
  126. if (this.props.route.category.description) {
  127. /* jshint ignore:start */
  128. return <div className="category-description">
  129. <div className="lead" dangerouslySetInnerHTML={{
  130. __html: this.props.route.category.description.html
  131. }} />
  132. </div>;
  133. /* jshint ignore:end */
  134. } else {
  135. return null;
  136. }
  137. }
  138. getToolbarLabel() {
  139. if (this.state.isLoaded) {
  140. let label = null;
  141. if (this.props.route.list.path) {
  142. label = ngettext(
  143. "%(threads)s thread found.",
  144. "%(threads)s threads found.",
  145. this.state.count);
  146. } else if (this.props.route.category.parent) {
  147. label = ngettext(
  148. "There is %(threads)s thread in this category.",
  149. "There are %(threads)s threads in this category.",
  150. this.state.count);
  151. } else {
  152. label = ngettext(
  153. "There is %(threads)s thread on our forums.",
  154. "There are %(threads)s threads on our forums.",
  155. this.state.count);
  156. }
  157. return interpolate(label, {
  158. 'threads': this.state.count
  159. }, true);
  160. } else {
  161. return gettext("Loading threads...");
  162. }
  163. }
  164. getToolbar() {
  165. if (this.state.subcategories.length || this.props.user.id) {
  166. /* jshint ignore:start */
  167. return <Toolbar subcategories={this.state.subcategories}
  168. categories={this.props.route.categoriesMap}
  169. list={this.props.route.list}
  170. threads={this.props.threads}
  171. selection={this.props.selection}
  172. isLoaded={this.state.isLoaded}
  173. user={this.props.user}>
  174. {this.getToolbarLabel()}
  175. </Toolbar>;
  176. /* jshint ignore:end */
  177. } else {
  178. return null;
  179. }
  180. }
  181. getMoreButton() {
  182. if (this.state.more) {
  183. /* jshint ignore:start */
  184. return <div className="pager-more">
  185. <Button loading={this.state.isBusy || this.state.busyThreads.length}
  186. onClick={this.loadMore}>
  187. {gettext("Show more")}
  188. </Button>
  189. </div>;
  190. /* jshint ignore:end */
  191. } else {
  192. return null;
  193. }
  194. }
  195. render() {
  196. /* jshint ignore:start */
  197. return <div className={this.getClassName()}>
  198. <Header title={this.getTitle()}
  199. route={this.props.route}
  200. user={this.props.user}
  201. isLoaded={this.state.isLoaded}
  202. dropdown={this.state.dropdown}
  203. toggleNav={this.toggleNav}
  204. hideNav={this.hideNav} />
  205. <div className={this.getCompactNavClassName()}>
  206. {this.getCompactNav()}
  207. </div>
  208. <div className="container">
  209. {this.getCategoryDescription()}
  210. {this.getToolbar()}
  211. <ThreadsList user={this.props.user}
  212. threads={this.props.threads}
  213. categories={this.props.route.categoriesMap}
  214. list={this.props.route.list}
  215. selectThread={this.selectThread}
  216. selection={this.state.selection}
  217. busyThreads={this.state.busyThreads}
  218. isLoaded={this.state.isLoaded}>
  219. <ThreadsListEmpty category={this.props.route.category}
  220. list={this.props.route.list} />
  221. </ThreadsList>
  222. {this.getMoreButton()}
  223. </div>
  224. </div>;
  225. /* jshint ignore:end */
  226. }
  227. }