route.js 7.2 KB

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